diff --git a/meson.build b/meson.build index 584f41e..3108943 100644 --- a/meson.build +++ b/meson.build @@ -411,6 +411,8 @@ singularity_core_sources = files( 'src/core/wallpaper_rotation_state.vala', 'src/core/wallpaper_rotator.vala', 'src/core/artist_pack_manager.vala', + 'src/core/wallpaper_history.vala', + 'src/core/wallpaper_favorites.vala', 'src/core/wallpaper_sidecar.vala', 'src/core/wallpaper_browse_cache.vala', 'src/core/wallpaper_thumbnail_cache.vala', @@ -923,6 +925,31 @@ wallpaper_rotation_state_test = executable('wallpaper-rotation-state-test', ) test('wallpaper-rotation-state', wallpaper_rotation_state_test) +glib_compile_schemas = find_program('glib-compile-schemas') +wallpaper_test_schema = custom_target('wallpaper-test-schema', + input: 'tests/dev.sinty.desktop.gschema.xml', + output: 'gschemas.compiled', + command: [glib_compile_schemas, '--strict', '--targetdir', meson.current_build_dir(), + join_paths(meson.current_source_dir(), 'tests')], + build_by_default: true, +) +wallpaper_test_env = environment() +wallpaper_test_env.set('GSETTINGS_SCHEMA_DIR', meson.current_build_dir()) + +wallpaper_history_test = executable('wallpaper-history-test', + sources: ['src/core/wallpaper_history.vala', 'tests/wallpaper_history_test.vala'], + dependencies: [dependency('gobject-2.0'), dependency('gio-2.0'), dependency('gio-unix-2.0'), gee_dep], +) +test('wallpaper-history', wallpaper_history_test) + +wallpaper_favorites_test = executable('wallpaper-favorites-test', + sources: ['src/core/wallpaper_favorites.vala', 'tests/wallpaper_favorites_test.vala'], + dependencies: [dependency('gobject-2.0'), dependency('gio-2.0'), dependency('gio-unix-2.0'), gee_dep], +) +test('wallpaper-favorites', wallpaper_favorites_test, + depends: wallpaper_test_schema, + env: wallpaper_test_env) + wallpaper_gallery_test = executable('wallpaper-gallery-test', sources: ['src/core/wallpaper_gallery.vala', 'src/core/dynamic_wallpaper.vala', 'tests/wallpaper_gallery_test.vala'], dependencies: [dependency('gobject-2.0'), dependency('gio-2.0'), gee_dep, json_dep, cc.find_library('m')], diff --git a/src/components/panel/panel.vala b/src/components/panel/panel.vala index 43ace97..cc9efa5 100644 --- a/src/components/panel/panel.vala +++ b/src/components/panel/panel.vala @@ -5,6 +5,110 @@ using Singularity.Widgets; namespace Singularity { + private class WallpaperIndicator : Gtk.Box { + private GLib.Settings settings; + private WallpaperHistory history; + private WallpaperFavorites favorites; + private WallpaperRotationState rotation_state; + private Button previous_button; + private Button next_button; + private Button pause_button; + private Button favorite_button; + private string? navigation_target = null; + + public WallpaperIndicator(GLib.Settings settings) { + Object(orientation: Orientation.HORIZONTAL, spacing: 0); + valign = Align.CENTER; + this.settings = settings; + history = new WallpaperHistory(Path.build_filename( + WallpaperRotationState.default_config_dir(), "history.ini")); + favorites = new WallpaperFavorites(settings); + rotation_state = new WallpaperRotationState( + WallpaperRotationState.default_config_dir()); + + var button = new MenuButton(); + button.add_css_class("flat"); + button.tooltip_text = _("Wallpaper controls"); + var icon = new Image.from_icon_name("preferences-desktop-wallpaper-symbolic"); + icon.pixel_size = 16; + button.child = icon; + append(button); + + var controls = new Box(Orientation.HORIZONTAL, 6); + controls.margin_top = 10; + controls.margin_bottom = 10; + controls.margin_start = 12; + controls.margin_end = 12; + + previous_button = new Button.from_icon_name("go-previous-symbolic"); + previous_button.tooltip_text = _("Previous wallpaper"); + previous_button.clicked.connect(() => navigate(history.go_back())); + controls.append(previous_button); + + next_button = new Button.from_icon_name("go-next-symbolic"); + next_button.tooltip_text = _("Next wallpaper"); + next_button.clicked.connect(() => navigate(history.go_forward())); + controls.append(next_button); + + pause_button = new Button(); + pause_button.clicked.connect(() => { + rotation_state.set_rotate_enabled(!rotation_state.get_rotate_enabled()); + update_controls(); + }); + controls.append(pause_button); + + favorite_button = new Button(); + favorite_button.clicked.connect(() => { + string? path = WallpaperManager.get_default().wallpaper_path; + if (path != null) favorites.toggle_favorite(path); + }); + controls.append(favorite_button); + + var popover = new Popover(); + popover.child = controls; + button.popover = popover; + popover.notify["visible"].connect(() => { + if (popover.visible) update_controls(); + }); + + var manager = WallpaperManager.get_default(); + manager.wallpaper_path_changed.connect((path) => { + if (navigation_target == path) navigation_target = null; + else history.record(path); + update_controls(); + }); + favorites.favorites_changed.connect(update_controls); + if (manager.wallpaper_path != null && history.current_path != manager.wallpaper_path) + history.record(manager.wallpaper_path); + update_controls(); + } + + private void navigate(string? path) { + if (path == null) return; + navigation_target = path; + settings.delay(); + SettingsSafety.set_string(settings, "background-picture-uri", + File.new_for_path(path).get_uri()); + SettingsSafety.set_string(settings, "background-attribution-title", ""); + SettingsSafety.set_string(settings, "background-attribution-author", ""); + settings.apply(); + update_controls(); + } + + private void update_controls() { + previous_button.sensitive = history.can_go_back(); + next_button.sensitive = history.can_go_forward(); + bool rotating = rotation_state.get_rotate_enabled(); + pause_button.icon_name = rotating ? "media-playback-pause-symbolic" : "media-playback-start-symbolic"; + pause_button.tooltip_text = rotating ? _("Pause wallpaper rotation") : _("Resume wallpaper rotation"); + string? path = WallpaperManager.get_default().wallpaper_path; + bool is_favorite = path != null && favorites.is_favorite(path); + favorite_button.icon_name = is_favorite ? "starred-symbolic" : "non-starred-symbolic"; + favorite_button.tooltip_text = is_favorite ? _("Remove from favorites") : _("Add to favorites"); + favorite_button.sensitive = path != null; + } + } + private class TilingPositionIndicator : Gtk.Fixed { private const int TRACK_WIDTH = 58; private const int TRACK_HEIGHT = 18; @@ -751,6 +855,8 @@ namespace Singularity { clock_box.append(clock_btn); clock_box.append(clock_suffix_box); layout_items["clock"] = clock_box; + if (is_primary && !is_greeter_mode) + layout_items["wallpaper"] = new WallpaperIndicator(_settings); reload_bar_layout(); _settings.changed["panel-layout-left"].connect(() => { if (!saving_bar_layout) reload_bar_layout(); @@ -769,8 +875,8 @@ namespace Singularity { center_box, right_box, layout_items, - { "overview", "workspaces", "tiling-position", "app-title", "global-menu", "system", "notifications", "clock" }, - { _("Overview"), _("Workspaces"), _("Scrolling Position"), _("App Title"), _("Global Menu"), _("System Status"), _("Notifications"), _("Clock") } + { "overview", "workspaces", "tiling-position", "app-title", "global-menu", "system", "notifications", "clock", "wallpaper" }, + { _("Overview"), _("Workspaces"), _("Scrolling Position"), _("App Title"), _("Global Menu"), _("System Status"), _("Notifications"), _("Clock"), _("Wallpaper") } ); layout_editor.move_requested.connect((item_id, section, index) => { if (bar_layout != null && bar_layout.move(item_id, section, index)) save_bar_layout(); @@ -1121,7 +1227,7 @@ namespace Singularity { private void reload_bar_layout() { string[] item_ids = { "overview", "workspaces", "tiling-position", "app-title", "global-menu", - "system", "notifications", "clock" + "system", "notifications", "clock", "wallpaper" }; bar_layout = new BarLayout( item_ids, diff --git a/src/core/wallpaper_favorites.vala b/src/core/wallpaper_favorites.vala new file mode 100644 index 0000000..892fe3f --- /dev/null +++ b/src/core/wallpaper_favorites.vala @@ -0,0 +1,45 @@ +using GLib; +using Gee; + +namespace Singularity { + + public class WallpaperFavorites : Object { + private const string FAVORITES_KEY = "wallpaper-favorites"; + private GLib.Settings settings; + private ArrayList favorites = new ArrayList(); + + public signal void favorites_changed(); + + public WallpaperFavorites(GLib.Settings? settings = null) { + this.settings = settings ?? new GLib.Settings("dev.sinty.desktop"); + SettingsSchema? schema = this.settings.settings_schema; + if (schema == null || !schema.has_key(FAVORITES_KEY)) return; + foreach (string path in this.settings.get_strv(FAVORITES_KEY)) { + if (path != "" && !favorites.contains(path)) favorites.add(path); + } + } + + private void save() { + SettingsSchema? schema = settings.settings_schema; + if (schema == null || !schema.has_key(FAVORITES_KEY)) return; + settings.set_strv(FAVORITES_KEY, list_favorites()); + } + + public bool is_favorite(string path) { + return path != "" && favorites.contains(path); + } + + public void toggle_favorite(string path) { + if (path == "") return; + if (!favorites.remove(path)) favorites.add(path); + save(); + favorites_changed(); + } + + public string[] list_favorites() { + string[] result = {}; + foreach (string path in favorites) result += path; + return result; + } + } +} diff --git a/src/core/wallpaper_history.vala b/src/core/wallpaper_history.vala new file mode 100644 index 0000000..e267ac9 --- /dev/null +++ b/src/core/wallpaper_history.vala @@ -0,0 +1,109 @@ +using GLib; +using Gee; + +namespace Singularity { + + public class WallpaperHistory : Object { + public const int MAX_ENTRIES = 200; + private const string GROUP = "History"; + + private string path; + private ArrayList back = new ArrayList(); + private ArrayList forward = new ArrayList(); + private int max_entries; + public string? current_path { get; private set; default = null; } + public int size { get { return back.size + forward.size + (current_path != null ? 1 : 0); } } + + public WallpaperHistory(string path, int max_entries = MAX_ENTRIES) { + this.path = path; + this.max_entries = int.max(1, max_entries); + load(); + } + + private void load_stack(KeyFile file, ArrayList destination, string key) { + try { + foreach (string entry in file.get_string_list(GROUP, key)) { + if (entry != "") destination.add(entry); + } + } catch (Error e) { + } + } + + private void load() { + var file = new KeyFile(); + try { + file.load_from_file(path, KeyFileFlags.NONE); + } catch (Error e) { + return; + } + load_stack(file, back, "back"); + load_stack(file, forward, "forward"); + try { + string saved = file.get_string(GROUP, "current"); + if (saved != "") current_path = saved; + } catch (Error e) { + } + trim_to_limit(); + } + + private string[] stack_to_array(ArrayList stack) { + string[] result = {}; + foreach (string entry in stack) result += entry; + return result; + } + + private void save() { + var file = new KeyFile(); + file.set_string_list(GROUP, "back", stack_to_array(back)); + file.set_string_list(GROUP, "forward", stack_to_array(forward)); + file.set_string(GROUP, "current", current_path ?? ""); + try { + DirUtils.create_with_parents(Path.get_dirname(path), 0700); + FileUtils.set_contents(path, file.to_data()); + } catch (Error e) { + warning("Wallpaper history could not be saved: %s", e.message); + } + } + + private void trim_to_limit() { + while (size > max_entries) { + if (back.size > 0) back.remove_at(0); + else if (forward.size > 0) forward.remove_at(0); + else break; + } + } + + public void record(string entry) { + if (entry == "" || entry == current_path) return; + if (current_path != null) back.add(current_path); + current_path = entry; + forward.clear(); + trim_to_limit(); + save(); + } + + public bool can_go_back() { + return current_path != null && back.size > 0; + } + + public bool can_go_forward() { + return current_path != null && forward.size > 0; + } + + public string? go_back() { + if (!can_go_back()) return null; + forward.add(current_path); + current_path = back.remove_at(back.size - 1); + save(); + return current_path; + } + + public string? go_forward() { + if (!can_go_forward()) return null; + back.add(current_path); + current_path = forward.remove_at(forward.size - 1); + save(); + return current_path; + } + } +} diff --git a/src/core/wallpaper_manager.vala b/src/core/wallpaper_manager.vala index fbc32d8..4ffabdc 100644 --- a/src/core/wallpaper_manager.vala +++ b/src/core/wallpaper_manager.vala @@ -31,6 +31,7 @@ namespace Singularity { public string attribution_author { get; private set; default = ""; } public signal void wallpaper_changed(); + public signal void wallpaper_path_changed(string new_path); public static WallpaperManager get_default() { if (_instance == null) { @@ -245,6 +246,7 @@ namespace Singularity { if (pb_small != null) preview_texture = Texture.for_pixbuf(pb_small); message("Wallpaper loaded: %s", load_path); wallpaper_changed(); + wallpaper_path_changed(load_path); return false; }); }); diff --git a/tests/dev.sinty.desktop.gschema.xml b/tests/dev.sinty.desktop.gschema.xml new file mode 100644 index 0000000..755faa1 --- /dev/null +++ b/tests/dev.sinty.desktop.gschema.xml @@ -0,0 +1,6 @@ + + + + [] + + diff --git a/tests/wallpaper_favorites_test.vala b/tests/wallpaper_favorites_test.vala new file mode 100644 index 0000000..98d006d --- /dev/null +++ b/tests/wallpaper_favorites_test.vala @@ -0,0 +1,45 @@ +using GLib; +using Singularity; + +private GLib.Settings make_settings(GLib.SettingsBackend? backend = null) { + var schema = SettingsSchemaSource.get_default().lookup("dev.sinty.desktop", true); + assert(schema != null); + return new GLib.Settings.full(schema, + backend ?? SettingsBackend.memory_settings_backend_new(), null); +} + +private void test_toggle_and_list() { + var favorites = new WallpaperFavorites(make_settings()); + int changes = 0; + favorites.favorites_changed.connect(() => changes++); + favorites.toggle_favorite("/a"); + favorites.toggle_favorite("/b"); + assert(favorites.is_favorite("/a")); + assert(favorites.list_favorites().length == 2); + favorites.toggle_favorite("/a"); + assert(!favorites.is_favorite("/a")); + assert(favorites.list_favorites()[0] == "/b"); + assert(changes == 3); +} + +private void test_persistence_roundtrip() { + var backend = SettingsBackend.memory_settings_backend_new(); + var first = new WallpaperFavorites(make_settings(backend)); + first.toggle_favorite("/a"); + first.toggle_favorite("/b"); + + var restored = new WallpaperFavorites(make_settings(backend)); + assert(restored.is_favorite("/a")); + assert(restored.is_favorite("/b")); + restored.toggle_favorite("/a"); + var again = new WallpaperFavorites(make_settings(backend)); + assert(!again.is_favorite("/a")); + assert(again.is_favorite("/b")); +} + +public int main(string[] args) { + Test.init(ref args); + Test.add_func("/wallpaper-favorites/toggle-and-list", test_toggle_and_list); + Test.add_func("/wallpaper-favorites/persistence-roundtrip", test_persistence_roundtrip); + return Test.run(); +} diff --git a/tests/wallpaper_history_test.vala b/tests/wallpaper_history_test.vala new file mode 100644 index 0000000..59c2182 --- /dev/null +++ b/tests/wallpaper_history_test.vala @@ -0,0 +1,58 @@ +using GLib; +using Singularity; + +private string history_path() { + try { + return Path.build_filename(DirUtils.make_tmp("wallpaper-history-XXXXXX"), "history.ini"); + } catch (Error e) { + error("temporary directory: %s", e.message); + } +} + +private void test_back_forward_semantics() { + var history = new WallpaperHistory(history_path()); + history.record("/a"); + history.record("/b"); + history.record("/c"); + assert(history.go_back() == "/b"); + assert(history.go_back() == "/a"); + assert(history.go_forward() == "/b"); + history.record("/d"); + assert(!history.can_go_forward()); + assert(history.go_back() == "/b"); +} + +private void test_bounded_size() { + var history = new WallpaperHistory(history_path(), 3); + history.record("/a"); + history.record("/b"); + history.record("/c"); + history.record("/d"); + assert(history.size == 3); + assert(history.go_back() == "/c"); + assert(history.go_back() == "/b"); + assert(history.go_back() == null); +} + +private void test_persistence_roundtrip() { + string path = history_path(); + var first = new WallpaperHistory(path); + first.record("/a"); + first.record("/b"); + first.record("/c"); + assert(first.go_back() == "/b"); + + var restored = new WallpaperHistory(path); + assert(restored.current_path == "/b"); + assert(restored.go_back() == "/a"); + assert(restored.go_forward() == "/b"); + assert(restored.go_forward() == "/c"); +} + +public int main(string[] args) { + Test.init(ref args); + Test.add_func("/wallpaper-history/back-forward-semantics", test_back_forward_semantics); + Test.add_func("/wallpaper-history/bounded-size", test_bounded_size); + Test.add_func("/wallpaper-history/persistence-roundtrip", test_persistence_roundtrip); + return Test.run(); +}