ShellClients: Add Panels API

This commit is contained in:
Leonhard 2024-05-24 12:47:02 +02:00 committed by Leonhard
parent e4933a652f
commit 7ac65758ed
7 changed files with 667 additions and 7 deletions

View File

@ -439,4 +439,12 @@
<description></description>
</key>
</schema>
<schema path="/io/elementary/desktop/wm/shell/" id="io.elementary.desktop.wm.shell">
<key type="aas" name="trusted-clients">
<default>[['io.elementary.dock'], ["io.elementary.wingpanel"]]</default>
<summary>Clients to be launched by gala as trusted for interacting with the shell wl protcol.</summary>
<description>An array of arrays of arguments to be used to launch the client.</description>
</key>
</schema>
</schemalist>

View File

@ -0,0 +1,191 @@
/*
* Copyright 2024 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/
public class Gala.HideTracker : Object {
private const uint UPDATE_TIMEOUT = 200;
public signal void hide ();
public signal void show ();
public Meta.Display display { get; construct; }
public PanelWindow panel { get; construct; }
public PanelWindow.HideMode hide_mode { get; set; default = NEVER; }
private bool hovered = false;
private bool overlap = false;
private bool focus_overlap = false;
private bool focus_maximized_overlap = false;
private uint update_timeout_id = 0;
public HideTracker (Meta.Display display, PanelWindow panel) {
Object (display: display, panel: panel);
}
construct {
var current_focus_window = display.focus_window;
track_focus_window (current_focus_window);
display.notify["focus-window"].connect (() => {
untrack_focus_window (current_focus_window);
current_focus_window = display.focus_window;
track_focus_window (current_focus_window);
});
display.window_created.connect ((window) => {
schedule_update ();
window.unmanaged.connect (schedule_update);
});
var cursor_tracker = display.get_cursor_tracker ();
cursor_tracker.position_invalidated.connect (() => {
#if HAS_MUTTER45
var has_pointer = panel.window.has_pointer ();
#else
var has_pointer = window_has_pointer ();
#endif
if (hovered != has_pointer) {
hovered = has_pointer;
schedule_update ();
}
});
display.get_workspace_manager ().active_workspace_changed.connect (schedule_update);
}
//Can be removed with mutter > 45
private bool window_has_pointer () {
var cursor_tracker = display.get_cursor_tracker ();
Graphene.Point pointer_pos;
cursor_tracker.get_pointer (out pointer_pos, null);
var window_rect = panel.get_custom_window_rect ();
Graphene.Rect graphene_window_rect = {
{
window_rect.x,
window_rect.y
},
{
window_rect.width,
window_rect.height
}
};
return graphene_window_rect.contains_point (pointer_pos);
}
private void track_focus_window (Meta.Window? window) {
if (window == null) {
return;
}
window.position_changed.connect (schedule_update);
window.size_changed.connect (schedule_update);
schedule_update ();
}
private void untrack_focus_window (Meta.Window? window) {
if (window == null) {
return;
}
window.position_changed.disconnect (schedule_update);
window.size_changed.disconnect (schedule_update);
schedule_update ();
}
public void schedule_update () {
if (update_timeout_id != 0) {
return;
}
update_timeout_id = Timeout.add (UPDATE_TIMEOUT, () => {
update_overlap ();
update_timeout_id = 0;
return Source.REMOVE;
});
}
private void update_overlap () {
overlap = false;
focus_overlap = false;
focus_maximized_overlap = false;
foreach (var window in display.get_workspace_manager ().get_active_workspace ().list_windows ()) {
if (window == panel.window) {
continue;
}
if (window.minimized) {
continue;
}
var type = window.get_window_type ();
if (type == DESKTOP || type == DOCK || type == MENU || type == SPLASHSCREEN) {
continue;
}
if (!panel.get_custom_window_rect ().overlap (window.get_frame_rect ())) {
continue;
}
overlap = true;
if (window != display.focus_window) {
continue;
}
focus_overlap = true;
focus_maximized_overlap = window.get_maximized () == BOTH;
}
update_hidden ();
}
private void update_hidden () {
switch (hide_mode) {
case NEVER:
toggle_display (false);
break;
case MAXIMIZED_FOCUS_WINDOW:
toggle_display (focus_maximized_overlap);
break;
case OVERLAPPING_FOCUS_WINDOW:
toggle_display (focus_overlap);
break;
case OVERLAPPING_WINDOW:
toggle_display (overlap);
break;
case ALWAYS:
toggle_display (true);
break;
}
}
private void toggle_display (bool should_hide) {
if (should_hide) {
// Don't hide if we have transients, e.g. an open popover, dialog, etc.
var has_transients = false;
panel.window.foreach_transient (() => {
has_transients = true;
return false;
});
if (hovered || has_transients) {
return;
}
hide ();
} else {
show ();
}
}
}

View File

@ -0,0 +1,132 @@
/*
* Copyright 2024 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/
public class Gala.PanelClone : Object {
private const int ANIMATION_DURATION = 250;
public WindowManager wm { get; construct; }
public PanelWindow panel { get; construct; }
public PanelWindow.HideMode hide_mode {
get {
return hide_tracker.hide_mode;
}
set {
hide_tracker.hide_mode = value;
}
}
public bool panel_hidden { get; private set; default = false; }
private SafeWindowClone clone;
private Meta.WindowActor actor;
private HideTracker hide_tracker;
public PanelClone (WindowManager wm, PanelWindow panel) {
Object (wm: wm, panel: panel);
}
construct {
clone = new SafeWindowClone (panel.window, true) {
visible = false
};
wm.ui_group.add_child (clone);
actor = (Meta.WindowActor) panel.window.get_compositor_private ();
// WindowActor position and Window position aren't necessarily the same.
// The clone needs the actor position
actor.notify["x"].connect (update_clone_position);
actor.notify["y"].connect (update_clone_position);
// Actor visibility might be changed by something else e.g. workspace switch
// but we want to keep it in sync with us
actor.notify["visible"].connect (update_visible);
notify["panel-hidden"].connect (() => {
update_visible ();
// When hidden changes schedule an update to make sure it's actually
// correct since things might have changed during the animation
hide_tracker.schedule_update ();
});
hide_tracker = new HideTracker (wm.get_display (), panel);
hide_tracker.hide.connect (hide);
hide_tracker.show.connect (show);
update_visible ();
update_clone_position ();
}
private void update_visible () {
actor.visible = !panel_hidden;
}
private void update_clone_position () {
clone.set_position (calculate_clone_x (panel_hidden), calculate_clone_y (panel_hidden));
}
private float calculate_clone_x (bool hidden) {
switch (panel.anchor) {
case TOP:
case BOTTOM:
return actor.x;
default:
return 0;
}
}
private float calculate_clone_y (bool hidden) {
switch (panel.anchor) {
case TOP:
return hidden ? actor.y - actor.height : actor.y;
case BOTTOM:
return hidden ? actor.y + actor.height : actor.y;
default:
return 0;
}
}
private void hide () {
if (panel_hidden) {
return;
}
panel_hidden = true;
if (panel.anchor != TOP && panel.anchor != BOTTOM) {
warning ("Animated hide not supported for side yet.");
return;
}
clone.visible = true;
clone.save_easing_state ();
clone.set_easing_mode (Clutter.AnimationMode.EASE_OUT_QUAD);
clone.set_easing_duration (wm.enable_animations && !wm.workspace_view.is_opened () ? ANIMATION_DURATION : 0);
clone.y = calculate_clone_y (true);
clone.restore_easing_state ();
}
public void show () {
if (!panel_hidden) {
return;
}
var animation_duration = wm.enable_animations && !wm.workspace_view.is_opened () ? ANIMATION_DURATION : 0;
clone.save_easing_state ();
clone.set_easing_mode (Clutter.AnimationMode.EASE_OUT_QUAD);
clone.set_easing_duration (animation_duration);
clone.y = calculate_clone_y (false);
clone.restore_easing_state ();
Timeout.add (animation_duration, () => {
clone.visible = false;
panel_hidden = false;
return Source.REMOVE;
});
}
}

View File

@ -0,0 +1,258 @@
/*
* Copyright 2024 elementary, Inc. (https://elementary.io)
* SPDX-License-Identifier: GPL-3.0-or-later
*
* Authored by: Leonhard Kargl <leo.kargl@proton.me>
*/
public class Gala.PanelWindow : Object {
public enum HideMode {
NEVER,
MAXIMIZED_FOCUS_WINDOW,
OVERLAPPING_FOCUS_WINDOW,
OVERLAPPING_WINDOW,
ALWAYS
}
private const int BARRIER_OFFSET = 50; // Allow hot corner trigger
private static HashTable<Meta.Window, Meta.Strut?> window_struts = new HashTable<Meta.Window, Meta.Strut?> (null, null);
public WindowManager wm { get; construct; }
public Meta.Window window { get; construct; }
public bool hidden { get; private set; default = false; }
public Meta.Side anchor;
private Barrier? barrier;
private PanelClone clone;
private int width = -1;
private int height = -1;
public PanelWindow (WindowManager wm, Meta.Window window, Meta.Side anchor) {
Object (wm: wm, window: window);
// Meta.Side seems to be currently not supported as GLib.Object property ...?
// At least it always crashed for me with some paramspec, g_type_fundamental backtrace
this.anchor = anchor;
}
construct {
window.size_changed.connect (position_window);
window.unmanaged.connect (() => {
destroy_barrier ();
if (window_struts.remove (window)) {
update_struts ();
}
});
window.stick ();
clone = new PanelClone (wm, this);
}
#if HAS_MUTTER46
public Mtk.Rectangle get_custom_window_rect () {
#else
public Meta.Rectangle get_custom_window_rect () {
#endif
var window_rect = window.get_frame_rect ();
if (width > 0) {
window_rect.width = width;
}
if (height > 0) {
window_rect.height = height;
}
return window_rect;
}
public void set_size (int width, int height) {
this.width = width;
this.height = height;
position_window ();
set_hide_mode (clone.hide_mode); // Resetup barriers etc.
}
public void update_anchor (Meta.Side anchor) {
this.anchor = anchor;
position_window ();
set_hide_mode (clone.hide_mode); // Resetup barriers etc.
}
private void position_window () {
var display = wm.get_display ();
var monitor_geom = display.get_monitor_geometry (display.get_primary_monitor ());
var window_rect = get_custom_window_rect ();
switch (anchor) {
case TOP:
position_window_top (monitor_geom, window_rect);
break;
case BOTTOM:
position_window_bottom (monitor_geom, window_rect);
break;
default:
warning ("Side not supported yet");
break;
}
}
#if HAS_MUTTER45
private void position_window_top (Mtk.Rectangle monitor_geom, Mtk.Rectangle window_rect) {
#else
private void position_window_top (Meta.Rectangle monitor_geom, Meta.Rectangle window_rect) {
#endif
var x = monitor_geom.x + (monitor_geom.width - window_rect.width) / 2;
move_window_idle (x, monitor_geom.y);
}
#if HAS_MUTTER45
private void position_window_bottom (Mtk.Rectangle monitor_geom, Mtk.Rectangle window_rect) {
#else
private void position_window_bottom (Meta.Rectangle monitor_geom, Meta.Rectangle window_rect) {
#endif
var x = monitor_geom.x + (monitor_geom.width - window_rect.width) / 2;
var y = monitor_geom.y + monitor_geom.height - window_rect.height;
move_window_idle (x, y);
}
private void move_window_idle (int x, int y) {
Idle.add (() => {
window.move_frame (false, x, y);
return Source.REMOVE;
});
}
public void set_hide_mode (HideMode hide_mode) {
clone.hide_mode = hide_mode;
destroy_barrier ();
if (hide_mode != NEVER) {
unmake_exclusive ();
setup_barrier ();
} else {
make_exclusive ();
}
}
private void make_exclusive () {
window.size_changed.connect (update_strut);
update_strut ();
}
private void update_strut () {
var rect = get_custom_window_rect ();
Meta.Strut strut = {
rect,
anchor
};
window_struts[window] = strut;
update_struts ();
}
private void update_struts () {
var list = new SList<Meta.Strut?> ();
foreach (var window_strut in window_struts.get_values ()) {
list.append (window_strut);
}
foreach (var workspace in wm.get_display ().get_workspace_manager ().get_workspaces ()) {
workspace.set_builtin_struts (list);
}
}
private void unmake_exclusive () {
if (window in window_struts) {
window.size_changed.disconnect (update_strut);
window_struts.remove (window);
update_struts ();
}
}
private void destroy_barrier () {
if (barrier != null) {
barrier.destroy ();
barrier = null;
}
}
private void setup_barrier () {
var display = wm.get_display ();
var monitor_geom = display.get_monitor_geometry (display.get_primary_monitor ());
var scale = display.get_monitor_scale (display.get_primary_monitor ());
var offset = InternalUtils.scale_to_int (BARRIER_OFFSET, scale);
switch (anchor) {
case TOP:
setup_barrier_top (monitor_geom, offset);
break;
case BOTTOM:
setup_barrier_bottom (monitor_geom, offset);
break;
default:
warning ("Barrier side not supported yet");
break;
}
}
#if HAS_MUTTER45
private void setup_barrier_top (Mtk.Rectangle monitor_geom, int offset) {
#else
private void setup_barrier_top (Meta.Rectangle monitor_geom, int offset) {
#endif
barrier = new Barrier (
monitor_geom.x + offset,
monitor_geom.y,
monitor_geom.x + monitor_geom.width - offset,
monitor_geom.y,
POSITIVE_Y,
0,
0,
int.MAX,
int.MAX
);
barrier.trigger.connect (clone.show);
}
#if HAS_MUTTER45
private void setup_barrier_bottom (Mtk.Rectangle monitor_geom, int offset) {
#else
private void setup_barrier_bottom (Meta.Rectangle monitor_geom, int offset) {
#endif
barrier = new Barrier (
monitor_geom.x + offset,
monitor_geom.y + monitor_geom.height,
monitor_geom.x + monitor_geom.width - offset,
monitor_geom.y + monitor_geom.height,
NEGATIVE_Y,
0,
0,
int.MAX,
int.MAX
);
barrier.trigger.connect (clone.show);
}
}

View File

@ -6,15 +6,85 @@
*/
public class Gala.ShellClientsManager : Object {
public Meta.Display display { get; construct; }
private static ShellClientsManager instance;
public static void init (WindowManager wm) {
if (instance != null) {
return;
}
instance = new ShellClientsManager (wm);
}
public static ShellClientsManager? get_instance () {
return instance;
}
public WindowManager wm { get; construct; }
private NotificationsClient notifications_client;
private ManagedClient[] protocol_clients = {};
public ShellClientsManager (Meta.Display display) {
Object (display: display);
private GLib.HashTable<Meta.Window, PanelWindow> windows = new GLib.HashTable<Meta.Window, PanelWindow> (null, null);
private ShellClientsManager (WindowManager wm) {
Object (wm: wm);
}
construct {
notifications_client = new NotificationsClient (display);
notifications_client = new NotificationsClient (wm.get_display ());
var shell_settings = new Settings ("io.elementary.desktop.wm.shell");
var clients = shell_settings.get_value ("trusted-clients");
foreach (var client in clients) {
protocol_clients += new ManagedClient (wm.get_display (), client.get_strv ());
}
}
public void set_anchor (Meta.Window window, Meta.Side side) {
if (window in windows) {
windows[window].update_anchor (side);
return;
}
#if HAS_MUTTER46
foreach (var client in protocol_clients) {
if (client.wayland_client.owns_window (window)) {
client.wayland_client.make_dock (window);
break;
}
}
#endif
// TODO: Return if requested by window that's not a trusted client?
windows[window] = new PanelWindow (wm, window, side);
// connect_after so we make sure the PanelWindow can destroy its barriers and struts
window.unmanaged.connect_after (() => windows.remove (window));
}
/**
* The size given here is only used for the hide mode. I.e. struts
* and collision detection with other windows use this size. By default
* or if set to -1 the size of the window is used.
*
* TODO: Maybe use for strut only?
*/
public void set_size (Meta.Window window, int width, int height) {
if (!(window in windows)) {
warning ("Set anchor for window before size.");
return;
}
windows[window].set_size (width, height);
}
public void set_hide_mode (Meta.Window window, PanelWindow.HideMode hide_mode) {
if (!(window in windows)) {
warning ("Set anchor for window before hide mode.");
return;
}
windows[window].set_hide_mode (hide_mode);
}
}

View File

@ -83,8 +83,6 @@ namespace Gala {
private DaemonManager daemon_manager;
private ShellClientsManager shell_clients_manager;
private WindowGrabTracker window_grab_tracker;
private NotificationStack notification_stack;
@ -145,7 +143,7 @@ namespace Gala {
}
public override void start () {
shell_clients_manager = new ShellClientsManager (get_display ());
ShellClientsManager.init (this);
daemon_manager = new DaemonManager (get_display ());
window_grab_tracker = new WindowGrabTracker (get_display ());

View File

@ -39,8 +39,11 @@ gala_bin_sources = files(
'HotCorners/Barrier.vala',
'HotCorners/HotCorner.vala',
'HotCorners/HotCornerManager.vala',
'ShellClients/HideTracker.vala',
'ShellClients/ManagedClient.vala',
'ShellClients/NotificationsClient.vala',
'ShellClients/PanelClone.vala',
'ShellClients/PanelWindow.vala',
'ShellClients/ShellClientsManager.vala',
'Widgets/DwellClickTimer.vala',
'Widgets/IconGroup.vala',