8423ba44fe
These traditionally got the various ClutterEvent subtype structs as their argument, so it was not allowed to use ClutterEvent generic getter methods in these vfuncs. These methods used direct access to struct fields instead. This got spoiled with the move to make ClutterEvent opaque types, since these are no longer public structs so GNOME Shell most silently failed to fetch the expected values from event fields. But since they are not ClutterEvents either, the getters could not be used on them. Mutter is changing so that these vmethods all contain an alias to the one and only Clutter.Event type, thus lifting those barriers, and making it possible to use the ClutterEvent methods in these vfuncs. Closes: https://gitlab.gnome.org/GNOME/mutter/-/issues/2950 Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/2872>
2957 lines
100 KiB
JavaScript
2957 lines
100 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
|
|
import Clutter from 'gi://Clutter';
|
|
import Cogl from 'gi://Cogl';
|
|
import Gio from 'gi://Gio';
|
|
import GObject from 'gi://GObject';
|
|
import GLib from 'gi://GLib';
|
|
import Graphene from 'gi://Graphene';
|
|
import Meta from 'gi://Meta';
|
|
import Shell from 'gi://Shell';
|
|
import St from 'gi://St';
|
|
|
|
import * as GrabHelper from './grabHelper.js';
|
|
import * as Layout from './layout.js';
|
|
import * as Lightbox from './lightbox.js';
|
|
import * as Main from './main.js';
|
|
import * as MessageTray from './messageTray.js';
|
|
import * as Workspace from './workspace.js';
|
|
|
|
Gio._promisify(Shell.Screenshot.prototype, 'pick_color');
|
|
Gio._promisify(Shell.Screenshot.prototype, 'screenshot');
|
|
Gio._promisify(Shell.Screenshot.prototype, 'screenshot_window');
|
|
Gio._promisify(Shell.Screenshot.prototype, 'screenshot_area');
|
|
Gio._promisify(Shell.Screenshot.prototype, 'screenshot_stage_to_content');
|
|
Gio._promisify(Shell.Screenshot, 'composite_to_stream');
|
|
|
|
import {loadInterfaceXML} from '../misc/fileUtils.js';
|
|
import {DBusSenderChecker} from '../misc/util.js';
|
|
|
|
const ScreenshotIface = loadInterfaceXML('org.gnome.Shell.Screenshot');
|
|
|
|
const ScreencastIface = loadInterfaceXML('org.gnome.Shell.Screencast');
|
|
const ScreencastProxy = Gio.DBusProxy.makeProxyWrapper(ScreencastIface);
|
|
|
|
const 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,
|
|
}));
|
|
}
|
|
});
|
|
|
|
export const Tooltip = GObject.registerClass(
|
|
class Tooltip extends St.Label {
|
|
_init(widget, params) {
|
|
super._init(params);
|
|
|
|
this._widget = widget;
|
|
this._timeoutId = null;
|
|
|
|
this._widget.connect('notify::hover', () => {
|
|
if (this._widget.hover)
|
|
this.open();
|
|
else
|
|
this.close();
|
|
});
|
|
}
|
|
|
|
open() {
|
|
if (this._timeoutId)
|
|
return;
|
|
|
|
this._timeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 300, () => {
|
|
this.opacity = 0;
|
|
this.show();
|
|
|
|
const extents = this._widget.get_transformed_extents();
|
|
|
|
const xOffset = Math.floor((extents.get_width() - this.width) / 2);
|
|
const x =
|
|
Math.clamp(extents.get_x() + xOffset, 0, global.stage.width - this.width);
|
|
|
|
const node = this.get_theme_node();
|
|
const yOffset = node.get_length('-y-offset');
|
|
|
|
const y = extents.get_y() - this.height - yOffset;
|
|
|
|
this.set_position(x, y);
|
|
this.ease({
|
|
opacity: 255,
|
|
duration: 150,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
});
|
|
|
|
this._timeoutId = null;
|
|
return GLib.SOURCE_REMOVE;
|
|
});
|
|
GLib.Source.set_name_by_id(this._timeoutId, '[gnome-shell] tooltip.open');
|
|
}
|
|
|
|
close() {
|
|
if (this._timeoutId) {
|
|
GLib.source_remove(this._timeoutId);
|
|
this._timeoutId = null;
|
|
return;
|
|
}
|
|
|
|
if (!this.visible)
|
|
return;
|
|
|
|
this.remove_all_transitions();
|
|
this.ease({
|
|
opacity: 0,
|
|
duration: 100,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
onComplete: () => this.hide(),
|
|
});
|
|
}
|
|
});
|
|
|
|
const 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);
|
|
}
|
|
});
|
|
|
|
const 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._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._dragGrab) {
|
|
this._dragGrab.dismiss();
|
|
this._dragGrab = null;
|
|
}
|
|
|
|
this._dragButton = 0;
|
|
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 [x, y] = event.get_coords();
|
|
const cursor = this._computeCursorType(x, 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.get_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.get_coords();
|
|
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, this._dragStartY] = event.get_coords();
|
|
|
|
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) {
|
|
this._dragGrab = global.stage.grab(this);
|
|
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.
|
|
const [x, y] = event.get_coords();
|
|
this._updateCursor(x, y);
|
|
|
|
return Clutter.EVENT_STOP;
|
|
}
|
|
|
|
_onMotion(event, sequence) {
|
|
if (!this._dragButton) {
|
|
const [x, y] = event.get_coords();
|
|
this._updateCursor(x, 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.get_coords();
|
|
this._lastX = Math.floor(this._lastX);
|
|
this._lastY = Math.floor(this._lastY);
|
|
} else {
|
|
const [x, y] = event.get_coords();
|
|
let dx = Math.round(x - this._dragStartX);
|
|
let dy = Math.round(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) {
|
|
const button = event.get_button();
|
|
if (button === Clutter.BUTTON_PRIMARY ||
|
|
button === Clutter.BUTTON_SECONDARY)
|
|
return this._onPress(event, button, null);
|
|
|
|
return Clutter.EVENT_PROPAGATE;
|
|
}
|
|
|
|
vfunc_button_release_event(event) {
|
|
const button = event.get_button();
|
|
if (button === Clutter.BUTTON_PRIMARY ||
|
|
button === Clutter.BUTTON_SECONDARY)
|
|
return this._onRelease(event, button, null);
|
|
|
|
return Clutter.EVENT_PROPAGATE;
|
|
}
|
|
|
|
vfunc_motion_event(event) {
|
|
return this._onMotion(event, null);
|
|
}
|
|
|
|
vfunc_touch_event(event) {
|
|
const eventType = event.type();
|
|
if (eventType === Clutter.EventType.TOUCH_BEGIN)
|
|
return this._onPress(event, 'touch', event.get_event_sequence());
|
|
else if (eventType === Clutter.EventType.TOUCH_END)
|
|
return this._onRelease(event, 'touch', event.get_event_sequence());
|
|
else if (eventType === Clutter.EventType.TOUCH_UPDATE)
|
|
return this._onMotion(event, event.get_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);
|
|
}
|
|
});
|
|
|
|
const 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;
|
|
}
|
|
});
|
|
|
|
const 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._cursor = null;
|
|
this._cursorPoint = { x: 0, y: 0 };
|
|
this._shouldShowCursor = window.has_pointer && window.has_pointer();
|
|
|
|
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 cursorPoint() {
|
|
return {
|
|
x: this._cursorPoint.x + this._boundingBox.x - this._bufferRect.x,
|
|
y: this._cursorPoint.y + this._boundingBox.y - this._bufferRect.y,
|
|
};
|
|
}
|
|
|
|
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;
|
|
|
|
if (this._cursor) {
|
|
this.remove_child(this._cursor);
|
|
this._cursor.destroy();
|
|
this._cursor = 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);
|
|
|
|
// Allocate the cursor if we have one.
|
|
if (!this._cursor)
|
|
return;
|
|
|
|
let [, , w, h] = this._cursor.get_preferred_size();
|
|
w *= this._cursorScale;
|
|
h *= this._cursorScale;
|
|
|
|
const cursorBox = new Clutter.ActorBox({
|
|
x1: this._cursorPoint.x,
|
|
y1: this._cursorPoint.y,
|
|
x2: this._cursorPoint.x + w,
|
|
y2: this._cursorPoint.y + h,
|
|
});
|
|
cursorBox.x1 *= xScale;
|
|
cursorBox.x2 *= xScale;
|
|
cursorBox.y1 *= yScale;
|
|
cursorBox.y2 *= yScale;
|
|
|
|
this._cursor.allocate(cursorBox);
|
|
}
|
|
|
|
addCursorTexture(content, point, scale) {
|
|
if (!this._shouldShowCursor)
|
|
return;
|
|
|
|
// Add the cursor.
|
|
this._cursor = new St.Widget({
|
|
content,
|
|
request_mode: Clutter.RequestMode.CONTENT_SIZE,
|
|
});
|
|
|
|
this._cursorPoint = {
|
|
x: point.x - this._boundingBox.x,
|
|
y: point.y - this._boundingBox.y,
|
|
};
|
|
this._cursorScale = scale;
|
|
|
|
this.insert_child_below(this._cursor, this._border);
|
|
}
|
|
|
|
getCursorTexture() {
|
|
return this._cursor?.content;
|
|
}
|
|
|
|
setCursorVisible(visible) {
|
|
if (!this._cursor)
|
|
return;
|
|
|
|
this._cursor.visible = visible;
|
|
}
|
|
});
|
|
|
|
const 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;
|
|
}
|
|
});
|
|
|
|
const UIMode = {
|
|
SCREENSHOT: 0,
|
|
SCREENCAST: 1,
|
|
};
|
|
|
|
export const ScreenshotUI = GObject.registerClass({
|
|
Properties: {
|
|
'screencast-in-progress': GObject.ParamSpec.boolean(
|
|
'screencast-in-progress',
|
|
'screencast-in-progress',
|
|
'screencast-in-progress',
|
|
GObject.ParamFlags.READABLE,
|
|
false),
|
|
},
|
|
}, 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,
|
|
reactive: true,
|
|
});
|
|
|
|
this._screencastInProgress = false;
|
|
this._screencastSupported = false;
|
|
|
|
this._screencastProxy = new ScreencastProxy(
|
|
Gio.DBus.session,
|
|
'org.gnome.Shell.Screencast',
|
|
'/org/gnome/Shell/Screencast',
|
|
(object, error) => {
|
|
if (error !== null) {
|
|
log('Error connecting to the screencast service');
|
|
return;
|
|
}
|
|
|
|
this._screencastSupported = this._screencastProxy.ScreencastSupported;
|
|
this._castButton.visible = this._screencastSupported;
|
|
});
|
|
|
|
this._screencastProxy.connectSignal('Error',
|
|
() => this._screencastFailed());
|
|
|
|
this._screencastProxy.connect('notify::g-name-owner', () => {
|
|
if (this._screencastProxy.g_name_owner)
|
|
return;
|
|
|
|
if (!this._screencastInProgress)
|
|
return;
|
|
|
|
this._screencastFailed();
|
|
});
|
|
|
|
this._lockdownSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.lockdown' });
|
|
|
|
// 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);
|
|
|
|
this._screencastAreaIndicator = new UIAreaIndicator({
|
|
style_class: 'screenshot-ui-screencast-area-indicator',
|
|
visible: false,
|
|
});
|
|
this._screencastAreaIndicator.add_constraint(new Clutter.BindConstraint({
|
|
source: global.stage,
|
|
coordinate: Clutter.BindCoordinate.ALL,
|
|
}));
|
|
this.bind_property(
|
|
'screencast-in-progress',
|
|
this._screencastAreaIndicator,
|
|
'visible',
|
|
GObject.BindingFlags.DEFAULT);
|
|
// Add it directly to the stage so that it's above popup menus.
|
|
global.stage.add_child(this._screencastAreaIndicator);
|
|
Shell.util_set_hidden_from_pick(this._screencastAreaIndicator, true);
|
|
|
|
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._cursor = new St.Widget();
|
|
this._stageScreenshotContainer.add_child(this._cursor);
|
|
|
|
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,
|
|
offscreen_redirect: Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY,
|
|
});
|
|
this._primaryMonitorBin.add_child(this._panel);
|
|
|
|
this._closeButton = new St.Button({
|
|
style_class: 'screenshot-ui-close-button',
|
|
icon_name: 'preview-close-symbolic',
|
|
});
|
|
this._closeButton.add_constraint(new Clutter.BindConstraint({
|
|
source: this._panel,
|
|
coordinate: Clutter.BindCoordinate.POSITION,
|
|
}));
|
|
this._closeButton.add_constraint(new Clutter.AlignConstraint({
|
|
source: this._panel,
|
|
align_axis: Clutter.AlignAxis.Y_AXIS,
|
|
pivot_point: new Graphene.Point({ x: -1, y: 0.5 }),
|
|
factor: 0,
|
|
}));
|
|
this._closeButtonXAlignConstraint = new Clutter.AlignConstraint({
|
|
source: this._panel,
|
|
align_axis: Clutter.AlignAxis.X_AXIS,
|
|
pivot_point: new Graphene.Point({ x: 0.5, y: -1 }),
|
|
});
|
|
this._closeButton.add_constraint(this._closeButtonXAlignConstraint);
|
|
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.add_child(new Tooltip(this._selectionButton, {
|
|
text: _('Area Selection'),
|
|
style_class: 'screenshot-ui-tooltip',
|
|
visible: false,
|
|
}));
|
|
|
|
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.add_child(new Tooltip(this._screenButton, {
|
|
text: _('Screen Selection'),
|
|
style_class: 'screenshot-ui-tooltip',
|
|
visible: false,
|
|
}));
|
|
|
|
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.add_child(new Tooltip(this._windowButton, {
|
|
text: _('Window Selection'),
|
|
style_class: 'screenshot-ui-tooltip',
|
|
visible: false,
|
|
}));
|
|
|
|
this._bottomRowContainer = new St.Widget({ layout_manager: new Clutter.BinLayout() });
|
|
this._panel.add_child(this._bottomRowContainer);
|
|
|
|
this._shotCastContainer = new St.BoxLayout({
|
|
style_class: 'screenshot-ui-shot-cast-container',
|
|
x_align: Clutter.ActorAlign.START,
|
|
x_expand: true,
|
|
});
|
|
this._bottomRowContainer.add_child(this._shotCastContainer);
|
|
|
|
this._shotButton = new St.Button({
|
|
style_class: 'screenshot-ui-shot-cast-button',
|
|
icon_name: 'camera-photo-symbolic',
|
|
checked: true,
|
|
});
|
|
this._shotButton.connect('notify::checked',
|
|
this._onShotButtonToggled.bind(this));
|
|
this._shotCastContainer.add_child(this._shotButton);
|
|
|
|
this.add_child(new Tooltip(this._shotButton, {
|
|
text: _('Take Screenshot'),
|
|
style_class: 'screenshot-ui-tooltip',
|
|
visible: false,
|
|
}));
|
|
|
|
this._castButton = new St.Button({
|
|
style_class: 'screenshot-ui-shot-cast-button',
|
|
icon_name: 'camera-web-symbolic',
|
|
toggle_mode: true,
|
|
visible: false,
|
|
});
|
|
this._castButton.connect('notify::checked',
|
|
this._onCastButtonToggled.bind(this));
|
|
this._shotCastContainer.add_child(this._castButton);
|
|
|
|
this.add_child(new Tooltip(this._castButton, {
|
|
text: _('Record Screen'),
|
|
style_class: 'screenshot-ui-tooltip',
|
|
visible: false,
|
|
}));
|
|
|
|
this._shotButton.bind_property('checked', this._castButton, 'checked',
|
|
GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.INVERT_BOOLEAN);
|
|
|
|
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.add_child(new Tooltip(this._captureButton, {
|
|
/* Translators: since this string refers to an action,
|
|
it needs to be phrased as a verb. */
|
|
text: _('Capture'),
|
|
style_class: 'screenshot-ui-tooltip',
|
|
visible: false,
|
|
}));
|
|
this._captureButton.connect('clicked',
|
|
this._onCaptureButtonClicked.bind(this));
|
|
this._bottomRowContainer.add_child(this._captureButton);
|
|
|
|
this._showPointerButtonContainer = new St.BoxLayout({
|
|
x_align: Clutter.ActorAlign.END,
|
|
x_expand: true,
|
|
});
|
|
this._bottomRowContainer.add_child(this._showPointerButtonContainer);
|
|
|
|
this._showPointerButton = new St.Button({
|
|
style_class: 'screenshot-ui-show-pointer-button',
|
|
icon_name: 'screenshot-ui-show-pointer-symbolic',
|
|
toggle_mode: true,
|
|
});
|
|
this._showPointerButtonContainer.add_child(this._showPointerButton);
|
|
|
|
this.add_child(new Tooltip(this._showPointerButton, {
|
|
text: _('Show Pointer'),
|
|
style_class: 'screenshot-ui-tooltip',
|
|
visible: false,
|
|
}));
|
|
|
|
this._showPointerButton.connect('notify::checked', () => {
|
|
const state = this._showPointerButton.checked;
|
|
this._cursor.visible = state;
|
|
|
|
const windows =
|
|
this._windowSelectors.flatMap(selector => selector.windows());
|
|
for (const window of windows)
|
|
window.setCursorVisible(state);
|
|
});
|
|
this._cursor.visible = false;
|
|
|
|
this._monitorBins = [];
|
|
this._windowSelectors = [];
|
|
this._rebuildMonitorBins();
|
|
|
|
Main.layoutManager.connect('monitors-changed', () => {
|
|
// Nope, not dealing with monitor changes.
|
|
this.close(true);
|
|
this._rebuildMonitorBins();
|
|
});
|
|
|
|
const uiModes =
|
|
Shell.ActionMode.ALL & ~Shell.ActionMode.LOGIN_SCREEN;
|
|
const restrictedModes =
|
|
uiModes &
|
|
~(Shell.ActionMode.LOCK_SCREEN | Shell.ActionMode.UNLOCK_SCREEN);
|
|
|
|
Main.wm.addKeybinding(
|
|
'show-screenshot-ui',
|
|
new Gio.Settings({ schema_id: 'org.gnome.shell.keybindings' }),
|
|
Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
|
|
uiModes,
|
|
showScreenshotUI
|
|
);
|
|
|
|
Main.wm.addKeybinding(
|
|
'show-screen-recording-ui',
|
|
new Gio.Settings({ schema_id: 'org.gnome.shell.keybindings' }),
|
|
Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
|
|
restrictedModes,
|
|
showScreenRecordingUI
|
|
);
|
|
|
|
Main.wm.addKeybinding(
|
|
'screenshot-window',
|
|
new Gio.Settings({ schema_id: 'org.gnome.shell.keybindings' }),
|
|
Meta.KeyBindingFlags.IGNORE_AUTOREPEAT | Meta.KeyBindingFlags.PER_WINDOW,
|
|
restrictedModes,
|
|
async (_display, window, _binding) => {
|
|
try {
|
|
const actor = window.get_compositor_private();
|
|
const content = actor.paint_to_content(null);
|
|
const texture = content.get_texture();
|
|
|
|
await captureScreenshot(texture, null, 1, null);
|
|
} catch (e) {
|
|
logError(e, 'Error capturing screenshot');
|
|
}
|
|
}
|
|
);
|
|
|
|
Main.wm.addKeybinding(
|
|
'screenshot',
|
|
new Gio.Settings({ schema_id: 'org.gnome.shell.keybindings' }),
|
|
Meta.KeyBindingFlags.IGNORE_AUTOREPEAT,
|
|
uiModes,
|
|
async () => {
|
|
try {
|
|
const shooter = new Shell.Screenshot();
|
|
const [content] = await shooter.screenshot_stage_to_content();
|
|
const texture = content.get_texture();
|
|
|
|
await captureScreenshot(texture, null, 1, null);
|
|
} catch (e) {
|
|
logError(e, 'Error capturing screenshot');
|
|
}
|
|
}
|
|
);
|
|
|
|
Main.sessionMode.connect('updated',
|
|
() => this._sessionUpdated());
|
|
this._sessionUpdated();
|
|
}
|
|
|
|
_sessionUpdated() {
|
|
this.close(true);
|
|
this._castButton.reactive = Main.sessionMode.allowScreencast;
|
|
}
|
|
|
|
_refreshButtonLayout() {
|
|
const buttonLayout = Meta.prefs_get_button_layout();
|
|
|
|
this._closeButton.remove_style_class_name('left');
|
|
this._closeButton.remove_style_class_name('right');
|
|
|
|
if (buttonLayout.left_buttons.includes(Meta.ButtonFunction.CLOSE)) {
|
|
this._closeButton.add_style_class_name('left');
|
|
this._closeButtonXAlignConstraint.factor = 0;
|
|
} else {
|
|
this._closeButton.add_style_class_name('right');
|
|
this._closeButtonXAlignConstraint.factor = 1;
|
|
}
|
|
}
|
|
|
|
_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(mode = UIMode.SCREENSHOT) {
|
|
if (this._openingCoroutineInProgress)
|
|
return;
|
|
|
|
if (this._screencastInProgress)
|
|
return;
|
|
|
|
if (mode === UIMode.SCREENCAST && !this._screencastSupported)
|
|
return;
|
|
|
|
this._castButton.checked = mode === UIMode.SCREENCAST;
|
|
|
|
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 =
|
|
Main.sessionMode.hasWindows &&
|
|
windows.length > 0 &&
|
|
!this._castButton.checked;
|
|
if (!this._windowButton.reactive)
|
|
this._selectionButton.checked = true;
|
|
|
|
this._shooter = new Shell.Screenshot();
|
|
|
|
this._openingCoroutineInProgress = true;
|
|
try {
|
|
const [content, scale, cursorContent, cursorPoint, cursorScale] =
|
|
await this._shooter.screenshot_stage_to_content();
|
|
this._stageScreenshot.set_content(content);
|
|
this._scale = scale;
|
|
|
|
if (cursorContent !== null) {
|
|
this._cursor.set_content(cursorContent);
|
|
this._cursor.set_position(cursorPoint.x, cursorPoint.y);
|
|
|
|
let [, w, h] = cursorContent.get_preferred_size();
|
|
w *= cursorScale;
|
|
h *= cursorScale;
|
|
this._cursor.set_size(w, h);
|
|
|
|
this._cursorScale = cursorScale;
|
|
|
|
for (const window of windows) {
|
|
window.addCursorTexture(cursorContent, cursorPoint, cursorScale);
|
|
window.setCursorVisible(this._showPointerButton.checked);
|
|
}
|
|
}
|
|
|
|
this._stageScreenshotContainer.show();
|
|
} catch (e) {
|
|
log(`Error capturing screenshot: ${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 { screenshotUIGroup } = Main.layoutManager;
|
|
screenshotUIGroup.get_parent().set_child_above_sibling(
|
|
screenshotUIGroup, null);
|
|
|
|
const grabResult = this._grabHelper.grab({
|
|
actor: this,
|
|
onUngrab: () => this.close(),
|
|
});
|
|
if (!grabResult) {
|
|
this.close(true);
|
|
return;
|
|
}
|
|
|
|
this._refreshButtonLayout();
|
|
|
|
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;
|
|
|
|
// Switch back to screenshot mode.
|
|
this._shotButton.checked = true;
|
|
|
|
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._cursor.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(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
_onShotButtonToggled() {
|
|
if (this._shotButton.checked) {
|
|
this._shotButton.toggle_mode = false;
|
|
|
|
this._stageScreenshotContainer.show();
|
|
this._stageScreenshotContainer.remove_all_transitions();
|
|
this._stageScreenshotContainer.ease({
|
|
opacity: 255,
|
|
duration: 200,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
});
|
|
} else {
|
|
this._shotButton.toggle_mode = true;
|
|
}
|
|
}
|
|
|
|
_onCastButtonToggled() {
|
|
if (this._castButton.checked) {
|
|
this._castButton.toggle_mode = false;
|
|
|
|
this._captureButton.add_style_pseudo_class('cast');
|
|
|
|
this._stageScreenshotContainer.remove_all_transitions();
|
|
this._stageScreenshotContainer.ease({
|
|
opacity: 0,
|
|
duration: 200,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
onComplete: () => this._stageScreenshotContainer.hide(),
|
|
});
|
|
|
|
// Screen recording doesn't support window selection yet.
|
|
if (this._windowButton.checked)
|
|
this._selectionButton.checked = true;
|
|
|
|
this._windowButton.reactive = false;
|
|
} else {
|
|
this._castButton.toggle_mode = true;
|
|
|
|
this._captureButton.remove_style_pseudo_class('cast');
|
|
|
|
const windows =
|
|
this._windowSelectors.flatMap(selector => selector.windows());
|
|
this._windowButton.reactive = windows.length > 0;
|
|
}
|
|
}
|
|
|
|
_getSelectedGeometry(rescale) {
|
|
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;
|
|
}
|
|
|
|
if (rescale) {
|
|
x *= this._scale;
|
|
y *= this._scale;
|
|
w *= this._scale;
|
|
h *= this._scale;
|
|
}
|
|
|
|
return [x, y, w, h];
|
|
}
|
|
|
|
_onCaptureButtonClicked() {
|
|
if (this._shotButton.checked) {
|
|
this._saveScreenshot();
|
|
this.close();
|
|
} else {
|
|
// Screencast closes the UI on its own.
|
|
this._startScreencast();
|
|
}
|
|
}
|
|
|
|
_saveScreenshot() {
|
|
if (this._selectionButton.checked || this._screenButton.checked) {
|
|
const content = this._stageScreenshot.get_content();
|
|
if (!content)
|
|
return; // Failed to capture the screenshot for some reason.
|
|
|
|
const texture = content.get_texture();
|
|
const geometry = this._getSelectedGeometry(true);
|
|
|
|
let cursorTexture = this._cursor.content?.get_texture();
|
|
if (!this._cursor.visible)
|
|
cursorTexture = null;
|
|
|
|
captureScreenshot(
|
|
texture, geometry, this._scale,
|
|
{
|
|
texture: cursorTexture ?? null,
|
|
x: this._cursor.x * this._scale,
|
|
y: this._cursor.y * this._scale,
|
|
scale: this._cursorScale,
|
|
}
|
|
).catch(e => logError(e, '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)
|
|
return;
|
|
|
|
const texture = content.get_texture();
|
|
|
|
let cursorTexture = window.getCursorTexture()?.get_texture();
|
|
if (!this._cursor.visible)
|
|
cursorTexture = null;
|
|
|
|
captureScreenshot(
|
|
texture,
|
|
null,
|
|
window.bufferScale,
|
|
{
|
|
texture: cursorTexture ?? null,
|
|
x: window.cursorPoint.x * window.bufferScale,
|
|
y: window.cursorPoint.y * window.bufferScale,
|
|
scale: this._cursorScale,
|
|
}
|
|
).catch(e => logError(e, 'Error capturing screenshot'));
|
|
}
|
|
}
|
|
|
|
async _startScreencast() {
|
|
if (this._windowButton.checked)
|
|
return; // TODO
|
|
|
|
const [x, y, w, h] = this._getSelectedGeometry(false);
|
|
const drawCursor = this._cursor.visible;
|
|
|
|
// Set up the screencast indicator rect.
|
|
if (this._selectionButton.checked) {
|
|
this._screencastAreaIndicator.setSelectionRect(
|
|
...this._areaSelector.getGeometry());
|
|
} else if (this._screenButton.checked) {
|
|
const index =
|
|
this._screenSelectors.findIndex(screen => screen.checked);
|
|
const monitor = Main.layoutManager.monitors[index];
|
|
|
|
this._screencastAreaIndicator.setSelectionRect(
|
|
monitor.x, monitor.y, monitor.width, monitor.height);
|
|
}
|
|
|
|
// Close instantly so the fade-out doesn't get recorded.
|
|
this.close(true);
|
|
|
|
// This is a bit awkward because creating a proxy synchronously hangs Shell.
|
|
let method =
|
|
this._screencastProxy.ScreencastAsync.bind(this._screencastProxy);
|
|
if (w !== -1) {
|
|
method = this._screencastProxy.ScreencastAreaAsync.bind(
|
|
this._screencastProxy, x, y, w, h);
|
|
}
|
|
|
|
// Set this before calling the method as the screen recording indicator
|
|
// will check it before the success callback fires.
|
|
this._setScreencastInProgress(true);
|
|
|
|
try {
|
|
const [success, path] = await method(
|
|
GLib.build_filenamev([
|
|
/* Translators: this is the folder where recorded
|
|
screencasts are stored. */
|
|
_('Screencasts'),
|
|
/* Translators: this is a filename used for screencast
|
|
* recording, where "%d" and "%t" date and time, e.g.
|
|
* "Screencast from 07-17-2013 10:00:46 PM.webm" */
|
|
/* xgettext:no-c-format */
|
|
_('Screencast from %d %t.webm'),
|
|
]),
|
|
{'draw-cursor': new GLib.Variant('b', drawCursor)});
|
|
if (!success)
|
|
throw new Error();
|
|
this._screencastPath = path;
|
|
} catch (error) {
|
|
this._setScreencastInProgress(false);
|
|
const {message} = error;
|
|
if (message)
|
|
log(`Error starting screencast: ${message}`);
|
|
else
|
|
log('Error starting screencast');
|
|
}
|
|
}
|
|
|
|
async stopScreencast() {
|
|
if (!this._screencastInProgress)
|
|
return;
|
|
|
|
// Set this before calling the method as the screen recording indicator
|
|
// will check it before the success callback fires.
|
|
this._setScreencastInProgress(false);
|
|
|
|
try {
|
|
const [success] = await this._screencastProxy.StopScreencastAsync();
|
|
if (!success)
|
|
throw new Error();
|
|
} catch (error) {
|
|
const {message} = error;
|
|
if (message)
|
|
log(`Error stopping screencast: ${message}`);
|
|
else
|
|
log('Error stopping screencast');
|
|
return;
|
|
}
|
|
|
|
// Translators: notification title.
|
|
this._showNotification(_('Screencast recorded'));
|
|
}
|
|
|
|
_screencastFailed() {
|
|
this._setScreencastInProgress(false);
|
|
|
|
// Translators: notification title.
|
|
this._showNotification(_('Screencast ended unexpectedly'));
|
|
}
|
|
|
|
_showNotification(title) {
|
|
// Show a notification.
|
|
const file = Gio.file_new_for_path(this._screencastPath);
|
|
|
|
const source = new MessageTray.Source(
|
|
// Translators: notification source name.
|
|
_('Screenshot'),
|
|
'screencast-recorded-symbolic'
|
|
);
|
|
const notification = new MessageTray.Notification(
|
|
source,
|
|
title,
|
|
// Translators: notification body when a screencast was recorded.
|
|
_('Click here to view the video.')
|
|
);
|
|
// Translators: button on the screencast notification.
|
|
notification.addAction(_('Show in Files'), () => {
|
|
const app =
|
|
Gio.app_info_get_default_for_type('inode/directory', false);
|
|
|
|
if (app === null) {
|
|
// It may be null e.g. in a toolbox without nautilus.
|
|
log('Error showing in files: no default app set for inode/directory');
|
|
return;
|
|
}
|
|
|
|
app.launch([file], global.create_app_launch_context(0, -1));
|
|
});
|
|
notification.connect('activated', () => {
|
|
try {
|
|
Gio.app_info_launch_default_for_uri(
|
|
file.get_uri(), global.create_app_launch_context(0, -1));
|
|
} catch (err) {
|
|
logError(err, 'Error opening screencast');
|
|
}
|
|
});
|
|
notification.setTransient(true);
|
|
|
|
Main.messageTray.add(source);
|
|
source.showNotification(notification);
|
|
}
|
|
|
|
get screencast_in_progress() {
|
|
return this._screencastInProgress;
|
|
}
|
|
|
|
_setScreencastInProgress(inProgress) {
|
|
if (this._screencastInProgress === inProgress)
|
|
return;
|
|
|
|
this._screencastInProgress = inProgress;
|
|
this.notify('screencast-in-progress');
|
|
}
|
|
|
|
vfunc_key_press_event(event) {
|
|
const symbol = event.get_key_symbol();
|
|
if (symbol === Clutter.KEY_Return || symbol === Clutter.KEY_space ||
|
|
((event.get_state() & Clutter.ModifierType.CONTROL_MASK) &&
|
|
(symbol === Clutter.KEY_c || symbol === Clutter.KEY_C))) {
|
|
this._onCaptureButtonClicked();
|
|
return Clutter.EVENT_STOP;
|
|
}
|
|
|
|
if (symbol === Clutter.KEY_s || symbol === Clutter.KEY_S) {
|
|
this._selectionButton.checked = true;
|
|
return Clutter.EVENT_STOP;
|
|
}
|
|
|
|
if (symbol === Clutter.KEY_c || symbol === Clutter.KEY_C) {
|
|
this._screenButton.checked = true;
|
|
return Clutter.EVENT_STOP;
|
|
}
|
|
|
|
if (this._windowButton.reactive &&
|
|
(symbol === Clutter.KEY_w || symbol === Clutter.KEY_W)) {
|
|
this._windowButton.checked = true;
|
|
return Clutter.EVENT_STOP;
|
|
}
|
|
|
|
if (symbol === Clutter.KEY_p || symbol === Clutter.KEY_P) {
|
|
this._showPointerButton.checked = !this._showPointerButton.checked;
|
|
return Clutter.EVENT_STOP;
|
|
}
|
|
|
|
if (symbol === Clutter.KEY_v || symbol === Clutter.KEY_V) {
|
|
this._castButton.checked = !this._castButton.checked;
|
|
return Clutter.EVENT_STOP;
|
|
}
|
|
|
|
if (symbol === Clutter.KEY_Left || symbol === Clutter.KEY_Right ||
|
|
symbol === Clutter.KEY_Up || symbol === Clutter.KEY_Down) {
|
|
let direction;
|
|
if (symbol === Clutter.KEY_Left)
|
|
direction = St.DirectionType.LEFT;
|
|
else if (symbol === Clutter.KEY_Right)
|
|
direction = St.DirectionType.RIGHT;
|
|
else if (symbol === Clutter.KEY_Up)
|
|
direction = St.DirectionType.UP;
|
|
else if (symbol === Clutter.KEY_Down)
|
|
direction = St.DirectionType.DOWN;
|
|
|
|
if (this._windowButton.checked) {
|
|
const window =
|
|
this._windowSelectors.flatMap(selector => selector.windows())
|
|
.find(win => win.checked) ?? null;
|
|
this.navigate_focus(window, direction, false);
|
|
} else if (this._screenButton.checked) {
|
|
const screen =
|
|
this._screenSelectors.find(selector => selector.checked) ?? null;
|
|
this.navigate_focus(screen, direction, false);
|
|
}
|
|
|
|
return Clutter.EVENT_STOP;
|
|
}
|
|
|
|
return super.vfunc_key_press_event(event);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* Stores a PNG-encoded screenshot into the clipboard and a file, and shows a
|
|
* notification.
|
|
*
|
|
* @param {GLib.Bytes} bytes - The PNG-encoded screenshot.
|
|
* @param {GdkPixbuf.Pixbuf} pixbuf - The Pixbuf with the screenshot.
|
|
*/
|
|
function _storeScreenshot(bytes, pixbuf) {
|
|
// Store to the clipboard first in case storing to file fails.
|
|
const clipboard = St.Clipboard.get_default();
|
|
clipboard.set_content(St.ClipboardType.CLIPBOARD, 'image/png', bytes);
|
|
|
|
const time = GLib.DateTime.new_now_local();
|
|
|
|
// This will be set in the first save to disk branch and then accessed
|
|
// in the second save to disk branch, so we need to declare it outside.
|
|
let file;
|
|
|
|
// The function is declared here rather than inside the condition to
|
|
// satisfy eslint.
|
|
|
|
/**
|
|
* Returns a filename suffix with an increasingly large index.
|
|
*
|
|
* @returns {Generator<string|*, void, *>} suffix string
|
|
*/
|
|
function* suffixes() {
|
|
yield '';
|
|
|
|
for (let i = 1; ; i++)
|
|
yield `-${i}`;
|
|
}
|
|
|
|
/**
|
|
* Adds a record of a screenshot file in the recently used files list.
|
|
*
|
|
* @param {Gio.File} screenshotFile - The screenshot file.
|
|
*/
|
|
function saveRecentFile(screenshotFile) {
|
|
const recentFile =
|
|
GLib.build_filenamev([GLib.get_user_data_dir(), 'recently-used.xbel']);
|
|
const uri = screenshotFile.get_uri();
|
|
const bookmarks = new GLib.BookmarkFile();
|
|
try {
|
|
bookmarks.load_from_file(recentFile);
|
|
} catch (e) {
|
|
if (!e.matches(GLib.BookmarkFileError, GLib.BookmarkFileError.FILE_NOT_FOUND)) {
|
|
log(`Could not open recent file ${uri}: ${e.message}`);
|
|
return;
|
|
}
|
|
}
|
|
|
|
try {
|
|
bookmarks.add_application(uri, GLib.get_prgname(), 'gio open %u');
|
|
bookmarks.to_file(recentFile);
|
|
} catch (e) {
|
|
log(`Could not save recent file ${uri}: ${e.message}`);
|
|
}
|
|
}
|
|
|
|
const lockdownSettings =
|
|
new Gio.Settings({ schema_id: 'org.gnome.desktop.lockdown' });
|
|
const disableSaveToDisk =
|
|
lockdownSettings.get_boolean('disable-save-to-disk');
|
|
|
|
if (!disableSaveToDisk) {
|
|
const dir = Gio.File.new_for_path(GLib.build_filenamev([
|
|
GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES) || GLib.get_home_dir(),
|
|
// Translators: name of the folder under ~/Pictures for screenshots.
|
|
_('Screenshots'),
|
|
]));
|
|
|
|
try {
|
|
dir.make_directory_with_parents(null);
|
|
} catch (e) {
|
|
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS))
|
|
throw e;
|
|
}
|
|
|
|
const timestamp = time.format('%Y-%m-%d %H-%M-%S');
|
|
// Translators: this is the name of the file that the screenshot is
|
|
// saved to. The placeholder is a timestamp, e.g. "2017-05-21 12-24-03".
|
|
const name = _('Screenshot from %s').format(timestamp);
|
|
|
|
// If the target file already exists, try appending a suffix with an
|
|
// increasing number to it.
|
|
for (const suffix of suffixes()) {
|
|
file = Gio.File.new_for_path(GLib.build_filenamev([
|
|
dir.get_path(), `${name}${suffix}.png`,
|
|
]));
|
|
|
|
try {
|
|
const stream = file.create(Gio.FileCreateFlags.NONE, null);
|
|
stream.write_bytes(bytes, null);
|
|
break;
|
|
} catch (e) {
|
|
if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS))
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
// Add it to recent files.
|
|
saveRecentFile(file);
|
|
}
|
|
|
|
// Create a St.ImageContent icon for the notification. We want
|
|
// St.ImageContent specifically because it preserves the aspect ratio when
|
|
// shown in a notification.
|
|
const pixels = pixbuf.read_pixel_bytes();
|
|
const content =
|
|
St.ImageContent.new_with_preferred_size(pixbuf.width, pixbuf.height);
|
|
content.set_bytes(
|
|
pixels,
|
|
Cogl.PixelFormat.RGBA_8888,
|
|
pixbuf.width,
|
|
pixbuf.height,
|
|
pixbuf.rowstride
|
|
);
|
|
|
|
// Show a notification.
|
|
const source = new MessageTray.Source(
|
|
// Translators: notification source name.
|
|
_('Screenshot'),
|
|
'screenshot-recorded-symbolic'
|
|
);
|
|
const notification = new MessageTray.Notification(
|
|
source,
|
|
// Translators: notification title.
|
|
_('Screenshot captured'),
|
|
// Translators: notification body when a screenshot was captured.
|
|
_('You can paste the image from the clipboard.'),
|
|
{ datetime: time, gicon: content }
|
|
);
|
|
|
|
if (!disableSaveToDisk) {
|
|
// Translators: button on the screenshot notification.
|
|
notification.addAction(_('Show in Files'), () => {
|
|
const app =
|
|
Gio.app_info_get_default_for_type('inode/directory', false);
|
|
|
|
if (app === null) {
|
|
// It may be null e.g. in a toolbox without nautilus.
|
|
log('Error showing in files: no default app set for inode/directory');
|
|
return;
|
|
}
|
|
|
|
app.launch([file], global.create_app_launch_context(0, -1));
|
|
});
|
|
notification.connect('activated', () => {
|
|
try {
|
|
Gio.app_info_launch_default_for_uri(
|
|
file.get_uri(), global.create_app_launch_context(0, -1));
|
|
} catch (err) {
|
|
logError(err, 'Error opening screenshot');
|
|
}
|
|
});
|
|
}
|
|
|
|
notification.setTransient(true);
|
|
Main.messageTray.add(source);
|
|
source.showNotification(notification);
|
|
}
|
|
|
|
/**
|
|
* Captures a screenshot from a texture, given a region, scale and optional
|
|
* cursor data.
|
|
*
|
|
* @param {Cogl.Texture} texture - The texture to take the screenshot from.
|
|
* @param {number[4]} [geometry] - The region to use: x, y, width and height.
|
|
* @param {number} scale - The texture scale.
|
|
* @param {Object} [cursor] - Cursor data to include in the screenshot.
|
|
* @param {Cogl.Texture} cursor.texture - The cursor texture.
|
|
* @param {number} cursor.x - The cursor x coordinate.
|
|
* @param {number} cursor.y - The cursor y coordinate.
|
|
* @param {number} cursor.scale - The cursor texture scale.
|
|
*/
|
|
async function captureScreenshot(texture, geometry, scale, cursor) {
|
|
const stream = Gio.MemoryOutputStream.new_resizable();
|
|
const [x, y, w, h] = geometry ?? [0, 0, -1, -1];
|
|
if (cursor === null)
|
|
cursor = { texture: null, x: 0, y: 0, scale: 1 };
|
|
|
|
global.display.get_sound_player().play_from_theme(
|
|
'screen-capture', _('Screenshot taken'), null);
|
|
|
|
const pixbuf = await Shell.Screenshot.composite_to_stream(
|
|
texture,
|
|
x, y, w, h,
|
|
scale,
|
|
cursor.texture, cursor.x, cursor.y, cursor.scale,
|
|
stream
|
|
);
|
|
|
|
stream.close(null);
|
|
_storeScreenshot(stream.steal_as_bytes(), pixbuf);
|
|
}
|
|
|
|
/**
|
|
* Shows the screenshot UI.
|
|
*/
|
|
function showScreenshotUI() {
|
|
Main.screenshotUI.open().catch(err => {
|
|
logError(err, 'Error opening the screenshot UI');
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Shows the screen recording UI.
|
|
*/
|
|
function showScreenRecordingUI() {
|
|
Main.screenshotUI.open(UIMode.SCREENCAST).catch(err => {
|
|
logError(err, 'Error opening the screenshot UI');
|
|
});
|
|
}
|
|
|
|
export class ScreenshotService {
|
|
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, `${filename}.png`]));
|
|
|
|
for (let idx = 1; ; idx++) {
|
|
yield Gio.File.new_for_path(
|
|
GLib.build_filenamev([path, `${filename}-${idx}.png`]));
|
|
}
|
|
}
|
|
|
|
_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());
|
|
}
|
|
}
|
|
}
|
|
|
|
export const 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);
|
|
|
|
const 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(event) {
|
|
if (this._startX == -1 || this._startY == -1 || this._result)
|
|
return Clutter.EVENT_PROPAGATE;
|
|
|
|
[this._lastX, this._lastY] = event.get_coords();
|
|
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(event) {
|
|
if (this._result)
|
|
return Clutter.EVENT_PROPAGATE;
|
|
|
|
[this._startX, this._startY] = event.get_coords();
|
|
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;
|
|
}
|
|
});
|
|
|
|
const 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);
|
|
}
|
|
});
|
|
|
|
export const 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);
|
|
|
|
const 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(event) {
|
|
const [x, y] = event.get_coords();
|
|
this._pickColor(x, y);
|
|
return Clutter.EVENT_PROPAGATE;
|
|
}
|
|
});
|
|
|
|
const FLASHSPOT_ANIMATION_OUT_TIME = 500; // milliseconds
|
|
|
|
export const 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();
|
|
},
|
|
});
|
|
}
|
|
});
|