mirror of
https://github.com/elementary/gala.git
synced 2024-10-27 00:12:08 +03:00
rewrite background system
This commit is contained in:
parent
162890aae4
commit
be906a98cd
@ -77,7 +77,6 @@ vala_precompile(VALA_C
|
||||
src/TextShadowEffect.vala
|
||||
src/Utils.vala
|
||||
src/Zooming.vala
|
||||
src/Background/Animation.vala
|
||||
src/Background/Background.vala
|
||||
src/Background/BackgroundCache.vala
|
||||
src/Background/BackgroundManager.vala
|
||||
|
@ -1,76 +0,0 @@
|
||||
//
|
||||
// Copyright (C) 2013 Tom Beckmann, Rico Tzschichholz
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
namespace Gala
|
||||
{
|
||||
public class Animation : Object
|
||||
{
|
||||
public string filename { get; construct set; }
|
||||
public Meta.Screen screen { get; construct set; }
|
||||
|
||||
public Gee.LinkedList<string> key_frame_files { get; private set; }
|
||||
public double transition_progress { get; private set; default = 0.0; }
|
||||
public double transition_duration { get; private set; default = 0.0; }
|
||||
public bool loaded { get; private set; default = false; }
|
||||
|
||||
Gnome.BGSlideShow? show = null;
|
||||
|
||||
public Animation (Meta.Screen screen, string filename)
|
||||
{
|
||||
Object (filename: filename, screen: screen);
|
||||
key_frame_files = new Gee.LinkedList<string> ();
|
||||
}
|
||||
|
||||
public async void load ()
|
||||
{
|
||||
show = new Gnome.BGSlideShow (filename);
|
||||
|
||||
//FIXME yield show.load_async (null);
|
||||
show.load ();
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
public void update (int monitor_index)
|
||||
{
|
||||
key_frame_files = new Gee.LinkedList<string> ();
|
||||
|
||||
if (show == null)
|
||||
return;
|
||||
|
||||
if (show.get_num_slides () < 1)
|
||||
return;
|
||||
|
||||
var monitor = screen.get_monitor_geometry (monitor_index);
|
||||
|
||||
bool is_fixed;
|
||||
string file1, file2;
|
||||
double progress, duration;
|
||||
show.get_current_slide (monitor.width, monitor.height, out progress,
|
||||
out duration, out is_fixed, out file1, out file2);
|
||||
|
||||
transition_progress = progress;
|
||||
transition_duration = duration;
|
||||
|
||||
if (file1 != null)
|
||||
key_frame_files.add (file1);
|
||||
|
||||
if (file2 != null)
|
||||
key_frame_files.add (file2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,312 +1,255 @@
|
||||
//
|
||||
// Copyright (C) 2013 Tom Beckmann, Rico Tzschichholz
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
// Code has been ported from gnome-shell's js/ui/background.js
|
||||
|
||||
namespace Gala
|
||||
{
|
||||
public class Background : Object
|
||||
/**
|
||||
* Group that holds a pattern at the very bottom and then an image showing the
|
||||
* current wallpaper above (and one more additional image for transitions).
|
||||
* It listens to changes on the provided settings object and updates accordingly.
|
||||
*/
|
||||
public class Background : Meta.BackgroundGroup
|
||||
{
|
||||
const string BACKGROUND_SCHEMA = "org.gnome.desktop.background";
|
||||
const string PRIMARY_COLOR_KEY = "primary-color";
|
||||
const string SECONDARY_COLOR_KEY = "secondary-color";
|
||||
const string COLOR_SHADING_TYPE_KEY = "color-shading-type";
|
||||
const string BACKGROUND_STYLE_KEY = "picture-options";
|
||||
const string PICTURE_OPACITY_KEY = "picture-opacity";
|
||||
const string PICTURE_URI_KEY = "picture-uri";
|
||||
Meta.BackgroundActor pattern;
|
||||
Meta.BackgroundActor? image = null;
|
||||
|
||||
const uint FADE_ANIMATION_TIME = 1000;
|
||||
const uint ANIMATION_TRANSITION_DURATION = 1500;
|
||||
|
||||
// These parameters affect how often we redraw.
|
||||
// The first is how different (percent crossfaded) the slide show
|
||||
// has to look before redrawing and the second is the minimum
|
||||
// frequency (in seconds) we're willing to wake up
|
||||
public Meta.Screen screen { get; construct set; }
|
||||
public int monitor { get; construct set; }
|
||||
public Settings settings { get; construct set; }
|
||||
|
||||
Gnome.BGSlideShow? animation = null;
|
||||
Meta.BackgroundActor? second_image = null;
|
||||
double animation_duration = 0.0;
|
||||
double animation_progress = 0.0;
|
||||
uint update_animation_timeout_id;
|
||||
const double ANIMATION_OPACITY_STEP_INCREMENT = 4.0;
|
||||
const double ANIMATION_MIN_WAKEUP_INTERVAL = 1.0;
|
||||
|
||||
public Meta.BackgroundEffects effects { get; construct set; }
|
||||
public Settings settings { get; construct set; }
|
||||
public int monitor_index { get; construct set; }
|
||||
|
||||
public Meta.BackgroundGroup actor { get; private set; }
|
||||
public bool is_loaded { get; private set; }
|
||||
|
||||
// those two are set by the BackgroundManager
|
||||
internal ulong change_signal_id = 0;
|
||||
internal ulong loaded_signal_id = 0;
|
||||
|
||||
float _brightness;
|
||||
public float brightness {
|
||||
get {
|
||||
return _brightness;
|
||||
}
|
||||
set {
|
||||
_brightness = value;
|
||||
if (pattern != null && pattern.content != null)
|
||||
(pattern.content as Meta.Background).brightness = value;
|
||||
|
||||
foreach (var image in images) {
|
||||
if (image != null && image.content != null)
|
||||
(image.content as Meta.Background).brightness = brightness;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
float _vignette_sharpness;
|
||||
public float vignette_sharpness {
|
||||
get {
|
||||
return _vignette_sharpness;
|
||||
}
|
||||
set {
|
||||
_vignette_sharpness = value;
|
||||
if (pattern != null && pattern.content != null)
|
||||
(pattern.content as Meta.Background).vignette_sharpness = value;
|
||||
|
||||
foreach (var image in images) {
|
||||
if (image != null && image.content != null)
|
||||
(image.content as Meta.Background).vignette_sharpness = vignette_sharpness;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
GDesktop.BackgroundStyle style;
|
||||
|
||||
Meta.BackgroundActor? pattern;
|
||||
BackgroundCache cache;
|
||||
|
||||
Animation animation;
|
||||
|
||||
Cancellable? cancellable = null;
|
||||
uint update_animation_timeout_id = 0;
|
||||
|
||||
Meta.BackgroundActor images[2];
|
||||
Gee.HashMap<string,ulong> file_watches;
|
||||
|
||||
string filename;
|
||||
uint num_pending_images;
|
||||
|
||||
public signal void changed ();
|
||||
public signal void loaded ();
|
||||
|
||||
public Background (int monitor_index, Meta.BackgroundEffects effects, Settings settings)
|
||||
public Background (Meta.Screen screen, int monitor, Settings settings)
|
||||
{
|
||||
Object (monitor_index: monitor_index, effects: effects, settings: settings);
|
||||
actor = new Meta.BackgroundGroup ();
|
||||
|
||||
file_watches = new Gee.HashMap<string,ulong> ();
|
||||
pattern = null;
|
||||
// contains a single image for static backgrounds and
|
||||
// two images (from and to) for slide shows
|
||||
images = { null, null };
|
||||
|
||||
brightness = 1.0f;
|
||||
vignette_sharpness = 0.2f;
|
||||
cancellable = new Cancellable ();
|
||||
is_loaded = false;
|
||||
|
||||
settings.changed.connect (() => {
|
||||
changed ();
|
||||
});
|
||||
|
||||
load ();
|
||||
|
||||
actor.destroy.connect (destroy);
|
||||
}
|
||||
|
||||
public void destroy ()
|
||||
{
|
||||
if (cancellable != null)
|
||||
cancellable.cancel ();
|
||||
|
||||
if (update_animation_timeout_id != 0) {
|
||||
Source.remove (update_animation_timeout_id);
|
||||
update_animation_timeout_id = 0;
|
||||
}
|
||||
|
||||
foreach (var key in file_watches.keys) {
|
||||
cache.disconnect (file_watches.get (key));
|
||||
}
|
||||
file_watches = null;
|
||||
|
||||
if (pattern != null) {
|
||||
if (pattern.content != null)
|
||||
cache.remove_pattern_content (pattern.content as Meta.Background);
|
||||
|
||||
pattern.destroy ();
|
||||
pattern = null;
|
||||
}
|
||||
|
||||
foreach (var image in images) {
|
||||
if (image == null)
|
||||
continue;
|
||||
|
||||
if (image.content != null)
|
||||
cache.remove_image_content (image.content as Meta.Background);
|
||||
|
||||
image.destroy ();
|
||||
}
|
||||
}
|
||||
|
||||
public void set_loaded ()
|
||||
{
|
||||
if (is_loaded)
|
||||
return;
|
||||
|
||||
is_loaded = true;
|
||||
|
||||
Idle.add (() => {
|
||||
loaded ();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
public void load_pattern ()
|
||||
{
|
||||
var color = Clutter.Color.from_string (settings.get_string (PRIMARY_COLOR_KEY));
|
||||
var second_color = Clutter.Color.from_string (settings.get_string (SECONDARY_COLOR_KEY));
|
||||
|
||||
var shading_type = (GDesktop.BackgroundShading)settings.get_enum (COLOR_SHADING_TYPE_KEY);
|
||||
|
||||
var content = cache.get_pattern_content (monitor_index, color, second_color, shading_type, effects);
|
||||
Object (screen: screen, monitor: monitor, settings: settings);
|
||||
|
||||
pattern = new Meta.BackgroundActor ();
|
||||
actor.add_child (pattern);
|
||||
pattern.add_constraint (new Clutter.BindConstraint (this, Clutter.BindCoordinate.ALL, 0));
|
||||
add_child (pattern);
|
||||
|
||||
pattern.content = content;
|
||||
load (null);
|
||||
|
||||
settings.changed.connect (load);
|
||||
}
|
||||
|
||||
public void watch_cache_file (string filename)
|
||||
/**
|
||||
* (Re)loads all components if key_changed is null or only the key_changed component
|
||||
*/
|
||||
void load (string? key_changed)
|
||||
{
|
||||
if (file_watches.has_key (filename))
|
||||
var all = key_changed == null;
|
||||
var cache = BackgroundCache.get_default ();
|
||||
|
||||
// update images
|
||||
if (all || key_changed == "picture-uri" || key_changed == "picture-options") {
|
||||
var file = File.new_for_commandline_arg (settings.get_string ("picture-uri")).get_path ();
|
||||
var style = style_string_to_enum (settings.get_string ("picture-options"));
|
||||
|
||||
// no image at all
|
||||
if (style == GDesktop.BackgroundStyle.NONE) {
|
||||
if (image != null) {
|
||||
image.destroy ();
|
||||
image = null;
|
||||
}
|
||||
if (second_image != null) {
|
||||
second_image.destroy ();
|
||||
second_image = null;
|
||||
}
|
||||
animation = null;
|
||||
// animation
|
||||
} else if (file.has_suffix (".xml")) {
|
||||
animation = new Gnome.BGSlideShow (file);
|
||||
try {
|
||||
if (animation.load ()) {
|
||||
update_animation ();
|
||||
}
|
||||
} catch (Error e) {
|
||||
warning (e.message);
|
||||
}
|
||||
// normal wallpaper
|
||||
} else {
|
||||
animation = null;
|
||||
if (second_image != null) {
|
||||
second_image.destroy ();
|
||||
second_image = null;
|
||||
}
|
||||
cache.load_image.begin (file, monitor, style, (obj, res) => {
|
||||
var content = cache.load_image.end (res);
|
||||
if (content != null) {
|
||||
set_image (content);
|
||||
// if loading failed, destroy our image and show the pattern
|
||||
} else if (image != null) {
|
||||
image.destroy ();
|
||||
image = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// update image opacity
|
||||
if (all || key_changed == "picture-opacity") {
|
||||
if (image != null)
|
||||
image.opacity = (uint8)(settings.get_int ("picture-opacity") / 100.0 * 255);
|
||||
}
|
||||
|
||||
// update pattern
|
||||
if (all
|
||||
|| key_changed == "primary-color"
|
||||
|| key_changed == "secondary-color"
|
||||
|| key_changed == "color-shading-type") {
|
||||
var primary_color = Clutter.Color.from_string (settings.get_string ("primary-color"));
|
||||
var secondary_color = Clutter.Color.from_string (settings.get_string ("secondary-color"));
|
||||
var shading_type = shading_string_to_enum (settings.get_string ("color-shading-type"));
|
||||
pattern.content = cache.load_pattern (monitor, primary_color, secondary_color, shading_type);
|
||||
}
|
||||
}
|
||||
|
||||
void set_image (Meta.Background content)
|
||||
{
|
||||
var new_image = new Meta.BackgroundActor ();
|
||||
new_image.add_constraint (new Clutter.BindConstraint (this, Clutter.BindCoordinate.ALL, 0));
|
||||
new_image.content = content;
|
||||
new_image.opacity = 0;
|
||||
|
||||
insert_child_above (new_image, null);
|
||||
|
||||
var dest_opacity = (uint8)(settings.get_int ("picture-opacity") / 100.0 * 255);
|
||||
new_image.animate (Clutter.AnimationMode.EASE_OUT_QUAD, ANIMATION_TRANSITION_DURATION,
|
||||
opacity: dest_opacity).completed.connect (() => {
|
||||
if (image != null)
|
||||
image.destroy ();
|
||||
image = new_image;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* translates the string returned from gsettings for the color-shading-type key to the
|
||||
* appropriate GDesktop.BackgroundShading enum value
|
||||
*/
|
||||
GDesktop.BackgroundShading shading_string_to_enum (string shading)
|
||||
{
|
||||
switch (shading) {
|
||||
case "horizontal":
|
||||
return GDesktop.BackgroundShading.HORIZONTAL;
|
||||
case "vertical":
|
||||
return GDesktop.BackgroundShading.VERTICAL;
|
||||
}
|
||||
|
||||
return GDesktop.BackgroundShading.SOLID;
|
||||
}
|
||||
|
||||
/**
|
||||
* translates the string returned from gsettings for the picture-options key to the
|
||||
* appropriate GDesktop.BackgroundStyle enum value
|
||||
*/
|
||||
GDesktop.BackgroundStyle style_string_to_enum (string style)
|
||||
{
|
||||
switch (style) {
|
||||
case "wallpaper":
|
||||
return GDesktop.BackgroundStyle.WALLPAPER;
|
||||
case "centered":
|
||||
return GDesktop.BackgroundStyle.CENTERED;
|
||||
case "scaled":
|
||||
return GDesktop.BackgroundStyle.SCALED;
|
||||
case "stretched":
|
||||
return GDesktop.BackgroundStyle.STRETCHED;
|
||||
case "zoom":
|
||||
return GDesktop.BackgroundStyle.ZOOM;
|
||||
case "spanned":
|
||||
return GDesktop.BackgroundStyle.SPANNED;
|
||||
}
|
||||
|
||||
return GDesktop.BackgroundStyle.NONE;
|
||||
}
|
||||
|
||||
/**
|
||||
* SlideShow animation related functions
|
||||
*/
|
||||
void update_animation ()
|
||||
{
|
||||
if (animation == null)
|
||||
return;
|
||||
|
||||
var signal_id = cache.file_changed.connect ((changed_file) => {
|
||||
if (changed_file == filename) {
|
||||
changed ();
|
||||
}
|
||||
});
|
||||
|
||||
file_watches.set (filename, signal_id);
|
||||
}
|
||||
|
||||
public void add_image (Meta.Background content, int index, string filename) {
|
||||
content.brightness = brightness;
|
||||
content.vignette_sharpness = vignette_sharpness;
|
||||
|
||||
var actor = new Meta.BackgroundActor ();
|
||||
actor.content = content;
|
||||
|
||||
// The background pattern is the first actor in
|
||||
// the group, and all images should be above that.
|
||||
this.actor.insert_child_at_index (actor, index + 1);
|
||||
|
||||
images[index] = actor;
|
||||
watch_cache_file (filename);
|
||||
}
|
||||
|
||||
public void update_image (Meta.Background content, int index, string filename) {
|
||||
content.brightness = brightness;
|
||||
content.vignette_sharpness = vignette_sharpness;
|
||||
|
||||
cache.remove_image_content (images[index].content as Meta.Background);
|
||||
images[index].content = content;
|
||||
watch_cache_file (filename);
|
||||
}
|
||||
|
||||
public void update_animation_progress ()
|
||||
{
|
||||
if (images[1] != null)
|
||||
images[1].opacity = (uint)(animation.transition_progress * 255);
|
||||
|
||||
queue_update_animation();
|
||||
}
|
||||
|
||||
public void update_animation ()
|
||||
{
|
||||
update_animation_timeout_id = 0;
|
||||
|
||||
animation.update (monitor_index);
|
||||
var files = animation.key_frame_files;
|
||||
var geom = screen.get_monitor_geometry (monitor);
|
||||
|
||||
if (files.size == 0) {
|
||||
set_loaded ();
|
||||
bool is_fixed;
|
||||
string file_from, file_to;
|
||||
double progress, duration;
|
||||
animation.get_current_slide (geom.width, geom.height, out progress,
|
||||
out duration, out is_fixed, out file_from, out file_to);
|
||||
|
||||
animation_duration = duration;
|
||||
animation_progress = progress;
|
||||
|
||||
print ("Animation Update\nfrom: %s to: %s, progress: %f, duration: %f\n", file_from, file_to, progress, duration);
|
||||
if (file_from == null && file_to == null) {
|
||||
queue_update_animation ();
|
||||
return;
|
||||
}
|
||||
|
||||
num_pending_images = files.size;
|
||||
for (var i = 0; i < files.size; i++) {
|
||||
var image = images[i];
|
||||
if (image != null && image.content != null &&
|
||||
(image.content as Meta.Background).get_filename () == files.get (i)) {
|
||||
|
||||
num_pending_images--;
|
||||
if (num_pending_images == 0)
|
||||
update_animation_progress ();
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
cache.get_image_content (monitor_index, style, files[i], effects,
|
||||
this, get_update_animation_callback (i), cancellable);
|
||||
if (image == null || image.content == null
|
||||
|| (image.content as Meta.Background).get_filename () != file_from) {
|
||||
image = update_image (image, file_from, false);
|
||||
}
|
||||
if (second_image == null || second_image.content == null
|
||||
|| (second_image.content as Meta.Background).get_filename () != file_to) {
|
||||
second_image = update_image (second_image, file_to, true);
|
||||
}
|
||||
|
||||
update_animation_progress ();
|
||||
}
|
||||
|
||||
// FIXME wrap callback method to keep the i at the correct value, I suppose
|
||||
// we should find a nicer way to do this
|
||||
PendingFileLoadFinished get_update_animation_callback (int i) {
|
||||
return (userdata, content) => {
|
||||
var self = userdata as Background;
|
||||
self.num_pending_images--;
|
||||
|
||||
if (content == null) {
|
||||
self.set_loaded ();
|
||||
if (self.num_pending_images == 0)
|
||||
self.update_animation_progress ();
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.images[i] == null) {
|
||||
self.add_image (content, i, self.animation.key_frame_files.get (i));
|
||||
} else {
|
||||
self.update_image (content, i, self.animation.key_frame_files.get (i));
|
||||
}
|
||||
|
||||
if (self.num_pending_images == 0) {
|
||||
self.set_loaded ();
|
||||
self.update_animation_progress ();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public void queue_update_animation ()
|
||||
/**
|
||||
* Returns the passed orig_image with the correct content or a new one if orig_image was null
|
||||
*/
|
||||
Meta.BackgroundActor? update_image (Meta.BackgroundActor? orig_image, string? file, bool topmost)
|
||||
{
|
||||
if (update_animation_timeout_id != 0)
|
||||
return;
|
||||
Meta.BackgroundActor image = null;
|
||||
|
||||
if (cancellable == null || cancellable.is_cancelled ())
|
||||
return;
|
||||
if (orig_image != null)
|
||||
image = orig_image;
|
||||
|
||||
if (animation.transition_duration == 0.0)
|
||||
if (file == null) {
|
||||
if (image != null) {
|
||||
image.destroy ();
|
||||
image = null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (image == null) {
|
||||
image = new Meta.BackgroundActor ();
|
||||
image.add_constraint (new Clutter.BindConstraint (this, Clutter.BindCoordinate.ALL, 0));
|
||||
if (topmost)
|
||||
insert_child_above (image, null);
|
||||
else
|
||||
insert_child_above (image, pattern);
|
||||
}
|
||||
|
||||
var cache = BackgroundCache.get_default ();
|
||||
var style = style_string_to_enum (settings.get_string ("picture-options"));
|
||||
cache.load_image.begin (file, monitor, style, (obj, res) => {
|
||||
image.content = cache.load_image.end (res);
|
||||
});
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
void queue_update_animation ()
|
||||
{
|
||||
if (update_animation_timeout_id != 0 || animation_duration == 0.0)
|
||||
return;
|
||||
|
||||
var n_steps = 255 / ANIMATION_OPACITY_STEP_INCREMENT;
|
||||
var time_per_step = (uint)((animation.transition_duration * 1000) / n_steps);
|
||||
|
||||
var time_per_step = (uint)((animation_duration * 1000) / n_steps);
|
||||
var interval = uint.max ((uint)(ANIMATION_MIN_WAKEUP_INTERVAL * 1000), time_per_step);
|
||||
|
||||
if (interval > uint.MAX)
|
||||
@ -319,62 +262,12 @@ namespace Gala
|
||||
});
|
||||
}
|
||||
|
||||
public void load_animation (string filename)
|
||||
void update_animation_progress ()
|
||||
{
|
||||
cache.get_animation.begin (filename, (obj, res) => {
|
||||
animation = cache.get_animation.end (res);
|
||||
if (second_image != null)
|
||||
second_image.opacity = (uint)(animation_progress * 255);
|
||||
|
||||
if (animation == null || cancellable.is_cancelled ()) {
|
||||
set_loaded ();
|
||||
return;
|
||||
}
|
||||
|
||||
update_animation ();
|
||||
watch_cache_file (filename);
|
||||
});
|
||||
}
|
||||
|
||||
public void load_file (string filename)
|
||||
{
|
||||
this.filename = filename;
|
||||
cache.get_image_content (monitor_index, style, filename, effects, this, (userdata, content) => {
|
||||
var self = userdata as Background;
|
||||
if (content == null) {
|
||||
if (!self.cancellable.is_cancelled ())
|
||||
self.load_animation (self.filename);
|
||||
return;
|
||||
}
|
||||
|
||||
self.add_image (content, 0, self.filename);
|
||||
self.set_loaded ();
|
||||
}, cancellable);
|
||||
}
|
||||
|
||||
public void load ()
|
||||
{
|
||||
cache = BackgroundCache.get_default ();
|
||||
|
||||
load_pattern ();
|
||||
|
||||
style = (GDesktop.BackgroundStyle)settings.get_enum (BACKGROUND_STYLE_KEY);
|
||||
if (style == GDesktop.BackgroundStyle.NONE) {
|
||||
set_loaded ();
|
||||
return;
|
||||
}
|
||||
|
||||
var uri = settings.get_string (PICTURE_URI_KEY);
|
||||
string filename;
|
||||
if (Uri.parse_scheme (uri) != null)
|
||||
filename = File.new_for_uri (uri).get_path ();
|
||||
else
|
||||
filename = uri;
|
||||
|
||||
if (filename == null) {
|
||||
set_loaded ();
|
||||
return;
|
||||
}
|
||||
|
||||
load_file (filename);
|
||||
queue_update_animation ();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,281 +1,101 @@
|
||||
//
|
||||
// Copyright (C) 2013 Tom Beckmann, Rico Tzschichholz
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
/* TODO
|
||||
- GNOME monitors the images and removes them from cache when they change
|
||||
so they reload. Do we need that?
|
||||
*/
|
||||
|
||||
namespace Gala
|
||||
{
|
||||
public delegate void PendingFileLoadFinished (Object userdata, Meta.Background? content);
|
||||
|
||||
struct PendingFileLoad
|
||||
{
|
||||
string filename;
|
||||
GDesktop.BackgroundStyle style;
|
||||
Gee.LinkedList<PendingFileLoadCaller?> callers;
|
||||
}
|
||||
|
||||
struct PendingFileLoadCaller
|
||||
{
|
||||
bool should_copy;
|
||||
int monitor_index;
|
||||
Meta.BackgroundEffects effects;
|
||||
PendingFileLoadFinished on_finished;
|
||||
Object userdata;
|
||||
}
|
||||
|
||||
public class BackgroundCache : Object
|
||||
{
|
||||
static BackgroundCache? instance = null;
|
||||
public Meta.Screen screen { get; construct set; }
|
||||
|
||||
Meta.Screen screen;
|
||||
|
||||
Gee.LinkedList<Meta.Background> patterns;
|
||||
Gee.LinkedList<Meta.Background> images;
|
||||
Gee.LinkedList<PendingFileLoad?> pending_file_loads;
|
||||
Gee.HashMap<string,FileMonitor> file_monitors;
|
||||
|
||||
string animation_filename;
|
||||
Animation animation;
|
||||
|
||||
public signal void file_changed (string filename);
|
||||
|
||||
public BackgroundCache (Meta.Screen _screen)
|
||||
struct WaitingCallback
|
||||
{
|
||||
screen = _screen;
|
||||
|
||||
patterns = new Gee.LinkedList<Meta.Background> ();
|
||||
images = new Gee.LinkedList<Meta.Background> ();
|
||||
pending_file_loads = new Gee.LinkedList<PendingFileLoad?> ();
|
||||
file_monitors = new Gee.HashMap<string,FileMonitor> ();
|
||||
SourceFunc func;
|
||||
string hash;
|
||||
}
|
||||
|
||||
public Meta.Background get_pattern_content (int monitor_index, Clutter.Color color,
|
||||
Clutter.Color second_color, GDesktop.BackgroundShading shading_type, Meta.BackgroundEffects effects)
|
||||
Gee.HashMap<string,Meta.Background> image_cache;
|
||||
Gee.HashMap<string,Meta.Background> pattern_cache;
|
||||
Gee.LinkedList<WaitingCallback?> waiting_callbacks;
|
||||
|
||||
BackgroundCache (Meta.Screen screen)
|
||||
{
|
||||
Meta.Background? content = null, candidate_content = null;
|
||||
Object (screen: screen);
|
||||
|
||||
foreach (var pattern in patterns) {
|
||||
if (pattern == null)
|
||||
continue;
|
||||
image_cache = new Gee.HashMap<string,Meta.Background> ();
|
||||
pattern_cache = new Gee.HashMap<string,Meta.Background> ();
|
||||
waiting_callbacks = new Gee.LinkedList<WaitingCallback?> ();
|
||||
}
|
||||
|
||||
if (pattern.get_shading() != shading_type)
|
||||
continue;
|
||||
public async Meta.Background? load_image (string file, int monitor,
|
||||
GDesktop.BackgroundStyle style)
|
||||
{
|
||||
string hash = file + "#" + ((int)style).to_string ();
|
||||
Meta.Background? content = image_cache.get (hash);
|
||||
|
||||
if (color.equal(pattern.get_color ()))
|
||||
continue;
|
||||
if (content != null) {
|
||||
/*FIXME apparently we can just copy the content at any point
|
||||
print ("FILENAME: %s\n", content.get_filename ());
|
||||
// the content has been created, but the file is still loading, so we wait
|
||||
if (content.get_filename () == null) {
|
||||
waiting_callbacks.add ({ load_image.callback, hash });
|
||||
yield;
|
||||
}*/
|
||||
|
||||
if (shading_type != GDesktop.BackgroundShading.SOLID &&
|
||||
!second_color.equal(pattern.get_second_color ()))
|
||||
continue;
|
||||
|
||||
candidate_content = pattern;
|
||||
|
||||
if (effects != pattern.effects)
|
||||
continue;
|
||||
|
||||
break;
|
||||
return content.copy (monitor, Meta.BackgroundEffects.NONE);
|
||||
}
|
||||
|
||||
if (candidate_content != null) {
|
||||
content = candidate_content.copy (monitor_index, effects);
|
||||
} else {
|
||||
content = new Meta.Background (screen, monitor_index, effects);
|
||||
content = new Meta.Background (screen, monitor, Meta.BackgroundEffects.NONE);
|
||||
|
||||
if (shading_type == GDesktop.BackgroundShading.SOLID) {
|
||||
content.load_color (color);
|
||||
} else {
|
||||
content.load_gradient (shading_type, color, second_color);
|
||||
try {
|
||||
yield content.load_file_async (file, style, null);
|
||||
} catch (Error e) {
|
||||
warning (e.message);
|
||||
return null;
|
||||
}
|
||||
|
||||
image_cache.set (hash, content);
|
||||
foreach (var callback in waiting_callbacks) {
|
||||
if (callback.hash == hash) {
|
||||
callback.func ();
|
||||
waiting_callbacks.remove (callback);
|
||||
}
|
||||
}
|
||||
|
||||
patterns.add (content);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
public void monitor_file (string filename)
|
||||
public Meta.Background load_pattern (int monitor, Clutter.Color primary, Clutter.Color secondary,
|
||||
GDesktop.BackgroundShading shading_type)
|
||||
{
|
||||
if (file_monitors.has_key (filename))
|
||||
return;
|
||||
string hash = primary.to_string () + secondary.to_string () +
|
||||
((int)shading_type).to_string ();
|
||||
Meta.Background? content = pattern_cache.get (hash);
|
||||
|
||||
var file = File.new_for_path (filename);
|
||||
try {
|
||||
var monitor = file.monitor (FileMonitorFlags.NONE);
|
||||
if (content != null)
|
||||
return content.copy (monitor, Meta.BackgroundEffects.NONE);
|
||||
|
||||
//TODO maybe do this in a cleaner way
|
||||
ulong signal_id = 0;
|
||||
signal_id = monitor.changed.connect (() => {
|
||||
foreach (var image in images) {
|
||||
if (image.get_filename () == filename)
|
||||
images.remove (image);
|
||||
}
|
||||
content = new Meta.Background (screen, monitor, Meta.BackgroundEffects.NONE);
|
||||
if (shading_type == GDesktop.BackgroundShading.SOLID)
|
||||
content.load_color (primary);
|
||||
else
|
||||
content.load_gradient (shading_type, primary, secondary);
|
||||
|
||||
monitor.disconnect (signal_id);
|
||||
pattern_cache.set (hash, content);
|
||||
|
||||
file_changed (filename);
|
||||
});
|
||||
|
||||
file_monitors.set (filename, monitor);
|
||||
} catch (Error e) { warning (e.message); }
|
||||
return content;
|
||||
}
|
||||
|
||||
public void remove_content (Gee.LinkedList<Meta.Background> content_list, Meta.Background content) {
|
||||
content_list.remove (content);
|
||||
}
|
||||
static BackgroundCache? instance = null;
|
||||
|
||||
public void remove_pattern_content (Meta.Background content) {
|
||||
remove_content (patterns, content);
|
||||
}
|
||||
|
||||
public void remove_image_content (Meta.Background content) {
|
||||
var filename = content.get_filename();
|
||||
|
||||
if (filename != null && file_monitors.has_key (filename))
|
||||
//TODO disconnect filemonitor and delete it properly
|
||||
file_monitors.unset (filename);
|
||||
|
||||
remove_content(images, content);
|
||||
}
|
||||
|
||||
//FIXME as we may have to get a number of callbacks fired when this finishes,
|
||||
// we can't use vala's async system, but use a callback based system instead
|
||||
public void load_image_content (int monitor_index,
|
||||
GDesktop.BackgroundStyle style, string filename, Meta.BackgroundEffects effects,
|
||||
Object userdata, PendingFileLoadFinished on_finished, Cancellable? cancellable = null)
|
||||
{
|
||||
foreach (var pending_file_load in pending_file_loads) {
|
||||
if (pending_file_load.filename == filename &&
|
||||
pending_file_load.style == style) {
|
||||
pending_file_load.callers.add ({true, monitor_index, effects, on_finished, userdata});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
PendingFileLoad load = {filename, style, new Gee.LinkedList<PendingFileLoadCaller?> ()};
|
||||
load.callers.add ({false, monitor_index, effects, on_finished, userdata});
|
||||
pending_file_loads.add (load);
|
||||
|
||||
var content = new Meta.Background (screen, monitor_index, effects);
|
||||
content.load_file_async.begin (filename, style, cancellable, (obj, res) => {
|
||||
try {
|
||||
content.load_file_async.end (res);
|
||||
|
||||
monitor_file (filename);
|
||||
images.add (content);
|
||||
} catch (Error e) {
|
||||
content = null;
|
||||
}
|
||||
|
||||
foreach (var pending_load in pending_file_loads) {
|
||||
if (pending_load.filename != filename ||
|
||||
pending_load.style != style)
|
||||
continue;
|
||||
|
||||
foreach (var caller in pending_load.callers) {
|
||||
if (caller.on_finished != null) {
|
||||
if (content != null && caller.should_copy) {
|
||||
content = (obj as Meta.Background).copy (caller.monitor_index, caller.effects);
|
||||
}
|
||||
|
||||
caller.on_finished (caller.userdata, content);
|
||||
}
|
||||
}
|
||||
|
||||
pending_file_loads.remove (pending_load);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void get_image_content (int monitor_index, GDesktop.BackgroundStyle style,
|
||||
string filename, Meta.BackgroundEffects effects, Object userdata,
|
||||
PendingFileLoadFinished on_finished, Cancellable? cancellable = null)
|
||||
{
|
||||
Meta.Background content = null, candidate_content = null;
|
||||
foreach (var image in images) {
|
||||
if (image == null)
|
||||
continue;
|
||||
|
||||
if (image.get_style () != style)
|
||||
continue;
|
||||
|
||||
if (image.get_filename () != filename)
|
||||
continue;
|
||||
|
||||
if (style == GDesktop.BackgroundStyle.SPANNED &&
|
||||
image.monitor != monitor_index)
|
||||
continue;
|
||||
|
||||
candidate_content = image;
|
||||
|
||||
if (effects != image.effects)
|
||||
continue;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (candidate_content != null) {
|
||||
content = candidate_content.copy (monitor_index, effects);
|
||||
|
||||
if (cancellable != null && cancellable.is_cancelled ())
|
||||
content = null;
|
||||
else
|
||||
images.add (content);
|
||||
|
||||
on_finished (userdata, content);
|
||||
} else {
|
||||
load_image_content (monitor_index, style, filename, effects, userdata, on_finished, cancellable);
|
||||
}
|
||||
}
|
||||
|
||||
public async Animation get_animation (string filename)
|
||||
{
|
||||
Animation animation;
|
||||
|
||||
if (animation_filename == filename) {
|
||||
animation = this.animation;
|
||||
|
||||
//FIXME do we need those Idles?
|
||||
Idle.add (() => {
|
||||
get_animation.callback ();
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
animation = new Animation (screen, filename);
|
||||
|
||||
yield animation.load ();
|
||||
|
||||
monitor_file (filename);
|
||||
animation_filename = filename;
|
||||
this.animation = animation;
|
||||
|
||||
Idle.add (() => {
|
||||
get_animation.callback ();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
yield;
|
||||
return animation;
|
||||
}
|
||||
|
||||
public static void init (Meta.Screen screen)
|
||||
{
|
||||
instance = new BackgroundCache (screen);
|
||||
}
|
||||
|
||||
public static BackgroundCache get_default ()
|
||||
requires (instance != null)
|
||||
{
|
||||
return instance;
|
||||
}
|
||||
|
@ -1,120 +1,33 @@
|
||||
//
|
||||
// Copyright (C) 2013 Tom Beckmann, Rico Tzschichholz
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
namespace Gala
|
||||
{
|
||||
public class BackgroundManager : Object
|
||||
public class BackgroundManager : Meta.BackgroundGroup
|
||||
{
|
||||
const string BACKGROUND_SCHEMA = "org.gnome.desktop.background";
|
||||
const uint FADE_ANIMATION_TIME = 1000;
|
||||
|
||||
public Meta.BackgroundEffects effects { get; construct set; }
|
||||
public int monitor_index { get; construct set; }
|
||||
public bool control_position { get; construct set; }
|
||||
|
||||
public Settings settings { get; construct set; }
|
||||
public Meta.Screen screen { get; construct set; }
|
||||
|
||||
public Clutter.Actor container { get; construct set; }
|
||||
public Background background { get; private set; }
|
||||
Background? new_background = null;
|
||||
|
||||
public signal void changed ();
|
||||
|
||||
public BackgroundManager (Meta.Screen screen, Clutter.Actor container, int monitor_index,
|
||||
Meta.BackgroundEffects effects, bool control_position, string settings_schema = BACKGROUND_SCHEMA)
|
||||
public BackgroundManager (Meta.Screen screen)
|
||||
{
|
||||
Object (settings: new Settings (settings_schema),
|
||||
container: container,
|
||||
effects: effects,
|
||||
monitor_index: monitor_index,
|
||||
screen: screen,
|
||||
control_position: control_position);
|
||||
Object (screen: screen);
|
||||
|
||||
background = create_background ();
|
||||
update ();
|
||||
screen.monitors_changed.connect (update);
|
||||
}
|
||||
|
||||
public void destroy ()
|
||||
void update ()
|
||||
{
|
||||
if (new_background != null) {
|
||||
new_background.actor.destroy();
|
||||
new_background = null;
|
||||
remove_all_children ();
|
||||
|
||||
var settings = BackgroundSettings.get_default ().schema;
|
||||
|
||||
for (var i = 0; i < screen.get_n_monitors (); i++) {
|
||||
var geom = screen.get_monitor_geometry (i);
|
||||
var background = new Background (screen, i, settings);
|
||||
|
||||
background.set_position (geom.x, geom.y);
|
||||
background.set_size (geom.width, geom.height);
|
||||
|
||||
add_child (background);
|
||||
}
|
||||
|
||||
if (background != null) {
|
||||
background.actor.destroy();
|
||||
background = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void update_background (Background background, int monitor_index) {
|
||||
var new_background = create_background ();
|
||||
new_background.vignette_sharpness = background.vignette_sharpness;
|
||||
new_background.brightness = background.brightness;
|
||||
new_background.actor.visible = background.actor.visible;
|
||||
|
||||
new_background.loaded_signal_id = new_background.loaded.connect (() => {
|
||||
new_background.disconnect (new_background.loaded_signal_id);
|
||||
new_background.loaded_signal_id = 0;
|
||||
background.actor.animate(Clutter.AnimationMode.EASE_OUT_QUAD, FADE_ANIMATION_TIME,
|
||||
opacity : 0).completed.connect (() => {
|
||||
if (this.new_background == new_background) {
|
||||
this.background = new_background;
|
||||
this.new_background = null;
|
||||
} else {
|
||||
new_background.actor.destroy ();
|
||||
}
|
||||
|
||||
background.actor.destroy ();
|
||||
|
||||
changed ();
|
||||
});
|
||||
});
|
||||
|
||||
this.new_background = new_background;
|
||||
}
|
||||
|
||||
public Background create_background ()
|
||||
{
|
||||
var background = new Background (monitor_index, effects, settings);
|
||||
container.add_child (background.actor);
|
||||
|
||||
var monitor = screen.get_monitor_geometry (monitor_index);
|
||||
background.actor.set_size(monitor.width, monitor.height);
|
||||
if (control_position) {
|
||||
background.actor.set_position (monitor.x, monitor.y);
|
||||
background.actor.lower_bottom ();
|
||||
}
|
||||
|
||||
background.change_signal_id = background.changed.connect (() => {
|
||||
background.disconnect (background.change_signal_id);
|
||||
update_background (background, monitor_index);
|
||||
background.change_signal_id = 0;
|
||||
});
|
||||
|
||||
background.actor.destroy.connect (() => {
|
||||
if (background.change_signal_id != 0)
|
||||
background.disconnect (background.change_signal_id);
|
||||
|
||||
if (background.loaded_signal_id != 0)
|
||||
background.disconnect (background.loaded_signal_id);
|
||||
});
|
||||
|
||||
return background;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,14 @@
|
||||
//
|
||||
// Copyright (C) 2013 Tom Beckmann, Rico Tzschichholz
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
namespace Gala
|
||||
{
|
||||
public class SystemBackground : Object
|
||||
public class SystemBackground : Meta.BackgroundActor
|
||||
{
|
||||
public Meta.BackgroundActor actor { get; private set; }
|
||||
|
||||
public signal void loaded ();
|
||||
|
||||
public SystemBackground ()
|
||||
{
|
||||
actor = new Meta.BackgroundActor ();
|
||||
|
||||
BackgroundCache.get_default ().get_image_content (0, GDesktop.BackgroundStyle.WALLPAPER,
|
||||
Config.PKGDATADIR + "/texture.png", Meta.BackgroundEffects.NONE, this, (userdata, content) => {
|
||||
var self = userdata as SystemBackground;
|
||||
self.actor.content = content;
|
||||
self.loaded ();
|
||||
var cache = BackgroundCache.get_default ();
|
||||
cache.load_image.begin (Config.PKGDATADIR + "/texture.png", 0,
|
||||
GDesktop.BackgroundStyle.WALLPAPER, (obj, res) => {
|
||||
content = cache.load_image.end (res);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -121,9 +121,9 @@ namespace Gala
|
||||
*/
|
||||
|
||||
var system_background = new SystemBackground ();
|
||||
system_background.actor.add_constraint (new Clutter.BindConstraint (stage,
|
||||
system_background.add_constraint (new Clutter.BindConstraint (stage,
|
||||
Clutter.BindCoordinate.ALL, 0));
|
||||
stage.insert_child_below (system_background.actor, null);
|
||||
stage.insert_child_below (system_background, null);
|
||||
|
||||
ui_group = new Clutter.Actor ();
|
||||
ui_group.reactive = true;
|
||||
@ -133,12 +133,9 @@ namespace Gala
|
||||
stage.remove_child (window_group);
|
||||
ui_group.add_child (window_group);
|
||||
|
||||
background_group = new Meta.BackgroundGroup ();
|
||||
background_group = new BackgroundManager (screen);
|
||||
window_group.add_child (background_group);
|
||||
window_group.set_child_below_sibling (background_group, null);
|
||||
|
||||
setup_background_managers ();
|
||||
screen.monitors_changed.connect (setup_background_managers);
|
||||
#endif
|
||||
|
||||
workspace_view = new WorkspaceView (this);
|
||||
@ -327,17 +324,6 @@ namespace Gala
|
||||
Utils.set_input_area (screen, InputArea.NONE);
|
||||
}
|
||||
|
||||
void setup_background_managers ()
|
||||
{
|
||||
background_group.destroy_all_children ();
|
||||
|
||||
var screen = get_screen ();
|
||||
for (var i = 0; i < screen.get_n_monitors (); i++) {
|
||||
new BackgroundManager (screen, background_group, i,
|
||||
Meta.BackgroundEffects.NONE, true);
|
||||
}
|
||||
}
|
||||
|
||||
public uint32[] get_all_xids ()
|
||||
{
|
||||
var list = new Gee.ArrayList<uint32> ();
|
||||
|
@ -95,8 +95,8 @@ namespace Gala
|
||||
|
||||
// FIXME find a nice way to draw a border around it, maybe combinable with the indicator using a ShaderEffect
|
||||
#if HAS_MUTTER38
|
||||
//FIXME we probably want to keep the BackgroundManager and not just save its actor
|
||||
wallpaper = new BackgroundManager (screen, this, 0, Meta.BackgroundEffects.NONE, false).background.actor;
|
||||
wallpaper = new BackgroundManager (screen);
|
||||
wallpaper.background_color = { 255, 0, 0, 255 };
|
||||
#else
|
||||
wallpaper = new Clone (Compositor.get_background_actor_for_screen (screen));
|
||||
#endif
|
||||
@ -126,9 +126,7 @@ namespace Gala
|
||||
windows.clip_to_allocation = true;
|
||||
|
||||
add_child (indicator);
|
||||
#if !HAS_MUTTER38
|
||||
add_child (wallpaper);
|
||||
#endif
|
||||
add_child (windows);
|
||||
add_child (icons);
|
||||
add_child (close_button);
|
||||
|
Loading…
Reference in New Issue
Block a user