diff --git a/src/Launcher.vala b/src/Launcher.vala index d5528147..24f46ffd 100644 --- a/src/Launcher.vala +++ b/src/Launcher.vala @@ -3,7 +3,7 @@ * SPDX-FileCopyrightText: 2022 elementary, Inc. (https://elementary.io) */ -public class Dock.Launcher : Gtk.Button { +public class Dock.Launcher : Gtk.FlowBoxChild { // Matches icon size and padding in Launcher.css public const int ICON_SIZE = 48; public const int PADDING = 6; @@ -23,8 +23,8 @@ public class Dock.Launcher : Gtk.Button { private Gtk.PopoverMenu popover; - public Launcher (GLib.DesktopAppInfo app_info) { - Object (app_info: app_info); + public Launcher (GLib.DesktopAppInfo app_info, bool pinned) { + Object (app_info: app_info, pinned: pinned); } class construct { @@ -44,14 +44,14 @@ public class Dock.Launcher : Gtk.Button { foreach (var action in app_info.list_actions ()) { action_section.append ( app_info.get_action_name (action), - MainWindow.ACTION_PREFIX + MainWindow.LAUNCHER_ACTION_TEMPLATE.printf (app_info.get_id (), action) + LauncherManager.ACTION_PREFIX + LauncherManager.LAUNCHER_ACTION_TEMPLATE.printf (app_info.get_id (), action) ); } var pinned_section = new Menu (); pinned_section.append ( _("Keep in Dock"), - MainWindow.ACTION_PREFIX + MainWindow.LAUNCHER_PINNED_ACTION_TEMPLATE.printf (app_info.get_id ()) + LauncherManager.ACTION_PREFIX + LauncherManager.LAUNCHER_PINNED_ACTION_TEMPLATE.printf (app_info.get_id ()) ); var model = new Menu (); @@ -71,18 +71,13 @@ public class Dock.Launcher : Gtk.Button { }; image.get_style_context ().add_provider (css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); - // Needed to work around DnD bug where it - // would stop working once the button got clicked - var box = new Gtk.Box (VERTICAL, 0); - box.append (image); - - child = box; + child = image; tooltip_text = app_info.get_display_name (); var drag_source = new Gtk.DragSource () { actions = MOVE }; - box.add_controller (drag_source); + add_controller (drag_source); drag_source.prepare.connect (on_drag_prepare); drag_source.drag_begin.connect (on_drag_begin); drag_source.drag_cancel.connect (on_drag_cancel); @@ -91,18 +86,14 @@ public class Dock.Launcher : Gtk.Button { var drop_target = new Gtk.DropTarget (typeof (Launcher), MOVE) { preload = true }; - box.add_controller (drop_target); + add_controller (drop_target); drop_target.enter.connect (on_drop_enter); - notify["pinned"].connect (() => ((MainWindow) get_root ()).sync_pinned ()); - var gesture_click = new Gtk.GestureClick () { button = Gdk.BUTTON_SECONDARY }; add_controller (gesture_click); gesture_click.released.connect (popover.popup); - - clicked.connect (() => launch ()); } ~Launcher () { @@ -228,9 +219,9 @@ public class Dock.Launcher : Gtk.Button { popover.popup (); popover.start_animation (); - var box = (Gtk.Box) parent; + // var box = (Gtk.Box) parent; if (!windows.is_empty ()) { - window.move_launcher_after (this, (Launcher) box.get_last_child ()); + // LauncherManager.get_default ().move_launcher_after (this, (Launcher) box.get_last_child ()); } pinned = false; @@ -249,16 +240,16 @@ public class Dock.Launcher : Gtk.Button { if (obj != null && obj is Launcher) { Launcher source = (Launcher) obj; - Launcher target = this; + int target = get_index (); - if (source != target) { + if (source.get_index () != target) { if (((x > get_allocated_width () / 2) && get_next_sibling () == source) || ((x < get_allocated_width () / 2) && get_prev_sibling () != source) ) { - target = (Launcher) get_prev_sibling (); + target = target > 0 ? target-- : target; } - ((MainWindow) get_root ()).move_launcher_after (source, target); + LauncherManager.get_default ().move_launcher_after (source, target); } } } diff --git a/src/LauncherManager.vala b/src/LauncherManager.vala new file mode 100644 index 00000000..775f5d9f --- /dev/null +++ b/src/LauncherManager.vala @@ -0,0 +1,175 @@ +/* + * SPDX-License-Identifier: GPL-3.0 + * SPDX-FileCopyrightText: 2022 elementary, Inc. (https://elementary.io) + */ + +public class Dock.LauncherManager : GLib.Object { + public const string ACTION_GROUP_PREFIX = "app-actions"; + public const string ACTION_PREFIX = ACTION_GROUP_PREFIX + "."; + // First %s is the app id second %s the action name + public const string LAUNCHER_ACTION_TEMPLATE = "%s.%s"; + // %s is the app id + public const string LAUNCHER_PINNED_ACTION_TEMPLATE = "%s-pinned"; + + private static Settings settings; + + private static GLib.Once instance; + public static unowned LauncherManager get_default () { + return instance.once (() => { return new LauncherManager (); }); + } + + public ListStore launchers { get; construct; } + public SimpleActionGroup action_group { get; construct; } + + private Dock.DesktopIntegration desktop_integration; + private GLib.HashTable app_to_launcher; + + static construct { + settings = new Settings ("io.elementary.dock"); + } + + construct { + launchers = new ListStore (typeof (Launcher)); + app_to_launcher = new GLib.HashTable (str_hash, str_equal); + action_group = new SimpleActionGroup (); + + GLib.Bus.get_proxy.begin ( + GLib.BusType.SESSION, + "org.pantheon.gala", + "/org/pantheon/gala/DesktopInterface", + GLib.DBusProxyFlags.NONE, + null, + (obj, res) => { + try { + desktop_integration = GLib.Bus.get_proxy.end (res); + desktop_integration.windows_changed.connect (sync_windows); + + sync_windows (); + } catch (GLib.Error e) { + critical (e.message); + } + }); + + foreach (string app_id in settings.get_strv ("launchers")) { + var app_info = new GLib.DesktopAppInfo (app_id); + add_launcher (app_info, true); + } + } + + private unowned Launcher add_launcher (GLib.DesktopAppInfo app_info, bool pinned = false) { + var launcher = new Launcher (app_info, pinned); + + unowned var app_id = app_info.get_id (); + app_to_launcher.insert (app_id, launcher); + launchers.append (launcher); + + var pinned_action = new SimpleAction.stateful ( + LAUNCHER_PINNED_ACTION_TEMPLATE.printf (app_id), + null, + new Variant.boolean (launcher.pinned) + ); + pinned_action.change_state.connect ((new_state) => launcher.pinned = (bool) new_state); + action_group.add_action (pinned_action); + + foreach (var action in app_info.list_actions ()) { + var simple_action = new SimpleAction (LAUNCHER_ACTION_TEMPLATE.printf (app_id, action), null); + simple_action.activate.connect (() => launcher.launch (action)); + action_group.add_action (simple_action); + } + + launcher.notify["pinned"].connect (() => { + pinned_action.set_state (launcher.pinned); + sync_pinned (); + }); + + return app_to_launcher[app_id]; + } + + private void remove_launcher (Launcher launcher) { + foreach (var action in action_group.list_actions ()) { + if (action.has_prefix (launcher.app_info.get_id ())) { + action_group.remove_action (action); + } + } + + launchers.remove (launcher.get_index ()); + app_to_launcher.remove (launcher.app_info.get_id ()); + } + + private void sync_windows () requires (desktop_integration != null) { + DesktopIntegration.Window[] windows; + try { + windows = desktop_integration.get_windows (); + } catch (Error e) { + critical (e.message); + return; + } + + var launcher_window_list = new GLib.HashTable> (direct_hash, direct_equal); + foreach (unowned var window in windows) { + unowned var app_id = window.properties["app-id"].get_string (); + unowned Launcher? launcher = app_to_launcher[app_id]; + if (launcher == null) { + var app_info = new GLib.DesktopAppInfo (app_id); + if (app_info == null) { + continue; + } + + launcher = add_launcher (app_info); + } + + AppWindow? app_window = launcher.find_window (window.uid); + if (app_window == null) { + app_window = new AppWindow (window.uid); + } + + unowned var window_list = launcher_window_list.get (launcher); + if (window_list == null) { + var new_window_list = new GLib.List (); + new_window_list.append ((owned) app_window); + launcher_window_list.insert (launcher, (owned) new_window_list); + } else { + window_list.append ((owned) app_window); + } + } + + foreach (var launcher in app_to_launcher.get_values ()) { + var window_list = launcher_window_list.take (launcher); + launcher.update_windows ((owned) window_list); + } + + sync_pinned (); + } + + public void move_launcher_after (Launcher source, int target_index) { + int si = source.get_index (); + + launchers.remove (si); + launchers.insert (target_index, source); + + var dir = si > target_index ? Gtk.DirectionType.RIGHT : Gtk.DirectionType.LEFT; + + for (int i = (dir == RIGHT ? target_index : si); i <= (dir == RIGHT ? si : target_index); i++) { + ((Launcher) launchers.get_item (i)).animate_move (dir); + } + + sync_pinned (); + } + + public void sync_pinned () { + string[] new_pinned_ids = {}; + + for (int i = 0; i < launchers.get_n_items (); i++) { + var current_child = (Launcher) launchers.get_item (i); + + if (current_child.pinned) { + new_pinned_ids += current_child.app_info.get_id (); + } else if (!current_child.pinned && current_child.windows.is_empty ()) { + remove_launcher (current_child); + } + } + + var settings = new Settings ("io.elementary.dock"); + settings.set_strv ("launchers", new_pinned_ids); + } +} diff --git a/src/MainWindow.vala b/src/MainWindow.vala index 1c9fcd17..6860633d 100644 --- a/src/MainWindow.vala +++ b/src/MainWindow.vala @@ -4,20 +4,9 @@ */ public class Dock.MainWindow : Gtk.ApplicationWindow { - // First %s is the app id second %s the action name - public const string LAUNCHER_ACTION_TEMPLATE = "%s.%s"; - // %s is the app id - public const string LAUNCHER_PINNED_ACTION_TEMPLATE = "%s-pinned"; - public const string ACTION_GROUP_PREFIX = "win"; - public const string ACTION_PREFIX = ACTION_GROUP_PREFIX + "."; - private static Gtk.CssProvider css_provider; private static Settings settings; - private Gtk.Box box; - private Dock.DesktopIntegration desktop_integration; - private GLib.HashTable app_to_launcher; - class construct { set_css_name ("dock"); } @@ -30,200 +19,32 @@ public class Dock.MainWindow : Gtk.ApplicationWindow { } construct { - app_to_launcher = new GLib.HashTable (str_hash, str_equal); get_style_context ().add_provider (css_provider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION); - box = new Gtk.Box (Gtk.Orientation.HORIZONTAL, 0); + var launcher_manager = LauncherManager.get_default (); + + var flow_box = new Gtk.FlowBox () { + max_children_per_line = 1, + orientation = VERTICAL + }; + flow_box.bind_model (launcher_manager.launchers, (obj) => { + return (Launcher) obj; + }); var empty_title = new Gtk.Label ("") { visible = false }; - child = box; + child = flow_box; overflow = Gtk.Overflow.VISIBLE; resizable = false; set_titlebar (empty_title); + insert_action_group (LauncherManager.ACTION_GROUP_PREFIX, launcher_manager.action_group); // Fixes DnD reordering of launchers failing on a very small line between two launchers var drop_target_launcher = new Gtk.DropTarget (typeof (Launcher), MOVE); - box.add_controller (drop_target_launcher); - - GLib.Bus.get_proxy.begin ( - GLib.BusType.SESSION, - "org.pantheon.gala", - "/org/pantheon/gala/DesktopInterface", - GLib.DBusProxyFlags.NONE, - null, - (obj, res) => { - try { - desktop_integration = GLib.Bus.get_proxy.end (res); - desktop_integration.windows_changed.connect (() => { - sync_windows (); - }); - - sync_windows (); - } catch (GLib.Error e) { - critical (e.message); - } - }); - - foreach (string app_id in settings.get_strv ("launchers")) { - var app_info = new GLib.DesktopAppInfo (app_id); - unowned var launcher = add_launcher (app_info); - launcher.pinned = true; - } - } - - private unowned Launcher add_launcher (GLib.DesktopAppInfo app_info) { - var launcher = new Launcher (app_info); - unowned var app_id = app_info.get_id (); - app_to_launcher.insert (app_id, launcher); - box.append (launcher); - - var pinned_action = new SimpleAction.stateful ( - LAUNCHER_PINNED_ACTION_TEMPLATE.printf (app_id), - null, - new Variant.boolean (launcher.pinned) - ); - launcher.notify["pinned"].connect (() => pinned_action.set_state (launcher.pinned)); - pinned_action.change_state.connect ((new_state) => launcher.pinned = (bool) new_state); - add_action (pinned_action); - - foreach (var action in app_info.list_actions ()) { - var simple_action = new SimpleAction (LAUNCHER_ACTION_TEMPLATE.printf (app_id, action), null); - simple_action.activate.connect (() => launcher.launch (action)); - add_action (simple_action); - } - - return app_to_launcher[app_id]; - } - - private void sync_windows () requires (desktop_integration != null) { - DesktopIntegration.Window[] windows; - try { - windows = desktop_integration.get_windows (); - } catch (Error e) { - critical (e.message); - return; - } - - var launcher_window_list = new GLib.HashTable> (direct_hash, direct_equal); - foreach (unowned var window in windows) { - unowned var app_id = window.properties["app-id"].get_string (); - unowned Launcher? launcher = app_to_launcher.get (app_id); - if (launcher == null) { - var app_info = new GLib.DesktopAppInfo (app_id); - if (app_info == null) { - continue; - } - - launcher = add_launcher (app_info); - } - - AppWindow? app_window = launcher.find_window (window.uid); - if (app_window == null) { - app_window = new AppWindow (window.uid); - } - - unowned var window_list = launcher_window_list.get (launcher); - if (window_list == null) { - var new_window_list = new GLib.List (); - new_window_list.append ((owned) app_window); - launcher_window_list.insert (launcher, (owned) new_window_list); - } else { - window_list.append ((owned) app_window); - } - } - - app_to_launcher.foreach_remove ((app_id, launcher) => { - var window_list = launcher_window_list.take (launcher); - launcher.update_windows ((owned) window_list); - if (launcher.windows.is_empty () && !launcher.pinned) { - remove_launcher (launcher, false); - return true; - } - - return false; - }); - } - - public void move_launcher_after (Launcher source, Launcher? target) { - var before_source = source.get_prev_sibling (); - - box.reorder_child_after (source, target); - - /* - * should_animate toggles to true once either the launcher before the one - * that was moved is reached or once the one that was moved is reached - * and goes false again once the other one is reached. While true - * all launchers that are iterated over are animated to move in the appropriate - * direction. - */ - bool should_animate = false; - Gtk.DirectionType dir = UP; // UP is an invalid placeholder value - - // source was the first launcher in the box so we start animating right away - if (before_source == null) { - should_animate = true; - dir = LEFT; - } - - Launcher child = (Launcher) box.get_first_child (); - while (child != null) { - if (child == source) { - should_animate = !should_animate; - if (should_animate) { - dir = RIGHT; - } - } - - if (should_animate && child != source) { - child.animate_move (dir); - } - - if (child == before_source) { - should_animate = !should_animate; - if (should_animate) { - dir = LEFT; - } - } - - child = (Launcher) child.get_next_sibling (); - } - - sync_pinned (); - } - - public void remove_launcher (Launcher launcher, bool from_map = true) { - foreach (var action in list_actions ()) { - if (action.has_prefix (launcher.app_info.get_id ())) { - remove_action (action); - } - } - box.remove (launcher); - - if (from_map) { - app_to_launcher.remove (launcher.app_info.get_id ()); - } - } - - public void sync_pinned () { - string[] new_pinned_ids = {}; - - unowned Launcher child = (Launcher) box.get_first_child (); - while (child != null) { - unowned var current_child = child; - child = (Launcher) child.get_next_sibling (); - - if (current_child.pinned) { - new_pinned_ids += current_child.app_info.get_id (); - } else if (!current_child.pinned && current_child.windows.is_empty ()) { - remove_launcher (current_child); - } - } - + flow_box.add_controller (drop_target_launcher); - var settings = new Settings ("io.elementary.dock"); - settings.set_strv ("launchers", new_pinned_ids); + flow_box.child_activated.connect ((child) => ((Launcher) child).launch ()); } } diff --git a/src/meson.build b/src/meson.build index 82306818..fbeaa67b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -3,6 +3,7 @@ sources = [ 'AppWindow.vala', 'DesktopIntegration.vala', 'Launcher.vala', + 'LauncherManager.vala', 'MainWindow.vala', 'PoofPopover.vala' ]