// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- /* exported ScreenshotService, ScreenshotUI, showScreenshotUI */ const { Clutter, Gio, GObject, GLib, Meta, Shell, St } = imports.gi; const GrabHelper = imports.ui.grabHelper; const Layout = imports.ui.layout; const Lightbox = imports.ui.lightbox; const Main = imports.ui.main; const Workspace = imports.ui.workspace; Gio._promisify(Shell.Screenshot.prototype, 'pick_color', 'pick_color_finish'); Gio._promisify(Shell.Screenshot.prototype, 'screenshot', 'screenshot_finish'); Gio._promisify(Shell.Screenshot.prototype, 'screenshot_window', 'screenshot_window_finish'); Gio._promisify(Shell.Screenshot.prototype, 'screenshot_area', 'screenshot_area_finish'); Gio._promisify(Shell.Screenshot.prototype, 'screenshot_stage_to_content', 'screenshot_stage_to_content_finish'); Gio._promisify( Shell.Screenshot, 'composite_to_stream', 'composite_to_stream_finish'); const { loadInterfaceXML } = imports.misc.fileUtils; const { DBusSenderChecker } = imports.misc.util; const ScreenshotIface = loadInterfaceXML('org.gnome.Shell.Screenshot'); var IconLabelButton = GObject.registerClass( class IconLabelButton extends St.Button { _init(iconName, label, params) { super._init(params); this._container = new St.BoxLayout({ vertical: true, style_class: 'icon-label-button-container', }); this.set_child(this._container); this._container.add_child(new St.Icon({ icon_name: iconName })); this._container.add_child(new St.Label({ text: label, x_align: Clutter.ActorAlign.CENTER, })); } }); 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 UIWindowSelectorLayout = GObject.registerClass( class UIWindowSelectorLayout extends Workspace.WorkspaceLayout { _init(monitorIndex) { super._init(null, monitorIndex, null); } vfunc_set_container(container) { this._container = container; this._syncWorkareaTracking(); } vfunc_allocate(container, box) { const containerBox = container.allocation; const containerAllocationChanged = this._lastBox === null || !this._lastBox.equal(containerBox); this._lastBox = containerBox.copy(); let layoutChanged = false; if (this._layout === null) { this._layout = this._createBestLayout(this._workarea); layoutChanged = true; } if (layoutChanged || containerAllocationChanged) this._windowSlots = this._getWindowSlots(box.copy()); const childBox = new Clutter.ActorBox(); const nSlots = this._windowSlots.length; for (let i = 0; i < nSlots; i++) { let [x, y, width, height, child] = this._windowSlots[i]; childBox.set_origin(x, y); childBox.set_size(width, height); child.allocate(childBox); } } addWindow(window) { if (this._sortedWindows.includes(window)) return; this._sortedWindows.push(window); this._container.add_child(window); this._layout = null; this.layout_changed(); } reset() { for (const window of this._sortedWindows) window.destroy(); this._sortedWindows = []; this._windowSlots = []; this._layout = null; } get windows() { return this._sortedWindows; } }); var UIWindowSelectorWindow = GObject.registerClass( class UIWindowSelectorWindow extends St.Button { _init(actor, params) { super._init(params); const window = actor.metaWindow; this._boundingBox = window.get_frame_rect(); this._bufferRect = window.get_buffer_rect(); this._bufferScale = actor.get_resource_scale(); this._actor = new Clutter.Actor({ content: actor.paint_to_content(null), }); this.add_child(this._actor); this._border = new St.Bin({ style_class: 'screenshot-ui-window-selector-window-border' }); this._border.connect('style-changed', () => { this._borderSize = this._border.get_theme_node().get_border_width(St.Side.TOP); }); this.add_child(this._border); this._border.child = new St.Icon({ icon_name: 'object-select-symbolic', style_class: 'screenshot-ui-window-selector-check', x_align: Clutter.ActorAlign.CENTER, y_align: Clutter.ActorAlign.CENTER, }); this.connect('destroy', this._onDestroy.bind(this)); } get boundingBox() { return this._boundingBox; } get windowCenter() { const boundingBox = this.boundingBox; return { x: boundingBox.x + boundingBox.width / 2, y: boundingBox.y + boundingBox.height / 2, }; } chromeHeights() { return [0, 0]; } chromeWidths() { return [0, 0]; } overlapHeights() { return [0, 0]; } get bufferScale() { return this._bufferScale; } get windowContent() { return this._actor.content; } _onDestroy() { this.remove_child(this._actor); this._actor.destroy(); this._actor = null; this.remove_child(this._border); this._border.destroy(); this._border = null; } vfunc_allocate(box) { this.set_allocation(box); // Border goes around the window. const borderBox = box.copy(); borderBox.set_origin(0, 0); borderBox.x1 -= this._borderSize; borderBox.y1 -= this._borderSize; borderBox.x2 += this._borderSize; borderBox.y2 += this._borderSize; this._border.allocate(borderBox); // box should contain this._boundingBox worth of window. Compute // origin and size for the actor box to satisfy that. const xScale = box.get_width() / this._boundingBox.width; const yScale = box.get_height() / this._boundingBox.height; const [, windowW, windowH] = this._actor.content.get_preferred_size(); const actorBox = new Clutter.ActorBox(); actorBox.set_origin( (this._bufferRect.x - this._boundingBox.x) * xScale, (this._bufferRect.y - this._boundingBox.y) * yScale ); actorBox.set_size( windowW * xScale / this._bufferScale, windowH * yScale / this._bufferScale ); this._actor.allocate(actorBox); } }); var UIWindowSelector = GObject.registerClass( class UIWindowSelector extends St.Widget { _init(monitorIndex, params) { super._init(params); super.layout_manager = new Clutter.BinLayout(); this._monitorIndex = monitorIndex; this._layoutManager = new UIWindowSelectorLayout(monitorIndex); // Window screenshots this._container = new St.Widget({ style_class: 'screenshot-ui-window-selector-window-container', x_expand: true, y_expand: true, }); this._container.layout_manager = this._layoutManager; this.add_child(this._container); } capture() { for (const actor of global.get_window_actors()) { let window = actor.metaWindow; let workspaceManager = global.workspace_manager; let activeWorkspace = workspaceManager.get_active_workspace(); if (window.is_override_redirect() || !window.located_on_workspace(activeWorkspace) || window.get_monitor() !== this._monitorIndex) continue; const widget = new UIWindowSelectorWindow( actor, { style_class: 'screenshot-ui-window-selector-window', reactive: true, can_focus: true, toggle_mode: true, } ); widget.connect('key-focus-in', win => { Main.screenshotUI.grab_key_focus(); win.checked = true; }); if (window.has_focus()) { widget.checked = true; widget.toggle_mode = false; } this._layoutManager.addWindow(widget); } } reset() { this._layoutManager.reset(); } windows() { return this._layoutManager.windows; } }); var ScreenshotUI = GObject.registerClass( class ScreenshotUI extends St.Widget { _init() { super._init({ name: 'screenshot-ui', constraints: new Clutter.BindConstraint({ source: global.stage, coordinate: Clutter.BindCoordinate.ALL, }), layout_manager: new Clutter.BinLayout(), opacity: 0, visible: false, }); // The full-screen screenshot has a separate container so that we can // show it without the screenshot UI fade-in for a nicer animation. this._stageScreenshotContainer = new St.Widget({ visible: false }); this._stageScreenshotContainer.add_constraint(new Clutter.BindConstraint({ source: global.stage, coordinate: Clutter.BindCoordinate.ALL, })); Main.layoutManager.screenshotUIGroup.add_child( this._stageScreenshotContainer); Main.layoutManager.screenshotUIGroup.add_child(this); this._stageScreenshot = new St.Widget({ style_class: 'screenshot-ui-screen-screenshot' }); this._stageScreenshot.add_constraint(new Clutter.BindConstraint({ source: global.stage, coordinate: Clutter.BindCoordinate.ALL, })); this._stageScreenshotContainer.add_child(this._stageScreenshot); this._openingCoroutineInProgress = false; this._grabHelper = new GrabHelper.GrabHelper(this, { 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 })); this.add_child(this._primaryMonitorBin); this._panel = new St.BoxLayout({ style_class: 'screenshot-ui-panel', y_align: Clutter.ActorAlign.END, y_expand: true, vertical: true, }); this._primaryMonitorBin.add_child(this._panel); this._closeButton = new St.Button({ style_class: 'screenshot-ui-close-button', x_align: Clutter.ActorAlign.END, y_align: Clutter.ActorAlign.START, x_expand: true, y_expand: true, }); this._closeButton.set_child(new St.Icon({ icon_name: 'window-close-symbolic' })); 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({ spacing: 12, homogeneous: true, }), }); this._panel.add_child(this._typeButtonContainer); this._selectionButton = new IconLabelButton('screenshot-ui-area-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('screenshot-ui-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); this._windowButton = new IconLabelButton('screenshot-ui-window-symbolic', _('Window'), { style_class: 'screenshot-ui-type-button', toggle_mode: true, x_expand: true, }); this._windowButton.connect('notify::checked', this._onWindowButtonToggled.bind(this)); this._typeButtonContainer.add_child(this._windowButton); this._bottomRowContainer = new St.Widget({ layout_manager: new Clutter.BinLayout() }); this._panel.add_child(this._bottomRowContainer); this._captureButton = new St.Button({ style_class: 'screenshot-ui-capture-button' }); this._captureButton.set_child(new St.Widget({ style_class: 'screenshot-ui-capture-button-circle', })); this._captureButton.connect('clicked', this._onCaptureButtonClicked.bind(this)); this._bottomRowContainer.add_child(this._captureButton); this._monitorBins = []; this._windowSelectors = []; this._rebuildMonitorBins(); Main.layoutManager.connect('monitors-changed', () => { // Nope, not dealing with monitor changes. this.close(true); this._rebuildMonitorBins(); }); Main.wm.addKeybinding( 'show-screenshot-ui', new Gio.Settings({ schema_id: 'org.gnome.shell.keybindings' }), Meta.KeyBindingFlags.IGNORE_AUTOREPEAT, Shell.ActionMode.NORMAL | Shell.ActionMode.OVERVIEW | Shell.ActionMode.SYSTEM_MODAL | Shell.ActionMode.LOOKING_GLASS | Shell.ActionMode.POPUP, showScreenshotUI ); } _rebuildMonitorBins() { for (const bin of this._monitorBins) bin.destroy(); this._monitorBins = []; this._windowSelectors = []; this._screenSelectors = []; for (let i = 0; i < Main.layoutManager.monitors.length; i++) { const bin = new St.Widget({ layout_manager: new Clutter.BinLayout(), }); bin.add_constraint(new Layout.MonitorConstraint({ 'index': i })); this.insert_child_below(bin, this._primaryMonitorBin); this._monitorBins.push(bin); const windowSelector = new UIWindowSelector(i, { style_class: 'screenshot-ui-window-selector', x_expand: true, y_expand: true, visible: this._windowButton.checked, }); if (i === Main.layoutManager.primaryIndex) windowSelector.add_style_pseudo_class('primary-monitor'); bin.add_child(windowSelector); this._windowSelectors.push(windowSelector); const screenSelector = new St.Button({ style_class: 'screenshot-ui-screen-selector', x_expand: true, y_expand: true, visible: this._screenButton.checked, reactive: true, can_focus: true, toggle_mode: true, }); screenSelector.connect('key-focus-in', () => { this.grab_key_focus(); screenSelector.checked = true; }); bin.add_child(screenSelector); this._screenSelectors.push(screenSelector); screenSelector.connect('notify::checked', () => { if (!screenSelector.checked) return; screenSelector.toggle_mode = false; for (const otherSelector of this._screenSelectors) { if (screenSelector === otherSelector) continue; otherSelector.toggle_mode = true; otherSelector.checked = false; } }); } if (Main.layoutManager.primaryIndex !== -1) this._screenSelectors[Main.layoutManager.primaryIndex].checked = true; } async open() { if (this._openingCoroutineInProgress) return; if (!this.visible) { // Screenshot UI is opening from completely closed state // (rather than opening back from in process of closing). for (const selector of this._windowSelectors) selector.capture(); const windows = this._windowSelectors.flatMap(selector => selector.windows()); for (const window of windows) { window.connect('notify::checked', () => { if (!window.checked) return; window.toggle_mode = false; for (const otherWindow of windows) { if (window === otherWindow) continue; otherWindow.toggle_mode = true; otherWindow.checked = false; } }); } this._windowButton.reactive = windows.length > 0; if (!this._windowButton.reactive) this._selectionButton.checked = true; this._shooter = new Shell.Screenshot(); this._openingCoroutineInProgress = true; try { const [content, scale] = await this._shooter.screenshot_stage_to_content(); this._stageScreenshot.set_content(content); this._scale = scale; this._stageScreenshotContainer.show(); } catch (e) { log('Error capturing screenshot: %s'.format(e.message)); } this._openingCoroutineInProgress = false; } // Get rid of any popup menus. // We already have them captured on the screenshot anyway. // // This needs to happen before the grab below as closing menus will // pop their grabs. Main.layoutManager.emit('system-modal-opened'); const grabResult = this._grabHelper.grab({ actor: this, onUngrab: () => this.close(), }); if (!grabResult) return; this.remove_all_transitions(); this.visible = true; this.ease({ opacity: 255, duration: 200, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { this._stageScreenshotContainer.get_parent().remove_child( this._stageScreenshotContainer); this.insert_child_at_index(this._stageScreenshotContainer, 0); }, }); } _finishClosing() { this.hide(); this._shooter = null; this._stageScreenshotContainer.get_parent().remove_child( this._stageScreenshotContainer); Main.layoutManager.screenshotUIGroup.insert_child_at_index( this._stageScreenshotContainer, 0); this._stageScreenshotContainer.hide(); this._stageScreenshot.set_content(null); this._areaSelector.reset(); for (const selector of this._windowSelectors) selector.reset(); } close(instantly = false) { this._grabHelper.ungrab(); if (instantly) { this._finishClosing(); return; } this.remove_all_transitions(); this.ease({ opacity: 0, duration: 200, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: this._finishClosing.bind(this), }); } _onSelectionButtonToggled() { if (this._selectionButton.checked) { this._selectionButton.toggle_mode = false; this._windowButton.checked = 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; this._windowButton.checked = false; for (const selector of this._screenSelectors) { selector.show(); selector.remove_all_transitions(); selector.ease({ opacity: 255, duration: 200, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); } } else { this._screenButton.toggle_mode = true; for (const selector of this._screenSelectors) { selector.remove_all_transitions(); selector.ease({ opacity: 0, duration: 200, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => selector.hide(), }); } } } _onWindowButtonToggled() { if (this._windowButton.checked) { this._windowButton.toggle_mode = false; this._selectionButton.checked = false; this._screenButton.checked = false; for (const selector of this._windowSelectors) { selector.show(); selector.remove_all_transitions(); selector.ease({ opacity: 255, duration: 200, mode: Clutter.AnimationMode.EASE_OUT_QUAD, }); } } else { this._windowButton.toggle_mode = true; for (const selector of this._windowSelectors) { selector.remove_all_transitions(); selector.ease({ opacity: 0, duration: 200, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => selector.hide(), }); } } } _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._selectionButton.checked || this._screenButton.checked) { const content = this._stageScreenshot.get_content(); if (!content) { // Failed to capture the screenshot for some reason. this.close(); return; } const texture = content.get_texture(); const stream = Gio.MemoryOutputStream.new_resizable(); const [x, y, w, h] = this._getSelectedGeometry(); Shell.Screenshot.composite_to_stream( texture, x, y, w, h, stream ).then(() => { stream.close(null); const clipboard = St.Clipboard.get_default(); clipboard.set_content( St.ClipboardType.CLIPBOARD, 'image/png', stream.steal_as_bytes() ); }).catch(err => { logError(err, 'Error capturing screenshot'); }); } else if (this._windowButton.checked) { const window = this._windowSelectors.flatMap(selector => selector.windows()) .find(win => win.checked); if (!window) return; const content = window.windowContent; if (!content) { this.close(); return; } const texture = content.get_texture(); const stream = Gio.MemoryOutputStream.new_resizable(); Shell.Screenshot.composite_to_stream( texture, 0, 0, -1, -1, stream ).then(() => { stream.close(null); const clipboard = St.Clipboard.get_default(); clipboard.set_content( St.ClipboardType.CLIPBOARD, 'image/png', stream.steal_as_bytes() ); }).catch(err => { logError(err, 'Error capturing screenshot'); }); } this.close(); } vfunc_key_press_event(event) { const symbol = event.keyval; if (symbol === Clutter.KEY_Return || symbol === Clutter.KEY_space || ((event.modifier_state & Clutter.ModifierType.CONTROL_MASK) && (symbol === Clutter.KEY_c || symbol === Clutter.KEY_C))) { this._onCaptureButtonClicked(); return Clutter.EVENT_STOP; } return super.vfunc_key_press_event(event); } }); /** * Shows the screenshot UI. */ function showScreenshotUI() { Main.screenshotUI.open().catch(err => { logError(err, 'Error opening the screenshot UI'); }); } var ScreenshotService = class { constructor() { this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(ScreenshotIface, this); this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/Screenshot'); this._screenShooter = new Map(); this._senderChecker = new DBusSenderChecker([ 'org.gnome.SettingsDaemon.MediaKeys', 'org.freedesktop.impl.portal.desktop.gtk', 'org.freedesktop.impl.portal.desktop.gnome', 'org.gnome.Screenshot', ]); this._lockdownSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.lockdown' }); Gio.DBus.session.own_name('org.gnome.Shell.Screenshot', Gio.BusNameOwnerFlags.REPLACE, null, null); } async _createScreenshot(invocation, needsDisk = true, restrictCallers = true) { let lockedDown = false; if (needsDisk) lockedDown = this._lockdownSettings.get_boolean('disable-save-to-disk'); let sender = invocation.get_sender(); if (this._screenShooter.has(sender)) { invocation.return_error_literal( Gio.IOErrorEnum, Gio.IOErrorEnum.BUSY, 'There is an ongoing operation for this sender'); return null; } else if (lockedDown) { invocation.return_error_literal( Gio.IOErrorEnum, Gio.IOErrorEnum.PERMISSION_DENIED, 'Saving to disk is disabled'); return null; } else if (restrictCallers) { try { await this._senderChecker.checkInvocation(invocation); } catch (e) { invocation.return_gerror(e); return null; } } let shooter = new Shell.Screenshot(); shooter._watchNameId = Gio.bus_watch_name(Gio.BusType.SESSION, sender, 0, null, this._onNameVanished.bind(this)); this._screenShooter.set(sender, shooter); return shooter; } _onNameVanished(connection, name) { this._removeShooterForSender(name); } _removeShooterForSender(sender) { let shooter = this._screenShooter.get(sender); if (!shooter) return; Gio.bus_unwatch_name(shooter._watchNameId); this._screenShooter.delete(sender); } _checkArea(x, y, width, height) { return x >= 0 && y >= 0 && width > 0 && height > 0 && x + width <= global.screen_width && y + height <= global.screen_height; } *_resolveRelativeFilename(filename) { filename = filename.replace(/\.png$/, ''); let path = [ GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES), GLib.get_home_dir(), ].find(p => p && GLib.file_test(p, GLib.FileTest.EXISTS)); if (!path) return null; yield Gio.File.new_for_path( GLib.build_filenamev([path, '%s.png'.format(filename)])); for (let idx = 1; ; idx++) { yield Gio.File.new_for_path( GLib.build_filenamev([path, '%s-%s.png'.format(filename, idx)])); } } _createStream(filename, invocation) { if (filename == '') return [Gio.MemoryOutputStream.new_resizable(), null]; if (GLib.path_is_absolute(filename)) { try { let file = Gio.File.new_for_path(filename); let stream = file.replace(null, false, Gio.FileCreateFlags.NONE, null); return [stream, file]; } catch (e) { invocation.return_gerror(e); this._removeShooterForSender(invocation.get_sender()); return [null, null]; } } let err; for (let file of this._resolveRelativeFilename(filename)) { try { let stream = file.create(Gio.FileCreateFlags.NONE, null); return [stream, file]; } catch (e) { err = e; if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS)) break; } } invocation.return_gerror(err); this._removeShooterForSender(invocation.get_sender()); return [null, null]; } _flashAsync(shooter) { return new Promise((resolve, _reject) => { shooter.connect('screenshot_taken', (s, area) => { const flashspot = new Flashspot(area); flashspot.fire(resolve); global.display.get_sound_player().play_from_theme( 'screen-capture', _('Screenshot taken'), null); }); }); } _onScreenshotComplete(stream, file, invocation) { stream.close(null); let filenameUsed = ''; if (file) { filenameUsed = file.get_path(); } else { let bytes = stream.steal_as_bytes(); let clipboard = St.Clipboard.get_default(); clipboard.set_content(St.ClipboardType.CLIPBOARD, 'image/png', bytes); } let retval = GLib.Variant.new('(bs)', [true, filenameUsed]); invocation.return_value(retval); } _scaleArea(x, y, width, height) { let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; x *= scaleFactor; y *= scaleFactor; width *= scaleFactor; height *= scaleFactor; return [x, y, width, height]; } _unscaleArea(x, y, width, height) { let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor; x /= scaleFactor; y /= scaleFactor; width /= scaleFactor; height /= scaleFactor; return [x, y, width, height]; } async ScreenshotAreaAsync(params, invocation) { let [x, y, width, height, flash, filename] = params; [x, y, width, height] = this._scaleArea(x, y, width, height); if (!this._checkArea(x, y, width, height)) { invocation.return_error_literal(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED, "Invalid params"); return; } let screenshot = await this._createScreenshot(invocation); if (!screenshot) return; let [stream, file] = this._createStream(filename, invocation); if (!stream) return; try { await Promise.all([ flash ? this._flashAsync(screenshot) : null, screenshot.screenshot_area(x, y, width, height, stream), ]); this._onScreenshotComplete(stream, file, invocation); } catch (e) { invocation.return_value(new GLib.Variant('(bs)', [false, ''])); } finally { this._removeShooterForSender(invocation.get_sender()); } } async ScreenshotWindowAsync(params, invocation) { let [includeFrame, includeCursor, flash, filename] = params; let screenshot = await this._createScreenshot(invocation); if (!screenshot) return; let [stream, file] = this._createStream(filename, invocation); if (!stream) return; try { await Promise.all([ flash ? this._flashAsync(screenshot) : null, screenshot.screenshot_window(includeFrame, includeCursor, stream), ]); this._onScreenshotComplete(stream, file, invocation); } catch (e) { invocation.return_value(new GLib.Variant('(bs)', [false, ''])); } finally { this._removeShooterForSender(invocation.get_sender()); } } async ScreenshotAsync(params, invocation) { let [includeCursor, flash, filename] = params; let screenshot = await this._createScreenshot(invocation); if (!screenshot) return; let [stream, file] = this._createStream(filename, invocation); if (!stream) return; try { await Promise.all([ flash ? this._flashAsync(screenshot) : null, screenshot.screenshot(includeCursor, stream), ]); this._onScreenshotComplete(stream, file, invocation); } catch (e) { invocation.return_value(new GLib.Variant('(bs)', [false, ''])); } finally { this._removeShooterForSender(invocation.get_sender()); } } async SelectAreaAsync(params, invocation) { try { await this._senderChecker.checkInvocation(invocation); } catch (e) { invocation.return_gerror(e); return; } let selectArea = new SelectArea(); try { let areaRectangle = await selectArea.selectAsync(); let retRectangle = this._unscaleArea( areaRectangle.x, areaRectangle.y, areaRectangle.width, areaRectangle.height); invocation.return_value(GLib.Variant.new('(iiii)', retRectangle)); } catch (e) { invocation.return_error_literal( Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED, 'Operation was cancelled'); } } async FlashAreaAsync(params, invocation) { try { await this._senderChecker.checkInvocation(invocation); } catch (e) { invocation.return_gerror(e); return; } let [x, y, width, height] = params; [x, y, width, height] = this._scaleArea(x, y, width, height); if (!this._checkArea(x, y, width, height)) { invocation.return_error_literal(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED, "Invalid params"); return; } let flashspot = new Flashspot({ x, y, width, height }); flashspot.fire(); invocation.return_value(null); } async PickColorAsync(params, invocation) { const screenshot = await this._createScreenshot(invocation, false, false); if (!screenshot) return; const pickPixel = new PickPixel(screenshot); try { const color = await pickPixel.pickAsync(); const { red, green, blue } = color; const retval = GLib.Variant.new('(a{sv})', [{ color: GLib.Variant.new('(ddd)', [ red / 255.0, green / 255.0, blue / 255.0, ]), }]); invocation.return_value(retval); } catch (e) { invocation.return_error_literal( Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED, 'Operation was cancelled'); } finally { this._removeShooterForSender(invocation.get_sender()); } } }; var SelectArea = GObject.registerClass( class SelectArea extends St.Widget { _init() { this._startX = -1; this._startY = -1; this._lastX = 0; this._lastY = 0; this._result = null; super._init({ visible: false, reactive: true, x: 0, y: 0, }); Main.uiGroup.add_actor(this); this._grabHelper = new GrabHelper.GrabHelper(this); let constraint = new Clutter.BindConstraint({ source: global.stage, coordinate: Clutter.BindCoordinate.ALL }); this.add_constraint(constraint); this._rubberband = new St.Widget({ style_class: 'select-area-rubberband', visible: false, }); this.add_actor(this._rubberband); } async selectAsync() { global.display.set_cursor(Meta.Cursor.CROSSHAIR); Main.uiGroup.set_child_above_sibling(this, null); this.show(); try { await this._grabHelper.grabAsync({ actor: this }); } finally { global.display.set_cursor(Meta.Cursor.DEFAULT); GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { this.destroy(); return GLib.SOURCE_REMOVE; }); } return this._result; } _getGeometry() { return new Meta.Rectangle({ x: Math.min(this._startX, this._lastX), y: Math.min(this._startY, this._lastY), width: Math.abs(this._startX - this._lastX) + 1, height: Math.abs(this._startY - this._lastY) + 1, }); } vfunc_motion_event(motionEvent) { if (this._startX == -1 || this._startY == -1 || this._result) return Clutter.EVENT_PROPAGATE; [this._lastX, this._lastY] = [motionEvent.x, motionEvent.y]; this._lastX = Math.floor(this._lastX); this._lastY = Math.floor(this._lastY); let geometry = this._getGeometry(); this._rubberband.set_position(geometry.x, geometry.y); this._rubberband.set_size(geometry.width, geometry.height); this._rubberband.show(); return Clutter.EVENT_PROPAGATE; } vfunc_button_press_event(buttonEvent) { if (this._result) return Clutter.EVENT_PROPAGATE; [this._startX, this._startY] = [buttonEvent.x, buttonEvent.y]; this._startX = Math.floor(this._startX); this._startY = Math.floor(this._startY); this._rubberband.set_position(this._startX, this._startY); return Clutter.EVENT_PROPAGATE; } vfunc_button_release_event() { if (this._startX === -1 || this._startY === -1 || this._result) return Clutter.EVENT_PROPAGATE; this._result = this._getGeometry(); this.ease({ opacity: 0, duration: 200, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => this._grabHelper.ungrab(), }); return Clutter.EVENT_PROPAGATE; } }); var RecolorEffect = GObject.registerClass({ Properties: { color: GObject.ParamSpec.boxed( 'color', 'color', 'replacement color', GObject.ParamFlags.WRITABLE, Clutter.Color.$gtype), chroma: GObject.ParamSpec.boxed( 'chroma', 'chroma', 'color to replace', GObject.ParamFlags.WRITABLE, Clutter.Color.$gtype), threshold: GObject.ParamSpec.float( 'threshold', 'threshold', 'threshold', GObject.ParamFlags.WRITABLE, 0.0, 1.0, 0.0), smoothing: GObject.ParamSpec.float( 'smoothing', 'smoothing', 'smoothing', GObject.ParamFlags.WRITABLE, 0.0, 1.0, 0.0), }, }, class RecolorEffect extends Shell.GLSLEffect { _init(params) { this._color = new Clutter.Color(); this._chroma = new Clutter.Color(); this._threshold = 0; this._smoothing = 0; this._colorLocation = null; this._chromaLocation = null; this._thresholdLocation = null; this._smoothingLocation = null; super._init(params); this._colorLocation = this.get_uniform_location('recolor_color'); this._chromaLocation = this.get_uniform_location('chroma_color'); this._thresholdLocation = this.get_uniform_location('threshold'); this._smoothingLocation = this.get_uniform_location('smoothing'); this._updateColorUniform(this._colorLocation, this._color); this._updateColorUniform(this._chromaLocation, this._chroma); this._updateFloatUniform(this._thresholdLocation, this._threshold); this._updateFloatUniform(this._smoothingLocation, this._smoothing); } _updateColorUniform(location, color) { if (!location) return; this.set_uniform_float(location, 3, [color.red / 255, color.green / 255, color.blue / 255]); this.queue_repaint(); } _updateFloatUniform(location, value) { if (!location) return; this.set_uniform_float(location, 1, [value]); this.queue_repaint(); } set color(c) { if (this._color.equal(c)) return; this._color = c; this.notify('color'); this._updateColorUniform(this._colorLocation, this._color); } set chroma(c) { if (this._chroma.equal(c)) return; this._chroma = c; this.notify('chroma'); this._updateColorUniform(this._chromaLocation, this._chroma); } set threshold(value) { if (this._threshold === value) return; this._threshold = value; this.notify('threshold'); this._updateFloatUniform(this._thresholdLocation, this._threshold); } set smoothing(value) { if (this._smoothing === value) return; this._smoothing = value; this.notify('smoothing'); this._updateFloatUniform(this._smoothingLocation, this._smoothing); } vfunc_build_pipeline() { // Conversion parameters from https://en.wikipedia.org/wiki/YCbCr const decl = ` vec3 rgb2yCrCb(vec3 c) { \n float y = 0.299 * c.r + 0.587 * c.g + 0.114 * c.b; \n float cr = 0.7133 * (c.r - y); \n float cb = 0.5643 * (c.b - y); \n return vec3(y, cr, cb); \n } \n \n uniform vec3 chroma_color; \n uniform vec3 recolor_color; \n uniform float threshold; \n uniform float smoothing; \n`; const src = ` vec3 mask = rgb2yCrCb(chroma_color.rgb); \n vec3 yCrCb = rgb2yCrCb(cogl_color_out.rgb); \n float blend = \n smoothstep(threshold, \n threshold + smoothing, \n distance(yCrCb.gb, mask.gb)); \n cogl_color_out.rgb = \n mix(recolor_color, cogl_color_out.rgb, blend); \n`; this.add_glsl_snippet(Shell.SnippetHook.FRAGMENT, decl, src, false); } }); var PickPixel = GObject.registerClass( class PickPixel extends St.Widget { _init(screenshot) { super._init({ visible: false, reactive: true }); this._screenshot = screenshot; this._result = null; this._color = null; this._inPick = false; Main.uiGroup.add_actor(this); this._grabHelper = new GrabHelper.GrabHelper(this); let constraint = new Clutter.BindConstraint({ source: global.stage, coordinate: Clutter.BindCoordinate.ALL }); this.add_constraint(constraint); const action = new Clutter.ClickAction(); action.connect('clicked', async () => { await this._pickColor(...action.get_coords()); this._result = this._color; this._grabHelper.ungrab(); }); this.add_action(action); this._recolorEffect = new RecolorEffect({ chroma: new Clutter.Color({ red: 80, green: 219, blue: 181, }), threshold: 0.04, smoothing: 0.07, }); this._previewCursor = new St.Icon({ icon_name: 'color-pick', icon_size: Meta.prefs_get_cursor_size(), effect: this._recolorEffect, visible: false, }); Main.uiGroup.add_actor(this._previewCursor); } async pickAsync() { global.display.set_cursor(Meta.Cursor.BLANK); Main.uiGroup.set_child_above_sibling(this, null); this.show(); this._pickColor(...global.get_pointer()); try { await this._grabHelper.grabAsync({ actor: this }); } finally { global.display.set_cursor(Meta.Cursor.DEFAULT); this._previewCursor.destroy(); GLib.idle_add(GLib.PRIORITY_DEFAULT, () => { this.destroy(); return GLib.SOURCE_REMOVE; }); } return this._result; } async _pickColor(x, y) { if (this._inPick) return; this._inPick = true; this._previewCursor.set_position(x, y); [this._color] = await this._screenshot.pick_color(x, y); this._inPick = false; if (!this._color) return; this._recolorEffect.color = this._color; this._previewCursor.show(); } vfunc_motion_event(motionEvent) { const { x, y } = motionEvent; this._pickColor(x, y); return Clutter.EVENT_PROPAGATE; } }); var FLASHSPOT_ANIMATION_OUT_TIME = 500; // milliseconds var Flashspot = GObject.registerClass( class Flashspot extends Lightbox.Lightbox { _init(area) { super._init(Main.uiGroup, { inhibitEvents: true, width: area.width, height: area.height, }); this.style_class = 'flashspot'; this.set_position(area.x, area.y); } fire(doneCallback) { this.set({ visible: true, opacity: 255 }); this.ease({ opacity: 0, duration: FLASHSPOT_ANIMATION_OUT_TIME, mode: Clutter.AnimationMode.EASE_OUT_QUAD, onComplete: () => { if (doneCallback) doneCallback(); this.destroy(); }, }); } });