diff --git a/data/confirm.svg b/data/confirm.svg new file mode 100644 index 000000000..2d92d7322 --- /dev/null +++ b/data/confirm.svg @@ -0,0 +1,184 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + diff --git a/data/gala.gresource.xml b/data/gala.gresource.xml index 1bcc56851..92ef51d9e 100644 --- a/data/gala.gresource.xml +++ b/data/gala.gresource.xml @@ -19,6 +19,7 @@ close.svg resize.svg + confirm.svg shaders/box-blur.frag diff --git a/plugins/pip/Main.vala b/plugins/pip/Main.vala index c5976e7e2..0ec53febc 100644 --- a/plugins/pip/Main.vala +++ b/plugins/pip/Main.vala @@ -1,42 +1,21 @@ -// -// Copyright (C) 2017 Adam Bieńkowski -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2017 Adam Bieńkowski + * 2024-2025 elementary, Inc. (https://elementary.io) + */ public class Gala.Plugins.PIP.Plugin : Gala.Plugin { - private const int MIN_SELECTION_SIZE = 30; + public const int MIN_SELECTION_SIZE = 100; - private Gee.ArrayList windows; - private Gala.WindowManager? wm = null; + private Gee.ArrayList windows = new Gee.ArrayList (); + private WindowManager wm; private SelectionArea? selection_area; - private static inline bool meta_rectangle_contains (Mtk.Rectangle rect, int x, int y) { - return x >= rect.x && x < rect.x + rect.width - && y >= rect.y && y < rect.y + rect.height; - } - - construct { - windows = new Gee.ArrayList (); - } + public override void initialize (Gala.WindowManager _wm) { + wm = _wm; - public override void initialize (Gala.WindowManager wm) { - this.wm = wm; - var display = wm.get_display (); var settings = new GLib.Settings ("io.elementary.desktop.wm.keybindings"); - - display.add_keybinding ("pip", settings, IGNORE_AUTOREPEAT, on_initiate); + wm.get_display ().add_keybinding ("pip", settings, IGNORE_AUTOREPEAT, on_initiate); } public override void destroy () { @@ -49,10 +28,23 @@ public class Gala.Plugins.PIP.Plugin : Gala.Plugin { windows.clear (); } - private void on_initiate (Meta.Display display, Meta.Window? window, Clutter.KeyEvent? event, - Meta.KeyBinding binding) { - selection_area = new SelectionArea (wm); - selection_area.selected.connect (on_selection_actor_selected); + private void on_initiate (Meta.Display display, Meta.Window? window, Clutter.KeyEvent? event, Meta.KeyBinding binding) { + unowned var target_window = display.focus_window; + if (target_window == null || !Utils.get_window_is_normal (target_window) || target_window.skip_taskbar) { + return; + } + + var target_frame = target_window.get_frame_rect (); + if (target_frame.width < MIN_SELECTION_SIZE || target_frame.height < MIN_SELECTION_SIZE) { + return; + } + + unowned var target_actor = (Meta.WindowActor) target_window.get_compositor_private (); + if (target_actor == null) { + return; + } + + selection_area = new SelectionArea (wm, target_actor); selection_area.captured.connect (on_selection_actor_captured); selection_area.closed.connect (clear_selection_area); @@ -62,141 +54,48 @@ public class Gala.Plugins.PIP.Plugin : Gala.Plugin { selection_area.start_selection (); } - private void on_selection_actor_selected (int x, int y) { - clear_selection_area (); - select_window_at (x, y); - } - - private void on_selection_actor_captured (int x, int y, int width, int height) { - clear_selection_area (); - - var active = get_active_window_actor (); - if (active == null) { - return; - } - - var window = active.meta_window; - var buffer_rect = window.get_buffer_rect (); - var frame_rect = window.get_frame_rect (); + private void on_selection_actor_captured (Mtk.Rectangle selection) { + var popup_window = new PopupWindow (wm.get_display (), selection_area.target_actor); + windows.add (popup_window); + wm.ui_group.add_child (popup_window); - Mtk.Rectangle mtk_rect = {x, y, width, height}; - Mtk.Rectangle intersection; - mtk_rect.intersect (frame_rect, out intersection); + popup_window.show.connect ((_popup_window) => { + track_actor (_popup_window); + update_region (); + }); + popup_window.hide.connect ((_popup_window) => { + untrack_actor (_popup_window); + update_region (); + }); + popup_window.closed.connect ((_popup_window) => { + windows.remove (_popup_window); + untrack_window (_popup_window); + }); - // Compensate for server-side window decorations - var x_offset = buffer_rect.x - frame_rect.x; - var y_offset = buffer_rect.y - frame_rect.y; + var frame = selection_area.target_actor.meta_window.get_frame_rect (); - Graphene.Rect rect = { - {intersection.x - buffer_rect.x + x_offset, intersection.y - buffer_rect.y + y_offset}, - {intersection.width, intersection.height} - }; + // Don't clip if the entire window was selected + if (!frame.equal (selection)) { + selection.x -= frame.x; + selection.y -= frame.y; - if (rect.get_width () < MIN_SELECTION_SIZE || rect.get_height () < MIN_SELECTION_SIZE) { - select_window_at (x, y); - return; + // FIXME: Mtk.Rectangle.to_graphene_rect is broken in Vala + popup_window.set_container_clip ({ { selection.x, selection.y }, { selection.width, selection.height } }); } - var popup_window = new PopupWindow (wm.get_display (), active); - popup_window.set_container_clip (rect); - popup_window.show.connect (on_popup_window_show); - popup_window.hide.connect (on_popup_window_hide); - add_window (popup_window); - } - - private void on_popup_window_show (Clutter.Actor popup_window) { - track_actor (popup_window); - update_region (); - } - - private void on_popup_window_hide (Clutter.Actor popup_window) { - untrack_actor (popup_window); - update_region (); - } - - private void select_window_at (int x, int y) { - var selected = get_window_actor_at (x, y); - if (selected != null) { - var popup_window = new PopupWindow (wm.get_display (), selected); - popup_window.show.connect (on_popup_window_show); - popup_window.hide.connect (on_popup_window_hide); - add_window (popup_window); - } + clear_selection_area (); } private void clear_selection_area () { - if (selection_area != null) { - untrack_actor (selection_area); - update_region (); - - selection_area.destroy (); - selection_area = null; + if (selection_area == null) { + return; } - } - - private Meta.WindowActor? get_window_actor_at (int x, int y) { - unowned Meta.Display display = wm.get_display (); -#if HAS_MUTTER48 - unowned List actors = display.get_compositor ().get_window_actors (); -#else - unowned List actors = display.get_window_actors (); -#endif - - var copy = actors.copy (); - copy.reverse (); - - weak Meta.WindowActor? selected = null; - copy.@foreach ((actor) => { - if (selected != null) { - return; - } - - var window = actor.get_meta_window (); - var rect = window.get_frame_rect (); - - if (!actor.is_destroyed () && !window.is_hidden () && !window.is_skip_taskbar () && meta_rectangle_contains (rect, x, y)) { - selected = actor; - } - }); - - return selected; - } - - private Meta.WindowActor? get_active_window_actor () { - unowned Meta.Display display = wm.get_display (); -#if HAS_MUTTER48 - unowned List actors = display.get_compositor ().get_window_actors (); -#else - unowned List actors = display.get_window_actors (); -#endif - - var copy = actors.copy (); - copy.reverse (); - - weak Meta.WindowActor? active = null; - actors.@foreach ((actor) => { - if (active != null) { - return; - } - - var window = actor.get_meta_window (); - if (!actor.is_destroyed () && !window.is_hidden () && !window.is_skip_taskbar () && window.has_focus ()) { - active = actor; - } - }); - - return active; - } - private void add_window (PopupWindow popup_window) { - popup_window.closed.connect (() => remove_window (popup_window)); - windows.add (popup_window); - wm.ui_group.add_child (popup_window); - } + untrack_actor (selection_area); + update_region (); - private void remove_window (PopupWindow popup_window) { - windows.remove (popup_window); - untrack_window (popup_window); + selection_area.destroy (); + selection_area = null; } private void untrack_window (PopupWindow popup_window) { diff --git a/plugins/pip/PopupWindow.vala b/plugins/pip/PopupWindow.vala index 5e6fbb319..90170ea25 100644 --- a/plugins/pip/PopupWindow.vala +++ b/plugins/pip/PopupWindow.vala @@ -1,6 +1,6 @@ /* * Copyright 2017 Adam Bieńkowski - * Copyright 2023 elementary, Inc. + * Copyright 2023-2025 elementary, Inc. * SPDX-License-Identifier: GPL-3.0-or-later */ @@ -62,8 +62,8 @@ public class Gala.Plugins.PIP.PopupWindow : Clutter.Actor { clone = new Clutter.Clone (window_actor); clone_container = new Clutter.Actor () { - scale_x = 0.35f, - scale_y = 0.35f + scale_x = 0.35, + scale_y = 0.35 }; clone_container.add_child (clone); @@ -319,11 +319,7 @@ public class Gala.Plugins.PIP.PopupWindow : Clutter.Actor { opacity = 0; restore_easing_state (); -#if HAS_MUTTER48 - GLib.Timeout.add (duration, () => { -#else - Clutter.Threads.Timeout.add (duration, () => { -#endif + Timeout.add (duration, () => { closed (); return Source.REMOVE; }); diff --git a/plugins/pip/SelectionArea.vala b/plugins/pip/SelectionArea.vala index fb6ee4ac0..979a8ee14 100644 --- a/plugins/pip/SelectionArea.vala +++ b/plugins/pip/SelectionArea.vala @@ -1,117 +1,306 @@ -// -// Copyright (C) 2017 Santiago León O., Adam Bieńkowski -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -// +/* + * SPDX-License-Identifier: GPL-3.0-or-later + * SPDX-FileCopyrightText: 2017 Santiago León O. + * 2017 Adam Bieńkowski + * 2025 elementary, Inc. (https://elementary.io) + */ public class Gala.Plugins.PIP.SelectionArea : CanvasActor { - public signal void captured (int x, int y, int width, int height); - public signal void selected (int x, int y); + private const int HANDLER_RADIUS = 6; + private const int BORDER_WIDTH = 2; + private const int RESIZE_THRESHOLD = 10; + private const int CONFIRM_BUTTON_SIZE = 48; + + public signal void captured (Mtk.Rectangle selection); public signal void closed (); public Gala.WindowManager wm { get; construct; } - + public Meta.WindowActor target_actor { get; construct; } + + /** + * A clone of the "target_actor" displayed and clipped while the selection + * mask is visible. + */ + private Clutter.Actor clone; + + private Mtk.Rectangle max_size; + private Mtk.Rectangle selection; + private float monitor_scale; + private Clutter.Actor confirm_button; private Gala.ModalProxy? modal_proxy; - private Graphene.Point start_point; - private Graphene.Point end_point; + + /** + * If the user is resizing the selection area and the resize handler used. + */ + private bool resizing = false; + private bool resizing_top = false; + private bool resizing_bottom = false; + private bool resizing_left = false; + private bool resizing_right = false; + + /** + * If the user is dragging the selection area and the starting point. + */ private bool dragging = false; - private bool clicked = false; + private float drag_x = 0.0f; + private float drag_y = 0.0f; - public SelectionArea (Gala.WindowManager wm) { - Object (wm: wm); + public SelectionArea (WindowManager wm, Meta.WindowActor target_actor) { + Object (wm: wm, target_actor: target_actor); } construct { - start_point = { 0, 0 }; - end_point = { 0, 0 }; + unowned var window = target_actor.meta_window; + + max_size = window.get_frame_rect (); + selection = max_size; visible = true; reactive = true; + clone = new Clutter.Clone (target_actor) { + x = target_actor.x, + y = target_actor.y + }; + wm.ui_group.add_child (clone); + + unowned var display = wm.get_display (); + monitor_scale = display.get_monitor_scale (window.get_monitor ()); + int screen_width, screen_height; - wm.get_display ().get_size (out screen_width, out screen_height); + display.get_size (out screen_width, out screen_height); width = screen_width; height = screen_height; + +#if HAS_MUTTER49 + var click_action = new Clutter.ClickGesture (); +#else + var click_action = new Clutter.ClickAction (); +#endif + +#if HAS_MUTTER49 + click_action.recognize.connect (capture_selected_area); +#else + click_action.clicked.connect (capture_selected_area); +#endif + + confirm_button = new Gala.Icon.from_resource ( + CONFIRM_BUTTON_SIZE, + monitor_scale, + "/org/pantheon/desktop/gala/buttons/confirm.svg" + ) { + reactive = true + }; + confirm_button.add_action (click_action); + add_child (confirm_button); + + update_confirm_button_position (); } - public override bool key_press_event (Clutter.Event e) { - if (e.get_key_symbol () == Clutter.Key.Escape) { - close (); - closed (); - return true; + public override bool key_press_event (Clutter.Event event) { + switch (event.get_key_symbol ()) { + case Clutter.Key.Escape: + close (); + closed (); + + return Clutter.EVENT_STOP; + case Clutter.Key.Return: + case Clutter.Key.KP_Enter: + capture_selected_area (); + + return Clutter.EVENT_STOP; } - return false; + return Clutter.EVENT_PROPAGATE; } - public override bool button_press_event (Clutter.Event e) { - if (dragging || e.get_button () != Clutter.Button.PRIMARY) { - return true; + private void capture_selected_area () { + close (); + captured (selection); + } + + public override bool button_press_event (Clutter.Event event) { + if (dragging || resizing || event.get_button () != Clutter.Button.PRIMARY) { + return Clutter.EVENT_STOP; } - clicked = true; + float event_x, event_y; + event.get_coords (out event_x, out event_y); + + // Check that the user clicked on a resize handler + resizing_top = is_close_to_coord (event_y, selection.y, RESIZE_THRESHOLD); + resizing_bottom = is_close_to_coord (event_y, selection.y + selection.height, RESIZE_THRESHOLD); + resizing_left = is_close_to_coord (event_x, selection.x, RESIZE_THRESHOLD); + resizing_right = is_close_to_coord (event_x, selection.x + selection.width, RESIZE_THRESHOLD); + resizing = (resizing_top && resizing_left) || + (resizing_top && resizing_right) || + (resizing_bottom && resizing_left) || + (resizing_bottom && resizing_right); + + if (resizing) { + return Clutter.EVENT_STOP; + } - float press_x, press_y; - e.get_coords (out press_x, out press_y); - start_point = { (int) press_x, (int) press_y}; +#if HAS_MUTTER48 + dragging = selection.contains_pointf (event_x, event_y); +#else + dragging = selection.contains_rect ({ (int) event_x, (int) event_y, 0, 0 }); +#endif + if (dragging) { + drag_x = event_x - selection.x; + drag_y = event_y - selection.y; +#if HAS_MUTTER50 + set_cursor_type (MOVE); +#elif HAS_MUTTER48 + wm.get_display ().set_cursor (MOVE); +#else + wm.get_display ().set_cursor (MOVE_OR_RESIZE_WINDOW); +#endif + + return Clutter.EVENT_STOP; + } - return true; + return Clutter.EVENT_STOP; } - public override bool button_release_event (Clutter.Event e) { - if (e.get_button () != Clutter.Button.PRIMARY) { - return true; + private static bool is_close_to_coord (float c, int target, int threshold) { + return (c >= target - threshold) && + (c <= target + threshold); + } + + public override bool button_release_event (Clutter.Event event) { + if (event.get_button () != Clutter.Button.PRIMARY) { + return Clutter.EVENT_STOP; } - if (!dragging) { - float event_x, event_y; - e.get_coords (out event_x, out event_y); - selected ((int) event_x, (int) event_y); + float event_x, event_y; + event.get_coords (out event_x, out event_y); + +#if HAS_MUTTER48 + if (!resizing && !dragging && !selection.contains_pointf (event_x, event_y)) { +#else + if (!resizing && !dragging && !selection.contains_rect ({ (int) event_x, (int) event_y, 0, 0 })) { +#endif close (); + closed (); return true; } dragging = false; - clicked = false; + resizing = false; +#if HAS_MUTTER50 + set_cursor_type (DEFAULT); +#else + wm.get_display ().set_cursor (DEFAULT); +#endif - int x, y, w, h; - get_selection_rectangle (out x, out y, out w, out h); - close (); - start_point = { 0, 0 }; - end_point = { 0, 0 }; - this.hide (); - content.invalidate (); + return Clutter.EVENT_STOP; + } - captured (x, y, w, h); + public override bool motion_event (Clutter.Event event) { + set_mouse_cursor_on_motion (event); - return true; - } + if (!resizing && !dragging) { + return Clutter.EVENT_STOP; + } - public override bool motion_event (Clutter.Event e) { - if (!clicked) { - return true; + if (resizing) { + resize_selection_area (event); + } else if (dragging) { + drag_selection_area (event); } - float press_x, press_y; - e.get_coords (out press_x, out press_y); - end_point = { (int) press_x, (int) press_y}; content.invalidate (); - if (!dragging) { - dragging = true; + return Clutter.EVENT_STOP; + } + + private void resize_selection_area (Clutter.Event event) { + float event_x, event_y; + event.get_coords (out event_x, out event_y); + + var start_x = selection.x; + var end_x = selection.x + selection.width; + var start_y = selection.y; + var end_y = selection.y + selection.height; + + if (resizing_top) { + start_y = (int) event_y.clamp (max_size.y, end_y - Plugin.MIN_SELECTION_SIZE); + } else if (resizing_bottom) { + end_y = (int) event_y.clamp (start_y + Plugin.MIN_SELECTION_SIZE, max_size.y + max_size.height); + } + + if (resizing_left) { + start_x = (int) event_x.clamp (max_size.x, end_x - Plugin.MIN_SELECTION_SIZE); + } else if (resizing_right) { + end_x = (int) event_x.clamp (start_x + Plugin.MIN_SELECTION_SIZE, max_size.x + max_size.width); + } + + selection = { start_x, start_y, end_x - start_x, end_y - start_y }; + + update_confirm_button_position (); + } + + private void drag_selection_area (Clutter.Event event) { + float event_x, event_y; + event.get_coords (out event_x, out event_y); + + selection.x = (int) (event_x - drag_x).clamp (max_size.x, max_size.x + max_size.width - selection.width); + selection.y = (int) (event_y - drag_y).clamp (max_size.y, max_size.y + max_size.height - selection.height); + + update_confirm_button_position (); + } + + private void update_confirm_button_position () { + confirm_button.set_position ( + selection.x + (selection.width - (int) confirm_button.width) / 2, + selection.y + (selection.height - (int) confirm_button.height) / 2 + ); + } + + private void set_mouse_cursor_on_motion (Clutter.Event event) { + if (resizing || dragging) { + return; } - return true; + float event_x, event_y; + event.get_coords (out event_x, out event_y); + + var top = is_close_to_coord (event_y, selection.y, RESIZE_THRESHOLD); + var bottom = is_close_to_coord (event_y, selection.y + selection.height, RESIZE_THRESHOLD); + var left = is_close_to_coord (event_x, selection.x, RESIZE_THRESHOLD); + var right = is_close_to_coord (event_x, selection.x + selection.width, RESIZE_THRESHOLD); + + if (top && left) { +#if HAS_MUTTER50 + set_cursor_type (NW_RESIZE); +#else + wm.get_display ().set_cursor (NW_RESIZE); +#endif + } else if (top && right) { +#if HAS_MUTTER50 + set_cursor_type (NE_RESIZE); +#else + wm.get_display ().set_cursor (NE_RESIZE); +#endif + } else if (bottom && left) { +#if HAS_MUTTER50 + set_cursor_type (SW_RESIZE); +#else + wm.get_display ().set_cursor (SW_RESIZE); +#endif + } else if (bottom && right) { +#if HAS_MUTTER50 + set_cursor_type (SE_RESIZE); +#else + wm.get_display ().set_cursor (SE_RESIZE); +#endif + } else { +#if HAS_MUTTER50 + set_cursor_type (DEFAULT); +#else + wm.get_display ().set_cursor (DEFAULT); +#endif + } } public void close () { @@ -120,6 +309,7 @@ public class Gala.Plugins.PIP.SelectionArea : CanvasActor { #else wm.get_display ().set_cursor (Meta.Cursor.DEFAULT); #endif + wm.ui_group.remove_child (clone); if (modal_proxy != null) { wm.pop_modal (modal_proxy); @@ -127,45 +317,62 @@ public class Gala.Plugins.PIP.SelectionArea : CanvasActor { } public void start_selection () { -#if HAS_MUTTER50 - set_cursor_type (Clutter.CursorType.CROSSHAIR); -#else - wm.get_display ().set_cursor (Meta.Cursor.CROSSHAIR); -#endif grab_key_focus (); modal_proxy = wm.push_modal (this, true); } - private void get_selection_rectangle (out int x, out int y, out int width, out int height) { - x = (int) float.min (start_point.x, end_point.x); - y = (int) float.min (start_point.y, end_point.y); - width = (int) (start_point.x - end_point.x).abs (); - height = (int) (start_point.y - end_point.y).abs (); - } - protected override void draw (Cairo.Context ctx, int width, int height) { + // Draws a full-screen colored rectangle with a smaller transparent + // rectangle inside with border with handlers ctx.save (); - ctx.set_operator (Cairo.Operator.CLEAR); ctx.paint (); + ctx.restore (); + // Full-screen rectangle + ctx.save (); + ctx.set_operator (Cairo.Operator.OVER); + ctx.rectangle (0, 0, width, height); + ctx.set_source_rgba (0.0, 0.0, 0.0, 0.5); + ctx.fill (); ctx.restore (); - if (!dragging) { - return; - } + // Transparent rectangle + ctx.save (); + ctx.set_operator (Cairo.Operator.SOURCE); + ctx.rectangle (selection.x, selection.y, selection.width, selection.height); + ctx.set_source_rgba (0.0, 0.0, 0.0, 0.0); + ctx.fill (); + ctx.restore (); - int x, y, w, h; - get_selection_rectangle (out x, out y, out w, out h); + ctx.save (); - ctx.rectangle (x, y, w, h); - ctx.set_source_rgba (0.1, 0.1, 0.1, 0.2); - ctx.fill (); + var accent_color = Drawing.StyleManager.get_instance ().theme_accent_color; + ctx.set_source_rgba (accent_color.red / 255.0, accent_color.green / 255.0, accent_color.blue / 255.0, 1.0); - ctx.rectangle (x, y, w, h); - ctx.set_source_rgb (0.7, 0.7, 0.7); - ctx.set_line_width (1.0); + // Border + ctx.set_operator (Cairo.Operator.OVER); + ctx.rectangle (selection.x, selection.y, selection.width, selection.height); + ctx.set_line_width (Utils.scale_to_int (BORDER_WIDTH, monitor_scale)); ctx.stroke (); + + // Handlers + var start_x = selection.x; + var end_x = selection.x + selection.width; + var start_y = selection.y; + var end_y = selection.y + selection.height; + var scaled_handler_radius = Utils.scale_to_int (HANDLER_RADIUS, monitor_scale); + + ctx.arc (start_x, start_y, scaled_handler_radius, 0.0, 2.0 * Math.PI); + ctx.fill (); + ctx.arc (start_x, end_y, scaled_handler_radius, 0.0, 2.0 * Math.PI); + ctx.fill (); + ctx.arc (end_x, start_y, scaled_handler_radius, 0.0, 2.0 * Math.PI); + ctx.fill (); + ctx.arc (end_x, end_y, scaled_handler_radius, 0.0, 2.0 * Math.PI); + ctx.fill (); + + ctx.restore (); } }