[gnome-shell] screenshot-ui: Add capturing and screen selection



commit 6f42eaf17d9cb2d0f6c21e6cdcd5a96b750f8faa
Author: Ivan Molodetskikh <yalterz gmail com>
Date:   Sat Jan 15 18:23:32 2022 +0300

    screenshot-ui: Add capturing and screen selection
    
    Part-of: <https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1954>

 .../gnome-shell-sass/widgets/_screenshot.scss      |  49 +++++
 js/ui/screenshot.js                                | 223 ++++++++++++++++++++-
 2 files changed, 270 insertions(+), 2 deletions(-)
---
diff --git a/data/theme/gnome-shell-sass/widgets/_screenshot.scss 
b/data/theme/gnome-shell-sass/widgets/_screenshot.scss
index 5c98fce299..1c46146fa8 100644
--- a/data/theme/gnome-shell-sass/widgets/_screenshot.scss
+++ b/data/theme/gnome-shell-sass/widgets/_screenshot.scss
@@ -32,3 +32,52 @@
     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;
+  }
+}
diff --git a/js/ui/screenshot.js b/js/ui/screenshot.js
index 17e61531ec..364541ae4b 100644
--- a/js/ui/screenshot.js
+++ b/js/ui/screenshot.js
@@ -14,6 +14,11 @@ Gio._promisify(Shell.Screenshot.prototype,
     'screenshot_window', 'screenshot_window_finish');
 Gio._promisify(Shell.Screenshot.prototype,
     'screenshot_area', 'screenshot_area_finish');
+Gio._promisify(Shell.Screenshot.prototype,
+    'screenshot_stage_to_content', 'screenshot_stage_to_content_finish');
+Gio._promisify(
+    Shell.Screenshot,
+    'composite_to_stream', 'composite_to_stream_finish');
 
 const { loadInterfaceXML } = imports.misc.fileUtils;
 const { DBusSenderChecker } = imports.misc.util;
@@ -53,8 +58,26 @@ class ScreenshotUI extends St.Widget {
             visible: false,
         });
 
+        // The full-screen screenshot has a separate container so that we can
+        // show it without the screenshot UI fade-in for a nicer animation.
+        this._stageScreenshotContainer = new St.Widget({ visible: false });
+        this._stageScreenshotContainer.add_constraint(new Clutter.BindConstraint({
+            source: global.stage,
+            coordinate: Clutter.BindCoordinate.ALL,
+        }));
+        Main.layoutManager.screenshotUIGroup.add_child(
+            this._stageScreenshotContainer);
+
         Main.layoutManager.screenshotUIGroup.add_child(this);
 
+        this._stageScreenshot = new St.Widget({ style_class: 'screenshot-ui-screen-screenshot' });
+        this._stageScreenshot.add_constraint(new Clutter.BindConstraint({
+            source: global.stage,
+            coordinate: Clutter.BindCoordinate.ALL,
+        }));
+        this._stageScreenshotContainer.add_child(this._stageScreenshot);
+
+        this._openingCoroutineInProgress = false;
         this._grabHelper = new GrabHelper.GrabHelper(this, {
             actionMode: Shell.ActionMode.POPUP,
         });
@@ -83,9 +106,42 @@ class ScreenshotUI extends St.Widget {
         this._closeButton.connect('clicked', () => this.close());
         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', () => {
             // Nope, not dealing with monitor changes.
             this.close(true);
+            this._rebuildMonitorBins();
         });
 
         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.
         // We already have them captured on the screenshot anyway.
         //
@@ -122,11 +251,26 @@ class ScreenshotUI extends St.Widget {
             opacity: 255,
             duration: 200,
             mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+            onComplete: () => {
+                this._stageScreenshotContainer.get_parent().remove_child(
+                    this._stageScreenshotContainer);
+                this.insert_child_at_index(this._stageScreenshotContainer, 0);
+            },
         });
     }
 
     _finishClosing() {
         this.hide();
+
+        this._shooter = null;
+
+        this._stageScreenshotContainer.get_parent().remove_child(
+            this._stageScreenshotContainer);
+        Main.layoutManager.screenshotUIGroup.insert_child_at_index(
+            this._stageScreenshotContainer, 0);
+        this._stageScreenshotContainer.hide();
+
+        this._stageScreenshot.set_content(null);
     }
 
     close(instantly = false) {
@@ -145,13 +289,88 @@ class ScreenshotUI extends St.Widget {
             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.
  */
 function showScreenshotUI() {
-    Main.screenshotUI.open();
+    Main.screenshotUI.open().catch(err => {
+        logError(err, 'Error opening the screenshot UI');
+    });
 }
 
 var ScreenshotService = class {


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]