screenshot-ui: Add capturing and screen selection

Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1954>
This commit is contained in:
Ivan Molodetskikh 2022-01-15 18:23:32 +03:00 committed by Marge Bot
parent deb614a031
commit 6f42eaf17d
2 changed files with 270 additions and 2 deletions

View File

@ -32,3 +32,52 @@
icon-size: $base_icon_size * 2; icon-size: $base_icon_size * 2;
} }
} }
.screenshot-ui-type-button {
padding: $base_padding * 2 $base_padding * 3;
border-radius: 12px + 21px - 18px;
font-weight: bold;
&:hover, &:focus { background-color: $hover_bg_color; }
&:active { background-color: $active_bg_color; }
&:checked { background-color: $hover_bg_color; }
&:insensitive { color: $insensitive_fg_color; }
}
.screenshot-ui-capture-button {
width: 36px;
height: 36px;
border-radius: 99px;
border: 4px white;
padding: 4px;
.screenshot-ui-capture-button-circle {
background-color: white;
transition-duration: 200ms;
&:hover, &:focus { background-color: $hover_bg_color; }
border-radius: 99px;
}
&:hover, &:focus {
.screenshot-ui-capture-button-circle {
background-color: darken(white, 15%);
}
}
&:active {
.screenshot-ui-capture-button-circle {
background-color: darken(white, 50%);
}
}
}
.screenshot-ui-screen-selector {
transition-duration: 200ms;
background-color: rgba(0, 0, 0, .5);
&:hover { background-color: rgba(0, 0, 0, .3); }
&:active { background-color: rgba(0, 0, 0, .7); }
&:checked {
background-color: transparent;
border: 2px white;
}
}

View File

@ -14,6 +14,11 @@ Gio._promisify(Shell.Screenshot.prototype,
'screenshot_window', 'screenshot_window_finish'); 'screenshot_window', 'screenshot_window_finish');
Gio._promisify(Shell.Screenshot.prototype, Gio._promisify(Shell.Screenshot.prototype,
'screenshot_area', 'screenshot_area_finish'); 'screenshot_area', 'screenshot_area_finish');
Gio._promisify(Shell.Screenshot.prototype,
'screenshot_stage_to_content', 'screenshot_stage_to_content_finish');
Gio._promisify(
Shell.Screenshot,
'composite_to_stream', 'composite_to_stream_finish');
const { loadInterfaceXML } = imports.misc.fileUtils; const { loadInterfaceXML } = imports.misc.fileUtils;
const { DBusSenderChecker } = imports.misc.util; const { DBusSenderChecker } = imports.misc.util;
@ -53,8 +58,26 @@ class ScreenshotUI extends St.Widget {
visible: false, visible: false,
}); });
// The full-screen screenshot has a separate container so that we can
// show it without the screenshot UI fade-in for a nicer animation.
this._stageScreenshotContainer = new St.Widget({ visible: false });
this._stageScreenshotContainer.add_constraint(new Clutter.BindConstraint({
source: global.stage,
coordinate: Clutter.BindCoordinate.ALL,
}));
Main.layoutManager.screenshotUIGroup.add_child(
this._stageScreenshotContainer);
Main.layoutManager.screenshotUIGroup.add_child(this); Main.layoutManager.screenshotUIGroup.add_child(this);
this._stageScreenshot = new St.Widget({ style_class: 'screenshot-ui-screen-screenshot' });
this._stageScreenshot.add_constraint(new Clutter.BindConstraint({
source: global.stage,
coordinate: Clutter.BindCoordinate.ALL,
}));
this._stageScreenshotContainer.add_child(this._stageScreenshot);
this._openingCoroutineInProgress = false;
this._grabHelper = new GrabHelper.GrabHelper(this, { this._grabHelper = new GrabHelper.GrabHelper(this, {
actionMode: Shell.ActionMode.POPUP, actionMode: Shell.ActionMode.POPUP,
}); });
@ -83,9 +106,42 @@ class ScreenshotUI extends St.Widget {
this._closeButton.connect('clicked', () => this.close()); this._closeButton.connect('clicked', () => this.close());
this._primaryMonitorBin.add_child(this._closeButton); this._primaryMonitorBin.add_child(this._closeButton);
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._screenButton = new IconLabelButton('video-display-symbolic', _('Screen'), {
style_class: 'screenshot-ui-type-button',
checked: true,
x_expand: true,
});
this._screenButton.connect('notify::checked',
this._onScreenButtonToggled.bind(this));
this._typeButtonContainer.add_child(this._screenButton);
this._bottomRowContainer = new St.Widget({ layout_manager: new Clutter.BinLayout() });
this._panel.add_child(this._bottomRowContainer);
this._captureButton = new St.Button({ style_class: 'screenshot-ui-capture-button' });
this._captureButton.set_child(new St.Widget({
style_class: 'screenshot-ui-capture-button-circle',
}));
this._captureButton.connect('clicked',
this._onCaptureButtonClicked.bind(this));
this._bottomRowContainer.add_child(this._captureButton);
this._monitorBins = [];
this._rebuildMonitorBins();
Main.layoutManager.connect('monitors-changed', () => { Main.layoutManager.connect('monitors-changed', () => {
// Nope, not dealing with monitor changes. // Nope, not dealing with monitor changes.
this.close(true); this.close(true);
this._rebuildMonitorBins();
}); });
Main.wm.addKeybinding( Main.wm.addKeybinding(
@ -101,7 +157,80 @@ class ScreenshotUI extends St.Widget {
); );
} }
open() { _rebuildMonitorBins() {
for (const bin of this._monitorBins)
bin.destroy();
this._monitorBins = [];
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 screenSelector = new St.Button({
style_class: 'screenshot-ui-screen-selector',
x_expand: true,
y_expand: true,
visible: this._screenButton.checked,
reactive: true,
can_focus: true,
toggle_mode: true,
});
screenSelector.connect('key-focus-in', () => {
this.grab_key_focus();
screenSelector.checked = true;
});
bin.add_child(screenSelector);
this._screenSelectors.push(screenSelector);
screenSelector.connect('notify::checked', () => {
if (!screenSelector.checked)
return;
screenSelector.toggle_mode = false;
for (const otherSelector of this._screenSelectors) {
if (screenSelector === otherSelector)
continue;
otherSelector.toggle_mode = true;
otherSelector.checked = false;
}
});
}
if (Main.layoutManager.primaryIndex !== -1)
this._screenSelectors[Main.layoutManager.primaryIndex].checked = true;
}
async open() {
if (this._openingCoroutineInProgress)
return;
if (!this.visible) {
// Screenshot UI is opening from completely closed state
// (rather than opening back from in process of closing).
this._shooter = new Shell.Screenshot();
this._openingCoroutineInProgress = true;
try {
const [content, scale] =
await this._shooter.screenshot_stage_to_content();
this._stageScreenshot.set_content(content);
this._scale = scale;
this._stageScreenshotContainer.show();
} catch (e) {
log('Error capturing screenshot: %s'.format(e.message));
}
this._openingCoroutineInProgress = false;
}
// Get rid of any popup menus. // Get rid of any popup menus.
// We already have them captured on the screenshot anyway. // We already have them captured on the screenshot anyway.
// //
@ -122,11 +251,26 @@ class ScreenshotUI extends St.Widget {
opacity: 255, opacity: 255,
duration: 200, duration: 200,
mode: Clutter.AnimationMode.EASE_OUT_QUAD, mode: Clutter.AnimationMode.EASE_OUT_QUAD,
onComplete: () => {
this._stageScreenshotContainer.get_parent().remove_child(
this._stageScreenshotContainer);
this.insert_child_at_index(this._stageScreenshotContainer, 0);
},
}); });
} }
_finishClosing() { _finishClosing() {
this.hide(); this.hide();
this._shooter = null;
this._stageScreenshotContainer.get_parent().remove_child(
this._stageScreenshotContainer);
Main.layoutManager.screenshotUIGroup.insert_child_at_index(
this._stageScreenshotContainer, 0);
this._stageScreenshotContainer.hide();
this._stageScreenshot.set_content(null);
} }
close(instantly = false) { close(instantly = false) {
@ -145,13 +289,88 @@ class ScreenshotUI extends St.Widget {
onComplete: this._finishClosing.bind(this), onComplete: this._finishClosing.bind(this),
}); });
} }
_onScreenButtonToggled() {
if (this._screenButton.checked) {
this._screenButton.toggle_mode = 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(),
});
}
}
}
_onCaptureButtonClicked() {
global.display.get_sound_player().play_from_theme(
'screen-capture', _('Screenshot taken'), null);
if (this._screenButton.checked) {
const content = this._stageScreenshot.get_content();
if (!content) {
// Failed to capture the screenshot for some reason.
this.close();
return;
}
const texture = content.get_texture();
const stream = Gio.MemoryOutputStream.new_resizable();
const index =
this._screenSelectors.findIndex(screen => screen.checked);
const monitor = Main.layoutManager.monitors[index];
const x = monitor.x * this._scale;
const y = monitor.y * this._scale;
const w = monitor.width * this._scale;
const h = monitor.height * this._scale;
Shell.Screenshot.composite_to_stream(
texture,
x, y, w, h,
stream
).then(() => {
stream.close(null);
const clipboard = St.Clipboard.get_default();
clipboard.set_content(
St.ClipboardType.CLIPBOARD,
'image/png',
stream.steal_as_bytes()
);
}).catch(err => {
logError(err, 'Error capturing screenshot');
});
}
this.close();
}
}); });
/** /**
* Shows the screenshot UI. * Shows the screenshot UI.
*/ */
function showScreenshotUI() { function showScreenshotUI() {
Main.screenshotUI.open(); Main.screenshotUI.open().catch(err => {
logError(err, 'Error opening the screenshot UI');
});
} }
var ScreenshotService = class { var ScreenshotService = class {