Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,7 @@ wallpaper_test_schema = custom_target('wallpaper-test-schema',
)
wallpaper_test_env = environment()
wallpaper_test_env.set('GSETTINGS_SCHEMA_DIR', meson.current_build_dir())
wallpaper_test_env.set('GSETTINGS_BACKEND', 'memory')

wallpaper_history_test = executable('wallpaper-history-test',
sources: ['src/core/wallpaper_history.vala', 'tests/wallpaper_history_test.vala'],
Expand All @@ -953,20 +954,24 @@ test('wallpaper-favorites', wallpaper_favorites_test,

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')],
dependencies: [dependency('gobject-2.0'), dependency('gio-2.0'), gee_dep, json_dep, dependency('gdk-pixbuf-2.0'), cc.find_library('m')],
)
test('wallpaper-gallery', wallpaper_gallery_test)
wallpaper_rotator_test = executable('wallpaper-rotator-test',
sources: ['src/core/wallpaper_collections.vala', 'src/core/wallpaper_gallery.vala', 'src/core/dynamic_wallpaper.vala',
'src/core/wallpaper_favorites.vala',
'src/core/wallpaper_rotation_state.vala', 'src/core/wallpaper_rotator.vala',
'tests/wallpaper_rotator_test.vala'],
dependencies: [dependency('gobject-2.0'), dependency('glib-2.0'), dependency('gio-2.0'), gee_dep, json_dep, cc.find_library('m')],
dependencies: [dependency('gobject-2.0'), dependency('glib-2.0'), dependency('gio-2.0'),
dependency('gdk-pixbuf-2.0'), gee_dep, json_dep, cc.find_library('m')],
)
test('wallpaper-rotator', wallpaper_rotator_test)
test('wallpaper-rotator', wallpaper_rotator_test,
depends: wallpaper_test_schema,
env: wallpaper_test_env)

wallpaper_ocs_test = executable('wallpaper-ocs-test',
sources: ['src/core/wallpaper_ocs.vala', 'src/core/wallpaper_browse_cache.vala', 'src/core/wallpaper_thumbnail_cache.vala', 'src/core/wallpaper_provider.vala', 'src/core/wallpaper_collections.vala', 'src/core/wallpaper_gallery.vala', 'src/core/dynamic_wallpaper.vala', 'tests/wallpaper_ocs_test.vala'],
dependencies: [dependency('gobject-2.0'), dependency('gio-2.0'), gee_dep, json_dep, libsingularity_dep],
dependencies: [dependency('gobject-2.0'), dependency('gio-2.0'), gee_dep, json_dep, libsingularity_dep, dependency('gdk-pixbuf-2.0')],
)
test('wallpaper-ocs', wallpaper_ocs_test)

Expand Down
10 changes: 10 additions & 0 deletions src/components/sidebar/pages/desktop_page.vala
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,16 @@ namespace Singularity {
rotation_state.get_rotate_enabled());
grid_group.add_row(rotate_row);

var next_wallpaper_row = new PreferencesRow();
var next_wallpaper_button = new Button.with_label(_("Next Wallpaper Now"));
next_wallpaper_button.margin_start = next_wallpaper_button.margin_end = 10;
next_wallpaper_button.margin_top = next_wallpaper_button.margin_bottom = 8;
next_wallpaper_button.clicked.connect(() => {
WallpaperManager.get_default().rotate_wallpaper_now();
});
next_wallpaper_row.set_child(next_wallpaper_button);
grid_group.add_row(next_wallpaper_row);

// Wallpaper attribution overlay toggle. Background.vala listens
// for settings.changed["show-wallpaper-attribution"] and hides
// the overlay live; the gsettings key also gates the live
Expand Down
11 changes: 9 additions & 2 deletions src/core/wallpaper_gallery.vala
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,14 @@ namespace Singularity {
internal class WallpaperCandidate : Object {
public string uri { get; private set; }
public bool is_recent { get; private set; }
public int width { get; private set; }
public int height { get; private set; }

public WallpaperCandidate(string uri, bool is_recent) {
public WallpaperCandidate(string uri, bool is_recent, int width = 0, int height = 0) {
this.uri = uri;
this.is_recent = is_recent;
this.width = width;
this.height = height;
}
}

Expand Down Expand Up @@ -100,7 +104,10 @@ namespace Singularity {
string uri = child.get_uri();
if (thread_seen.contains(uri)) continue;
thread_seen.add(uri);
candidates.add(new WallpaperCandidate(uri, false));
int width = 0;
int height = 0;
Gdk.Pixbuf.get_file_info(child.get_path(), out width, out height);
candidates.add(new WallpaperCandidate(uri, false, width, height));
}
} catch (Error e) {
}
Expand Down
14 changes: 14 additions & 0 deletions src/core/wallpaper_manager.vala
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,15 @@ namespace Singularity {
public void start_rotation() {
if (rotator != null) return;
rotator = WallpaperRotator.get_default();
var display = Gdk.Display.get_default();
if (display != null && display.get_monitors().get_n_items() > 0) {
var monitor = display.get_monitors().get_item(0) as Gdk.Monitor;
if (monitor != null) {
var geometry = monitor.get_geometry();
if (geometry.height > 0)
rotator.target_aspect_ratio = (double) geometry.width / (double) geometry.height;
}
}
rotator.current_uri = settings.get_string("background-picture-uri");
rotator.wallpaper_selected.connect((uri) => {
settings.set_string("background-picture-uri", uri);
Expand All @@ -117,6 +126,11 @@ namespace Singularity {
rotator.start();
}

public void rotate_wallpaper_now() {
start_rotation();
rotator.rotate_async();
}

public void reload() {
// Read attribution keys defensively: a schema that doesn't
// have them yet (older deploy, ad-hoc bisect, dev mode) must
Expand Down
102 changes: 98 additions & 4 deletions src/core/wallpaper_rotator.vala
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ namespace Singularity {
// Selects rotation timing and images; WallpaperManager applies the signal.
public class WallpaperRotator : Object {
private static WallpaperRotator? _instance = null;
private const int FAVORITE_WEIGHT = 3;
private const double ASPECT_TOLERANCE = 0.15;

public signal void wallpaper_selected(string uri);

Expand All @@ -19,12 +21,21 @@ namespace Singularity {
private uint tick_id = 0;
private uint restart_id = 0;
private FileMonitor? state_monitor = null;
private GLib.Settings? selection_settings = null;
private WallpaperFavorites? favorites = null;
private int default_min_width = 1280;
private int default_min_height = 720;
private bool default_match_aspect = false;
private bool default_favor_favorites = true;
public double target_aspect_ratio { get; set; default = 0.0; }

public static WallpaperRotator get_default() {
if (_instance == null) {
_instance = new WallpaperRotator(
WallpaperRotationState.default_config_dir(),
WallpaperCollections.default_search_roots());
_instance.selection_settings = new GLib.Settings("dev.sinty.desktop");
_instance.favorites = new WallpaperFavorites(_instance.selection_settings);
}
return _instance;
}
Expand All @@ -35,6 +46,18 @@ namespace Singularity {
this.state = new WallpaperRotationState(config_dir);
}

public void configure_selection(int min_width, int min_height,
bool match_aspect, bool favor_favorites,
double target_aspect = 0.0,
WallpaperFavorites? favorites = null) {
default_min_width = min_width;
default_min_height = min_height;
default_match_aspect = match_aspect;
default_favor_favorites = favor_favorites;
target_aspect_ratio = target_aspect;
this.favorites = favorites;
}

public void start() {
watch_state_dir();
reschedule();
Expand Down Expand Up @@ -121,12 +144,83 @@ namespace Singularity {
var candidates = WallpaperGallery.scan(scan_dir, all_dirs.to_array(), {});
if (candidates.size == 0) return null;

var uris = new ArrayList<string>();
foreach (var candidate in candidates) uris.add(candidate.uri);
return pick(uris, current, Random.next_int());
int min_width = setting_int("wallpaper-rotation-min-width", default_min_width);
int min_height = setting_int("wallpaper-rotation-min-height", default_min_height);
bool match_aspect = setting_bool("wallpaper-rotation-match-aspect", default_match_aspect);
bool favor_favorites = setting_bool("wallpaper-rotation-favor-favorites", default_favor_favorites);
var regarded = regard(candidates, min_width, min_height,
match_aspect, target_aspect_ratio);
return pick_candidates(regarded, current, Random.next_int(),
favor_favorites, favorites);
}

private int setting_int(string key, int fallback) {
if (selection_settings == null || !selection_settings.settings_schema.has_key(key)) return fallback;
return selection_settings.get_int(key);
}

private bool setting_bool(string key, bool fallback) {
if (selection_settings == null || !selection_settings.settings_schema.has_key(key)) return fallback;
return selection_settings.get_boolean(key);
}

internal static ArrayList<WallpaperCandidate> regard(
Gee.List<WallpaperCandidate> candidates, int min_width, int min_height,
bool match_aspect, double target_aspect) {
var sized = new ArrayList<WallpaperCandidate>();
foreach (var candidate in candidates) {
if (candidate.width >= min_width && candidate.height >= min_height)
sized.add(candidate);
}
if (sized.size == 0 && candidates.size > 0) {
debug("wallpaper rotator: minimum resolution excluded every candidate; using unfiltered collection");
foreach (var candidate in candidates) sized.add(candidate);
}
if (!match_aspect || target_aspect <= 0.0) return sized;

var matched = new ArrayList<WallpaperCandidate>();
foreach (var candidate in sized) {
if (candidate.width <= 0 || candidate.height <= 0) continue;
double aspect = (double) candidate.width / (double) candidate.height;
if (Math.fabs(aspect - target_aspect) / target_aspect <= ASPECT_TOLERANCE)
matched.add(candidate);
}
if (matched.size == 0) {
debug("wallpaper rotator: aspect preference matched no candidates; using resolution-filtered collection");
return sized;
}
return matched;
}

internal static string? pick_candidates(Gee.List<WallpaperCandidate> candidates,
string? current_uri, uint32 roll,
bool favor_favorites,
WallpaperFavorites? favorites) {
var choices = new ArrayList<WallpaperCandidate>();
foreach (var candidate in candidates) {
if (candidates.size == 1 || candidate.uri != current_uri) choices.add(candidate);
}
if (choices.size == 0) return candidates.size > 0 ? candidates[0].uri : null;

int total_weight = 0;
foreach (var candidate in choices) {
string? path = File.new_for_uri(candidate.uri).get_path();
bool favorite = favor_favorites && favorites != null && path != null
&& favorites.is_favorite(path);
total_weight += favorite ? FAVORITE_WEIGHT : 1;
}
int selected = (int) (roll % total_weight);
foreach (var candidate in choices) {
string? path = File.new_for_uri(candidate.uri).get_path();
bool favorite = favor_favorites && favorites != null && path != null
&& favorites.is_favorite(path);
int weight = favorite ? FAVORITE_WEIGHT : 1;
if (selected < weight) return candidate.uri;
selected -= weight;
}
return choices[0].uri;
}

// Inject the random roll so selection remains deterministic in tests.
public static string? pick(Gee.List<string> uris, string? current_uri, uint32 roll) {
if (uris.size == 0) return null;
if (uris.size == 1) return uris[0];
Expand Down
4 changes: 4 additions & 0 deletions tests/dev.sinty.desktop.gschema.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,9 @@
<range min="1" max="64"/>
<default>8</default>
</key>
<key name="wallpaper-rotation-min-width" type="i"><range min="1" max="16384"/><default>1280</default></key>
<key name="wallpaper-rotation-min-height" type="i"><range min="1" max="16384"/><default>720</default></key>
<key name="wallpaper-rotation-match-aspect" type="b"><default>false</default></key>
<key name="wallpaper-rotation-favor-favorites" type="b"><default>true</default></key>
</schema>
</schemalist>
70 changes: 68 additions & 2 deletions tests/wallpaper_rotator_test.vala
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ private string make_dir(string name) {
return path;
}

private string image_file(string dir, string name) {
private string image_file(string dir, string name, int width = 1920, int height = 1080) {
string path = Path.build_filename(dir, name + ".svg");
try {
DirUtils.create_with_parents(dir, 0700);
FileUtils.set_contents(path,
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1\" height=\"1\"><rect width=\"1\" height=\"1\"/></svg>");
"<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"%d\" height=\"%d\"><rect width=\"100%%\" height=\"100%%\"/></svg>".printf(width, height));
} catch (Error e) { error("fixture: %s", e.message); }
return File.new_for_path(path).get_uri();
}
Expand Down Expand Up @@ -58,6 +58,49 @@ private void test_pick_on_empty_list() {
assert(WallpaperRotator.pick(new ArrayList<string>(), null, 0) == null);
}

private void test_minimum_resolution_excludes_small_candidates() {
var candidates = new ArrayList<WallpaperCandidate>();
candidates.add(new WallpaperCandidate("file:///small.jpg", false, 640, 480));
candidates.add(new WallpaperCandidate("file:///large.jpg", false, 1920, 1080));
var regarded = WallpaperRotator.regard(candidates, 1280, 720, false, 0.0);
assert(regarded.size == 1);
assert(regarded[0].uri == "file:///large.jpg");
}

private void test_empty_resolution_pool_falls_back() {
var candidates = new ArrayList<WallpaperCandidate>();
candidates.add(new WallpaperCandidate("file:///small-a.jpg", false, 640, 480));
candidates.add(new WallpaperCandidate("file:///small-b.jpg", false, 800, 600));
var regarded = WallpaperRotator.regard(candidates, 1280, 720, false, 0.0);
assert(regarded.size == 2);
}

private void test_aspect_preference_uses_display_ratio() {
var candidates = new ArrayList<WallpaperCandidate>();
candidates.add(new WallpaperCandidate("file:///portrait.jpg", false, 1080, 1920));
candidates.add(new WallpaperCandidate("file:///wide.jpg", false, 1920, 1080));
var regarded = WallpaperRotator.regard(candidates, 1, 1, true, 16.0 / 9.0);
assert(regarded.size == 1);
assert(regarded[0].uri == "file:///wide.jpg");
}

private void test_favorites_receive_three_times_the_weight() {
var settings = new GLib.Settings("dev.sinty.desktop");
var favorites = new WallpaperFavorites(settings);
string favorite_path = Path.build_filename(fixture_root, "favorite.jpg");
favorites.toggle_favorite(favorite_path);
var candidates = new ArrayList<WallpaperCandidate>();
candidates.add(new WallpaperCandidate(File.new_for_path(favorite_path).get_uri(), false, 1920, 1080));
candidates.add(new WallpaperCandidate("file:///ordinary.jpg", false, 1920, 1080));
int favorite_picks = 0;
for (uint32 roll = 0; roll < 400; roll++) {
string? chosen = WallpaperRotator.pick_candidates(candidates, null, roll, true, favorites);
if (chosen == candidates[0].uri) favorite_picks++;
}
assert(favorite_picks == 300);
favorites.toggle_favorite(favorite_path);
}

private void test_rotates_within_the_selected_collection_only() {
string registry = make_dir("registry-scoped");
string alpha = make_dir("alpha");
Expand Down Expand Up @@ -137,6 +180,24 @@ private void test_rotate_now_announces_nothing_when_there_is_nothing() {
assert(!announced);
}

private void test_manual_trigger_changes_immediately() {
string registry = make_dir("registry-manual");
string pack = make_dir("manual-pack");
string current = image_file(pack, "current");
string next = image_file(pack, "next");
write_collection(registry, "manual", pack);
string config = make_dir("config-manual");
new WallpaperRotationState(config).set_selected_collection("manual");

var rotator = new WallpaperRotator(config, { registry });
rotator.current_uri = current;
string? announced = null;
rotator.wallpaper_selected.connect((uri) => { announced = uri; });
rotator.rotate_now();
assert(announced == next);
assert(rotator.current_uri == next);
}

private void test_choose_next_for_excludes_the_supplied_wallpaper() {
string registry = make_dir("registry-exclude");
string pack = make_dir("exclude-pack");
Expand Down Expand Up @@ -232,12 +293,17 @@ public int main(string[] args) {
Test.add_func("/wallpaper-rotator/pick-deterministic-roll", test_pick_is_deterministic_for_a_given_roll);
Test.add_func("/wallpaper-rotator/pick-skips-current", test_pick_never_returns_the_current_wallpaper);
Test.add_func("/wallpaper-rotator/pick-empty", test_pick_on_empty_list);
Test.add_func("/wallpaper-rotator/regard-minimum-resolution", test_minimum_resolution_excludes_small_candidates);
Test.add_func("/wallpaper-rotator/regard-empty-fallback", test_empty_resolution_pool_falls_back);
Test.add_func("/wallpaper-rotator/regard-aspect", test_aspect_preference_uses_display_ratio);
Test.add_func("/wallpaper-rotator/favorites-weighting", test_favorites_receive_three_times_the_weight);
Test.add_func("/wallpaper-rotator/scoped-to-selected-collection", test_rotates_within_the_selected_collection_only);
Test.add_func("/wallpaper-rotator/stale-id-falls-back", test_stale_collection_id_falls_back_to_the_first);
Test.add_func("/wallpaper-rotator/empty-collection-is-a-no-op", test_empty_collection_leaves_the_wallpaper_alone);
Test.add_func("/wallpaper-rotator/no-registry", test_no_registry_at_all);
Test.add_func("/wallpaper-rotator/rotate-now-announces", test_rotate_now_announces_and_records_the_choice);
Test.add_func("/wallpaper-rotator/rotate-now-silent-when-empty", test_rotate_now_announces_nothing_when_there_is_nothing);
Test.add_func("/wallpaper-rotator/manual-trigger-changes", test_manual_trigger_changes_immediately);
Test.add_func("/wallpaper-rotator/choose-next-for-excludes", test_choose_next_for_excludes_the_supplied_wallpaper);
Test.add_func("/wallpaper-rotator/rotate-async-announces", test_rotate_async_announces_on_the_main_loop);
Test.add_func("/wallpaper-rotator/untouched-install-does-not-rotate", test_untouched_install_does_not_rotate);
Expand Down
Loading