├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── LICENSE ├── README.md └── window-corner-preview@fabiomereu.it ├── bundle.js ├── convenience.js ├── extension.js ├── icon-128.png ├── icon.svg ├── indicator.js ├── metadata.json ├── polygnome.js ├── popupSliderMenuItem.js ├── prefs.js ├── preview.js ├── schemas ├── gschemas.compiled └── org.gnome.shell.extensions.window-corner-preview.gschema.xml ├── settings.js ├── signaling.js └── stylesheet.css /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | **Describe the bug** 8 | A clear and concise description of what the bug is. 9 | 10 | **To Reproduce** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Expected behavior** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | - Device: [e.g. iPhone6] 30 | - OS: [e.g. iOS8.1] 31 | - Browser [e.g. stock browser, safari] 32 | - Version [e.g. 22] 33 | 34 | **Additional context** 35 | Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | local/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Fabio Mereu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Window Corner Preview 2 | GNOME Shell extension that creates a window preview and anchors it to a corner of the screen. 3 | May be useful to watch YouTube videos while working, or any kind of video or movie. 4 | 5 | **Window Corner Preview** is an extension for GNOME > 3.10 (for 3.32 please [see this issue](https://github.com/medenagan/window-corner-preview/issues/14). Find this extension on the [GNOME extensions repository](https://extensions.gnome.org/extension/1227/window-corner-preview/). 6 | 7 | ## Usage 8 | **Picking a window up** 9 | Choose a window to preview by selecting it from the monkey panel menu. 10 | 11 | **Zooming and cropping** 12 | You can adjust the zoom by scrolling in and out on the middle of the preview box. To resize the margins, scroll along the box edges. 13 | Alternatively, you can use the sliders on the monkey menu. 14 | 15 | **Moving** 16 | *LEFT BUTTON*: it will go to the opposite corner 17 | *CENTER BUTTON*: it will go to the previous corner 18 | *RIGHT BUTTON*: it will go to the next corner 19 | 20 | **Activating the window** 21 | CTRL + CLICK: it will focus the displayed window 22 | 23 | 24 | ## What's new? 25 | Here's a list of most changes for each version 26 | ### v. 2 27 | 28 | - Supports preview cropping 29 | - Scroll events handling 30 | - Improved tweening settings for a more natural GNOME look 31 | - Autohide when windows are not supposed to be displayed 32 | - Schema settings added 33 | - Can optionally hide itself when the mirrored window is focused on top 34 | 35 | ### v. 1 36 | 37 | - Forked and adapted 38 | 39 | ## F.A.Q. 40 | > **Can you make it work when the window is minimized?** 41 | Sorry, I think it's not possible for now. From what I can see some apps (like Chromium when video streaming is being played) get "frozen" when you minimize the window. I guess it's for keeping the system more efficient. 42 | **WORKAROUND**: Please just move the window to another workspace (SUPER + SHIFT + PAGE DOWN / UP), it won't bother you anymore. 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/bundle.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function normalizeRange(denormal, min, max, step) { 4 | if (step !== undefined) denormal = Math.round(denormal / step) * step; 5 | // To a range 0-1 6 | return (denormal - min) / (max - min); 7 | }; 8 | 9 | function deNormalizeRange(normal, min, max, step) { 10 | // from [0, 1] to MIN - MAX 11 | let denormal = (max - min) * normal + min; 12 | if (step !== undefined) denormal = Math.round(denormal / step) * step; 13 | return denormal; 14 | }; 15 | 16 | // Truncate too long window titles on the menu 17 | function spliceTitle(text, max) { 18 | text = text || ""; 19 | max = max || 25; 20 | if (text.length > max) { 21 | return text.substr(0, max - 2) + "..."; 22 | } 23 | else { 24 | return text; 25 | } 26 | }; 27 | 28 | function getWindowSignature(metawindow) { 29 | return "".concat( 30 | metawindow.get_pid(), 31 | metawindow.get_wm_class(), 32 | metawindow.get_title()//, 33 | // metawindow.get_stable_sequence() 34 | ); 35 | } 36 | 37 | function getWindowHash(metawindow) { 38 | return metawindow ? sdbm(getWindowSignature(metawindow)).toString(36) : ""; 39 | } 40 | 41 | // https://github.com/sindresorhus/sdbm 42 | function sdbm(string) { 43 | 44 | let hash = 0; 45 | 46 | for (let i = 0; i < string.length; i++) { 47 | hash = string.charCodeAt(i) + (hash << 6) + (hash << 16) - hash; 48 | } 49 | 50 | // Convert it to an unsigned 32-bit integer 51 | return hash >>> 0; 52 | } 53 | -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/convenience.js: -------------------------------------------------------------------------------- 1 | /* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 | /* 3 | Copyright (c) 2011-2012, Giovanni Campagna 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of the GNOME nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY 20 | DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 21 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 22 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 23 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 24 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 25 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | */ 27 | 28 | const Gettext = imports.gettext; 29 | const Gio = imports.gi.Gio; 30 | 31 | const Config = imports.misc.config; 32 | const ExtensionUtils = imports.misc.extensionUtils; 33 | 34 | /** 35 | * initTranslations: 36 | * @domain: (optional): the gettext domain to use 37 | * 38 | * Initialize Gettext to load translations from extensionsdir/locale. 39 | * If @domain is not provided, it will be taken from metadata['gettext-domain'] 40 | */ 41 | function initTranslations(domain) { 42 | let extension = ExtensionUtils.getCurrentExtension(); 43 | 44 | domain = domain || extension.metadata['gettext-domain']; 45 | 46 | // check if this extension was built with "make zip-file", and thus 47 | // has the locale files in a subfolder 48 | // otherwise assume that extension has been installed in the 49 | // same prefix as gnome-shell 50 | let localeDir = extension.dir.get_child('locale'); 51 | if (localeDir.query_exists(null)) 52 | Gettext.bindtextdomain(domain, localeDir.get_path()); 53 | else 54 | Gettext.bindtextdomain(domain, Config.LOCALEDIR); 55 | } 56 | 57 | /** 58 | * getSettings: 59 | * @schema: (optional): the GSettings schema id 60 | * 61 | * Builds and return a GSettings schema for @schema, using schema files 62 | * in extensionsdir/schemas. If @schema is not provided, it is taken from 63 | * metadata['settings-schema']. 64 | */ 65 | function getSettings(schema) { 66 | let extension = ExtensionUtils.getCurrentExtension(); 67 | 68 | schema = schema || extension.metadata['settings-schema']; 69 | 70 | const GioSSS = Gio.SettingsSchemaSource; 71 | 72 | // check if this extension was built with "make zip-file", and thus 73 | // has the schema files in a subfolder 74 | // otherwise assume that extension has been installed in the 75 | // same prefix as gnome-shell (and therefore schemas are available 76 | // in the standard folders) 77 | let schemaDir = extension.dir.get_child('schemas'); 78 | let schemaSource; 79 | if (schemaDir.query_exists(null)) 80 | schemaSource = GioSSS.new_from_directory(schemaDir.get_path(), 81 | GioSSS.get_default(), 82 | false); 83 | else 84 | schemaSource = GioSSS.get_default(); 85 | 86 | let schemaObj = schemaSource.lookup(schema, true); 87 | if (!schemaObj) 88 | throw new Error('Schema ' + schema + ' could not be found for extension ' 89 | + extension.metadata.uuid + '. Please check your installation.'); 90 | 91 | return new Gio.Settings({ settings_schema: schemaObj }); 92 | } 93 | 94 | -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/extension.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright (c) 2017 Fabius 3 | Released under the MIT license 4 | 5 | Window Corner Preview Gnome Extension 6 | 7 | Purpose: It adds a menu to the GNOME main panel from which you can turn the 8 | preview of any desktop window on. 9 | It can help you watch a movie or a video while studying or working. 10 | 11 | This is a fork of https://github.com/Exsul/float-youtube-for-gnome 12 | by "Enelar" Kirill Berezin which was originally forked itself 13 | from https://github.com/Shou/float-mpv by "Shou" Benedict Aas. 14 | 15 | Contributors: 16 | Scott Ames https://github.com/scottames 17 | Jan Tojnar https://github.com/jtojnar 18 | */ 19 | 20 | "use strict"; 21 | 22 | // Global modules 23 | const Lang = imports.lang; 24 | const Main = imports.ui.main; 25 | const Mainloop = imports.mainloop; 26 | 27 | // Internal modules 28 | const ExtensionUtils = imports.misc.extensionUtils; 29 | const Me = ExtensionUtils.getCurrentExtension(); 30 | const Preview = Me.imports.preview; 31 | const Indicator = Me.imports.indicator; 32 | const Settings = Me.imports.settings; 33 | const Signaling = Me.imports.signaling; 34 | const Bundle = Me.imports.bundle; 35 | const Polygnome = Me.imports.polygnome; 36 | 37 | const WindowCornerPreview = Preview.WindowCornerPreview; 38 | const WindowCornerIndicator = Indicator.WindowCornerIndicator; 39 | const WindowCornerSettings = Settings.WindowCornerSettings; 40 | const SignalConnector = Signaling.SignalConnector; 41 | 42 | const getWindowSignature = Bundle.getWindowSignature; 43 | const getWindowHash = Bundle.getWindowHash; 44 | const getMetawindows = Polygnome.getMetawindows; 45 | const getWorkspaceWindowsArray = Polygnome.getWorkspaceWindowsArray; 46 | const getWorkspaces = Polygnome.getWorkspaces; 47 | 48 | function onZoomChanged() { 49 | settings.initialZoom = this.zoom; 50 | } 51 | 52 | function onCropChanged() { 53 | settings.initialLeftCrop = this.leftCrop; 54 | settings.initialRightCrop = this.rightCrop; 55 | settings.initialTopCrop = this.topCrop; 56 | settings.initialBottomCrop = this.bottomCrop; 57 | } 58 | 59 | function onCornerChanged() { 60 | settings.initialCorner = this.corner; 61 | } 62 | 63 | function onWindowChanged(preview, window) { 64 | settings.lastWindowHash = getWindowHash(preview.visible && window); 65 | } 66 | 67 | function onSettingsChanged(settings, property) { 68 | if (["focusHidden"].indexOf(property) > -1) { 69 | // this = preview 70 | this[property] = settings[property]; 71 | } 72 | } 73 | 74 | function previewLastWindow(preview) { 75 | 76 | const lastWindowHash = settings.lastWindowHash; 77 | 78 | if (! lastWindowHash) return; 79 | 80 | const signals = new SignalConnector(); 81 | 82 | let done, timer; 83 | 84 | function shouldBePreviewed(anyWindow) { 85 | 86 | if (!done && lastWindowHash === getWindowHash(anyWindow)) { 87 | 88 | done = true; 89 | signals.disconnectAll(); 90 | 91 | if (timer) { 92 | Mainloop.source_remove(timer); 93 | timer = null; 94 | } 95 | 96 | // I don't know exactly the reason, but some windows 97 | // do not get shown properly without putting this on async 98 | // The thumbnail seems not to be ready yet 99 | Mainloop.timeout_add(100, function () { 100 | preview.window = anyWindow; 101 | preview.show(); 102 | }); 103 | } 104 | } 105 | 106 | // If the Extension is firstly activated the window list is empty [] and will 107 | // be filled in shortly, instead if it's enabled later (like via Tweak tool) 108 | // the array is already filled 109 | const windows = getMetawindows(); 110 | if (windows.length) { 111 | windows.forEach(function (window) { 112 | shouldBePreviewed(window); 113 | }); 114 | } 115 | else { 116 | 117 | getWorkspaces().forEach(function (workspace) { 118 | signals.tryConnectAfter(workspace, "window-added", function (workspace, window) { 119 | shouldBePreviewed(window); 120 | }); 121 | }); 122 | 123 | const TIMEOUT = 10000; 124 | timer = Mainloop.timeout_add(TIMEOUT, function () { 125 | // In case the last window previewed could not be found, stop listening 126 | done = true; 127 | signals.disconnectAll(); 128 | }); 129 | } 130 | } 131 | 132 | let preview, menu; 133 | let settings, signals; 134 | 135 | function init() { 136 | settings = new WindowCornerSettings(); 137 | signals = new SignalConnector(); 138 | } 139 | 140 | function enable() { 141 | preview = new WindowCornerPreview(); 142 | signals.tryConnect(settings, "changed", Lang.bind(preview, onSettingsChanged)); 143 | signals.tryConnect(preview, "zoom-changed", Lang.bind(preview, onZoomChanged)); 144 | signals.tryConnect(preview, "crop-changed", Lang.bind(preview, onCropChanged)); 145 | signals.tryConnect(preview, "corner-changed", Lang.bind(preview, onCornerChanged)); 146 | signals.tryConnect(preview, "window-changed", Lang.bind(preview, onWindowChanged)); 147 | 148 | // Initialize props 149 | preview.zoom = settings.initialZoom; 150 | preview.leftCrop = settings.initialLeftCrop; 151 | preview.rightCrop = settings.initialRightCrop; 152 | preview.topCrop = settings.initialTopCrop; 153 | preview.bottomCrop = settings.initialBottomCrop; 154 | preview.focusHidden = settings.focusHidden; 155 | preview.corner = settings.initialCorner; 156 | 157 | menu = new WindowCornerIndicator(); 158 | menu.preview = preview; 159 | 160 | menu.enable(); 161 | Main.panel.addToStatusArea("WindowCornerIndicator", menu); 162 | 163 | // The last window being previewed is reactivate 164 | previewLastWindow(preview); 165 | } 166 | 167 | function disable() { 168 | signals.disconnectAll(); 169 | // Save the last window on (or off) 170 | onWindowChanged.call(null, preview, preview.window); 171 | preview.passAway(); 172 | menu.disable(); 173 | menu.destroy(); 174 | preview = null; 175 | menu = null; 176 | } 177 | -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/icon-128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medenagan/window-corner-preview/9ee0d4b99f874c6544e21e6e0e87d6c1e5f42e6b/window-corner-preview@fabiomereu.it/icon-128.png -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | image/svg+xml -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/indicator.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Global modules 4 | const Lang = imports.lang; 5 | const St = imports.gi.St; 6 | const Main = imports.ui.main; 7 | const PanelMenu = imports.ui.panelMenu; 8 | const PopupMenu = imports.ui.popupMenu; 9 | 10 | // Internal modules 11 | const ExtensionUtils = imports.misc.extensionUtils; 12 | const Me = ExtensionUtils.getCurrentExtension(); 13 | const PopupSliderMenuItem = Me.imports.popupSliderMenuItem.PopupSliderMenuItem; 14 | const Bundle = Me.imports.bundle; 15 | const Polygnome = Me.imports.polygnome; 16 | const Preview = Me.imports.preview; 17 | 18 | // Utilities 19 | const getWorkspaceWindowsArray = Polygnome.getWorkspaceWindowsArray; 20 | const spliceTitle = Bundle.spliceTitle; 21 | 22 | // Preview default values 23 | const MIN_ZOOM = Preview.MIN_ZOOM; 24 | const MAX_ZOOM = Preview.MAX_ZOOM; 25 | const MAX_CROP_RATIO = Preview.MAX_CROP_RATIO; 26 | const DEFAULT_ZOOM = Preview.DEFAULT_ZOOM; 27 | const DEFAULT_CROP_RATIO = Preview.DEFAULT_CROP_RATIO; 28 | 29 | var WindowCornerIndicator = new Lang.Class({ 30 | 31 | Name: "WindowCornerPreview.indicator", 32 | Extends: PanelMenu.Button, 33 | 34 | _init: function() { 35 | this.parent(null, "WindowCornerPreview.indicator"); 36 | }, 37 | 38 | // Handler to turn preview on / off 39 | _onMenuIsEnabled: function(item) { 40 | (item.state) ? this.preview.show() : this.preview.hide(); 41 | }, 42 | 43 | _updateSliders: function() { 44 | this.menuZoom.value = this.preview.zoom; 45 | this.menuZoomLabel.label.set_text("Monitor Zoom: " + Math.floor(this.preview.zoom * 100).toString() + "%"); 46 | 47 | this.menuLeftCrop.value = this.preview.leftCrop; 48 | this.menuRightCrop.value = this.preview.rightCrop; 49 | this.menuTopCrop.value = this.preview.topCrop; 50 | this.menuBottomCrop.value = this.preview.bottomCrop; 51 | }, 52 | 53 | _onZoomChanged: function(source, value) { 54 | this.preview.zoom = value; 55 | this._updateSliders(); 56 | this.preview.emit("zoom-changed"); 57 | }, 58 | 59 | _onLeftCropChanged: function(source, value) { 60 | this.preview.leftCrop = value; 61 | this._updateSliders(); 62 | this.preview.emit("crop-changed"); 63 | }, 64 | 65 | _onRightCropChanged: function(source, value) { 66 | this.preview.rightCrop = value; 67 | this._updateSliders(); 68 | this.preview.emit("crop-changed"); 69 | }, 70 | 71 | _onTopCropChanged: function(source, value) { 72 | this.preview.topCrop = value; 73 | this._updateSliders(); 74 | this.preview.emit("crop-changed"); 75 | }, 76 | 77 | _onBottomCropChanged: function(source, value) { 78 | this.preview.bottomCrop = value; 79 | this._updateSliders(); 80 | this.preview.emit("crop-changed"); 81 | }, 82 | 83 | _onSettings: function() { 84 | Main.Util.trySpawnCommandLine("gnome-shell-extension-prefs window-corner-preview@fabiomereu.it"); 85 | }, 86 | 87 | // Update windows list and other menus before menu pops up 88 | _onUserTriggered: function() { 89 | this.menuIsEnabled.setToggleState(this.preview.visible); 90 | this.menuIsEnabled.actor.reactive = this.preview.window; 91 | this._updateSliders() 92 | this.menuWindows.menu.removeAll(); 93 | getWorkspaceWindowsArray().forEach(function(workspace, i) { 94 | if (i > 0) { 95 | this.menuWindows.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); 96 | } 97 | 98 | // Populate window list on submenu 99 | workspace.windows.forEach(function(window) { 100 | let winMenuItem = new PopupMenu.PopupMenuItem(spliceTitle(window.get_title())); 101 | winMenuItem.connect("activate", Lang.bind(this, function() { 102 | this.preview.window = window; 103 | this.preview.show(); 104 | })); 105 | 106 | this.menuWindows.menu.addMenuItem(winMenuItem); 107 | }, this); 108 | }, this); 109 | }, 110 | 111 | enable: function() { 112 | 113 | // Add icon 114 | this.icon = new St.Icon({ 115 | icon_name: "face-monkey-symbolic", 116 | style_class: "system-status-icon" 117 | }); 118 | this.actor.add_actor(this.icon); 119 | 120 | // Prepare Menu... 121 | 122 | // 1. Preview ON/OFF 123 | this.menuIsEnabled = new PopupMenu.PopupSwitchMenuItem("Preview", false, { 124 | hover: false, 125 | reactive: true 126 | }); 127 | this.menuIsEnabled.connect("toggled", Lang.bind(this, this._onMenuIsEnabled)); 128 | this.menu.addMenuItem(this.menuIsEnabled); 129 | this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); 130 | 131 | // 2. Windows list 132 | this.menuWindows = new PopupMenu.PopupSubMenuMenuItem("Windows"); 133 | this.menu.addMenuItem(this.menuWindows); 134 | this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); 135 | 136 | // 3a. Zoom label 137 | this.menuZoomLabel = new PopupMenu.PopupMenuItem("", { 138 | activate: false, 139 | reactive: false 140 | }); 141 | this.menu.addMenuItem(this.menuZoomLabel); 142 | 143 | // 3b, Zoom slider 144 | this.menuZoom = new PopupSliderMenuItem(false, DEFAULT_ZOOM, MIN_ZOOM, MAX_ZOOM, 0.005); // slider step: 0.5% 145 | this.menuZoom.connect("value-changed", Lang.bind(this, this._onZoomChanged)); 146 | this.menu.addMenuItem(this.menuZoom); 147 | 148 | // 4. Crop Sliders 149 | this.menuCrop = new PopupMenu.PopupSubMenuMenuItem("Crop"); 150 | this.menu.addMenuItem(this.menuCrop); 151 | 152 | this.menuTopCrop = new PopupSliderMenuItem("Top", DEFAULT_CROP_RATIO, 0.0, MAX_CROP_RATIO); 153 | this.menuTopCrop.connect("value-changed", Lang.bind(this, this._onTopCropChanged)); 154 | this.menuCrop.menu.addMenuItem(this.menuTopCrop); 155 | 156 | this.menuLeftCrop = new PopupSliderMenuItem("Left", DEFAULT_CROP_RATIO, 0.0, MAX_CROP_RATIO); 157 | this.menuLeftCrop.connect("value-changed", Lang.bind(this, this._onLeftCropChanged)); 158 | this.menuCrop.menu.addMenuItem(this.menuLeftCrop); 159 | 160 | this.menuRightCrop = new PopupSliderMenuItem("Right", DEFAULT_CROP_RATIO, 0.0, MAX_CROP_RATIO); 161 | this.menuRightCrop.connect("value-changed", Lang.bind(this, this._onRightCropChanged)); 162 | this.menuCrop.menu.addMenuItem(this.menuRightCrop); 163 | 164 | this.menuBottomCrop = new PopupSliderMenuItem("Bottom", DEFAULT_CROP_RATIO, 0.0, MAX_CROP_RATIO); 165 | this.menuBottomCrop.connect("value-changed", Lang.bind(this, this._onBottomCropChanged)); 166 | this.menuCrop.menu.addMenuItem(this.menuBottomCrop); 167 | this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); 168 | 169 | // 5. Settings 170 | this.menuSettings = new PopupMenu.PopupMenuItem("Settings"); 171 | this.menuSettings.connect("activate", Lang.bind(this, this._onSettings)); 172 | this.menu.addMenuItem(this.menuSettings); 173 | 174 | this.actor.connect("enter-event", Lang.bind(this, this._onUserTriggered)); 175 | 176 | }, 177 | 178 | disable: function() { 179 | this.menu.removeAll(); 180 | } 181 | }); 182 | -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "Watch your preferred video or movie while working\nOpen a window preview and pin it to the corner", 3 | "extension-id": "window-corner-preview", 4 | "name": "Window Corner Preview", 5 | "settings-schema": "org.gnome.shell.extensions.window-corner-preview", 6 | "gettext-domain": "gnome-shell-extensions", 7 | "shell-version": [ 8 | "3.10", 9 | "3.12", 10 | "3.14", 11 | "3.16", 12 | "3.18", 13 | "3.20", 14 | "3.22", 15 | "3.24", 16 | "3.26", 17 | "3.28", 18 | "3.30" 19 | ], 20 | "url": "https://github.com/medenagan/window-corner-preview", 21 | "uuid": "window-corner-preview@fabiomereu.it", 22 | "version": 3 23 | } 24 | -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/polygnome.js: -------------------------------------------------------------------------------- 1 | // Contributor: 2 | // Scott Ames https://github.com/scottames 3 | 4 | // Global modules 5 | const Meta = imports.gi.Meta; 6 | 7 | // This is wrapper to maintain compatibility with GNOME-Shell 3.30+ as well as 8 | // previous versions. 9 | var DisplayWrapper = { 10 | getScreen: function() { 11 | return global.screen || global.display; 12 | }, 13 | getWorkspaceManager: function() { 14 | return global.screen || global.workspace_manager; 15 | }, 16 | getMonitorManager: function() { 17 | return global.screen || Meta.MonitorManager.get(); 18 | } 19 | }; 20 | 21 | // Result: [{windows: [{win1}, {win2}, ...], workspace: {workspace}, index: nWorkspace, isActive: true|false}, ..., {...}] 22 | // Omit empty (with no windows) workspaces from the array 23 | function getWorkspaceWindowsArray() { 24 | let array = []; 25 | 26 | let wsActive = DisplayWrapper.getWorkspaceManager().get_active_workspace_index(); 27 | 28 | for (let i = 0; i < DisplayWrapper.getWorkspaceManager().n_workspaces; i++) { 29 | let workspace = DisplayWrapper.getWorkspaceManager().get_workspace_by_index(i); 30 | let windows = workspace.list_windows(); 31 | if (windows.length) array.push({ 32 | workspace: workspace, 33 | windows: windows, 34 | index: i, 35 | isActive: (i === wsActive) 36 | }); 37 | } 38 | return array; 39 | }; 40 | 41 | function getWorkspaces() { 42 | const workspaceManager = DisplayWrapper.getWorkspaceManager(); 43 | const workspaces = []; 44 | for (let i = 0; i < workspaceManager.n_workspaces; i++) { 45 | workspaces.push(workspaceManager.get_workspace_by_index(i)); 46 | } 47 | return workspaces; 48 | } 49 | 50 | function getMetawindows() { 51 | return global.get_window_actors().map(function (actor) { 52 | return actor.get_meta_window(); 53 | }); 54 | } 55 | -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/popupSliderMenuItem.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Global modules 4 | const Lang = imports.lang; 5 | const St = imports.gi.St; 6 | const Slider = imports.ui.slider; 7 | const PopupMenu = imports.ui.popupMenu; 8 | 9 | // Internal modules 10 | const ExtensionUtils = imports.misc.extensionUtils; 11 | const Me = ExtensionUtils.getCurrentExtension(); 12 | const Bundle = Me.imports.bundle; 13 | 14 | // Utilities 15 | const normalizeRange = Bundle.normalizeRange; 16 | const deNormalizeRange = Bundle.deNormalizeRange; 17 | 18 | var PopupSliderMenuItem = new Lang.Class({ 19 | Name: "WindowCornerPreview.PopupSliderMenuItem", 20 | Extends: PopupMenu.PopupBaseMenuItem, 21 | 22 | _init: function(text, value, min, max, step, params) { 23 | 24 | this.min = (min !== undefined ? min : 0.0); 25 | this.max = (max !== undefined ? max : 1.0); 26 | this.defaultValue = (value !== undefined ? value : (this.max + this.min) / 2.0); 27 | // *** KNOWN ISSUE: Scrolling may get stucked if step value > 1.0 (and |min-max| is a low value) 28 | // due to const SLIDER_SCROLL_STEP = 0.02 on js/ui/slider.js *** 29 | this.step = step; 30 | params = params || {}; 31 | 32 | params.activate = false; 33 | 34 | this.parent(params); 35 | 36 | this.label = new St.Label({ 37 | text: text || "" 38 | }); 39 | // Setting text to false allow a little bit extra space on the left 40 | if (text !== false) this.actor.add_child(this.label); 41 | this.actor.label_actor = this.label; 42 | 43 | this.slider = new Slider.Slider(0.0); 44 | this.value = this.defaultValue; 45 | 46 | // PopupSliderMenuItem emits its own value-change event which provides a normalized value 47 | this.slider.connect("value-changed", Lang.bind(this, function(x) { 48 | let normalValue = this.value; 49 | // Force the slider to set position on a stepped value (if necessary) 50 | if (this.step !== undefined) this.value = normalValue; 51 | // Don't through any event if step rounded it to the same value 52 | if (normalValue !== this._lastValue) this.emit("value-changed", normalValue); 53 | this._lastValue = normalValue; 54 | })); 55 | 56 | this.actor.add(this.slider.actor, { 57 | expand: true, 58 | align: St.Align.END 59 | }); 60 | }, 61 | 62 | get value() { 63 | return deNormalizeRange(this.slider.value, this.min, this.max, this.step); 64 | }, 65 | 66 | set value(newValue) { 67 | this._lastValue = normalizeRange(newValue, this.min, this.max, this.step); 68 | this.slider.setValue(this._lastValue); 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/prefs.js: -------------------------------------------------------------------------------- 1 | // Global modules 2 | const GObject = imports.gi.GObject; 3 | const Gtk = imports.gi.Gtk; 4 | const Lang = imports.lang; 5 | 6 | // Internal modules 7 | const ExtensionUtils = imports.misc.extensionUtils; 8 | const Me = ExtensionUtils.getCurrentExtension(); 9 | const Settings = Me.imports.settings; 10 | 11 | const WindowCornerSettings = Settings.WindowCornerSettings; 12 | 13 | function init() { 14 | // Nothing 15 | } 16 | 17 | const WindowCornerPreviewPrefsWidget = new GObject.Class({ 18 | Name: "WindowCornerPreview.Prefs.Widget", 19 | GTypeName: "WindowCornerPreviewPrefsWidget", 20 | Extends: Gtk.VBox, 21 | 22 | _init: function(params) { 23 | this.parent(params); 24 | 25 | this.margin = 24; 26 | this.spacing = 6; 27 | 28 | const settings = new WindowCornerSettings(); 29 | 30 | // 1. Behavior 31 | 32 | this.add(new Gtk.Label({ 33 | label: "Behavior when mouse is over (UNDER DEVELOPMENT)", 34 | use_markup: true, 35 | xalign: 0.0, 36 | yalign: 0.0 37 | })); 38 | 39 | let boxBehavior = new Gtk.VBox({ 40 | spacing: 6, 41 | margin_top: 6, 42 | margin_left: 12 43 | }); 44 | 45 | 46 | const behaviors = [ 47 | { 48 | mode: "seethrough", 49 | label: "See-through (one click to drive it away)" 50 | }, 51 | { 52 | mode: "autohide", 53 | label: "Hide-and-seek (vanish and turn up automatically)" 54 | } 55 | ]; 56 | 57 | const currentBehaviorMode = settings.behaviorMode; 58 | 59 | let radio = null; 60 | 61 | behaviors.forEach(function (behavior) { 62 | 63 | radio = new Gtk.RadioButton({ 64 | active: behavior.mode === currentBehaviorMode, 65 | label: behavior.label, 66 | group: radio, 67 | sensitive: false 68 | }); 69 | 70 | radio.connect("toggled", Lang.bind(this, function(button) { 71 | if (button.active) { 72 | settings.behaviorMode = behavior.mode; 73 | } 74 | })); 75 | 76 | boxBehavior.add(radio); 77 | }); 78 | 79 | this.add(boxBehavior); 80 | 81 | // 2. Hide on top 82 | 83 | let checkHideOnFocus = new Gtk.CheckButton({ 84 | label: "Hide when the mirrored window is on top", 85 | active: settings.focusHidden 86 | }); 87 | 88 | checkHideOnFocus.connect("toggled", function(button) { 89 | settings.focusHidden = button.active; 90 | }); 91 | 92 | let boxHideOnFocus = new Gtk.VBox({margin_top: 12}); 93 | 94 | boxHideOnFocus.add(checkHideOnFocus); 95 | this.add(boxHideOnFocus); 96 | } 97 | }); 98 | 99 | function buildPrefsWidget() { 100 | let widget = new WindowCornerPreviewPrefsWidget(); 101 | widget.show_all(); 102 | 103 | return widget; 104 | } 105 | -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/preview.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Global modules 4 | const Lang = imports.lang; 5 | const Main = imports.ui.main; 6 | const St = imports.gi.St; 7 | const Tweener = imports.ui.tweener; 8 | const Clutter = imports.gi.Clutter; 9 | const Signals = imports.signals; 10 | 11 | // Internal modules 12 | const ExtensionUtils = imports.misc.extensionUtils; 13 | const Me = ExtensionUtils.getCurrentExtension(); 14 | const Polygnome = Me.imports.polygnome; 15 | const Signaling = Me.imports.signaling; 16 | 17 | const DisplayWrapper = Polygnome.DisplayWrapper; 18 | const SignalConnector = Signaling.SignalConnector; 19 | 20 | // At the moment magnification hasn't been tested and it's clumsy 21 | const SETTING_MAGNIFICATION_ALLOWED = false; 22 | 23 | const CORNER_TOP_LEFT = 0; 24 | const CORNER_TOP_RIGHT = 1; 25 | const CORNER_BOTTOM_RIGHT = 2; 26 | const CORNER_BOTTOM_LEFT = 3; 27 | const DEFAULT_CORNER = CORNER_TOP_RIGHT; 28 | 29 | var MIN_ZOOM = 0.10; // User shouldn't be able to make the preview too small or big, as it may break normal experience 30 | var MAX_ZOOM = 0.75; 31 | var DEFAULT_ZOOM = 0.20; 32 | 33 | var MAX_CROP_RATIO = 0.85; 34 | var DEFAULT_CROP_RATIO = 0.0; 35 | 36 | const SCROLL_ACTOR_MARGIN = 0.2; // scrolling: 20% external margin to crop, 80% to zoom 37 | const SCROLL_ZOOM_STEP = 0.01; // 1% zoom for step 38 | const SCROLL_CROP_STEP = 0.0063; // cropping step when user scrolls 39 | 40 | // Animation constants 41 | const TWEEN_OPACITY_FULL = 255; 42 | const TWEEN_OPACITY_SEMIFULL = Math.round(TWEEN_OPACITY_FULL * 0.90); 43 | const TWEEN_OPACITY_HALF = Math.round(TWEEN_OPACITY_FULL * 0.50); 44 | const TWEEN_OPACITY_TENTH = Math.round(TWEEN_OPACITY_FULL * 0.10); 45 | const TWEEN_OPACITY_NULL = 0; 46 | 47 | const TWEEN_TIME_SHORT = 0.25; 48 | const TWEEN_TIME_MEDIUM = 0.6; 49 | const TWEEN_TIME_LONG = 0.80; 50 | 51 | const GTK_MOUSE_LEFT_BUTTON = 1; 52 | const GTK_MOUSE_MIDDLE_BUTTON = 2; 53 | const GTK_MOUSE_RIGHT_BUTTON = 3; 54 | 55 | const GDK_SHIFT_MASK = 1; 56 | const GDK_CONTROL_MASK = 4; 57 | const GDK_MOD1_MASK = 8; 58 | const GDK_ALT_MASK = GDK_MOD1_MASK; // Most cases 59 | 60 | var WindowCornerPreview = new Lang.Class({ 61 | 62 | Name: "WindowCornerPreview.preview", 63 | 64 | _init: function() { 65 | 66 | this._corner = DEFAULT_CORNER; 67 | this._zoom = DEFAULT_ZOOM; 68 | 69 | this._leftCrop = DEFAULT_CROP_RATIO; 70 | this._rightCrop = DEFAULT_CROP_RATIO; 71 | this._topCrop = DEFAULT_CROP_RATIO; 72 | this._bottomCrop = DEFAULT_CROP_RATIO; 73 | 74 | // The following properties are documented on _adjustVisibility() 75 | this._naturalVisibility = false; 76 | this._focusHidden = true; 77 | 78 | this._container = null; 79 | this._window = null; 80 | 81 | this._windowSignals = new SignalConnector(); 82 | this._environmentSignals = new SignalConnector(); 83 | 84 | this._handleZoomChange = null; 85 | }, 86 | 87 | _onClick: function(actor, event) { 88 | let button = event.get_button(); 89 | let state = event.get_state(); 90 | 91 | // CTRL + LEFT BUTTON activate the window on top 92 | if (button === GTK_MOUSE_LEFT_BUTTON && (state & GDK_CONTROL_MASK)) { 93 | this._window.activate(global.get_current_time()); 94 | } 95 | 96 | // Otherwise move the preview to another corner 97 | else { 98 | switch (button) { 99 | case GTK_MOUSE_RIGHT_BUTTON: 100 | this.corner += 1; 101 | break; 102 | 103 | case GTK_MOUSE_MIDDLE_BUTTON: 104 | this.corner += -1; 105 | break; 106 | 107 | default: // GTK_MOUSE_LEFT_BUTTON: 108 | this.corner += 2; 109 | } 110 | this.emit("corner-changed"); 111 | } 112 | }, 113 | 114 | _onScroll: function(actor, event) { 115 | let scroll_direction = event.get_scroll_direction(); 116 | 117 | let direction; 118 | switch (scroll_direction) { 119 | 120 | case Clutter.ScrollDirection.UP: 121 | case Clutter.ScrollDirection.LEFT: 122 | direction = +1.0 123 | break; 124 | 125 | case Clutter.ScrollDirection.DOWN: 126 | case Clutter.ScrollDirection.RIGHT: 127 | direction = -1.0 128 | break; 129 | 130 | default: 131 | direction = 0.0; 132 | } 133 | 134 | if (! direction) return; // Clutter.EVENT_PROPAGATE; 135 | 136 | // On mouse over it's normally pretty transparent, but user needs to see more for adjusting it 137 | Tweener.addTween(this._container, { 138 | opacity: TWEEN_OPACITY_SEMIFULL, 139 | time: TWEEN_TIME_SHORT, 140 | transition: "easeOutQuad" 141 | }); 142 | 143 | // Coords are absolute, screen related 144 | let [mouseX, mouseY] = event.get_coords(); 145 | 146 | // _container absolute rect 147 | let [actorX1, actorY1] = this._container.get_transformed_position(); 148 | let [actorWidth, actorHeight] = this._container.get_transformed_size(); 149 | let actorX2 = actorX1 + actorWidth; 150 | let actorY2 = actorY1 + actorHeight; 151 | 152 | // Distance of pointer from each side 153 | let deltaLeft = Math.abs(actorX1 - mouseX); 154 | let deltaRight = Math.abs(actorX2 - mouseX); 155 | let deltaTop = Math.abs(actorY1 - mouseY); 156 | let deltaBottom = Math.abs(actorY2 - mouseY); 157 | 158 | let sortedDeltas = [{ 159 | property: "leftCrop", 160 | pxDistance: deltaLeft, 161 | comparedDistance: deltaLeft / actorWidth, 162 | direction: -direction 163 | }, 164 | { 165 | property: "rightCrop", 166 | pxDistance: deltaRight, 167 | comparedDistance: deltaRight / actorWidth, 168 | direction: -direction 169 | }, 170 | { 171 | property: "topCrop", 172 | pxDistance: deltaTop, 173 | comparedDistance: deltaTop / actorHeight, 174 | direction: -direction /* feels more natural */ 175 | }, 176 | { 177 | property: "bottomCrop", 178 | pxDistance: deltaBottom, 179 | comparedDistance: deltaBottom / actorHeight, 180 | direction: -direction 181 | } 182 | ]; 183 | sortedDeltas.sort(function(a, b) { 184 | return a.pxDistance - b.pxDistance 185 | }); 186 | let deltaMinimum = sortedDeltas[0]; 187 | 188 | // Scrolling inside the preview triggers the zoom 189 | if (deltaMinimum.comparedDistance > SCROLL_ACTOR_MARGIN) { 190 | this.zoom += direction * SCROLL_ZOOM_STEP; 191 | this.emit("zoom-changed"); 192 | } 193 | 194 | // Scrolling along the margins triggers the cropping instead 195 | else { 196 | this[deltaMinimum.property] += deltaMinimum.direction * SCROLL_CROP_STEP; 197 | this.emit("crop-changed"); 198 | } 199 | }, 200 | 201 | _onEnter: function(actor, event) { 202 | let [x, y, state] = global.get_pointer(); 203 | 204 | // SHIFT: ignore standard behavior 205 | if (state & GDK_SHIFT_MASK) { 206 | return; // Clutter.EVENT_PROPAGATE; 207 | } 208 | 209 | Tweener.addTween(this._container, { 210 | opacity: TWEEN_OPACITY_TENTH, 211 | time: TWEEN_TIME_MEDIUM, 212 | transition: "easeOutQuad" 213 | }); 214 | }, 215 | 216 | _onLeave: function() { 217 | Tweener.addTween(this._container, { 218 | opacity: TWEEN_OPACITY_FULL, 219 | time: TWEEN_TIME_MEDIUM, 220 | transition: "easeOutQuad" 221 | }); 222 | }, 223 | 224 | _onParamsChange: function() { 225 | // Zoom or crop properties changed 226 | if (this.enabled) this._setThumbnail(); 227 | }, 228 | 229 | _onWindowUnmanaged: function() { 230 | this.disable(); 231 | this._window = null; 232 | // gnome-shell --replace will cause this event too 233 | this.emit("window-changed", null); 234 | }, 235 | 236 | _adjustVisibility: function(options) { 237 | options = options || {}; 238 | 239 | /* 240 | [Boolean] this._naturalVisibility: 241 | true === show the preview whenever is possible; 242 | false === don't show it in any case 243 | [Boolean] this._focusHidden: 244 | true === hide in case the mirrored window should be active 245 | 246 | options = { 247 | onComplete: [function] to call once the process is done. 248 | It's called even if visibility was already set as requested 249 | 250 | noAnimate: [Boolean] to skip animation. If switching from window A to window B, 251 | for example, the preview gets first destroyed (so hidden) then recreated. 252 | This would lead to a fade-out + fade-in, which is not what most users like. 253 | noAnimate === true avoids that. 254 | }; 255 | */ 256 | 257 | if (! this._container) { 258 | if (options.onComplete) options.onComplete(); 259 | return; 260 | } 261 | 262 | // Hide when overView is shown, or source window is on top, or user related reasons 263 | let canBeShownOnFocus = (! this._focusHidden) || (global.display.focus_window !== this._window); 264 | 265 | let calculatedVisibility = this._window && 266 | this._naturalVisibility && 267 | canBeShownOnFocus && 268 | (! Main.overview.visibleTarget); 269 | 270 | let calculatedOpacity = (calculatedVisibility) ? TWEEN_OPACITY_FULL : TWEEN_OPACITY_NULL; 271 | 272 | // Already OK (hidden / shown), no change needed 273 | if ((calculatedVisibility === this._container.visible) && (calculatedOpacity === this._container.get_opacity())) { 274 | if (options.onComplete) options.onComplete(); 275 | } 276 | 277 | // Quick set (show or hide), but don't animate 278 | else if (options.noAnimate) { 279 | this._container.set_opacity(calculatedOpacity) 280 | this._container.visible = calculatedVisibility; 281 | if (options.onComplete) options.onComplete(); 282 | } 283 | 284 | // Animation needed (either from less to more opacity or viceversa) 285 | else { 286 | this._container.reactive = false; 287 | if (! this._container.visible) { 288 | this._container.set_opacity(TWEEN_OPACITY_NULL); 289 | this._container.visible = true; 290 | } 291 | 292 | Tweener.addTween(this._container, { 293 | opacity: calculatedOpacity, 294 | time: TWEEN_TIME_SHORT, 295 | transition: "easeOutQuad", 296 | onComplete: Lang.bind(this, function() { 297 | this._container.visible = calculatedVisibility; 298 | this._container.reactive = true; 299 | if (options.onComplete) options.onComplete(); 300 | }) 301 | }); 302 | } 303 | }, 304 | 305 | _onNotifyFocusWindow: function() { 306 | this._adjustVisibility(); 307 | }, 308 | 309 | _onOverviewShowing: function() { 310 | this._adjustVisibility(); 311 | }, 312 | 313 | _onOverviewHiding: function() { 314 | this._adjustVisibility(); 315 | }, 316 | 317 | _onMonitorsChanged: function() { 318 | // TODO multiple monitors issue, the preview doesn't stick to the right monitor 319 | log("Monitors changed"); 320 | }, 321 | 322 | // Align the preview along the chrome area 323 | _setPosition: function() { 324 | 325 | if (! this._container) { 326 | return; 327 | } 328 | 329 | let posX, posY; 330 | 331 | let rectMonitor = Main.layoutManager.getWorkAreaForMonitor(DisplayWrapper.getScreen().get_current_monitor()); 332 | 333 | let rectChrome = { 334 | x1: rectMonitor.x, 335 | y1: rectMonitor.y, 336 | x2: rectMonitor.width + rectMonitor.x - this._container.get_width(), 337 | y2: rectMonitor.height + rectMonitor.y - this._container.get_height() 338 | }; 339 | 340 | switch (this._corner) { 341 | 342 | case CORNER_TOP_LEFT: 343 | posX = rectChrome.x1; 344 | posY = rectChrome.y1; 345 | break; 346 | 347 | case CORNER_BOTTOM_LEFT: 348 | posX = rectChrome.x1; 349 | posY = rectChrome.y2; 350 | break; 351 | 352 | case CORNER_BOTTOM_RIGHT: 353 | posX = rectChrome.x2; 354 | posY = rectChrome.y2; 355 | break; 356 | 357 | default: // CORNER_TOP_RIGHT: 358 | posX = rectChrome.x2; 359 | posY = rectChrome.y1; 360 | } 361 | this._container.set_position(posX, posY); 362 | }, 363 | 364 | // Create a window thumbnail and adds it to the container 365 | _setThumbnail: function() { 366 | 367 | if (! this._container) return; 368 | 369 | this._container.foreach(function(actor) { 370 | actor.destroy(); 371 | }); 372 | 373 | if (! this._window) return; 374 | 375 | let mutw = this._window.get_compositor_private(); 376 | 377 | if (! mutw) return; 378 | 379 | let windowTexture = mutw.get_texture(); 380 | let [windowWidth, windowHeight] = windowTexture.get_size(); 381 | 382 | /* To crop the window texture, for now I've found that: 383 | 1. Using a clip rect on Clutter.clone will hide the outside portion but also will KEEP the space along it 384 | 2. The Clutter.clone is stretched to fill all of its room when it's painted, so the transparent area outside 385 | cannot be easily left out by only adjusting the actor size (empty space only gets reproportioned). 386 | 387 | My current workaround: 388 | - Define a margin rect by using some proportional [0.0 - 1.0] trimming values for left, right, ... Zero: no trimming 1: all trimmed out 389 | - Set width and height of the Clutter.clone based on the crop rect and apply a translation to anchor it the top left margin 390 | (set_clip_to_allocation must be set true on the container to get rid of the translated texture overflow) 391 | - Ratio of the cropped texture is different from the original one, so this must be compensated with Clutter.clone scale_x/y parameters 392 | 393 | Known issues: 394 | - Strongly cropped textual windows like terminals get a little bit blurred. However, I was told this feature 395 | was useful for framed videos to peel off, particularly. So shouldn't affect that much. 396 | 397 | Hopefully, some kind guy will soon explain to me how to clone just a portion of the source :D 398 | */ 399 | 400 | // Get absolute margin values for cropping 401 | let margins = { 402 | left: windowWidth * this.leftCrop, 403 | right: windowWidth * this.rightCrop, 404 | top: windowHeight * this.topCrop, 405 | bottom: windowHeight * this.bottomCrop, 406 | }; 407 | 408 | // Calculate the size of the cropped rect (based on the 100% window size) 409 | let croppedWidth = windowWidth - (margins.left + margins.right); 410 | let croppedHeight = windowHeight - (margins.top + margins.bottom); 411 | 412 | // To mantain a similar thumbnail size whenever the user selects a different window to preview, 413 | // instead of zooming out based on the window size itself, it takes the window screen as a standard unit (= 100%) 414 | let rectMonitor = Main.layoutManager.getWorkAreaForMonitor(DisplayWrapper.getScreen().get_current_monitor()); 415 | let targetRatio = rectMonitor.width * this.zoom / windowWidth; 416 | 417 | // No magnification allowed (KNOWN ISSUE: there's no height control if used, it still needs optimizing) 418 | if (! SETTING_MAGNIFICATION_ALLOWED && targetRatio > 1.0) { 419 | targetRatio = 1.0; 420 | this._zoom = windowWidth / rectMonitor.width; // do NOT set this.zoom (the encapsulated prop for _zoom) or it will be looping! 421 | } 422 | 423 | let thumbnail = new Clutter.Clone({ // list parameters https://www.roojs.org/seed/gir-1.2-gtk-3.0/seed/Clutter.Clone.html 424 | source: windowTexture, 425 | reactive: false, 426 | 427 | magnification_filter: Clutter.ScalingFilter.NEAREST, //NEAREST, //TRILINEAR, 428 | 429 | translation_x: -margins.left * targetRatio, 430 | translation_y: -margins.top * targetRatio, 431 | 432 | // Compensating scales due the different ratio of the cropped window texture 433 | scale_x: windowWidth / croppedWidth, 434 | scale_y: windowHeight / croppedHeight, 435 | 436 | width: croppedWidth * targetRatio, 437 | height: croppedHeight * targetRatio, 438 | 439 | margin_left: 0, 440 | margin_right: 0, 441 | margin_bottom: 0, 442 | margin_top: 0 443 | 444 | }); 445 | 446 | this._container.add_actor(thumbnail); 447 | 448 | this._setPosition(); 449 | }, 450 | 451 | // xCrop properties normalize their opposite counterpart, so that margins won't ever overlap 452 | set leftCrop(value) { 453 | // [0, MAX] range 454 | this._leftCrop = Math.min(MAX_CROP_RATIO, Math.max(0.0, value)); 455 | // Decrease the opposite margin if necessary 456 | this._rightCrop = Math.min(this._rightCrop, MAX_CROP_RATIO - this._leftCrop); 457 | this._onParamsChange(); 458 | }, 459 | 460 | set rightCrop(value) { 461 | this._rightCrop = Math.min(MAX_CROP_RATIO, Math.max(0.0, value)); 462 | this._leftCrop = Math.min(this._leftCrop, MAX_CROP_RATIO - this._rightCrop); 463 | this._onParamsChange(); 464 | }, 465 | 466 | set topCrop(value) { 467 | this._topCrop = Math.min(MAX_CROP_RATIO, Math.max(0.0, value)); 468 | this._bottomCrop = Math.min(this._bottomCrop, MAX_CROP_RATIO - this._topCrop); 469 | this._onParamsChange(); 470 | }, 471 | 472 | set bottomCrop(value) { 473 | this._bottomCrop = Math.min(MAX_CROP_RATIO, Math.max(0.0, value)); 474 | this._topCrop = Math.min(this._topCrop, MAX_CROP_RATIO - this._bottomCrop); 475 | this._onParamsChange(); 476 | }, 477 | 478 | get leftCrop() { 479 | return this._leftCrop; 480 | }, 481 | 482 | get rightCrop() { 483 | return this._rightCrop; 484 | }, 485 | 486 | get topCrop() { 487 | return this._topCrop; 488 | }, 489 | 490 | get bottomCrop() { 491 | return this._bottomCrop; 492 | }, 493 | 494 | set zoom(value) { 495 | this._zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, value)); 496 | this._onParamsChange(); 497 | }, 498 | 499 | get zoom() { 500 | return this._zoom; 501 | }, 502 | 503 | set focusHidden(value) { 504 | this._focusHidden = !!value; 505 | this._adjustVisibility(); 506 | }, 507 | 508 | get focusHidden() { 509 | return this._focusHidden; 510 | }, 511 | 512 | set corner(value) { 513 | this._corner = (value %= 4) < 0 ? (value + 4) : (value); 514 | this._setPosition(); 515 | }, 516 | 517 | get corner() { 518 | return this._corner; 519 | }, 520 | 521 | get enabled() { 522 | return !!this._container; 523 | }, 524 | 525 | get visible() { 526 | return this._container && this._window && this._naturalVisibility; 527 | }, 528 | 529 | show: function(onComplete) { 530 | this._naturalVisibility = true; 531 | this._adjustVisibility({ 532 | onComplete: onComplete 533 | }); 534 | }, 535 | 536 | hide: function(onComplete) { 537 | this._naturalVisibility = false; 538 | this._adjustVisibility({ 539 | onComplete: onComplete 540 | }); 541 | }, 542 | 543 | toggle: function(onComplete) { 544 | this._naturalVisibility = !this._naturalVisibility; 545 | this._adjustVisibility({ 546 | onComplete: onComplete 547 | }); 548 | }, 549 | 550 | passAway: function() { 551 | this._naturalVisibility = false; 552 | this._adjustVisibility({ 553 | onComplete: Lang.bind(this, this.disable) 554 | }); 555 | }, 556 | 557 | get window() { 558 | return this._window; 559 | }, 560 | 561 | set window(metawindow) { 562 | 563 | this.enable(); 564 | 565 | this._windowSignals.disconnectAll(); 566 | 567 | this._window = metawindow; 568 | 569 | if (metawindow) { 570 | this._windowSignals.tryConnect(metawindow, "unmanaged", Lang.bind(this, this._onWindowUnmanaged)); 571 | // Version 3.10 does not support size-changed 572 | this._windowSignals.tryConnect(metawindow, "size-changed", Lang.bind(this, this._setThumbnail)); 573 | this._windowSignals.tryConnect(metawindow, "notify::maximized-vertically", Lang.bind(this, this._setThumbnail)); 574 | this._windowSignals.tryConnect(metawindow, "notify::maximized-horizontally", Lang.bind(this, this._setThumbnail)); 575 | } 576 | 577 | this._setThumbnail(); 578 | 579 | this.emit("window-changed", metawindow); 580 | }, 581 | 582 | enable: function() { 583 | 584 | if (this._container) return; 585 | 586 | let isSwitchingWindow = this.enabled; 587 | 588 | this._environmentSignals.tryConnect(Main.overview, "showing", Lang.bind(this, this._onOverviewShowing)); 589 | this._environmentSignals.tryConnect(Main.overview, "hiding", Lang.bind(this, this._onOverviewHiding)); 590 | this._environmentSignals.tryConnect(global.display, "notify::focus-window", Lang.bind(this, this._onNotifyFocusWindow)); 591 | this._environmentSignals.tryConnect(DisplayWrapper.getMonitorManager(), "monitors-changed", Lang.bind(this, this._onMonitorsChanged)); 592 | 593 | this._container = new St.Button({ 594 | style_class: "window-corner-preview" 595 | }); 596 | // Force content not to overlap, allowing cropping 597 | this._container.set_clip_to_allocation(true); 598 | 599 | this._container.connect("enter-event", Lang.bind(this, this._onEnter)); 600 | this._container.connect("leave-event", Lang.bind(this, this._onLeave)); 601 | // Don't use button-press-event, as set_position conflicts and Gtk would react for enter and leave event of ANY item on the chrome area 602 | this._container.connect("button-release-event", Lang.bind(this, this._onClick)); 603 | this._container.connect("scroll-event", Lang.bind(this, this._onScroll)); 604 | 605 | this._container.visible = false; 606 | Main.layoutManager.addChrome(this._container); 607 | 608 | return; 609 | // isSwitchingWindow = false means user only changed window, but preview was on, so does not animate 610 | this._adjustVisibility({ 611 | noAnimate: isSwitchingWindow 612 | }); 613 | }, 614 | 615 | disable: function() { 616 | 617 | this._windowSignals.disconnectAll(); 618 | this._environmentSignals.disconnectAll(); 619 | 620 | if (! this._container) return; 621 | 622 | Main.layoutManager.removeChrome(this._container); 623 | this._container.destroy(); 624 | this._container = null; 625 | } 626 | }) 627 | 628 | Signals.addSignalMethods(WindowCornerPreview.prototype); 629 | -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/schemas/gschemas.compiled: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/medenagan/window-corner-preview/9ee0d4b99f874c6544e21e6e0e87d6c1e5f42e6b/window-corner-preview@fabiomereu.it/schemas/gschemas.compiled -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/schemas/org.gnome.shell.extensions.window-corner-preview.gschema.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | "seethrough" 19 | Mouse over behavior 20 | Behavior when mouse is over the preview 21 | 22 | 23 | false 24 | Hide when window is focused 25 | Whether to automatically hide the preview when the mirrored window is on top. 26 | 27 | 28 | 29 | 0.20 30 | Initial zoom ratio 31 | Initial zoom ratio 32 | 33 | 34 | 35 | 0.0 36 | Initial Left Crop Ratio 37 | Initial Left Crop Ratio 38 | 39 | 40 | 41 | 0.0 42 | Initial Right Crop Ratio 43 | Initial Right Crop Ratio 44 | 45 | 46 | 47 | 0.0 48 | Initial Top Crop Ratio 49 | Initial Top Crop Ratio 50 | 51 | 52 | 53 | 0.0 54 | Initial Bottom Crop Ratio 55 | Initial Bottom Crop Ratio 56 | 57 | 58 | "top-right" 59 | Initial Corner 60 | Initial Corner 61 | 62 | 63 | "" 64 | Last Window 65 | Last Window Hash 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/settings.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Global modules 4 | const Lang = imports.lang; 5 | const Signals = imports.signals; 6 | 7 | // Internal modules 8 | const ExtensionUtils = imports.misc.extensionUtils; 9 | const Me = ExtensionUtils.getCurrentExtension(); 10 | const Convenience = Me.imports.convenience; 11 | 12 | // Schema keys 13 | var SETTING_BEHAVIOR_MODE = "behavior-mode"; 14 | var SETTING_FOCUS_HIDDEN = "focus-hidden"; 15 | var SETTING_INITIAL_ZOOM = "initial-zoom"; 16 | var SETTING_INITIAL_LEFT_CROP = "initial-left-crop"; 17 | var SETTING_INITIAL_RIGHT_CROP = "initial-right-crop"; 18 | var SETTING_INITIAL_TOP_CROP = "initial-top-crop"; 19 | var SETTING_INITIAL_BOTTOM_CROP = "initial-bottom-crop"; 20 | var SETTING_INITIAL_CORNER = "initial-corner"; 21 | var SETTING_LAST_WINDOW_HASH = "last-window-hash"; 22 | 23 | var WindowCornerSettings = new Lang.Class({ 24 | 25 | Name: "WindowCornerPreview.settings", 26 | 27 | _init: function() { 28 | this._settings = Convenience.getSettings(); 29 | this._settings.connect("changed", Lang.bind(this, this._onChanged)); 30 | }, 31 | 32 | _onChanged: function(settings, key) { 33 | // "my-property-name" => myPropertyName 34 | const property = key.replace(/-[a-z]/g, function (az) { 35 | return az.substr(1).toUpperCase(); 36 | }); 37 | this.emit("changed", property); 38 | }, 39 | 40 | get focusHidden() { 41 | return this._settings.get_boolean(SETTING_FOCUS_HIDDEN); 42 | }, 43 | 44 | set focusHidden(value) { 45 | this._settings.set_boolean(SETTING_FOCUS_HIDDEN, value); 46 | }, 47 | 48 | get initialZoom() { 49 | return this._settings.get_double(SETTING_INITIAL_ZOOM); 50 | }, 51 | 52 | set initialZoom(value) { 53 | this._settings.set_double(SETTING_INITIAL_ZOOM, value); 54 | }, 55 | 56 | get initialLeftCrop() { 57 | return this._settings.get_double(SETTING_INITIAL_LEFT_CROP); 58 | }, 59 | 60 | set initialLeftCrop(value) { 61 | this._settings.set_double(SETTING_INITIAL_LEFT_CROP, value); 62 | }, 63 | 64 | get initialRightCrop() { 65 | return this._settings.get_double(SETTING_INITIAL_RIGHT_CROP); 66 | }, 67 | 68 | set initialRightCrop(value) { 69 | this._settings.set_double(SETTING_INITIAL_RIGHT_CROP, value); 70 | }, 71 | 72 | get initialTopCrop() { 73 | return this._settings.get_double(SETTING_INITIAL_TOP_CROP); 74 | }, 75 | 76 | set initialTopCrop(value) { 77 | this._settings.set_double(SETTING_INITIAL_TOP_CROP, value); 78 | }, 79 | 80 | get initialBottomCrop() { 81 | return this._settings.get_double(SETTING_INITIAL_BOTTOM_CROP); 82 | }, 83 | 84 | set initialBottomCrop(value) { 85 | this._settings.set_double(SETTING_INITIAL_BOTTOM_CROP, value); 86 | }, 87 | 88 | get initialCorner() { 89 | return this._settings.get_enum(SETTING_INITIAL_CORNER); 90 | }, 91 | 92 | set initialCorner(value) { 93 | this._settings.set_enum(SETTING_INITIAL_CORNER, value); 94 | }, 95 | 96 | get behaviorMode() { 97 | return this._settings.get_string(SETTING_BEHAVIOR_MODE); 98 | }, 99 | 100 | set behaviorMode(value) { 101 | this._settings.set_string(SETTING_BEHAVIOR_MODE, value); 102 | }, 103 | 104 | get lastWindowHash() { 105 | return this._settings.get_string(SETTING_LAST_WINDOW_HASH); 106 | }, 107 | 108 | set lastWindowHash(value) { 109 | this._settings.set_string(SETTING_LAST_WINDOW_HASH, value); 110 | } 111 | }); 112 | 113 | Signals.addSignalMethods(WindowCornerSettings.prototype); 114 | -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/signaling.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | // Global modules 4 | const Lang = imports.lang; 5 | 6 | // Helper to disconnect more signals at once 7 | var SignalConnector = new Lang.Class({ 8 | 9 | Name: "WindowCornerPreview.SignalConnector", 10 | 11 | _init: function() { 12 | this._connections = []; 13 | }, 14 | 15 | tryConnect: function(actor, signal, callback) { 16 | try { 17 | let handle = actor.connect(signal, callback); 18 | this._connections.push({ 19 | actor: actor, 20 | handle: handle 21 | }); 22 | } 23 | 24 | catch (e) { 25 | logError(e, "SignalConnector.tryConnect failed"); 26 | } 27 | }, 28 | 29 | tryConnectAfter: function(actor, signal, callback) { 30 | try { 31 | let handle = actor.connect_after(signal, callback); 32 | this._connections.push({ 33 | actor: actor, 34 | handle: handle 35 | }); 36 | } 37 | 38 | catch (e) { 39 | logError(e, "SignalConnector.tryConnectAfter failed"); 40 | } 41 | }, 42 | 43 | disconnectAll: function() { 44 | for (let i = 0; i < this._connections.length; i++) { 45 | try { 46 | let connection = this._connections[i]; 47 | connection.actor.disconnect(connection.handle); 48 | this._connections[i] = null; 49 | } 50 | 51 | catch (e) { 52 | logError(e, "SignalConnector.disconnectAll failed"); 53 | } 54 | } 55 | this._connections = []; 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /window-corner-preview@fabiomereu.it/stylesheet.css: -------------------------------------------------------------------------------- 1 | /* Window Corner Preview css properties for the preview frame */ 2 | 3 | .window-corner-preview { 4 | /* Some windows may have space around */ 5 | /* debug background: blue; 6 | border: 20px solid red; */ 7 | } 8 | --------------------------------------------------------------------------------