1d17404471
When selecting an area for screenshot we monitor the events while we've valid coordinates in order to redraw the rubber band. However, we don't stop ignore the motion events after button release and so while animating. This might cause an unwanted effect if moving the mouse away during fade out that is way more visible slowing-down the animations. To fix this ignore any motion event once we've set the results. https://gitlab.gnome.org/GNOME/gnome-shell/merge_requests/711
394 lines
14 KiB
JavaScript
394 lines
14 KiB
JavaScript
// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
|
|
/* exported ScreenshotService */
|
|
|
|
const { Clutter, Gio, GLib, Meta, Shell, St } = imports.gi;
|
|
const Signals = imports.signals;
|
|
|
|
const GrabHelper = imports.ui.grabHelper;
|
|
const Lightbox = imports.ui.lightbox;
|
|
const Main = imports.ui.main;
|
|
|
|
const { loadInterfaceXML } = imports.misc.fileUtils;
|
|
|
|
const ScreenshotIface = loadInterfaceXML('org.gnome.Shell.Screenshot');
|
|
|
|
var ScreenshotService = class {
|
|
constructor() {
|
|
this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(ScreenshotIface, this);
|
|
this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/Screenshot');
|
|
|
|
this._screenShooter = new Map();
|
|
|
|
this._lockdownSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.lockdown' });
|
|
|
|
Gio.DBus.session.own_name('org.gnome.Shell.Screenshot', Gio.BusNameOwnerFlags.REPLACE, null, null);
|
|
}
|
|
|
|
_createScreenshot(invocation, needsDisk = 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) || lockedDown) {
|
|
invocation.return_value(GLib.Variant.new('(bs)', [false, '']));
|
|
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;
|
|
}
|
|
|
|
_onScreenshotComplete(result, area, filenameUsed, flash, invocation) {
|
|
if (result) {
|
|
if (flash) {
|
|
let flashspot = new Flashspot(area);
|
|
flashspot.fire(() => {
|
|
this._removeShooterForSender(invocation.get_sender());
|
|
});
|
|
} else {
|
|
this._removeShooterForSender(invocation.get_sender());
|
|
}
|
|
}
|
|
|
|
let retval = GLib.Variant.new('(bs)', [result, 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];
|
|
}
|
|
|
|
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 = this._createScreenshot(invocation);
|
|
if (!screenshot)
|
|
return;
|
|
screenshot.screenshot_area (x, y, width, height, filename,
|
|
(o, res) => {
|
|
try {
|
|
let [result, area, filenameUsed] =
|
|
screenshot.screenshot_area_finish(res);
|
|
this._onScreenshotComplete(result, area, filenameUsed,
|
|
flash, invocation);
|
|
} catch (e) {
|
|
invocation.return_gerror (e);
|
|
}
|
|
});
|
|
}
|
|
|
|
ScreenshotWindowAsync(params, invocation) {
|
|
let [includeFrame, includeCursor, flash, filename] = params;
|
|
let screenshot = this._createScreenshot(invocation);
|
|
if (!screenshot)
|
|
return;
|
|
screenshot.screenshot_window (includeFrame, includeCursor, filename,
|
|
(o, res) => {
|
|
try {
|
|
let [result, area, filenameUsed] =
|
|
screenshot.screenshot_window_finish(res);
|
|
this._onScreenshotComplete(result, area, filenameUsed,
|
|
flash, invocation);
|
|
} catch (e) {
|
|
invocation.return_gerror (e);
|
|
}
|
|
});
|
|
}
|
|
|
|
ScreenshotAsync(params, invocation) {
|
|
let [includeCursor, flash, filename] = params;
|
|
let screenshot = this._createScreenshot(invocation);
|
|
if (!screenshot)
|
|
return;
|
|
screenshot.screenshot(includeCursor, filename,
|
|
(o, res) => {
|
|
try {
|
|
let [result, area, filenameUsed] =
|
|
screenshot.screenshot_finish(res);
|
|
this._onScreenshotComplete(result, area, filenameUsed,
|
|
flash, invocation);
|
|
} catch (e) {
|
|
invocation.return_gerror (e);
|
|
}
|
|
});
|
|
}
|
|
|
|
SelectAreaAsync(params, invocation) {
|
|
let selectArea = new SelectArea();
|
|
selectArea.show();
|
|
selectArea.connect('finished', (selectArea, areaRectangle) => {
|
|
if (areaRectangle) {
|
|
let retRectangle = this._unscaleArea(areaRectangle.x, areaRectangle.y,
|
|
areaRectangle.width, areaRectangle.height);
|
|
let retval = GLib.Variant.new('(iiii)', retRectangle);
|
|
invocation.return_value(retval);
|
|
} else {
|
|
invocation.return_error_literal(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED,
|
|
"Operation was cancelled");
|
|
}
|
|
});
|
|
}
|
|
|
|
FlashAreaAsync(params, invocation) {
|
|
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: x, y: y, width: width, height: height });
|
|
flashspot.fire();
|
|
invocation.return_value(null);
|
|
}
|
|
|
|
PickColorAsync(params, invocation) {
|
|
let pickPixel = new PickPixel();
|
|
pickPixel.show();
|
|
pickPixel.connect('finished', (pickPixel, coords) => {
|
|
if (coords) {
|
|
let screenshot = this._createScreenshot(invocation, false);
|
|
if (!screenshot)
|
|
return;
|
|
screenshot.pick_color(...coords, (o, res) => {
|
|
let [success_, color] = screenshot.pick_color_finish(res);
|
|
let { red, green, blue } = color;
|
|
let retval = GLib.Variant.new('(a{sv})', [{
|
|
color: GLib.Variant.new('(ddd)', [
|
|
red / 255.0,
|
|
green / 255.0,
|
|
blue / 255.0
|
|
])
|
|
}]);
|
|
this._removeShooterForSender(invocation.get_sender());
|
|
invocation.return_value(retval);
|
|
});
|
|
} else {
|
|
invocation.return_error_literal(Gio.IOErrorEnum, Gio.IOErrorEnum.CANCELLED,
|
|
"Operation was cancelled");
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
var SelectArea = class {
|
|
constructor() {
|
|
this._startX = -1;
|
|
this._startY = -1;
|
|
this._lastX = 0;
|
|
this._lastY = 0;
|
|
this._result = null;
|
|
|
|
this._group = new St.Widget({ visible: false,
|
|
reactive: true,
|
|
x: 0,
|
|
y: 0 });
|
|
Main.uiGroup.add_actor(this._group);
|
|
|
|
this._grabHelper = new GrabHelper.GrabHelper(this._group);
|
|
|
|
this._group.connect('button-press-event',
|
|
this._onButtonPress.bind(this));
|
|
this._group.connect('button-release-event',
|
|
this._onButtonRelease.bind(this));
|
|
this._group.connect('motion-event',
|
|
this._onMotionEvent.bind(this));
|
|
|
|
let constraint = new Clutter.BindConstraint({ source: global.stage,
|
|
coordinate: Clutter.BindCoordinate.ALL });
|
|
this._group.add_constraint(constraint);
|
|
|
|
this._rubberband = new St.Widget({
|
|
style_class: 'select-area-rubberband',
|
|
visible: false
|
|
});
|
|
this._group.add_actor(this._rubberband);
|
|
}
|
|
|
|
show() {
|
|
if (!this._grabHelper.grab({ actor: this._group,
|
|
onUngrab: this._onUngrab.bind(this) }))
|
|
return;
|
|
|
|
global.display.set_cursor(Meta.Cursor.CROSSHAIR);
|
|
Main.uiGroup.set_child_above_sibling(this._group, null);
|
|
this._group.visible = true;
|
|
}
|
|
|
|
_getGeometry() {
|
|
return { 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 };
|
|
}
|
|
|
|
_onMotionEvent(actor, 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;
|
|
}
|
|
|
|
_onButtonPress(actor, event) {
|
|
[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;
|
|
}
|
|
|
|
_onButtonRelease() {
|
|
this._result = this._getGeometry();
|
|
this._group.ease({
|
|
opacity: 0,
|
|
duration: 200,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
onComplete: () => this._grabHelper.ungrab()
|
|
});
|
|
return Clutter.EVENT_PROPAGATE;
|
|
}
|
|
|
|
_onUngrab() {
|
|
global.display.set_cursor(Meta.Cursor.DEFAULT);
|
|
this.emit('finished', this._result);
|
|
|
|
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
|
this._group.destroy();
|
|
return GLib.SOURCE_REMOVE;
|
|
});
|
|
}
|
|
};
|
|
Signals.addSignalMethods(SelectArea.prototype);
|
|
|
|
var PickPixel = class {
|
|
constructor() {
|
|
this._result = null;
|
|
|
|
this._group = new St.Widget({ visible: false,
|
|
reactive: true });
|
|
Main.uiGroup.add_actor(this._group);
|
|
|
|
this._grabHelper = new GrabHelper.GrabHelper(this._group);
|
|
|
|
this._group.connect('button-release-event',
|
|
this._onButtonRelease.bind(this));
|
|
|
|
let constraint = new Clutter.BindConstraint({ source: global.stage,
|
|
coordinate: Clutter.BindCoordinate.ALL });
|
|
this._group.add_constraint(constraint);
|
|
}
|
|
|
|
show() {
|
|
if (!this._grabHelper.grab({ actor: this._group,
|
|
onUngrab: this._onUngrab.bind(this) }))
|
|
return;
|
|
|
|
global.display.set_cursor(Meta.Cursor.CROSSHAIR);
|
|
Main.uiGroup.set_child_above_sibling(this._group, null);
|
|
this._group.visible = true;
|
|
}
|
|
|
|
_onButtonRelease(actor, event) {
|
|
this._result = event.get_coords();
|
|
this._grabHelper.ungrab();
|
|
return Clutter.EVENT_PROPAGATE;
|
|
}
|
|
|
|
_onUngrab() {
|
|
global.display.set_cursor(Meta.Cursor.DEFAULT);
|
|
this.emit('finished', this._result);
|
|
|
|
GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
|
|
this._group.destroy();
|
|
return GLib.SOURCE_REMOVE;
|
|
});
|
|
}
|
|
};
|
|
Signals.addSignalMethods(PickPixel.prototype);
|
|
|
|
var FLASHSPOT_ANIMATION_OUT_TIME = 500; // milliseconds
|
|
|
|
var Flashspot = class extends Lightbox.Lightbox {
|
|
constructor(area) {
|
|
super(Main.uiGroup, { inhibitEvents: true,
|
|
width: area.width,
|
|
height: area.height });
|
|
|
|
this.actor.style_class = 'flashspot';
|
|
this.actor.set_position(area.x, area.y);
|
|
}
|
|
|
|
fire(doneCallback) {
|
|
this.actor.show();
|
|
this.actor.opacity = 255;
|
|
this.actor.ease({
|
|
opacity: 0,
|
|
duration: FLASHSPOT_ANIMATION_OUT_TIME,
|
|
mode: Clutter.AnimationMode.EASE_OUT_QUAD,
|
|
onComplete: () => {
|
|
if (doneCallback)
|
|
doneCallback();
|
|
this.destroy();
|
|
}
|
|
});
|
|
}
|
|
};
|