diff --git a/js/ui/screenshot.js b/js/ui/screenshot.js index e0ec8b76acd8c95dc0423e00ac05dacc09ba8a6d..5d2adab4e473c89aecad6f970a9c9bfd2898ef6c 100644 --- a/js/ui/screenshot.js +++ b/js/ui/screenshot.js @@ -1,3 +1,4 @@ +import Atk from 'gi://Atk'; import Clutter from 'gi://Clutter'; import Cogl from 'gi://Cogl'; import Gio from 'gi://Gio'; @@ -250,6 +251,9 @@ class UIAreaIndicator extends St.Widget { } }); +const CTRL_SELECTION_KEYBOARD_INCREMENT = 1; +const SELECTION_KEYBOARD_INCREMENT = 5; + const UIAreaSelector = GObject.registerClass({ Signals: {'drag-started': {}, 'drag-ended': {}}, }, class UIAreaSelector extends St.Widget { @@ -298,6 +302,9 @@ const UIAreaSelector = GObject.registerClass({ this._lastX = 0; this._lastY = 0; + this._currentSide = St.DirectionType.LEFT; + this._lastResizeDirection = St.DirectionType.LEFT; + this.reset(); } @@ -316,19 +323,246 @@ const UIAreaSelector = GObject.registerClass({ 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._resetArea(); + } + } - 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; - } + _resetArea() { + // This can called when running headless without any monitors. + if (Main.layoutManager.primaryIndex !== -1) { + const monitor = + Main.layoutManager.monitors[Main.layoutManager.primaryIndex]; - this._updateSelectionRect(); + 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(); + } + + _attachCursorToCenter() { + const [leftX, topY, selectionWidth, selectionHeight] = this.getGeometry(); + const [rightX, bottomY] = [leftX + selectionWidth - 1, topY + selectionHeight - 1]; + const seat = global.stage.context.get_backend().get_default_seat(); + const cursorX = ((rightX - leftX) / 2) + leftX; + const cursorY = ((topY - bottomY) / 2) + bottomY; + + seat.warp_pointer(cursorX, cursorY); + this._updateCursor(cursorX, cursorY); + } + + _attachCursorToSide(direction) { + const seat = global.stage.context.get_backend().get_default_seat(); + const [leftX, topY, width, height] = this.getGeometry(); + const [rightX, bottomY] = [leftX + width - 1, topY + height - 1]; + let cursorX, cursorY; + + switch (direction) { + case St.DirectionType.LEFT: + cursorX = leftX; + cursorY = ((topY - bottomY) / 2) + bottomY; + break; + case St.DirectionType.RIGHT: + cursorX = rightX; + cursorY = ((topY - bottomY) / 2) + bottomY; + break; + case St.DirectionType.UP: + cursorX = ((rightX - leftX) / 2) + leftX; + cursorY = topY; + break; + case St.DirectionType.DOWN: + cursorX = ((rightX - leftX) / 2) + leftX; + cursorY = bottomY; + break; } + seat.warp_pointer(cursorX, cursorY); + this._updateCursor(cursorX, cursorY); + } + + _attachCursor(direction) { + this._attachCursorToSide(direction); + this._currentSide = direction; + this._lastResizeDirection = direction; + + // Announce change of attached side to screen reader. + this._announceSelectionChanges('Attach To Side', direction); + } + + _announceSelectionChanges(message, direction) { + // Announce change of attached side to screen . + let directionString = ''; + switch (direction) { + case St.DirectionType.LEFT: + directionString = _('Left'); + break; + case St.DirectionType.RIGHT: + directionString = _('Right'); + break; + case St.DirectionType.UP: + directionString = _('Up'); + break; + case St.DirectionType.DOWN: + directionString = _('Down'); + break; + } + + const announcedMessage = _('%s %s'.format(message, directionString)); + this.get_accessible().emit('notification', announcedMessage, Atk.Live.ASSERTIVE); + } + + _moveInDirection(direction, increment) { + const [,, selectionWidth, selectionHeight] = this.getGeometry(); + + let newStartX = this._startX; + let newStartY = this._startY; + let newLastX = this._lastX; + let newLastY = this._lastY; + + switch (direction) { + case St.DirectionType.LEFT: + newStartX -= increment; + newLastX -= increment; + break; + case St.DirectionType.RIGHT: + newStartX += increment; + newLastX += increment; + break; + case St.DirectionType.UP: + newStartY -= increment; + newLastY -= increment; + break; + case St.DirectionType.DOWN: + newStartY += increment; + newLastY += increment; + break; + } + + // Ensure area does not move off the stage edge. + if (newStartX < 0) { + newStartX = 0; + newLastX = newStartX + (selectionWidth - 1); + } else if (newLastX > this.width - 1) { + newLastX = this.width - 1; + newStartX = newLastX - (selectionWidth - 1); + } + + if (newStartY < 0) { + newStartY = 0; + newLastY = newStartY + (selectionHeight - 1); + } else if (newLastY > this.height - 1) { + newLastY = this.height - 1; + newStartY = newLastY - (selectionHeight - 1); + } + // Update selection rectangle props, then announce + // move direction to screen reader. + + this._startX = newStartX; + this._startY = newStartY; + this._lastX = newLastX; + this._lastY = newLastY; + this._updateSelectionRect(); + + this._announceSelectionChanges('Move Selection', direction); + + // Update cursor to center of the selection rectangle + this._attachCursorToCenter(); + } + + _resizeInDirection(direction, increment) { + // If the direction is in the opposite plane to the last direction, + // and position cursor to new side. Return early so the first press + // in the new direction does not resize the selection. + if ((this._lastResizeDirection === St.DirectionType.UP || + this._lastResizeDirection === St.DirectionType.DOWN) && + direction === St.DirectionType.LEFT) { + this._attachCursor(direction); + return; + } else if ((this._lastResizeDirection === St.DirectionType.UP || + this._lastResizeDirection === St.DirectionType.DOWN) && + direction === St.DirectionType.RIGHT) { + this._attachCursor(direction); + return; + } else if ((this._lastResizeDirection === St.DirectionType.LEFT || + this._lastResizeDirection === St.DirectionType.RIGHT) && + direction === St.DirectionType.UP) { + this._attachCursor(direction); + return; + } else if ((this._lastResizeDirection === St.DirectionType.LEFT || + this._lastResizeDirection === St.DirectionType.RIGHT) && + direction === St.DirectionType.DOWN) { + this._attachCursor(direction); + return; + } + + let newStartX = this._startX; + let newStartY = this._startY; + let newLastX = this._lastX; + let newLastY = this._lastY; + + switch (direction) { + case St.DirectionType.LEFT: + if (this._currentSide === St.DirectionType.LEFT) + newStartX -= increment; + else if (this._currentSide === St.DirectionType.RIGHT) + newLastX -= increment; + break; + + case St.DirectionType.RIGHT: + if (this._currentSide === St.DirectionType.LEFT) + newStartX += increment; + else if (this._currentSide === St.DirectionType.RIGHT) + newLastX += increment; + break; + + case St.DirectionType.UP: + if (this._currentSide === St.DirectionType.UP) + newStartY -= increment; + else if (this._currentSide === St.DirectionType.DOWN) + newLastY -= increment; + break; + + case St.DirectionType.DOWN: + if (this._currentSide === St.DirectionType.UP) + newStartY += increment; + else if (this._currentSide === St.DirectionType.DOWN) + newLastY += increment; + break; + } + + // Ensure new resized area does not go off the stage edge. + if (newStartX < 0) + newStartX = 0; + else if (newStartX > this.width - 1) + newStartX = this.width - 1; + + if (newLastX < 0) + newLastX = 0; + else if (newLastX > this.width - 1) + newLastX = this.width - 1; + + if (newStartY < 0) + newStartY = 0; + else if (newStartY > this.height - 1) + newStartY = this.height - 1; + + if (newLastY < 0) + newLastY = 0; + else if (newLastY > this.height - 1) + newLastY = this.height - 1; + + // Update selection rectangle props and cursor position/type, then + // announce change of direction to screen reader. + this._startX = newStartX; + this._startY = newStartY; + this._lastX = newLastX; + this._lastY = newLastY; + this._lastResizeDirection = direction; + this._updateSelectionRect(); + + this._attachCursorToSide(this._currentSide); + this._announceSelectionChanges('Resize Selection Area', direction); } getGeometry() { @@ -2194,6 +2428,13 @@ export const ScreenshotUI = GObject.registerClass({ return Clutter.EVENT_STOP; } + if (this._selectionButton.checked && + (symbol === Clutter.KEY_r || symbol === Clutter.KEY_R)) { + this._areaSelector._resetArea(); + this.get_accessible().emit('notification', _('Reset Selection Area'), Atk.Live.ASSERTIVE); + return Clutter.EVENT_STOP; + } + if (symbol === Clutter.KEY_Left || symbol === Clutter.KEY_Right || symbol === Clutter.KEY_Up || symbol === Clutter.KEY_Down) { let direction; @@ -2215,6 +2456,25 @@ export const ScreenshotUI = GObject.registerClass({ const screen = this._screenSelectors.find(selector => selector.checked) ?? null; this.navigate_focus(screen, direction, false); + } else if (this._selectionButton.checked) { + const [pressed,,] = event.get_key_state(); + + let increment; + if (pressed & Clutter.ModifierType.CONTROL_MASK) { + increment = CTRL_SELECTION_KEYBOARD_INCREMENT; + } else if (pressed & Clutter.ModifierType.SHIFT_MASK) { + if (direction === St.DirectionType.LEFT || direction === St.DirectionType.RIGHT) + increment = Main.layoutManager.monitors[Main.layoutManager.primaryIndex].width; + else if (direction === St.DirectionType.DOWN || direction === St.DirectionType.UP) + increment = Main.layoutManager.monitors[Main.layoutManager.primaryIndex].width; + } else { + increment = SELECTION_KEYBOARD_INCREMENT; + } + + if (pressed & Clutter.ModifierType.MOD1_MASK) + this._areaSelector._moveInDirection(direction, increment); + else + this._areaSelector._resizeInDirection(direction, increment); } return Clutter.EVENT_STOP;