[gnome-shell] screenshot-ui: Add area selection



commit f3d59912eca29c6018db3b519fd06cbf0f52200d
Author: Ivan Molodetskikh <yalterz gmail com>
Date:   Mon Aug 16 14:34:49 2021 +0300

    screenshot-ui: Add area selection
    
    Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1954>

 .../gnome-shell-sass/widgets/_screenshot.scss      |  22 +
 js/ui/screenshot.js                                | 700 ++++++++++++++++++++-
 2 files changed, 712 insertions(+), 10 deletions(-)
---
diff --git a/data/theme/gnome-shell-sass/widgets/_screenshot.scss 
b/data/theme/gnome-shell-sass/widgets/_screenshot.scss
index 1c46146fa8..6df6e5f59e 100644
--- a/data/theme/gnome-shell-sass/widgets/_screenshot.scss
+++ b/data/theme/gnome-shell-sass/widgets/_screenshot.scss
@@ -70,6 +70,28 @@
   }
 }
 
+.screenshot-ui-area-indicator-shade {
+  background-color: rgba(0, 0, 0, .3);
+}
+
+.screenshot-ui-area-selector {
+  .screenshot-ui-area-indicator-shade {
+    background-color: rgba(0, 0, 0, .5);
+  }
+
+  .screenshot-ui-area-indicator-selection {
+    border: 2px white;
+  }
+}
+
+.screenshot-ui-area-selector-handle {
+  border-radius: 99px;
+  background-color: white;
+  box-shadow: 0 1px 3px 2px rgba(0, 0, 0, 0.2);
+  width: 24px;
+  height: 24px;
+}
+
 .screenshot-ui-screen-selector {
   transition-duration: 200ms;
   background-color: rgba(0, 0, 0, .5);
diff --git a/js/ui/screenshot.js b/js/ui/screenshot.js
index 60e383f17c..1871300343 100644
--- a/js/ui/screenshot.js
+++ b/js/ui/screenshot.js
@@ -44,6 +44,594 @@ class IconLabelButton extends St.Button {
     }
 });
 
+var UIAreaIndicator = GObject.registerClass(
+class UIAreaIndicator extends St.Widget {
+    _init(params) {
+        super._init(params);
+
+        this._topRect = new St.Widget({ style_class: 'screenshot-ui-area-indicator-shade' });
+        this._topRect.add_constraint(new Clutter.BindConstraint({
+            source: this,
+            coordinate: Clutter.BindCoordinate.WIDTH,
+        }));
+        this._topRect.add_constraint(new Clutter.SnapConstraint({
+            source: this,
+            from_edge: Clutter.SnapEdge.TOP,
+            to_edge: Clutter.SnapEdge.TOP,
+        }));
+        this._topRect.add_constraint(new Clutter.SnapConstraint({
+            source: this,
+            from_edge: Clutter.SnapEdge.LEFT,
+            to_edge: Clutter.SnapEdge.LEFT,
+        }));
+        this.add_child(this._topRect);
+
+        this._bottomRect = new St.Widget({ style_class: 'screenshot-ui-area-indicator-shade' });
+        this._bottomRect.add_constraint(new Clutter.BindConstraint({
+            source: this,
+            coordinate: Clutter.BindCoordinate.WIDTH,
+        }));
+        this._bottomRect.add_constraint(new Clutter.SnapConstraint({
+            source: this,
+            from_edge: Clutter.SnapEdge.BOTTOM,
+            to_edge: Clutter.SnapEdge.BOTTOM,
+        }));
+        this._bottomRect.add_constraint(new Clutter.SnapConstraint({
+            source: this,
+            from_edge: Clutter.SnapEdge.LEFT,
+            to_edge: Clutter.SnapEdge.LEFT,
+        }));
+        this.add_child(this._bottomRect);
+
+        this._leftRect = new St.Widget({ style_class: 'screenshot-ui-area-indicator-shade' });
+        this._leftRect.add_constraint(new Clutter.SnapConstraint({
+            source: this,
+            from_edge: Clutter.SnapEdge.LEFT,
+            to_edge: Clutter.SnapEdge.LEFT,
+        }));
+        this._leftRect.add_constraint(new Clutter.SnapConstraint({
+            source: this._topRect,
+            from_edge: Clutter.SnapEdge.TOP,
+            to_edge: Clutter.SnapEdge.BOTTOM,
+        }));
+        this._leftRect.add_constraint(new Clutter.SnapConstraint({
+            source: this._bottomRect,
+            from_edge: Clutter.SnapEdge.BOTTOM,
+            to_edge: Clutter.SnapEdge.TOP,
+        }));
+        this.add_child(this._leftRect);
+
+        this._rightRect = new St.Widget({ style_class: 'screenshot-ui-area-indicator-shade' });
+        this._rightRect.add_constraint(new Clutter.SnapConstraint({
+            source: this,
+            from_edge: Clutter.SnapEdge.RIGHT,
+            to_edge: Clutter.SnapEdge.RIGHT,
+        }));
+        this._rightRect.add_constraint(new Clutter.SnapConstraint({
+            source: this._topRect,
+            from_edge: Clutter.SnapEdge.TOP,
+            to_edge: Clutter.SnapEdge.BOTTOM,
+        }));
+        this._rightRect.add_constraint(new Clutter.SnapConstraint({
+            source: this._bottomRect,
+            from_edge: Clutter.SnapEdge.BOTTOM,
+            to_edge: Clutter.SnapEdge.TOP,
+        }));
+        this.add_child(this._rightRect);
+
+        this._selectionRect = new St.Widget({ style_class: 'screenshot-ui-area-indicator-selection' });
+        this.add_child(this._selectionRect);
+
+        this._topRect.add_constraint(new Clutter.SnapConstraint({
+            source: this._selectionRect,
+            from_edge: Clutter.SnapEdge.BOTTOM,
+            to_edge: Clutter.SnapEdge.TOP,
+        }));
+
+        this._bottomRect.add_constraint(new Clutter.SnapConstraint({
+            source: this._selectionRect,
+            from_edge: Clutter.SnapEdge.TOP,
+            to_edge: Clutter.SnapEdge.BOTTOM,
+        }));
+
+        this._leftRect.add_constraint(new Clutter.SnapConstraint({
+            source: this._selectionRect,
+            from_edge: Clutter.SnapEdge.RIGHT,
+            to_edge: Clutter.SnapEdge.LEFT,
+        }));
+
+        this._rightRect.add_constraint(new Clutter.SnapConstraint({
+            source: this._selectionRect,
+            from_edge: Clutter.SnapEdge.LEFT,
+            to_edge: Clutter.SnapEdge.RIGHT,
+        }));
+    }
+
+    setSelectionRect(x, y, width, height) {
+        this._selectionRect.set_position(x, y);
+        this._selectionRect.set_size(width, height);
+    }
+});
+
+var UIAreaSelector = GObject.registerClass({
+    Signals: { 'drag-started': {}, 'drag-ended': {} },
+}, class UIAreaSelector extends St.Widget {
+    _init(params) {
+        super._init(params);
+
+        // During a drag, this can be Clutter.BUTTON_PRIMARY,
+        // Clutter.BUTTON_SECONDARY or the string "touch" to identify the source
+        // of the drag operation.
+        this._dragButton = 0;
+        this._dragDevice = null;
+        this._dragSequence = null;
+
+        this._areaIndicator = new UIAreaIndicator();
+        this._areaIndicator.add_constraint(new Clutter.BindConstraint({
+            source: this,
+            coordinate: Clutter.BindCoordinate.ALL,
+        }));
+        this.add_child(this._areaIndicator);
+
+        this._topLeftHandle = new St.Widget({ style_class: 'screenshot-ui-area-selector-handle' });
+        this.add_child(this._topLeftHandle);
+        this._topRightHandle = new St.Widget({ style_class: 'screenshot-ui-area-selector-handle' });
+        this.add_child(this._topRightHandle);
+        this._bottomLeftHandle = new St.Widget({ style_class: 'screenshot-ui-area-selector-handle' });
+        this.add_child(this._bottomLeftHandle);
+        this._bottomRightHandle = new St.Widget({ style_class: 'screenshot-ui-area-selector-handle' });
+        this.add_child(this._bottomRightHandle);
+
+        // This will be updated before the first drawn frame.
+        this._handleSize = 0;
+        this._topLeftHandle.connect('style-changed', widget => {
+            this._handleSize = widget.get_theme_node().get_width();
+            this._updateSelectionRect();
+        });
+
+        this.connect('notify::mapped', () => {
+            if (this.mapped) {
+                const [x, y] = global.get_pointer();
+                this._updateCursor(x, y);
+            }
+        });
+
+        // Initialize area to out of bounds so reset() below resets it.
+        this._startX = -1;
+        this._startY = 0;
+        this._lastX = 0;
+        this._lastY = 0;
+
+        this.reset();
+    }
+
+    reset() {
+        this.stopDrag();
+        global.display.set_cursor(Meta.Cursor.DEFAULT);
+
+        // Preserve area selection if possible. If the area goes out of bounds,
+        // the monitors might have changed, so reset the area.
+        const [x, y, w, h] = this.getGeometry();
+        if (x < 0 || y < 0 || x + w > this.width || y + h > this.height) {
+            // Initialize area to out of bounds so if there's no monitor,
+            // the area will be reset once a monitor does appear.
+            this._startX = -1;
+            this._startY = 0;
+            this._lastX = 0;
+            this._lastY = 0;
+
+            // This can happen when running headless without any monitors.
+            if (Main.layoutManager.primaryIndex !== -1) {
+                const monitor =
+                    Main.layoutManager.monitors[Main.layoutManager.primaryIndex];
+
+                this._startX = monitor.x + Math.floor(monitor.width * 3 / 8);
+                this._startY = monitor.y + Math.floor(monitor.height * 3 / 8);
+                this._lastX = monitor.x + Math.floor(monitor.width * 5 / 8) - 1;
+                this._lastY = monitor.y + Math.floor(monitor.height * 5 / 8) - 1;
+            }
+
+            this._updateSelectionRect();
+        }
+    }
+
+    getGeometry() {
+        const leftX = Math.min(this._startX, this._lastX);
+        const topY = Math.min(this._startY, this._lastY);
+        const rightX = Math.max(this._startX, this._lastX);
+        const bottomY = Math.max(this._startY, this._lastY);
+
+        return [leftX, topY, rightX - leftX + 1, bottomY - topY + 1];
+    }
+
+    _updateSelectionRect() {
+        const [x, y, w, h] = this.getGeometry();
+        this._areaIndicator.setSelectionRect(x, y, w, h);
+
+        const offset = this._handleSize / 2;
+        this._topLeftHandle.set_position(x - offset, y - offset);
+        this._topRightHandle.set_position(x + w - 1 - offset, y - offset);
+        this._bottomLeftHandle.set_position(x - offset, y + h - 1 - offset);
+        this._bottomRightHandle.set_position(x + w - 1 - offset, y + h - 1 - offset);
+    }
+
+    _computeCursorType(cursorX, cursorY) {
+        const [leftX, topY, width, height] = this.getGeometry();
+        const [rightX, bottomY] = [leftX + width - 1, topY + height - 1];
+        const [x, y] = [cursorX, cursorY];
+
+        // Check if the cursor overlaps the handles first.
+        const limit = (this._handleSize / 2) ** 2;
+        if ((leftX - x) ** 2 + (topY - y) ** 2 <= limit)
+            return Meta.Cursor.NW_RESIZE;
+        else if ((rightX - x) ** 2 + (topY - y) ** 2 <= limit)
+            return Meta.Cursor.NE_RESIZE;
+        else if ((leftX - x) ** 2 + (bottomY - y) ** 2 <= limit)
+            return Meta.Cursor.SW_RESIZE;
+        else if ((rightX - x) ** 2 + (bottomY - y) ** 2 <= limit)
+            return Meta.Cursor.SE_RESIZE;
+
+        // Now check the rest of the rectangle.
+        const threshold =
+            10 * St.ThemeContext.get_for_stage(global.stage).scaleFactor;
+
+        if (leftX - x >= 0 && leftX - x <= threshold) {
+            if (topY - y >= 0 && topY - y <= threshold)
+                return Meta.Cursor.NW_RESIZE;
+            else if (y - bottomY >= 0 && y - bottomY <= threshold)
+                return Meta.Cursor.SW_RESIZE;
+            else if (topY - y < 0 && y - bottomY < 0)
+                return Meta.Cursor.WEST_RESIZE;
+        } else if (x - rightX >= 0 && x - rightX <= threshold) {
+            if (topY - y >= 0 && topY - y <= threshold)
+                return Meta.Cursor.NE_RESIZE;
+            else if (y - bottomY >= 0 && y - bottomY <= threshold)
+                return Meta.Cursor.SE_RESIZE;
+            else if (topY - y < 0 && y - bottomY < 0)
+                return Meta.Cursor.EAST_RESIZE;
+        } else if (leftX - x < 0 && x - rightX < 0) {
+            if (topY - y >= 0 && topY - y <= threshold)
+                return Meta.Cursor.NORTH_RESIZE;
+            else if (y - bottomY >= 0 && y - bottomY <= threshold)
+                return Meta.Cursor.SOUTH_RESIZE;
+            else if (topY - y < 0 && y - bottomY < 0)
+                return Meta.Cursor.MOVE_OR_RESIZE_WINDOW;
+        }
+
+        return Meta.Cursor.CROSSHAIR;
+    }
+
+    stopDrag() {
+        if (!this._dragButton)
+            return;
+
+        if (this._dragSequence)
+            this._dragDevice.sequence_ungrab(this._dragSequence);
+        else
+            this._dragDevice.ungrab();
+
+        this._dragButton = 0;
+        this._dragDevice = null;
+        this._dragSequence = null;
+
+        if (this._dragCursor === Meta.Cursor.CROSSHAIR &&
+            this._lastX === this._startX && this._lastY === this._startY) {
+            // The user clicked without dragging. Make up a larger selection
+            // to reduce confusion.
+            const offset =
+                20 * St.ThemeContext.get_for_stage(global.stage).scaleFactor;
+            this._startX -= offset;
+            this._startY -= offset;
+            this._lastX += offset;
+            this._lastY += offset;
+
+            // Keep the coordinates inside the stage.
+            if (this._startX < 0) {
+                this._lastX -= this._startX;
+                this._startX = 0;
+            } else if (this._lastX >= this.width) {
+                this._startX -= this._lastX - this.width + 1;
+                this._lastX = this.width - 1;
+            }
+
+            if (this._startY < 0) {
+                this._lastY -= this._startY;
+                this._startY = 0;
+            } else if (this._lastY >= this.height) {
+                this._startY -= this._lastY - this.height + 1;
+                this._lastY = this.height - 1;
+            }
+
+            this._updateSelectionRect();
+        }
+
+        this.emit('drag-ended');
+    }
+
+    _updateCursor(x, y) {
+        const cursor = this._computeCursorType(x, y);
+        global.display.set_cursor(cursor);
+    }
+
+    _onPress(event, button, sequence) {
+        if (this._dragButton)
+            return Clutter.EVENT_PROPAGATE;
+
+        const cursor = this._computeCursorType(event.x, event.y);
+
+        // Clicking outside of the selection, or using the right mouse button,
+        // or with Ctrl results in dragging a new selection from scratch.
+        if (cursor === Meta.Cursor.CROSSHAIR ||
+            button === Clutter.BUTTON_SECONDARY ||
+            (event.modifier_state & Clutter.ModifierType.CONTROL_MASK)) {
+            this._dragButton = button;
+
+            this._dragCursor = Meta.Cursor.CROSSHAIR;
+            global.display.set_cursor(Meta.Cursor.CROSSHAIR);
+
+            [this._startX, this._startY] = [event.x, event.y];
+            this._lastX = this._startX = Math.floor(this._startX);
+            this._lastY = this._startY = Math.floor(this._startY);
+
+            this._updateSelectionRect();
+        } else {
+            // This is a move or resize operation.
+            this._dragButton = button;
+
+            this._dragCursor = cursor;
+            this._dragStartX = event.x;
+            this._dragStartY = event.y;
+
+            const [leftX, topY, width, height] = this.getGeometry();
+            const rightX = leftX + width - 1;
+            const bottomY = topY + height - 1;
+
+            // For moving, start X and Y are the top left corner, while
+            // last X and Y are the bottom right corner.
+            if (cursor === Meta.Cursor.MOVE_OR_RESIZE_WINDOW) {
+                this._startX = leftX;
+                this._startY = topY;
+                this._lastX = rightX;
+                this._lastY = bottomY;
+            }
+
+            // Start X and Y are set to the stationary sides, while last X
+            // and Y are set to the moving sides.
+            if (cursor === Meta.Cursor.NW_RESIZE ||
+                cursor === Meta.Cursor.WEST_RESIZE ||
+                cursor === Meta.Cursor.SW_RESIZE) {
+                this._startX = rightX;
+                this._lastX = leftX;
+            }
+            if (cursor === Meta.Cursor.NE_RESIZE ||
+                cursor === Meta.Cursor.EAST_RESIZE ||
+                cursor === Meta.Cursor.SE_RESIZE) {
+                this._startX = leftX;
+                this._lastX = rightX;
+            }
+            if (cursor === Meta.Cursor.NW_RESIZE ||
+                cursor === Meta.Cursor.NORTH_RESIZE ||
+                cursor === Meta.Cursor.NE_RESIZE) {
+                this._startY = bottomY;
+                this._lastY = topY;
+            }
+            if (cursor === Meta.Cursor.SW_RESIZE ||
+                cursor === Meta.Cursor.SOUTH_RESIZE ||
+                cursor === Meta.Cursor.SE_RESIZE) {
+                this._startY = topY;
+                this._lastY = bottomY;
+            }
+        }
+
+        if (this._dragButton) {
+            const device = event.device;
+            if (sequence)
+                device.sequence_grab(sequence, this);
+            else
+                device.grab(this);
+
+            this._dragDevice = device;
+            this._dragSequence = sequence;
+
+            this.emit('drag-started');
+
+            return Clutter.EVENT_STOP;
+        }
+
+        return Clutter.EVENT_PROPAGATE;
+    }
+
+    _onRelease(event, button, sequence) {
+        if (this._dragButton !== button ||
+            this._dragSequence?.get_slot() !== sequence?.get_slot())
+            return Clutter.EVENT_PROPAGATE;
+
+        this.stopDrag();
+
+        // We might have finished creating a new selection, so we need to
+        // update the cursor.
+        this._updateCursor(event.x, event.y);
+
+        return Clutter.EVENT_STOP;
+    }
+
+    _onMotion(event, sequence) {
+        if (!this._dragButton) {
+            this._updateCursor(event.x, event.y);
+            return Clutter.EVENT_PROPAGATE;
+        }
+
+        if (sequence?.get_slot() !== this._dragSequence?.get_slot())
+            return Clutter.EVENT_PROPAGATE;
+
+        if (this._dragCursor === Meta.Cursor.CROSSHAIR) {
+            [this._lastX, this._lastY] = [event.x, event.y];
+            this._lastX = Math.floor(this._lastX);
+            this._lastY = Math.floor(this._lastY);
+        } else {
+            let dx = Math.round(event.x - this._dragStartX);
+            let dy = Math.round(event.y - this._dragStartY);
+
+            if (this._dragCursor === Meta.Cursor.MOVE_OR_RESIZE_WINDOW) {
+                const [,, selectionWidth, selectionHeight] = this.getGeometry();
+
+                let newStartX = this._startX + dx;
+                let newStartY = this._startY + dy;
+                let newLastX = this._lastX + dx;
+                let newLastY = this._lastY + dy;
+
+                let overshootX = 0;
+                let overshootY = 0;
+
+                // Keep the size intact if we bumped into the stage edge.
+                if (newStartX < 0) {
+                    overshootX = 0 - newStartX;
+                    newStartX = 0;
+                    newLastX = newStartX + (selectionWidth - 1);
+                } else if (newLastX > this.width - 1) {
+                    overshootX = (this.width - 1) - newLastX;
+                    newLastX = this.width - 1;
+                    newStartX = newLastX - (selectionWidth - 1);
+                }
+
+                if (newStartY < 0) {
+                    overshootY = 0 - newStartY;
+                    newStartY = 0;
+                    newLastY = newStartY + (selectionHeight - 1);
+                } else if (newLastY > this.height - 1) {
+                    overshootY = (this.height - 1) - newLastY;
+                    newLastY = this.height - 1;
+                    newStartY = newLastY - (selectionHeight - 1);
+                }
+
+                // Add the overshoot to the delta to create a "rubberbanding"
+                // behavior of the pointer when dragging.
+                dx += overshootX;
+                dy += overshootY;
+
+                this._startX = newStartX;
+                this._startY = newStartY;
+                this._lastX = newLastX;
+                this._lastY = newLastY;
+            } else {
+                if (this._dragCursor === Meta.Cursor.WEST_RESIZE ||
+                    this._dragCursor === Meta.Cursor.EAST_RESIZE)
+                    dy = 0;
+                if (this._dragCursor === Meta.Cursor.NORTH_RESIZE ||
+                    this._dragCursor === Meta.Cursor.SOUTH_RESIZE)
+                    dx = 0;
+
+                // Make sure last X and Y are clamped between 0 and size - 1,
+                // while always preserving the cursor dragging position relative
+                // to the selection rectangle.
+                this._lastX += dx;
+                if (this._lastX >= this.width) {
+                    dx -= this._lastX - this.width + 1;
+                    this._lastX = this.width - 1;
+                } else if (this._lastX < 0) {
+                    dx -= this._lastX;
+                    this._lastX = 0;
+                }
+
+                this._lastY += dy;
+                if (this._lastY >= this.height) {
+                    dy -= this._lastY - this.height + 1;
+                    this._lastY = this.height - 1;
+                } else if (this._lastY < 0) {
+                    dy -= this._lastY;
+                    this._lastY = 0;
+                }
+
+                // If we drag the handle past a selection side, update which
+                // handles are which.
+                if (this._lastX > this._startX) {
+                    if (this._dragCursor === Meta.Cursor.NW_RESIZE)
+                        this._dragCursor = Meta.Cursor.NE_RESIZE;
+                    else if (this._dragCursor === Meta.Cursor.SW_RESIZE)
+                        this._dragCursor = Meta.Cursor.SE_RESIZE;
+                    else if (this._dragCursor === Meta.Cursor.WEST_RESIZE)
+                        this._dragCursor = Meta.Cursor.EAST_RESIZE;
+                } else {
+                    // eslint-disable-next-line no-lonely-if
+                    if (this._dragCursor === Meta.Cursor.NE_RESIZE)
+                        this._dragCursor = Meta.Cursor.NW_RESIZE;
+                    else if (this._dragCursor === Meta.Cursor.SE_RESIZE)
+                        this._dragCursor = Meta.Cursor.SW_RESIZE;
+                    else if (this._dragCursor === Meta.Cursor.EAST_RESIZE)
+                        this._dragCursor = Meta.Cursor.WEST_RESIZE;
+                }
+
+                if (this._lastY > this._startY) {
+                    if (this._dragCursor === Meta.Cursor.NW_RESIZE)
+                        this._dragCursor = Meta.Cursor.SW_RESIZE;
+                    else if (this._dragCursor === Meta.Cursor.NE_RESIZE)
+                        this._dragCursor = Meta.Cursor.SE_RESIZE;
+                    else if (this._dragCursor === Meta.Cursor.NORTH_RESIZE)
+                        this._dragCursor = Meta.Cursor.SOUTH_RESIZE;
+                } else {
+                    // eslint-disable-next-line no-lonely-if
+                    if (this._dragCursor === Meta.Cursor.SW_RESIZE)
+                        this._dragCursor = Meta.Cursor.NW_RESIZE;
+                    else if (this._dragCursor === Meta.Cursor.SE_RESIZE)
+                        this._dragCursor = Meta.Cursor.NE_RESIZE;
+                    else if (this._dragCursor === Meta.Cursor.SOUTH_RESIZE)
+                        this._dragCursor = Meta.Cursor.NORTH_RESIZE;
+                }
+
+                global.display.set_cursor(this._dragCursor);
+            }
+
+            this._dragStartX += dx;
+            this._dragStartY += dy;
+        }
+
+        this._updateSelectionRect();
+
+        return Clutter.EVENT_STOP;
+    }
+
+    vfunc_button_press_event(event) {
+        if (event.button === Clutter.BUTTON_PRIMARY ||
+            event.button === Clutter.BUTTON_SECONDARY)
+            return this._onPress(event, event.button, null);
+
+        return Clutter.EVENT_PROPAGATE;
+    }
+
+    vfunc_button_release_event(event) {
+        if (event.button === Clutter.BUTTON_PRIMARY ||
+            event.button === Clutter.BUTTON_SECONDARY)
+            return this._onRelease(event, event.button, null);
+
+        return Clutter.EVENT_PROPAGATE;
+    }
+
+    vfunc_motion_event(event) {
+        return this._onMotion(event, null);
+    }
+
+    vfunc_touch_event(event) {
+        if (event.type === Clutter.EventType.TOUCH_BEGIN)
+            return this._onPress(event, 'touch', event.sequence);
+        else if (event.type === Clutter.EventType.TOUCH_END)
+            return this._onRelease(event, 'touch', event.sequence);
+        else if (event.type === Clutter.EventType.TOUCH_UPDATE)
+            return this._onMotion(event, event.sequence);
+
+        return Clutter.EVENT_PROPAGATE;
+    }
+
+    vfunc_leave_event(event) {
+        // If we're dragging and go over the panel we still get a leave event
+        // for some reason, even though we have a grab. We don't want to switch
+        // the cursor when we're dragging.
+        if (!this._dragButton)
+            global.display.set_cursor(Meta.Cursor.DEFAULT);
+
+        return super.vfunc_leave_event(event);
+    }
+});
+
 var ScreenshotUI = GObject.registerClass(
 class ScreenshotUI extends St.Widget {
     _init() {
@@ -82,6 +670,14 @@ class ScreenshotUI extends St.Widget {
             actionMode: Shell.ActionMode.POPUP,
         });
 
+        this._areaSelector = new UIAreaSelector({
+            style_class: 'screenshot-ui-area-selector',
+            x_expand: true,
+            y_expand: true,
+            reactive: true,
+        });
+        this.add_child(this._areaSelector);
+
         this._primaryMonitorBin = new St.Widget({ layout_manager: new Clutter.BinLayout() });
         this._primaryMonitorBin.add_constraint(
             new Layout.MonitorConstraint({ 'primary': true }));
@@ -106,6 +702,31 @@ class ScreenshotUI extends St.Widget {
         this._closeButton.connect('clicked', () => this.close());
         this._primaryMonitorBin.add_child(this._closeButton);
 
+        this._areaSelector.connect('drag-started', () => {
+            this._panel.ease({
+                opacity: 100,
+                duration: 200,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+            });
+            this._closeButton.ease({
+                opacity: 100,
+                duration: 200,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+            });
+        });
+        this._areaSelector.connect('drag-ended', () => {
+            this._panel.ease({
+                opacity: 255,
+                duration: 200,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+            });
+            this._closeButton.ease({
+                opacity: 255,
+                duration: 200,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+            });
+        });
+
         this._typeButtonContainer = new St.Widget({
             style_class: 'screenshot-ui-type-button-container',
             layout_manager: new Clutter.BoxLayout({
@@ -115,11 +736,20 @@ class ScreenshotUI extends St.Widget {
         });
         this._panel.add_child(this._typeButtonContainer);
 
-        this._screenButton = new IconLabelButton('video-display-symbolic', _('Screen'), {
+        this._selectionButton = new IconLabelButton('input-mouse-symbolic', _('Selection'), {
             style_class: 'screenshot-ui-type-button',
             checked: true,
             x_expand: true,
         });
+        this._selectionButton.connect('notify::checked',
+            this._onSelectionButtonToggled.bind(this));
+        this._typeButtonContainer.add_child(this._selectionButton);
+
+        this._screenButton = new IconLabelButton('video-display-symbolic', _('Screen'), {
+            style_class: 'screenshot-ui-type-button',
+            toggle_mode: true,
+            x_expand: true,
+        });
         this._screenButton.connect('notify::checked',
             this._onScreenButtonToggled.bind(this));
         this._typeButtonContainer.add_child(this._screenButton);
@@ -271,6 +901,8 @@ class ScreenshotUI extends St.Widget {
         this._stageScreenshotContainer.hide();
 
         this._stageScreenshot.set_content(null);
+
+        this._areaSelector.reset();
     }
 
     close(instantly = false) {
@@ -290,9 +922,40 @@ class ScreenshotUI extends St.Widget {
         });
     }
 
+    _onSelectionButtonToggled() {
+        if (this._selectionButton.checked) {
+            this._selectionButton.toggle_mode = false;
+            this._screenButton.checked = false;
+
+            this._areaSelector.show();
+            this._areaSelector.remove_all_transitions();
+            this._areaSelector.reactive = true;
+            this._areaSelector.ease({
+                opacity: 255,
+                duration: 200,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+            });
+        } else {
+            this._selectionButton.toggle_mode = true;
+
+            this._areaSelector.stopDrag();
+            global.display.set_cursor(Meta.Cursor.DEFAULT);
+
+            this._areaSelector.remove_all_transitions();
+            this._areaSelector.reactive = false;
+            this._areaSelector.ease({
+                opacity: 0,
+                duration: 200,
+                mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+                onComplete: () => this._areaSelector.hide(),
+            });
+        }
+    }
+
     _onScreenButtonToggled() {
         if (this._screenButton.checked) {
             this._screenButton.toggle_mode = false;
+            this._selectionButton.checked = false;
 
             for (const selector of this._screenSelectors) {
                 selector.show();
@@ -318,11 +981,35 @@ class ScreenshotUI extends St.Widget {
         }
     }
 
+    _getSelectedGeometry() {
+        let x, y, w, h;
+
+        if (this._selectionButton.checked) {
+            [x, y, w, h] = this._areaSelector.getGeometry();
+        } else if (this._screenButton.checked) {
+            const index =
+                this._screenSelectors.findIndex(screen => screen.checked);
+            const monitor = Main.layoutManager.monitors[index];
+
+            x = monitor.x;
+            y = monitor.y;
+            w = monitor.width;
+            h = monitor.height;
+        }
+
+        x *= this._scale;
+        y *= this._scale;
+        w *= this._scale;
+        h *= this._scale;
+
+        return [x, y, w, h];
+    }
+
     _onCaptureButtonClicked() {
         global.display.get_sound_player().play_from_theme(
             'screen-capture', _('Screenshot taken'), null);
 
-        if (this._screenButton.checked) {
+        if (this._selectionButton.checked || this._screenButton.checked) {
             const content = this._stageScreenshot.get_content();
             if (!content) {
                 // Failed to capture the screenshot for some reason.
@@ -333,14 +1020,7 @@ class ScreenshotUI extends St.Widget {
             const texture = content.get_texture();
             const stream = Gio.MemoryOutputStream.new_resizable();
 
-            const index =
-                this._screenSelectors.findIndex(screen => screen.checked);
-            const monitor = Main.layoutManager.monitors[index];
-
-            const x = monitor.x * this._scale;
-            const y = monitor.y * this._scale;
-            const w = monitor.width * this._scale;
-            const h = monitor.height * this._scale;
+            const [x, y, w, h] = this._getSelectedGeometry();
 
             Shell.Screenshot.composite_to_stream(
                 texture,


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