diff --git a/data/Application.css b/data/Application.css index bfd5c8a1..802db231 100644 --- a/data/Application.css +++ b/data/Application.css @@ -176,3 +176,15 @@ backgrounditem .close-button { .running-indicator:disabled { color: @fg_color; } + +.application-menu { + background: alpha(@bg_color, 0.8); +} + +.results-list { + background: none; +} + +.app-grid { + /* border-spacing: 20px; */ +} diff --git a/meson.build b/meson.build index b3a8c617..c7fb2cd7 100644 --- a/meson.build +++ b/meson.build @@ -21,6 +21,8 @@ endif add_project_arguments(vala_flags, language: 'vala') glib_version = '2.74' + +detective_dep = dependency('detective') gio_dep = dependency('gio-2.0', version: '>=@0@'.format(glib_version)) gio_unix_dep = dependency('gio-unix-2.0', version: '>=@0@'.format(glib_version)) glib_dep = dependency('glib-2.0', version: '>=@0@'.format(glib_version)) @@ -32,11 +34,13 @@ x11_dep = dependency('x11') granite_dep = dependency('granite-7', version: '>=7.7.0') adw_dep = dependency('libadwaita-1') m_dep = cc.find_library('m') +tracker_dep = dependency('tracker-sparql-3.0') wl_client_dep = dependency('wayland-client') subdir('protocol') dependencies = [ + detective_dep, gio_dep, gio_unix_dep, glib_dep, @@ -48,6 +52,7 @@ dependencies = [ granite_dep, adw_dep, m_dep, + tracker_dep, wl_client_dep, pantheon_desktop_shell_dep ] diff --git a/src/AppSystem/App.vala b/src/AppSystem/App.vala index e4aabd30..075fecaf 100644 --- a/src/AppSystem/App.vala +++ b/src/AppSystem/App.vala @@ -23,11 +23,10 @@ public class Dock.App : Object { } } - public signal void removed (); - - public bool pinned { get; construct set; } public GLib.DesktopAppInfo app_info { get; construct; } + public bool pinned { get; set; } + public bool count_visible { get; private set; default = false; } public int64 current_count { get; private set; default = 0; } public bool progress_visible { get; set; default = false; } @@ -59,8 +58,8 @@ public class Dock.App : Object { private string appstream_comp_id = ""; - public App (GLib.DesktopAppInfo app_info, bool pinned) { - Object (app_info: app_info, pinned: pinned); + public App (GLib.DesktopAppInfo app_info) { + Object (app_info: app_info); } static construct { @@ -123,10 +122,6 @@ public class Dock.App : Object { on_appcenter_dbus_changed.begin (); } - notify["pinned"].connect (() => { - check_remove (); - }); - WindowSystem.get_default ().notify["active-workspace"].connect (() => { notify_property ("running-on-active-workspace"); }); @@ -187,12 +182,6 @@ public class Dock.App : Object { return false; } - private void check_remove () { - if (!pinned && !running) { - removed (); - } - } - public void update_windows (GLib.GenericArray? new_windows) { if (new_windows == null) { windows = new GLib.GenericArray (); @@ -206,8 +195,6 @@ public class Dock.App : Object { if (launching && running) { launching = false; } - - check_remove (); } public void perform_unity_update (VariantIter prop_iter) { diff --git a/src/AppSystem/AppIconWidget.vala b/src/AppSystem/AppIconWidget.vala new file mode 100644 index 00000000..719056c8 --- /dev/null +++ b/src/AppSystem/AppIconWidget.vala @@ -0,0 +1,164 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2022-2025 elementary, Inc. (https://elementary.io) + */ + +public class Dock.AppIconWidget : Granite.Bin { + private static Settings? notify_settings; + + static construct { + if (SettingsSchemaSource.get_default ().lookup ("io.elementary.notifications", true) != null) { + notify_settings = new Settings ("io.elementary.notifications"); + } + } + + public App app { get; construct; } + + public int icon_size { get; construct set; } + + private Gtk.Label badge; + private Gtk.Revealer progress_revealer; + private Adw.TimedAnimation badge_fade; + private Adw.TimedAnimation badge_scale; + + private Binding current_count_binding; + + public AppIconWidget (App app) { + Object (app: app); + } + + construct { + var image = new Gtk.Image (); + bind_property ("icon-size", image, "pixel-size", SYNC_CREATE); + + var icon = app.app_info.get_icon (); + if (icon != null && Gtk.IconTheme.get_for_display (Gdk.Display.get_default ()).has_gicon (icon)) { + image.gicon = icon; + } else { + image.gicon = new ThemedIcon ("application-default-icon"); + } + + badge = new Gtk.Label ("!"); + badge.add_css_class (Granite.STYLE_CLASS_BADGE); + + var badge_container = new Granite.Bin () { + can_target = false, + child = badge, + halign = END, + valign = START, + overflow = VISIBLE + }; + + progress_revealer = new Gtk.Revealer () { + can_target = false, + transition_type = CROSSFADE + }; + + var overlay = new Gtk.Overlay () { + child = image, + }; + overlay.add_overlay (badge_container); + overlay.add_overlay (progress_revealer); + + child = overlay; + + // We have to destroy the progressbar when it is not needed otherwise it will + // cause continuous layouting of the surface see https://github.com/elementary/dock/issues/279 + progress_revealer.notify["child-revealed"].connect (() => { + if (!progress_revealer.child_revealed) { + progress_revealer.child = null; + } + }); + + badge_scale = new Adw.TimedAnimation ( + badge, 0.25, 1, + Granite.TRANSITION_DURATION_OPEN, + new Adw.CallbackAnimationTarget ((val) => { + var height = badge_container.get_height (); + var width = badge_container.get_width (); + + var x = (float) (width - (val * width)) / 2; + var y = (float) (height - (val * height)) / 2; + + badge.allocate ( + width, height, -1, + new Gsk.Transform ().scale ((float) val, (float) val).translate (Graphene.Point ().init (x, y)) + ); + }) + ); + + badge_fade = new Adw.TimedAnimation ( + badge, 0, 1, + Granite.TRANSITION_DURATION_OPEN, + new Adw.CallbackAnimationTarget ((val) => { + badge.opacity = val; + }) + ) { + easing = EASE_IN_OUT_QUAD + }; + + app.notify["count-visible"].connect (update_badge_revealed); + update_badge_revealed (); + current_count_binding = app.bind_property ("current_count", badge, "label", SYNC_CREATE, + (binding, srcval, ref targetval) => { + var src = (int64) srcval; + + if (src > 0) { + targetval.set_string ("%lld".printf (src)); + } else { + targetval.set_string ("!"); + } + + return true; + }, null + ); + + if (notify_settings != null) { + notify_settings.changed["do-not-disturb"].connect (update_badge_revealed); + } + + app.notify["progress-visible"].connect (update_progress_revealer); + update_progress_revealer (); + } + + private void update_badge_revealed () { + badge_fade.skip (); + badge_scale.skip (); + + // Avoid a stutter at the beginning + badge.opacity = 0; + + if (app.count_visible && (notify_settings == null || !notify_settings.get_boolean ("do-not-disturb"))) { + badge_fade.duration = Granite.TRANSITION_DURATION_OPEN; + badge_fade.reverse = false; + + badge_scale.duration = Granite.TRANSITION_DURATION_OPEN; + badge_scale.easing = EASE_OUT_BACK; + badge_scale.reverse = false; + } else { + badge_fade.duration = Granite.TRANSITION_DURATION_CLOSE; + badge_fade.reverse = true; + + badge_scale.duration = Granite.TRANSITION_DURATION_CLOSE; + badge_scale.easing = EASE_OUT_QUAD; + badge_scale.reverse = true; + } + + badge_fade.play (); + badge_scale.play (); + } + + private void update_progress_revealer () { + progress_revealer.reveal_child = app.progress_visible; + + // See comment above and https://github.com/elementary/dock/issues/279 + if (progress_revealer.reveal_child && progress_revealer.child == null) { + var progress_bar = new Gtk.ProgressBar () { + valign = END + }; + app.bind_property ("progress", progress_bar, "fraction", SYNC_CREATE); + + progress_revealer.child = progress_bar; + } + } +} diff --git a/src/AppSystem/AppMenuFactory.vala b/src/AppSystem/AppMenuFactory.vala new file mode 100644 index 00000000..9995f81a --- /dev/null +++ b/src/AppSystem/AppMenuFactory.vala @@ -0,0 +1,34 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2026 elementary, Inc. (https://elementary.io) + */ + +namespace Dock.AppMenuFactory { + private const string ACTION_GROUP_PREFIX = "launcher"; + private const string ACTION_PREFIX = ACTION_GROUP_PREFIX + "."; + private const string PINNED_ACTION = "pinned"; + + public Gtk.Popover create_app_menu (App app) { + var shell_section = new Menu (); + shell_section.append (_("Keep in Dock"), ACTION_PREFIX + PINNED_ACTION); + + if (Environment.find_program_in_path ("io.elementary.appcenter") != null) { + shell_section.append (_("Uninstall"), App.ACTION_PREFIX + App.UNINSTALL_ACTION); + shell_section.append (_("View in AppCenter"), App.ACTION_PREFIX + App.VIEW_ACTION); + } + + var menu = new Menu (); + menu.append_section (null, app.app_action_menu); + menu.append_section (null, shell_section); + + var popover = new Gtk.PopoverMenu.from_model (menu); + + var action_group = new SimpleActionGroup (); + action_group.add_action (new PropertyAction (PINNED_ACTION, app, "pinned")); + popover.insert_action_group (ACTION_GROUP_PREFIX, action_group); + + popover.insert_action_group (App.ACTION_GROUP_PREFIX, app.app_action_group); + + return popover; + } +} diff --git a/src/AppSystem/AppSystem.vala b/src/AppSystem/AppSystem.vala index 3ba2fc52..f7cfde79 100644 --- a/src/AppSystem/AppSystem.vala +++ b/src/AppSystem/AppSystem.vala @@ -29,38 +29,51 @@ public class Dock.AppSystem : Object, UnityClient { id_to_app = new HashTable (str_hash, str_equal); } - public App? get_app (string id) { - return id_to_app[id]; - } + public App? get_app_by_id (string id) { + if (!(id in id_to_app)) { + var app_info = new DesktopAppInfo (id); - public async void load () { - foreach (string app_id in settings.get_strv ("launchers")) { - var app_info = new GLib.DesktopAppInfo (app_id); - add_app (app_info, true); - } + if (app_info == null) { + return null; + } - yield sync_windows (); - WindowSystem.get_default ().notify["windows"].connect (sync_windows); - } + var app = new App (app_info); + app.notify["running"].connect (check_app); + app.notify["pinned"].connect (check_app); + app.notify["pinned"].connect (save_pinned); + + id_to_app[app_info.get_id ()] = app; + } - private App add_app (DesktopAppInfo app_info, bool pinned) { - var app = new App (app_info, pinned); - app.removed.connect (remove_app); - app.notify["pinned"].connect (save_pinned); - id_to_app[app_info.get_id ()] = app; - apps_store.append (app); - return app; + return id_to_app[id]; } - private void remove_app (App app) { - id_to_app.remove (app.app_info.get_id ()); + private void check_app (Object obj, ParamSpec pspec) { + var app = (App) obj; uint pos; - if (apps_store.find (app, out pos)) { + var exists = apps_store.find (app, out pos); + + if ((app.pinned || app.running) && !exists) { + apps_store.append (app); + } else if ((!app.pinned && !app.running) && exists) { apps_store.remove (pos); } } + private void save_pinned () { + string[] new_pinned_ids = {}; + + for (uint i = 0; i < apps_store.get_n_items (); i++) { + var app = (App) apps_store.get_item (i); + if (app.pinned) { + new_pinned_ids += app.app_info.get_id (); + } + } + + settings.set_strv ("launchers", new_pinned_ids); + } + public void reorder_app (App app, uint new_index) { uint pos; if (!apps_store.find (app, out pos)) { @@ -72,17 +85,17 @@ public class Dock.AppSystem : Object, UnityClient { apps_store.insert (new_index, app); } - private void save_pinned () { - string[] new_pinned_ids = {}; - - for (uint i = 0; i < apps_store.get_n_items (); i++) { - var app = (App) apps_store.get_item (i); - if (app.pinned) { - new_pinned_ids += app.app_info.get_id (); + public async void load () { + foreach (string app_id in settings.get_strv ("launchers")) { + var app = get_app_by_id (app_id); + if (app == null) { + continue; } + app.pinned = true; } - settings.set_strv ("launchers", new_pinned_ids); + yield sync_windows (); + WindowSystem.get_default ().notify["windows"].connect (sync_windows); } public async void sync_windows () { @@ -90,14 +103,9 @@ public class Dock.AppSystem : Object, UnityClient { var app_window_list = new GLib.HashTable> (direct_hash, direct_equal); foreach (var window in windows) { - App? app = id_to_app[window.app_id]; + var app = get_app_by_id (window.app_id); if (app == null) { - var app_info = new GLib.DesktopAppInfo (window.app_id); - if (app_info == null) { - continue; - } - - app = add_app (app_info, false); + continue; } var window_list = app_window_list.get (app); @@ -117,25 +125,19 @@ public class Dock.AppSystem : Object, UnityClient { } } - public App? add_app_for_id (string app_id) { - if (app_id in id_to_app) { - id_to_app[app_id].pinned = true; - return id_to_app[app_id]; - } - - var app_info = new DesktopAppInfo (app_id); + public void add_app_for_id (string app_id) { + var app = get_app_by_id (app_id); - if (app_info == null) { - warning ("App not found: %s", app_id); - return null; + if (app != null) { + app.pinned = true; } - - return add_app (app_info, true); } public void remove_app_by_id (string app_id) { - if (app_id in id_to_app) { - id_to_app[app_id].pinned = false; + var app = get_app_by_id (app_id); + + if (app != null) { + app.pinned = false; } } @@ -160,8 +162,9 @@ public class Dock.AppSystem : Object, UnityClient { parameters.get ("(sa{sv})", out app_uri, out prop_iter); var app_id = app_uri.replace ("application://", ""); - if (id_to_app[app_id] != null) { - id_to_app[app_id].perform_unity_update (prop_iter); + var app = get_app_by_id (app_id); + if (app != null) { + app.perform_unity_update (prop_iter); } else { critical ("unable to update missing launcher: %s", app_id); } @@ -169,8 +172,9 @@ public class Dock.AppSystem : Object, UnityClient { private void remove_launcher_entry (string sender_name) { var app_id = sender_name + ".desktop"; - if (id_to_app[app_id] != null) { - id_to_app[app_id].remove_launcher_entry (); + var app = get_app_by_id (app_id); + if (app != null) { + app.remove_launcher_entry (); } } } diff --git a/src/Application.vala b/src/Application.vala index eea910c0..e5257826 100644 --- a/src/Application.vala +++ b/src/Application.vala @@ -4,6 +4,8 @@ */ public class Dock.Application : Gtk.Application { + private ApplicationMenu app_menu; + public Application () { Object (application_id: "io.elementary.dock"); } @@ -14,6 +16,12 @@ public class Dock.Application : Gtk.Application { Granite.init (); ShellKeyGrabber.init (); GalaDBus.init.begin (); + + app_menu = new ApplicationMenu (); + + var show_app_menu_action = new SimpleAction ("show-app-menu", null); + show_app_menu_action.activate.connect (app_menu.present); + add_action (show_app_menu_action); } protected override void activate () { diff --git a/src/ApplicationMenu/AppCarousel/AppCarousel.vala b/src/ApplicationMenu/AppCarousel/AppCarousel.vala new file mode 100644 index 00000000..443c3539 --- /dev/null +++ b/src/ApplicationMenu/AppCarousel/AppCarousel.vala @@ -0,0 +1,75 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Leonhard Kargl + */ + +public class Dock.AppCarousel : Granite.Bin { + private const int PAGE_SIZE = AppCarouselPage.COLUMNS * AppCarouselPage.ROWS; + + private AppStore app_store; + + private Adw.Carousel carousel; + + construct { + carousel = new Adw.Carousel (); + + var dots = new Adw.CarouselIndicatorDots () { + carousel = carousel + }; + + var box = new Granite.Box (VERTICAL, NONE); + box.append (carousel); + box.append (dots); + + child = box; + + app_store = new AppStore (); + app_store.apps.items_changed.connect (on_apps_changed); + on_apps_changed (0, 0, app_store.apps.get_n_items ()); + } + + private void on_apps_changed (uint pos, uint removed, uint added) { + while (carousel.get_first_child () != null) { + carousel.remove (carousel.get_first_child ()); + } + + for (uint i = 0; i < app_store.apps.get_n_items (); i += PAGE_SIZE) { + var slice = new Gtk.SliceListModel (app_store.apps, i, PAGE_SIZE); + var page = new AppCarouselPage (slice); + + carousel.append (page); + } + } + + public override bool focus (Gtk.DirectionType direction) { + var active_page = carousel.get_nth_page ((int) carousel.position); + + if (active_page.child_focus (direction)) { + return true; + } + + Gtk.Widget? next; + + switch (direction) { + case RIGHT: + case TAB_FORWARD: + next = active_page.get_next_sibling (); + break; + + case LEFT: + case TAB_BACKWARD: + next = active_page.get_prev_sibling (); + break; + + default: + return false; + } + + if (next != null) { + carousel.scroll_to (next, true); + return next.child_focus (direction); + } + + return false; + } +} diff --git a/src/ApplicationMenu/AppCarousel/AppCarouselPage.vala b/src/ApplicationMenu/AppCarousel/AppCarouselPage.vala new file mode 100644 index 00000000..f0f36159 --- /dev/null +++ b/src/ApplicationMenu/AppCarousel/AppCarouselPage.vala @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Leonhard Kargl + */ + +public class Dock.AppCarouselPage : Granite.Bin { + public const int COLUMNS = 5; + public const int ROWS = 3; + + public ListModel apps { private get; construct; } + + public AppCarouselPage (ListModel apps) { + Object (apps: apps); + } + + private Gtk.Grid grid; + + construct { + grid = new Gtk.Grid () { + row_homogeneous = true, + column_homogeneous = true, + row_spacing = 36, + column_spacing = 24, + }; + grid.add_css_class ("app-grid"); + + child = grid; + hexpand = true; + vexpand = true; + + apps.items_changed.connect (update_apps); + update_apps (); + } + + private void update_apps () { + while (grid.get_first_child () != null) { + grid.remove (grid.get_first_child ()); + } + + for (var row = 0; row < ROWS; row++) { + for (var column = 0; column < COLUMNS; column++) { + add_app (row, column); + } + } + } + + private inline void add_app (int row, int column) { + var n = row * COLUMNS + column; + + Gtk.Widget? widget; + if (n >= apps.get_n_items ()) { + widget = new Gtk.Grid (); + } else { + widget = new AppListItem (); + ((AppListItem) widget).bind_app ((App) apps.get_item (n)); + } + + grid.attach (widget, column, row); + } +} diff --git a/src/ApplicationMenu/AppCarousel/AppListItem.vala b/src/ApplicationMenu/AppCarousel/AppListItem.vala new file mode 100644 index 00000000..f4bbf446 --- /dev/null +++ b/src/ApplicationMenu/AppCarousel/AppListItem.vala @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Leonhard Kargl + */ + +public class Dock.AppListItem : Gtk.Button { + private const int ICON_SIZE = 64; + + private Granite.Bin app_icon_container; + private new Gtk.Label label; + private Gtk.Popover? popover_menu; + + construct { + app_icon_container = new Granite.Bin (); + + label = new Gtk.Label (null) { + ellipsize = END, + wrap_mode = WORD_CHAR, + max_width_chars = 16, + width_chars = 16, + lines = 2, + justify = CENTER, + }; + + var box = new Granite.Box (VERTICAL); + box.append (app_icon_container); + box.append (label); + + child = box; + add_css_class ("flat"); + + var gesture_click = new Gtk.GestureClick () { + button = Gdk.BUTTON_SECONDARY + }; + gesture_click.released.connect (on_secondary_click); + add_controller (gesture_click); + } + + private void on_secondary_click (int n_press, double x, double y) { + if (popover_menu == null) { + return; + } + + popover_menu.set_pointing_to ({ (int) x, (int) y, 0, 0 }); + popover_menu.popup (); + } + + public void bind_app (App app) { + app_icon_container.child = new AppIconWidget (app) { + icon_size = ICON_SIZE + }; + label.label = app.app_info.get_display_name (); + + popover_menu = AppMenuFactory.create_app_menu (app); + popover_menu.has_arrow = false; + popover_menu.halign = START; + popover_menu.position = BOTTOM; + popover_menu.set_parent (this); + } +} diff --git a/src/ApplicationMenu/AppStore.vala b/src/ApplicationMenu/AppStore.vala new file mode 100644 index 00000000..7c7fd7c0 --- /dev/null +++ b/src/ApplicationMenu/AppStore.vala @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2026 Leonhard Kargl + */ + +public class Dock.AppStore : Object { + private ListStore _apps; + public ListModel apps { get { return _apps; } } + + construct { + _apps = new ListStore (typeof (App)); + + load.begin (); + } + + private async void load () { + var app_system = AppSystem.get_default (); + var infos = AppInfo.get_all (); + var known_apps = new GenericSet (null, null); + + foreach (var info in infos) { + var app = app_system.get_app_by_id (info.get_id ()); + + if (app == null || app in known_apps) { + continue; + } + + if (!app.app_info.should_show ()) { + continue; + } + + _apps.append (app); + known_apps.add (app); + } + } +} diff --git a/src/ApplicationMenu/ApplicationMenu.vala b/src/ApplicationMenu/ApplicationMenu.vala new file mode 100644 index 00000000..ab151b0b --- /dev/null +++ b/src/ApplicationMenu/ApplicationMenu.vala @@ -0,0 +1,56 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 Leonhard Kargl + */ + +public class Dock.ApplicationMenu : Gtk.ApplicationWindow { + private Detective.Engine engine; + + construct { + engine = new Detective.Engine (); + + resizable = false; + child = new MainView (engine); + titlebar = new Gtk.Grid () { visible = false }; + hide_on_close = true; + + notify["is-active"].connect (on_is_active_changed); + + child.map.connect (init_panel); + + add_css_class ("application-menu"); + } + + private void on_is_active_changed () { + if (!is_active) { + // close (); + } + } + + private static Wl.RegistryListener registry_listener; + private void init_panel () { + warning ("Initializing panel"); + registry_listener.global = registry_handle_global; + + unowned var display = (Gdk.Wayland.Display) Gdk.Display.get_default (); + + unowned var wl_display = display.get_wl_display (); + var wl_registry = wl_display.get_registry (); + wl_registry.add_listener (registry_listener, this); + + wl_display.roundtrip (); + } + + private void registry_handle_global (Wl.Registry wl_registry, uint32 name, string @interface, uint32 version) { + if (@interface == "io_elementary_pantheon_shell_v1") { + var desktop_shell = wl_registry.bind (name, ref Pantheon.Desktop.Shell.iface, uint32.min (version, 1)); + + unowned var surface = (Gdk.Wayland.Surface) get_surface (); + unowned var wl_surface = surface.get_wl_surface (); + + var panel = desktop_shell.get_panel (wl_surface); + panel.add_blur (0, 0, 0, 0, 8); + warning ("Add blur"); + } + } +} diff --git a/src/ApplicationMenu/MainView.vala b/src/ApplicationMenu/MainView.vala new file mode 100644 index 00000000..9f296f2e --- /dev/null +++ b/src/ApplicationMenu/MainView.vala @@ -0,0 +1,90 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 Leonhard Kargl + */ + +public class Dock.MainView : Granite.Bin { + private const string APP_GRID = "app-grid"; + private const string SEARCH_VIEW = "search-view"; + + public Detective.Engine engine { private get; construct; } + + private Gtk.SearchEntry entry; + private SearchView search_view; + private Gtk.Stack stack; + + public MainView (Detective.Engine engine) { + Object (engine: engine); + } + + construct { + entry = new Gtk.SearchEntry () { + margin_top = 6, + margin_bottom = 6, + margin_start = 6, + margin_end = 6, + placeholder_text = _("Search apps, files and more..."), + search_delay = 0 + }; + + var app_carousel = new AppCarousel (); + + search_view = new SearchView (engine); + + stack = new Gtk.Stack () { + transition_type = CROSSFADE + }; + stack.add_named (app_carousel, APP_GRID); + stack.add_named (search_view, SEARCH_VIEW); + + var toolbar_view = new Adw.ToolbarView () { + content = stack + }; + toolbar_view.add_top_bar (entry); + + child = toolbar_view; + + map.connect (() => entry.grab_focus ()); + unmap.connect (on_unmap); + + entry.search_changed.connect (on_search_changed); + entry.activate.connect (on_entry_activated); + entry.stop_search.connect (close); + + var key_controller = new Gtk.EventControllerKey (); + key_controller.key_pressed.connect (on_key_pressed); + child.add_controller (key_controller); + } + + private void on_unmap () { + engine.clear_search (); + entry.text = ""; + } + + private void on_search_changed () { + if (entry.text.strip () != "") { + engine.search (entry.text); + stack.visible_child_name = SEARCH_VIEW; + } else { + engine.clear_search (); + stack.visible_child_name = APP_GRID; + } + } + + private void on_entry_activated () { + // search_view.activate_selected (); + } + + private bool on_key_pressed (uint keyval, uint keycode) { + if (keyval == Gdk.Key.Escape) { + close (); + return Gdk.EVENT_STOP; + } + + return Gdk.EVENT_PROPAGATE; + } + + private void close () { + activate_action_variant ("window.close", null); + } +} diff --git a/src/ApplicationMenu/SearchView/Preview/GenericPreview.vala b/src/ApplicationMenu/SearchView/Preview/GenericPreview.vala new file mode 100644 index 00000000..0bc032e8 --- /dev/null +++ b/src/ApplicationMenu/SearchView/Preview/GenericPreview.vala @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 Leonhard Kargl + */ + +public class Dock.GenericPreview : Granite.Bin { + private Gtk.Image icon; + + private Granite.HeaderLabel label; + + construct { + icon = new Gtk.Image () { + pixel_size = 64 + }; + + label = new Granite.HeaderLabel (""); + + var content = new Granite.Box (VERTICAL, HALF) { + margin_top = 12, + margin_start = 6, + margin_end = 12, + margin_bottom = 12 + }; + content.append (icon); + content.append (label); + + child = content; + } + + public void bind (Detective.Result result) { + icon.gicon = result.icon; + label.label = result.title; + label.secondary_text = result.description; + } +} diff --git a/src/ApplicationMenu/SearchView/Preview/Preview.vala b/src/ApplicationMenu/SearchView/Preview/Preview.vala new file mode 100644 index 00000000..c81169f9 --- /dev/null +++ b/src/ApplicationMenu/SearchView/Preview/Preview.vala @@ -0,0 +1,43 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 Leonhard Kargl + */ + +public class Dock.Preview : Granite.Bin { + public Gtk.SingleSelection selection { get; construct; } + + private Gtk.ScrolledWindow scrolled_window; + private GenericPreview generic_preview; + + public Preview (Gtk.SingleSelection selection) { + Object (selection: selection); + } + + construct { + scrolled_window = new Gtk.ScrolledWindow (); + + child = scrolled_window; + + generic_preview = new GenericPreview (); + + selection.notify["selected-item"].connect (on_selected_item_changed); + } + + private void on_selected_item_changed () { + var result = (Detective.Result) selection.selected_item; + + if (result == null) { + scrolled_window.child = null; + return; + } + + var custom_preview = result.get_custom_preview (); + if (custom_preview != null) { + scrolled_window.child = custom_preview; + return; + } + + generic_preview.bind (result); + scrolled_window.child = generic_preview; + } +} diff --git a/src/ApplicationMenu/SearchView/ResultRow.vala b/src/ApplicationMenu/SearchView/ResultRow.vala new file mode 100644 index 00000000..4913f88f --- /dev/null +++ b/src/ApplicationMenu/SearchView/ResultRow.vala @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 Leonhard Kargl + */ + +public class Dock.ResultRow : Granite.Bin { + private Gtk.Image icon; + private Gtk.Label label; + + construct { + icon = new Gtk.Image (); + + label = new Gtk.Label ("") { + hexpand = true, + xalign = 0, + ellipsize = END + }; + + var content = new Granite.Box (HORIZONTAL, HALF) { + margin_top = 6, + margin_bottom = 6, + margin_start = 6, + margin_end = 6, + }; + content.append (icon); + content.append (label); + + child = content; + } + + public void bind (Detective.Result result) { + icon.gicon = result.icon; + label.label = result.title; + } +} diff --git a/src/ApplicationMenu/SearchView/SearchView.vala b/src/ApplicationMenu/SearchView/SearchView.vala new file mode 100644 index 00000000..bce38078 --- /dev/null +++ b/src/ApplicationMenu/SearchView/SearchView.vala @@ -0,0 +1,116 @@ +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2025 Leonhard Kargl + */ + +public class Dock.SearchView : Granite.Bin { + private const int MAX_HEIGHT = 300; + + public Detective.Engine engine { get; construct; } + + private Gtk.SingleSelection selection_model; + private Gtk.ListView list_view; + private Gtk.ScrolledWindow scrolled_window; + + public SearchView (Detective.Engine engine) { + Object (engine: engine); + } + + construct { + selection_model = new Gtk.SingleSelection (engine.results) { + autoselect = false, + can_unselect = true + }; + + var factory = new Gtk.SignalListItemFactory (); + factory.setup.connect (on_row_setup); + factory.bind.connect (on_row_bind); + + var header_factory = new Gtk.SignalListItemFactory (); + header_factory.setup.connect (on_header_setup); + header_factory.bind.connect (on_header_bind); + + list_view = new Gtk.ListView (selection_model, factory) { + single_click_activate = true, + header_factory = header_factory + }; + list_view.add_css_class ("results-list"); + list_view.remove_css_class (Granite.STYLE_CLASS_VIEW); + + scrolled_window = new Gtk.ScrolledWindow () { + child = list_view, + hscrollbar_policy = NEVER, + }; + + var preview = new Preview (selection_model); + + var box = new Granite.Box (HORIZONTAL, DOUBLE) { + homogeneous = true, + }; + box.append (scrolled_window); + box.append (preview); + + child = box; + + list_view.activate.connect (activate_result); + + selection_model.items_changed.connect_after (on_items_changed); + } + + private void on_row_setup (Object obj) { + var list_item = (Gtk.ListItem) obj; + list_item.child = new ResultRow (); + } + + private void on_row_bind (Object obj) { + var list_item = (Gtk.ListItem) obj; + var item = (Detective.Result) list_item.item; + ((ResultRow) list_item.child).bind (item); + } + + private void on_header_setup (Object obj) { + var label = new Gtk.Label (null) { + halign = START + }; + label.add_css_class ("result-heading"); + + var list_header = (Gtk.ListHeader) obj; + list_header.child = label; + } + + private void on_header_bind (Object obj) { + var list_header = (Gtk.ListHeader) obj; + var item = (Detective.Result) list_header.item; + ((Gtk.Label) list_header.child).label = item.result_type_name; + } + + public void activate_selected () { + activate_result.begin (selection_model.selected); + } + + private async void activate_result (uint position) { + var result = (Detective.Result) engine.results.get_item (position); + + if (result == null) { + return; + } + + try { + yield result.activate (); + } catch (Error e) { + warning (e.message); + } + + close (); + } + + private void on_items_changed () { + if (selection_model.get_n_items () > 0) { + list_view.scroll_to (0, SELECT, null); + } + } + + private void close () { + activate_action_variant ("window.close", null); + } +} diff --git a/src/ItemManager.vala b/src/ItemManager.vala index 56408603..c81f0d47 100644 --- a/src/ItemManager.vala +++ b/src/ItemManager.vala @@ -19,6 +19,10 @@ } construct { + var app_menu_button = new Gtk.Button.from_icon_name ("open-menu") { + action_name = "app.show-app-menu", + }; + var app_group = new ItemGroup (AppSystem.get_default ().apps, (obj) => new Launcher ((App) obj)); var background_item = new BackgroundItem (); @@ -34,6 +38,7 @@ settings.bind ("icon-size", separator, "height-request", GET); #endif + append (app_menu_button); append (app_group); append (background_group); #if WORKSPACE_SWITCHER @@ -77,15 +82,19 @@ var app_system = AppSystem.get_default (); - var app = app_system.get_app (app_info.get_id ()); - if (app != null) { + var app = app_system.get_app_by_id (app_info.get_id ()); + + if (app == null) { + return; + } + + if (app.pinned || app.running) { + // Already in the dock app.pinned = true; drop_target_file.reject (); return; } - app = app_system.add_app_for_id (app_info.get_id ()); - for (var child = app_group.get_first_child (); child != null; child = child.get_next_sibling ()) { if (child is Launcher && child.app == app) { added_launcher = (Launcher) child; diff --git a/src/meson.build b/src/meson.build index 4e860dee..bda76a08 100644 --- a/src/meson.build +++ b/src/meson.build @@ -8,7 +8,19 @@ sources = [ 'ItemManager.vala', 'MainWindow.vala', 'RenderNodeWalker.vala', + 'ApplicationMenu' / 'ApplicationMenu.vala', + 'ApplicationMenu' / 'AppStore.vala', + 'ApplicationMenu' / 'MainView.vala', + 'ApplicationMenu' / 'AppCarousel' / 'AppCarousel.vala', + 'ApplicationMenu' / 'AppCarousel' / 'AppCarouselPage.vala', + 'ApplicationMenu' / 'AppCarousel' / 'AppListItem.vala', + 'ApplicationMenu' / 'SearchView' / 'ResultRow.vala', + 'ApplicationMenu' / 'SearchView' / 'SearchView.vala', + 'ApplicationMenu' / 'SearchView' / 'Preview' / 'GenericPreview.vala', + 'ApplicationMenu' / 'SearchView' / 'Preview' / 'Preview.vala', 'AppSystem' / 'App.vala', + 'AppSystem' / 'AppIconWidget.vala', + 'AppSystem' / 'AppMenuFactory.vala', 'AppSystem' / 'AppSystem.vala', 'AppSystem' / 'Launcher.vala', 'AppSystem' / 'PoofPopover.vala',