[gnome-shell] workspace: Add window overlay to WindowClone



commit 33ab53068e890d033f27a4ddcd086bb821c332de
Author: Jonas Dreßler <verdre v0yd nl>
Date:   Tue Jun 2 13:52:33 2020 +0200

    workspace: Add window overlay to WindowClone
    
    Add the window overlays we're currently showing using the WindowOverlay
    class to the WindowClone class and implement them using
    ClutterConstraints instead of the old fixed position/size layout, which
    had to be used because the workspaces were scaled, and the title and app
    icon were kept unscaled using a separate layer.
    
    Specifically, this is done by adding the ClutterClones to a static
    container owned by the WindowClone and adding the elements of the
    overlay as children to the WindowClone itself. That way the
    overlay-elements can use the container as a source for their constraints
    and we avoid having to make sure the overlays remain visible above the
    ClutterClones.
    
    We're not using the new overlays yet, they're hidden by default and
    showOverlay() isn't called anywhere yet.
    
    https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1298

 js/ui/workspace.js | 248 +++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 239 insertions(+), 9 deletions(-)
---
diff --git a/js/ui/workspace.js b/js/ui/workspace.js
index ef34860da2..97b5f8cc9a 100644
--- a/js/ui/workspace.js
+++ b/js/ui/workspace.js
@@ -216,9 +216,16 @@ var WindowClone = GObject.registerClass({
             reactive: true,
             can_focus: true,
             accessible_role: Atk.Role.PUSH_BUTTON,
-            layout_manager: new WindowCloneLayout(),
         });
 
+        this._windowContainer = new Clutter.Actor();
+        // gjs currently can't handle setting an actors layout manager during
+        // the initialization of the actor if that layout manager keeps track
+        // of its container, so set the layout manager after creating the
+        // container
+        this._windowContainer.layout_manager = new WindowCloneLayout();
+        this.add_child(this._windowContainer);
+
         this.set_offscreen_redirect(Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY);
 
         this._addWindow(realWindow.meta_window);
@@ -230,8 +237,8 @@ var WindowClone = GObject.registerClass({
         this._dragSlot = [0, 0, 0, 0];
         this._stackAbove = null;
 
-        this.layout_manager.connect('notify::bounding-box', () =>
-            this.emit('size-changed'));
+        this._windowContainer.layout_manager.connect(
+            'notify::bounding-box', () => this.emit('size-changed'));
 
         this._windowDestroyId =
             this.realWindow.connect('destroy', () => this.destroy());
@@ -258,6 +265,200 @@ var WindowClone = GObject.registerClass({
 
         this._selected = false;
         this._closeRequested = false;
+        this._idleHideOverlayId = 0;
+
+        this._border = new St.Widget({
+            visible: false,
+            style_class: 'window-clone-border',
+        });
+        this._borderConstraint = new Clutter.BindConstraint({
+            source: this._windowContainer,
+            coordinate: Clutter.BindCoordinate.SIZE,
+        });
+        this._border.add_constraint(this._borderConstraint);
+        this._border.add_constraint(new Clutter.AlignConstraint({
+            source: this._windowContainer,
+            align_axis: Clutter.AlignAxis.BOTH,
+            factor: 0.5,
+        }));
+        this._border.connect('style-changed',
+            this._onBorderStyleChanged.bind(this));
+
+        this._title = new St.Label({
+            visible: false,
+            style_class: 'window-caption',
+            text: this._getCaption(),
+            reactive: true,
+        });
+        this._title.add_constraint(new Clutter.AlignConstraint({
+            source: this._windowContainer,
+            align_axis: Clutter.AlignAxis.X_AXIS,
+            factor: 0.5,
+        }));
+        this._title.add_constraint(new Clutter.AlignConstraint({
+            source: this._windowContainer,
+            align_axis: Clutter.AlignAxis.Y_AXIS,
+            pivot_point: new Graphene.Point({ x: -1, y: 0.5 }),
+            factor: 1,
+        }));
+        this._title.clutter_text.ellipsize = Pango.EllipsizeMode.END;
+        this.label_actor = this._title;
+        this._updateCaptionId = this.metaWindow.connect('notify::title', () => {
+            this._title.text = this._getCaption();
+        });
+
+        const layout = Meta.prefs_get_button_layout();
+        const side = layout.left_buttons.includes(Meta.ButtonFunction.CLOSE)
+            ? St.Side.LEFT : St.Side.RIGHT;
+
+        this._closeButton = new St.Button({
+            visible: false,
+            style_class: 'window-close',
+            child: new St.Icon({ icon_name: 'window-close-symbolic' }),
+        });
+        this._closeButton.add_constraint(new Clutter.BindConstraint({
+            source: this._border,
+            coordinate: Clutter.BindCoordinate.POSITION,
+        }));
+        this._closeButton.add_constraint(new Clutter.AlignConstraint({
+            source: this._border,
+            align_axis: Clutter.AlignAxis.X_AXIS,
+            pivot_point: new Graphene.Point({ x: 0.5, y: -1 }),
+            factor: side === St.Side.LEFT ? 0 : 1,
+        }));
+        this._closeButton.add_constraint(new Clutter.AlignConstraint({
+            source: this._border,
+            align_axis: Clutter.AlignAxis.Y_AXIS,
+            pivot_point: new Graphene.Point({ x: -1, y: 0.5 }),
+            factor: 0,
+        }));
+        this._closeButton.connect('clicked', () => this.deleteAll());
+
+        this.add_child(this._border);
+        this.add_child(this._title);
+        this.add_child(this._closeButton);
+    }
+
+    vfunc_get_preferred_width(forHeight) {
+        const themeNode = this.get_theme_node();
+
+        // Only include window previews in size request, not chrome
+        const [minWidth, natWidth] =
+            this._windowContainer.get_preferred_width(
+                themeNode.adjust_for_height(forHeight));
+
+        return themeNode.adjust_preferred_width(minWidth, natWidth);
+    }
+
+    vfunc_get_preferred_height(forWidth) {
+        const themeNode = this.get_theme_node();
+        const [minHeight, natHeight] =
+            this._windowContainer.get_preferred_height(
+                themeNode.adjust_for_width(forWidth));
+
+        return themeNode.adjust_preferred_height(minHeight, natHeight);
+    }
+
+    vfunc_allocate(box) {
+        this.set_allocation(box);
+
+        for (const child of this)
+            child.allocate_available_size(0, 0, box.get_width(), box.get_height());
+    }
+
+    _onBorderStyleChanged() {
+        let borderNode = this._border.get_theme_node();
+        this._borderSize = borderNode.get_border_width(St.Side.TOP);
+
+        // Increase the size of the border actor so the border outlines
+        // the bounding box
+        this._borderConstraint.offset = this._borderSize * 2;
+    }
+
+    _windowCanClose() {
+        return this.metaWindow.can_close() &&
+               !this.hasAttachedDialogs();
+    }
+
+    _getCaption() {
+        if (this.metaWindow.title)
+            return this.metaWindow.title;
+
+        let tracker = Shell.WindowTracker.get_default();
+        let app = tracker.get_window_app(this.metaWindow);
+        return app.get_name();
+    }
+
+    chromeHeights() {
+        this._border.ensure_style();
+        this._title.ensure_style();
+        const [, closeButtonHeight] = this._closeButton.get_preferred_height(-1);
+        const [, titleHeight] = this._title.get_preferred_height(-1);
+
+        return [this._borderSize + closeButtonHeight / 2,
+                Math.max(this._borderSize, (titleHeight / 2) + (this._borderSize / 2))];
+    }
+
+    chromeWidths() {
+        this._border.ensure_style();
+        const [, closeButtonWidth] = this._closeButton.get_preferred_width(-1);
+
+        return [this._borderSize,
+                this._borderSize + closeButtonWidth / 2];
+    }
+
+    showOverlay(animate) {
+        const ongoingTransition = this._border.get_transition('opacity');
+
+        // Don't do anything if we're fully visible already
+        if (this._border.visible && !ongoingTransition)
+            return;
+
+        // If we're supposed to animate and an animation in our direction
+        // is already happening, let that one continue
+        if (animate &&
+            ongoingTransition &&
+            ongoingTransition.get_interval().peek_final_value() === 255)
+            return;
+
+        const toShow = this._windowCanClose()
+            ? [this._border, this._title, this._closeButton]
+            : [this._border, this._title];
+
+        toShow.forEach(a => {
+            a.opacity = 0;
+            a.show();
+            a.ease({
+                opacity: 255,
+                duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+            });
+        });
+    }
+
+    hideOverlay(animate) {
+        const ongoingTransition = this._border.get_transition('opacity');
+
+        // Don't do anything if we're fully hidden already
+        if (!this._border.visible && !ongoingTransition)
+            return;
+
+        // If we're supposed to animate and an animation in our direction
+        // is already happening, let that one continue
+        if (animate &&
+            ongoingTransition &&
+            ongoingTransition.get_interval().peek_final_value() === 0)
+            return;
+
+        [this._border, this._title, this._closeButton].forEach(a => {
+            a.opacity = 255;
+            a.ease({
+                opacity: 0,
+                duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+                onComplete: () => a.hide(),
+            });
+        });
     }
 
     _addWindow(metaWindow) {
@@ -271,7 +472,7 @@ var WindowClone = GObject.registerClass({
         // To avoid this, we hide it from pick.
         Shell.util_set_hidden_from_pick(clone, true);
 
-        this.layout_manager.addWindow(clone, metaWindow);
+        this._windowContainer.layout_manager.addWindow(clone, metaWindow);
     }
 
     vfunc_has_overlaps() {
@@ -290,11 +491,12 @@ var WindowClone = GObject.registerClass({
     }
 
     deleteAll() {
-        const windows = this.layout_manager.getWindows();
+        const windows = this._windowContainer.layout_manager.getWindows();
 
         // Delete all windows, starting from the bottom-most (most-modal) one
         for (const window of windows.reverse()) {
-            const metaWindow = this.layout_manager.getMetaWindow(window);
+            const metaWindow =
+                this._windowContainer.layout_manager.getMetaWindow(window);
 
             metaWindow.delete(global.get_current_time());
         }
@@ -318,7 +520,7 @@ var WindowClone = GObject.registerClass({
     }
 
     hasAttachedDialogs() {
-        return this.get_n_children() > 1;
+        return this._windowContainer.layout_manager.getWindows().length > 1;
     }
 
     _updateAttachedDialogs() {
@@ -338,7 +540,7 @@ var WindowClone = GObject.registerClass({
     }
 
     get boundingBox() {
-        const box = this.layout_manager.bounding_box;
+        const box = this._windowContainer.layout_manager.bounding_box;
 
         return {
             x: box.x1,
@@ -349,7 +551,7 @@ var WindowClone = GObject.registerClass({
     }
 
     get windowCenter() {
-        const box = this.layout_manager.bounding_box;
+        const box = this._windowContainer.layout_manager.bounding_box;
 
         return new Graphene.Point({
             x: box.get_x() + box.get_width() / 2,
@@ -392,11 +594,18 @@ var WindowClone = GObject.registerClass({
         this.metaWindow._delegate = null;
         this._delegate = null;
 
+        this.metaWindow.disconnect(this._updateCaptionId);
+
         if (this._longPressLater) {
             Meta.later_remove(this._longPressLater);
             delete this._longPressLater;
         }
 
+        if (this._idleHideOverlayId > 0) {
+            GLib.source_remove(this._idleHideOverlayId);
+            this._idleHideOverlayId = 0;
+        }
+
         if (this.inDrag) {
             this.emit('drag-end');
             this.inDrag = false;
@@ -415,6 +624,26 @@ var WindowClone = GObject.registerClass({
 
     vfunc_leave_event(crossingEvent) {
         this.emit('hide-chrome');
+
+        if (this._idleHideOverlayId > 0)
+            GLib.source_remove(this._idleHideOverlayId);
+
+        this._idleHideOverlayId = GLib.timeout_add(
+            GLib.PRIORITY_DEFAULT,
+            WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT, () => {
+                if (this._closeButton['has-pointer'] ||
+                    this._title['has-pointer'])
+                    return GLib.SOURCE_CONTINUE;
+
+                if (!this['has-pointer'])
+                    this.hideOverlay(true);
+
+                this._idleHideOverlayId = 0;
+                return GLib.SOURCE_REMOVE;
+            });
+
+        GLib.Source.set_name_by_id(this._idleHideOverlayId, '[gnome-shell] this._idleHideOverlayId');
+
         return super.vfunc_leave_event(crossingEvent);
     }
 
@@ -425,6 +654,7 @@ var WindowClone = GObject.registerClass({
 
     vfunc_key_focus_out() {
         super.vfunc_key_focus_out();
+        this.hideOverlay(true);
         this.emit('hide-chrome');
     }
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]