diff --git a/windows-alttab@samuel-ifrs/README.md b/windows-alttab@samuel-ifrs/README.md new file mode 100644 index 00000000..9f2add5e --- /dev/null +++ b/windows-alttab@samuel-ifrs/README.md @@ -0,0 +1,22 @@ +# Windows Alt+Tab + +An Alt+Tab window switcher for Cinnamon that shows **live window thumbnails (previews)**, in the classic Windows style. + +## Features + +- **Live thumbnails** of every window while you hold Alt and press Tab. +- Each entry shows the application icon and the window title. +- Items wrap into multiple rows when there are many windows, centered on the active monitor. +- Minimized windows are dimmed so you can tell them apart. +- **Middle-click** a thumbnail to close that window without leaving the switcher. +- **Right-click** a thumbnail to open the standard window menu (minimize, maximize, move, close, ...). + +## Usage + +Enable the extension in **System Settings → Extensions**. From then on, Alt+Tab uses the thumbnail switcher. + +While the extension is enabled it sets the Cinnamon `alttab-switcher-style` to `windows`. When you disable it, the previous switcher style is restored automatically. + +## Notes + +This extension provides only the Alt+Tab switcher. It does not change any other part of the desktop. diff --git a/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/classic_switcher_windows_style.js b/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/classic_switcher_windows_style.js new file mode 100644 index 00000000..8a989bd0 --- /dev/null +++ b/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/classic_switcher_windows_style.js @@ -0,0 +1,431 @@ +// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*- + +const Clutter = imports.gi.Clutter; +const St = imports.gi.St; +const Cinnamon = imports.gi.Cinnamon; +const Meta = imports.gi.Meta; +const Pango = imports.gi.Pango; + +const Main = imports.ui.main; +const BaseAppSwitcher = imports.ui.appSwitcher.appSwitcher; +const ClassicSwitcherModule = imports.ui.appSwitcher.classicSwitcher; + +const WindowUtils = imports.misc.windowUtils; + +const PREVIEW_HEIGHT = 120; +const MIN_PREVIEW_WIDTH = 170; +const MAX_PREVIEW_WIDTH = 330; +const MAX_LIST_WIDTH_RATIO = 0.9; + +function WindowsAppIcon(window, showThumbnail) { + this._init(window, showThumbnail); +} + +WindowsAppIcon.prototype = { + _init: function (window, showThumbnail) { + this.window = window; + this.showThumbnail = showThumbnail; + + let tracker = Cinnamon.WindowTracker.get_default(); + this.app = tracker.get_window_app(window); + + this.actor = new St.BoxLayout({ + style_class: "alt-tab-app windows-alt-tab-app", + vertical: true, + }); + + this.icon = null; + + this._infoBox = new St.BoxLayout({ + style_class: "windows-alt-tab-info-box", + vertical: false, + }); + + this._appIconBin = new St.Bin({ + x_align: St.Align.START, + y_align: St.Align.MIDDLE, + }); + + this._smallAppIcon = this.app + ? this.app.create_icon_texture(18) + : new St.Icon({ + icon_name: "application-default-icon", + icon_type: St.IconType.FULLCOLOR, + icon_size: 18, + }); + + this._appIconBin.set_child(this._smallAppIcon); + + let title = window.get_title(); + this.label = new St.Label({ + style_class: "windows-alt-tab-title", + text: title || (this.app ? this.app.get_name() : window.title), + y_align: Clutter.ActorAlign.CENTER, + }); + this.label.clutter_text.ellipsize = Pango.EllipsizeMode.END; + this.label.clutter_text.single_line_mode = true; + + this._infoBox.add(this._appIconBin, { + x_fill: false, + y_fill: false, + }); + + this._infoBox.add(this.label, { + x_fill: true, + y_fill: false, + expand: true, + }); + + this._iconBin = new St.Bin({ + style_class: "windows-alt-tab-preview-bin", + x_align: St.Align.START, + y_align: St.Align.MIDDLE, + }); + + this.actor.add(this._infoBox, { + x_fill: true, + y_fill: false, + expand: true, + }); + + this.actor.add(this._iconBin, { + x_fill: true, + y_fill: false, + expand: true, + }); + + if (window.minimized) { + let contrastEffect = new Clutter.BrightnessContrastEffect(); + contrastEffect.set_brightness_full(-0.5, -0.5, -0.5); + this.actor.add_effect(contrastEffect); + } + }, + + set_size: function (size) { + this.icon = new St.Widget({ + layout_manager: new Clutter.BinLayout(), + clip_to_allocation: true, + }); + + let frame = this.window.get_frame_rect(); + let ratio = frame.height > 0 ? frame.width / frame.height : 1.6; + ratio = Math.max(0.6, Math.min(2.5, ratio)); + + let previewHeight = Math.round(PREVIEW_HEIGHT * global.ui_scale); + let previewWidth = Math.round(previewHeight * ratio); + previewWidth = Math.max( + Math.round(MIN_PREVIEW_WIDTH * global.ui_scale), + Math.min(previewWidth, Math.round(MAX_PREVIEW_WIDTH * global.ui_scale)), + ); + + let clones = WindowUtils.createWindowClone( + this.window, + previewWidth, + previewHeight, + true, + true, + ); + + for (let i in clones) { + let clone = clones[i]; + this.icon.add_actor(clone.actor); + clone.actor.set_position(clone.x, clone.y); + } + + this._iconBin.set_size(previewWidth, previewHeight); + this._iconBin.set_child(this.icon); + + this.label.clutter_text.set_width(Math.max(80, previewWidth - 34)); + }, +}; + +function WindowsAppList() { + this._init.apply(this, arguments); +} + +WindowsAppList.prototype = { + __proto__: ClassicSwitcherModule.AppList.prototype, + + _init: function (windows, showThumbnails, activeMonitor) { + // Call SwitcherList._init directly (not AppList._init) because AppList._init + // would create AppIcons of the wrong type. AppList._init sets up no signals — + // only icon creation — so nothing important is skipped here. + ClassicSwitcherModule.SwitcherList.prototype._init.call( + this, + false, + activeMonitor, + ); + + let workspaceIcons = []; + for (let i = 0; i < windows.length; i++) { + workspaceIcons.push(new WindowsAppIcon(windows[i], showThumbnails)); + } + + this.icons = []; + for (let i = 0; i < workspaceIcons.length; i++) { + this._addIcon(workspaceIcons[i]); + } + + this._curApp = -1; + this._iconSize = 0; + this._mouseTimeOutId = 0; + this._activeMonitor = activeMonitor; + this._rowsCache = null; + }, + + // Sobrescreve para interceptar o clique com o botão do meio: ao invés de + // deixar o evento subir até o actor do switcher (que chama destroy e fecha + // o alt+tab), fecha apenas a janela daquele item e para a propagação. + // O Cinnamon trata a destruição da janela em _removeDestroyedWindow, + // reconstruindo a lista (ou fechando o switcher se era a última janela). + _addIcon: function (appIcon) { + this._rowsCache = null; + this.icons.push(appIcon); + this.addItem(appIcon.actor, appIcon.label); + + let bbox = this._items[this._items.length - 1]; + bbox.connect( + "button-press-event", + (actor, event) => { + let button = event.get_button(); + + // Botão do meio: fecha a janela daquele item (igual ao "X"), + // sem fechar o alt+tab. + if (button === Clutter.BUTTON_MIDDLE) { + if (appIcon.window) { + appIcon.window.delete(global.get_current_time()); + } + return Clutter.EVENT_STOP; + } + + // Botão direito: abre o menu de janela do Cinnamon (o mesmo da barra + // de título: minimizar, maximizar, mover, fechar, etc.), posicionado + // sobre a miniatura. O menu empilha seu próprio grab modal por cima do + // grab do switcher, então o alt+tab continua aberto. + if (button === Clutter.BUTTON_SECONDARY) { + if (appIcon.window) { + let [x, y] = actor.get_transformed_position(); + let [w, h] = actor.get_transformed_size(); + let rect = new Meta.Rectangle({ + x: Math.round(x), + y: Math.round(y), + width: Math.round(w), + height: Math.round(h), + }); + Main.wm._windowMenuManager.showWindowMenuForWindow( + appIcon.window, + Meta.WindowMenuType.WM, + rect, + ); + } + return Clutter.EVENT_STOP; + } + + return Clutter.EVENT_PROPAGATE; + }, + ); + }, + + _ensureIconsSized: function () { + this._iconSize = 96; + for (let i = 0; i < this.icons.length; i++) { + if (this.icons[i].icon !== null) continue; + this.icons[i].set_size(this._iconSize); + this._rowsCache = null; // icon preferred size changed + } + }, + + // Distribui os itens em linhas, quebrando quando a largura passar do + // limite (90% da largura do monitor). Retorna { rows, width, height } com a + // largura/altura totais já considerando o espaçamento entre itens e linhas. + _computeRows: function () { + if (this._rowsCache) return this._rowsCache; + + let spacing = this._list.spacing > -1 ? this._list.spacing : 10; + // Usa o monitor onde o alt+tab está sendo exibido (não necessariamente o + // primário). _activeMonitor é o objeto de geometria do monitor ativo. + let monitor = + this._activeMonitor || Main.layoutManager.primaryMonitor; + let maxWidth = monitor + ? Math.round(monitor.width * MAX_LIST_WIDTH_RATIO) + : Math.round(1700 * global.ui_scale); + + let rows = []; + let current = { items: [], width: 0, height: 0 }; + + for (let i = 0; i < this._items.length; i++) { + let [, childNatW] = this._items[i].get_preferred_width(-1); + let [, childNatH] = this._items[i].get_preferred_height(childNatW); + + let extra = current.items.length > 0 ? spacing : 0; + if ( + current.items.length > 0 && + current.width + extra + childNatW > maxWidth + ) { + rows.push(current); + current = { items: [], width: 0, height: 0 }; + extra = 0; + } + + current.items.push({ + child: this._items[i], + width: childNatW, + height: childNatH, + }); + current.width += extra + childNatW; + current.height = Math.max(current.height, childNatH); + } + + if (current.items.length > 0) rows.push(current); + + let width = 0; + let height = 0; + for (let r = 0; r < rows.length; r++) { + width = Math.max(width, rows[r].width); + height += rows[r].height; + } + height += spacing * Math.max(0, rows.length - 1); + + this._rowsCache = { rows: rows, width: width, height: height }; + return this._rowsCache; + }, + + _getPreferredWidth: function (actor, forHeight, alloc) { + if (this._items.length < 1) { + alloc.min_size = 32; + alloc.natural_size = 32; + return; + } + + this._ensureIconsSized(); + + let layout = this._computeRows(); + this._minSize = layout.width; + alloc.min_size = layout.width; + alloc.natural_size = layout.width; + }, + + _getPreferredHeight: function (actor, forWidth, alloc) { + if (this._items.length < 1) { + alloc.min_size = alloc.natural_size = 32; + return; + } + + this._ensureIconsSized(); + + let layout = this._computeRows(); + alloc.min_size = layout.height; + alloc.natural_size = layout.height; + }, + + _allocate: function (actor, box, flags) { + let spacing = this._list.spacing > -1 ? this._list.spacing : 10; + let childBox = new Clutter.ActorBox(); + + let layout = this._computeRows(); + let availableWidth = box.x2 - box.x1; + let y = 0; + + for (let r = 0; r < layout.rows.length; r++) { + let row = layout.rows[r]; + let x = Math.max(0, Math.floor((availableWidth - row.width) / 2)); + + for (let i = 0; i < row.items.length; i++) { + let item = row.items[i]; + let itemY = y + Math.max(0, Math.floor((row.height - item.height) / 2)); + + childBox.x1 = x; + childBox.y1 = itemY; + childBox.x2 = x + item.width; + childBox.y2 = itemY + item.height; + item.child.allocate(childBox, flags); + + x += item.width + spacing; + } + + y += row.height + spacing; + } + + let leftPadding = this.actor.get_theme_node().get_padding(St.Side.LEFT); + let rightPadding = this.actor.get_theme_node().get_padding(St.Side.RIGHT); + let topPadding = this.actor.get_theme_node().get_padding(St.Side.TOP); + let bottomPadding = this.actor.get_theme_node().get_padding(St.Side.BOTTOM); + + this._clipBin.set_clip( + 0, + -topPadding, + this.actor.allocation.x2 - + this.actor.allocation.x1 - + leftPadding - + rightPadding, + this.actor.height + bottomPadding, + ); + }, +}; + +function WindowsClassicSwitcher() { + this._init.apply(this, arguments); +} + +WindowsClassicSwitcher.prototype = { + __proto__: ClassicSwitcherModule.ClassicSwitcher.prototype, + + _updateList: function (direction) { + if (direction !== 0) return; + + if (this._appList) { + if (this._applist_act_id !== 0) { + this._appList.disconnect(this._applist_act_id); + this._applist_act_id = 0; + } + + if (this._applist_enter_id !== 0) { + this._appList.disconnect(this._applist_enter_id); + this._applist_enter_id = 0; + } + + this._clearPreview(); + this._destroyThumbnails(); + this.actor.remove_actor(this._appList.actor); + this._appList.actor.destroy(); + } + + this._appList = new WindowsAppList( + this._windows, + this._showThumbnails, + this._activeMonitor, + ); + this._appList.actor.add_style_class_name("windows-switcher-list"); + this.actor.add_actor(this._appList.actor); + + if (!this._iconsEnabled && !this._thumbnailsEnabled) { + this._appList.actor.hide(); + } + + this._applist_act_id = this._appList.connect( + "item-activated", + this._appActivated.bind(this), + ); + this._applist_enter_id = this._appList.connect( + "item-entered", + this._appEntered.bind(this), + ); + + this._appIcons = this._appList.icons; + this.actor.get_allocation_box(); + }, + + _appActivated: function (appSwitcher, n) { + this._select(n); + this._activateSelected(); + }, + + _appEntered: function (appSwitcher, n) { + if (!this._mouseActive) return; + this._select(n); + }, +}; + +function getWindowsForBinding(binding) { + return BaseAppSwitcher.getWindowsForBinding(binding); +} diff --git a/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/extension.js b/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/extension.js new file mode 100644 index 00000000..72b57f57 --- /dev/null +++ b/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/extension.js @@ -0,0 +1,59 @@ +const Main = imports.ui.main; +const BaseAppSwitcher = imports.ui.appSwitcher.appSwitcher; + +const CustomSwitcherModule = require("./classic_switcher_windows_style"); + +let enabled = false; +let _originalCreateAppSwitcher = null; +let _previousStyle = null; + +function init() {} + +function enable() { + if (enabled) return; + + if (!CustomSwitcherModule || !CustomSwitcherModule.WindowsClassicSwitcher) { + Main.notify("Windows Alt+Tab", "Could not load the switcher."); + return; + } + + // --- Alt+Tab estilo Windows --- + if (!_originalCreateAppSwitcher) { + _originalCreateAppSwitcher = Main.wm._createAppSwitcher; + } + + Main.wm._createAppSwitcher = function (binding) { + if (BaseAppSwitcher.getWindowsForBinding(binding).length === 0) return; + + if (global.settings.get_string("alttab-switcher-style") === "windows") { + new CustomSwitcherModule.WindowsClassicSwitcher(binding); + return; + } + + _originalCreateAppSwitcher.call(this, binding); + }; + + _previousStyle = global.settings.get_string("alttab-switcher-style"); + global.settings.set_string("alttab-switcher-style", "windows"); + + enabled = true; +} + +function disable() { + if (!enabled) return; + + if (_originalCreateAppSwitcher) { + Main.wm._createAppSwitcher = _originalCreateAppSwitcher; + } + + if ( + _previousStyle !== null && + global.settings.get_string("alttab-switcher-style") === "windows" + ) { + global.settings.set_string("alttab-switcher-style", _previousStyle); + } + + enabled = false; + + Main.notify("Windows Alt+Tab", "Windows-style switcher disabled."); +} diff --git a/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/icon.png b/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/icon.png new file mode 100644 index 00000000..4105dc0c Binary files /dev/null and b/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/icon.png differ diff --git a/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/metadata.json b/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/metadata.json new file mode 100644 index 00000000..c0657532 --- /dev/null +++ b/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/metadata.json @@ -0,0 +1,8 @@ +{ + "uuid": "windows-alttab@samuel-ifrs", + "name": "Windows Alt+Tab", + "description": "Alt+Tab window switcher with live window thumbnails (previews), in the classic Windows style. Middle-click a thumbnail to close that window; right-click for the window menu.", + "version": 1, + "cinnamon-version": ["6.6"], + "url": "https://github.com/samuel-ifrs/windows-alttab" +} diff --git a/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/stylesheet.css b/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/stylesheet.css new file mode 100644 index 00000000..8644d26c --- /dev/null +++ b/windows-alttab@samuel-ifrs/files/windows-alttab@samuel-ifrs/stylesheet.css @@ -0,0 +1,46 @@ +#altTabPopup { + spacing: 10px; +} + +.windows-switcher-list { + background-color: rgba(120, 120, 120, 0.5); + border: 1px solid rgba(200, 200, 200, 0.2); + border-radius: 8px; + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45); + padding: 10px; +} + +.windows-switcher-list .switcher-list-item-container { + spacing: 10px; +} + +.windows-switcher-list .item-box { + padding: 8px; + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 6px; + background-color: rgba(18, 18, 18, 0.9); +} + +.windows-switcher-list .item-box:selected { + border-color: rgba(91, 178, 255, 0.95); + background-color: rgba(26, 26, 26, 0.95); +} + +.windows-alt-tab-app { + spacing: 7px; +} + +.windows-alt-tab-preview-bin { + border-radius: 4px; + background-color: #000; +} + +.windows-alt-tab-info-box { + spacing: 6px; + min-height: 22px; +} + +.windows-alt-tab-title { + font-size: 10pt; + color: #f5f5f5; +} diff --git a/windows-alttab@samuel-ifrs/info.json b/windows-alttab@samuel-ifrs/info.json new file mode 100644 index 00000000..90257339 --- /dev/null +++ b/windows-alttab@samuel-ifrs/info.json @@ -0,0 +1,7 @@ +{ + "uuid": "windows-alttab@samuel-ifrs", + "name": "Windows Alt+Tab", + "description": "Alt+Tab window switcher with live window thumbnails (previews), in the classic Windows style. Middle-click a thumbnail to close that window; right-click for the window menu.", + "author": "samuel-ifrs", + "license": "GPL-3.0" +} diff --git a/windows-alttab@samuel-ifrs/screenshot.png b/windows-alttab@samuel-ifrs/screenshot.png new file mode 100644 index 00000000..c543f304 Binary files /dev/null and b/windows-alttab@samuel-ifrs/screenshot.png differ