├── stylesheet.css ├── metadata.json ├── README.md └── extension.js /stylesheet.css: -------------------------------------------------------------------------------- 1 | /* This extensions requires no special styling */ 2 | .switcher-list .separator { 3 | width: 1px; 4 | background: rgba(255,255,255,0.33); 5 | } 6 | -------------------------------------------------------------------------------- /metadata.json: -------------------------------------------------------------------------------- 1 | { "shell-version": ["3.8","3.10","3.12","3.16","3.15","3.14","3.13"], "uuid": "workspaceAltTab@KrahnacK", "name": "workspaceAltTab", "description": "AltTab that groups by window and sort according to current workspace (as in gnome-shell 3.6)", "extension-id": "workspaceAltTab", "gettext-domain": "gnome-shell-extensions", "url": "http://krahnack.github.io/workspaceAltTab/" } 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | *** Workspace-aware AltTab for gnome-shell (3.8 and higher) 2 | 3 |

What's that

4 |

re-enables gnome-shell 3.6 AltTab behaviour in later versions, so that AltTab

5 | - Main Goal: ensure that a quick Alt-Tab won't switch workspace if you have at least 2 windows opened in the current workspace 6 | - Groups by applications (uses the AppSwitcher, instead of the simple WindowSwitcher) 7 | - Is not restricted to the current workspace (diplays every window) 8 | - Sorts the Apps and Windows according to who's running where (current workspace first, then others) 9 | 10 |

Official Gnome Extension URL

11 | https://extensions.gnome.org/extension/748/workspacealttab/ 12 | 13 |

Screenshot

14 | 15 | 16 |

Manual install instructions

17 | - cd ~/.local/share/gnome-shell/extensions 18 | - git clone https://github.com/KrahnacK/workspaceAltTab.git 19 | - mv workspaceAltTab workspaceAltTab@KrahnacK 20 | - restart gnome-shell: Alt+F2 + r, or gnome-shell --replace & 21 | 22 |

Debug instructions for gnome-shell extensions

23 | - see the logs in ~/.cache/gdm/session.log or ~/.xsession-errors 24 | - if none of the above work, then get the shell to output in a file: gnome-shell --replace &> shelllog & 25 | 26 | -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | /* -*- mode: js; js-basic-offset: 4; indent-tabs-mode: nil -*- */ 2 | 3 | const Config = imports.misc.config; 4 | 5 | const Clutter = imports.gi.Clutter; 6 | const Lang = imports.lang; 7 | const Meta = imports.gi.Meta; 8 | const Shell = imports.gi.Shell; 9 | 10 | const AltTab = imports.ui.altTab; 11 | const SwitcherPopup = imports.ui.switcherPopup; 12 | const St = imports.gi.St; 13 | const Main = imports.ui.main; 14 | 15 | 16 | let oldCreateSwitcherPopup; 17 | let oldInitAppSwitcher; 18 | let oldGetPreferredHeightAppSwitcher; 19 | let oldInitThumbnailList; 20 | 21 | function isOlderThan(version) { 22 | var myver = Config.PACKAGE_VERSION.split("."); 23 | var refver = version.split("."); 24 | return Number(myver[1]) < Number(refver[1]); 25 | } 26 | 27 | function newInitThumbnailList (windows) { 28 | var parent = Lang.bind(this, SwitcherPopup.SwitcherList.prototype._init); 29 | var addSep = Lang.bind(this, SwitcherPopup.SwitcherList.prototype._addSeparator); 30 | parent(false); 31 | 32 | let activeWorkspace = global.screen.get_active_workspace(); 33 | 34 | // We fake the value of 'separatorAdded' when the app has no window 35 | // on the current workspace, to avoid displaying a useless separator in 36 | // that case. 37 | let separatorAdded = windows.length == 0 || windows[0].get_workspace() != activeWorkspace; 38 | 39 | this._labels = new Array(); 40 | this._thumbnailBins = new Array(); 41 | this._clones = new Array(); 42 | this._windows = windows; 43 | 44 | for (let i = 0; i < windows.length; i++) { 45 | if (!separatorAdded && windows[i].get_workspace() != activeWorkspace) { 46 | addSep(); 47 | separatorAdded = true; 48 | } 49 | 50 | let box = new St.BoxLayout({ style_class: 'thumbnail-box', 51 | vertical: true }); 52 | 53 | let bin = new St.Bin({ style_class: 'thumbnail' }); 54 | 55 | box.add_actor(bin); 56 | this._thumbnailBins.push(bin); 57 | 58 | let title = windows[i].get_title(); 59 | if (title) { 60 | let name = new St.Label({ text: title }); 61 | // St.Label doesn't support text-align so use a Bin 62 | let bin = new St.Bin({ x_align: St.Align.MIDDLE }); 63 | this._labels.push(bin); 64 | bin.add_actor(name); 65 | box.add_actor(bin); 66 | 67 | this.addItem(box, name); 68 | } else { 69 | this.addItem(box, null); 70 | } 71 | } 72 | 73 | } 74 | 75 | function newGetPreferredHeightAppSwitcher (actor, forWidth, alloc) { 76 | if (isOlderThan("3.12.0")) { //for gnome-shell prior to 3.12 77 | let j = 0; 78 | while(this._items.length > 1 && this._items[j].style_class != 'item-box') { 79 | j++; 80 | } 81 | let themeNode = this._items[j].get_theme_node(); 82 | let iconPadding = themeNode.get_horizontal_padding(); 83 | let iconBorder = themeNode.get_border_width(St.Side.LEFT) + themeNode.get_border_width(St.Side.RIGHT); 84 | let [iconMinHeight, iconNaturalHeight] = this.icons[j].label.get_preferred_height(-1); 85 | let iconSpacing = iconNaturalHeight + iconPadding + iconBorder; 86 | let totalSpacing = this._list.spacing * (this._items.length - 1); 87 | if (this._separator) 88 | totalSpacing += this._separator.width + this._list.spacing; 89 | 90 | // We just assume the whole screen here due to weirdness happing with the passed width 91 | let primary = Main.layoutManager.primaryMonitor; 92 | let parentPadding = this.actor.get_parent().get_theme_node().get_horizontal_padding(); 93 | let availWidth = primary.width - parentPadding - this.actor.get_theme_node().get_horizontal_padding(); 94 | let height = 0; 95 | 96 | for(let i = 0; i < AltTab.iconSizes.length; i++) { 97 | this._iconSize = AltTab.iconSizes[i]; 98 | height = AltTab.iconSizes[i] + iconSpacing; 99 | let w = height * this._items.length + totalSpacing; 100 | if (w <= availWidth) 101 | break; 102 | } 103 | 104 | if (this._items.length == 1) { 105 | this._iconSize = AltTab.iconSizes[0]; 106 | height = AltTab.iconSizes[0] + iconSpacing; 107 | } 108 | 109 | for(let i = 0; i < this.icons.length; i++) { 110 | if (this.icons[i].icon != null) 111 | break; 112 | this.icons[i].set_size(this._iconSize); 113 | } 114 | 115 | alloc.min_size = height; 116 | alloc.natural_size = height; 117 | } else { //for gnome-shell 3.12+ 118 | //thanks to Pavel N. Krivitsky 119 | this._setIconSize(); 120 | 121 | let maxChildMin = 0; 122 | let maxChildNat = 0; 123 | 124 | for (let i = 0; i < this._items.length; i++) { 125 | let [childMin, childNat] = this._items[i].get_preferred_height(-1); 126 | maxChildMin = Math.max(childMin, maxChildMin); 127 | maxChildNat = Math.max(childNat, maxChildNat); 128 | } 129 | 130 | if (this._squareItems) { 131 | let [childMin, childNat] = this._maxChildWidth(-1); 132 | maxChildMin = Math.max(childMin, maxChildMin); 133 | maxChildNat = maxChildMin; 134 | } 135 | 136 | alloc.min_size = maxChildMin; 137 | alloc.natural_size = maxChildNat; 138 | } 139 | } 140 | 141 | function _newInitAppSwitcher(localApps, otherApps, altTabPopup) { 142 | var parent = Lang.bind(this, SwitcherPopup.SwitcherList.prototype._init); 143 | var addSep = Lang.bind(this, SwitcherPopup.SwitcherList.prototype._addSeparator); 144 | 145 | //from gnome-shell 3.8 146 | parent(true); 147 | 148 | //from gnome-shell 3.6 part 149 | 150 | // Construct the AppIcons, add to the popup 151 | let activeWorkspace = global.screen.get_active_workspace(); 152 | let workspaceIcons = []; 153 | let otherIcons = []; 154 | 155 | //re-order localApps in case a window from another 156 | //workspace was raised 157 | let localAppIndex = -1; 158 | for (let i = 0; i < localApps.length; i++) { 159 | let isRaised = true; 160 | let w = localApps[i].get_windows(); 161 | for (let j = 0; j < w.length; j++) { 162 | isRaised &= w[j].get_workspace() != activeWorkspace; 163 | } 164 | if (!isRaised) { 165 | localAppIndex = i; 166 | break; 167 | } 168 | } 169 | let localAppToMove = localApps.splice(localAppIndex, 1); 170 | localApps.unshift(localAppToMove[0]); 171 | 172 | for (let i = 0; i < localApps.length; i++) { 173 | let appIcon = new AltTab.AppIcon(localApps[i]); 174 | // Cache the window list now; we don't handle dynamic changes here, 175 | // and we don't want to be continually retrieving it 176 | appIcon.cachedWindows = appIcon.app.get_windows(); 177 | workspaceIcons.push(appIcon); 178 | } 179 | for (let i = 0; i < otherApps.length; i++) { 180 | let appIcon = new AltTab.AppIcon(otherApps[i]); 181 | appIcon.cachedWindows = appIcon.app.get_windows(); 182 | otherIcons.push(appIcon); 183 | } 184 | 185 | this.icons = []; 186 | this._arrows = []; 187 | for (let i = 0; i < workspaceIcons.length; i++) 188 | this._addIcon(workspaceIcons[i]); 189 | if (workspaceIcons.length > 0 && otherIcons.length > 0) 190 | addSep(); 191 | for (let i = 0; i < otherIcons.length; i++) 192 | this._addIcon(otherIcons[i]); 193 | 194 | this._curApp = -1; 195 | this._iconSize = 0; 196 | this._altTabPopup = altTabPopup; 197 | this._mouseTimeOutId = 0; 198 | 199 | //and end with a part from gnome-shell 3.8 200 | this.actor.connect('destroy', Lang.bind(this, this._onDestroy)); 201 | } 202 | 203 | function newCreateSwitcherPopup() { 204 | 205 | let tracker = Shell.WindowTracker.get_default(); 206 | let windows = global.display.get_tab_list(Meta.TabList.NORMAL, global.screen, global.screen.get_active_workspace()); 207 | let allApps = Shell.AppSystem.get_default().get_running (); 208 | 209 | // windows is only the windows on the current workspace. For 210 | // each one, if it corresponds to an app we know, move that 211 | // app from allApps to apps. 212 | let apps = []; 213 | for (let i = 0; i < windows.length && allApps.length != 0; i++) { 214 | let app = tracker.get_window_app(windows[i]); 215 | let index = allApps.indexOf(app); 216 | if (index != -1) { 217 | apps.push(app); 218 | allApps.splice(index, 1); 219 | } 220 | } 221 | 222 | if (apps.length == 0 && allApps.length == 0) 223 | return false; 224 | 225 | this._switcherList = new AltTab.AppSwitcher(apps, allApps, this); 226 | this._items = this._switcherList.icons; 227 | 228 | return true; 229 | } 230 | 231 | //SwitcherList modifications 232 | let oldAddSeparator; 233 | let oldGetPreferredWidth; 234 | let oldAllocate; 235 | 236 | function newAddSeparator() { 237 | let box = new St.Bin({ style_class: 'separator' }); 238 | this._separator = box; 239 | this._list.add_actor(box); 240 | } 241 | 242 | function newGetPreferredWidth(actor, forHeight, alloc) { 243 | let [maxChildMin, maxChildNat] = this._maxChildWidth(forHeight); 244 | 245 | let separatorWidth = 0; 246 | if (this._separator) { 247 | let [sepMin, sepNat] = this._separator.get_preferred_width(forHeight); 248 | separatorWidth = sepNat + this._list.spacing; 249 | } 250 | 251 | let totalSpacing = this._list.spacing * (this._items.length - 1); 252 | alloc.min_size = this._items.length * maxChildMin + separatorWidth + totalSpacing; 253 | alloc.natural_size = alloc.min_size; 254 | this._minSize = alloc.min_size; 255 | } 256 | 257 | function newAllocate(actor, box, flags) { 258 | let childHeight = box.y2 - box.y1; 259 | 260 | let [maxChildMin, maxChildNat] = this._maxChildWidth(childHeight); 261 | let totalSpacing = this._list.spacing * (this._items.length - 1); 262 | 263 | let separatorWidth = 0; 264 | if (this._separator) { 265 | let [sepMin, sepNat] = this._separator.get_preferred_width(childHeight); 266 | separatorWidth = sepNat; 267 | totalSpacing += this._list.spacing; 268 | } 269 | 270 | let childWidth = Math.floor(Math.max(0, box.x2 - box.x1 - totalSpacing - separatorWidth) / this._items.length); 271 | 272 | let x = 0; 273 | let children = this._list.get_children(); 274 | let childBox = new Clutter.ActorBox(); 275 | 276 | let primary = Main.layoutManager.primaryMonitor; 277 | let parentRightPadding = this.actor.get_parent().get_theme_node().get_padding(St.Side.RIGHT); 278 | 279 | for (let i = 0; i < children.length; i++) { 280 | if (this._items.indexOf(children[i]) != -1) { 281 | let [childMin, childNat] = children[i].get_preferred_height(childWidth); 282 | let vSpacing = (childHeight - childNat) / 2; 283 | childBox.x1 = x; 284 | childBox.y1 = vSpacing; 285 | childBox.x2 = x + childWidth; 286 | childBox.y2 = childBox.y1 + childNat; 287 | children[i].allocate(childBox, flags); 288 | 289 | x += this._list.spacing + childWidth; 290 | } else if (children[i] == this._separator) { 291 | // We want the separator to be more compact than the rest. 292 | childBox.x1 = x; 293 | childBox.y1 = 0; 294 | childBox.x2 = x + separatorWidth; 295 | childBox.y2 = childHeight; 296 | children[i].allocate(childBox, flags); 297 | x += this._list.spacing + separatorWidth; 298 | } else { 299 | // Something else, eg, AppSwitcher's arrows; 300 | // we don't allocate it. 301 | } 302 | } 303 | } 304 | 305 | function init() { 306 | oldCreateSwitcherPopup = AltTab.AppSwitcherPopup.prototype._createSwitcher; 307 | oldInitAppSwitcher = AltTab.AppSwitcher.prototype._init; 308 | oldGetPreferredHeightAppSwitcher = AltTab.AppSwitcher.prototype._getPreferredHeight; 309 | oldInitThumbnailList = AltTab.ThumbnailList.prototype._init; 310 | 311 | //SwitcherList modifications 312 | oldAddSeparator = SwitcherPopup.SwitcherList.prototype._addSeparator; //inexistant as of now 313 | oldGetPreferredWidth = SwitcherPopup.SwitcherList.prototype._getPreferredWidth; 314 | oldAllocate = SwitcherPopup.SwitcherList.prototype._allocate; 315 | } 316 | 317 | function enable() { 318 | AltTab.AppSwitcherPopup.prototype._createSwitcher = newCreateSwitcherPopup; 319 | AltTab.AppSwitcher.prototype._init = _newInitAppSwitcher; 320 | AltTab.AppSwitcher.prototype._getPreferredHeight = newGetPreferredHeightAppSwitcher; 321 | AltTab.ThumbnailList.prototype._init = newInitThumbnailList; 322 | 323 | //SwitcherList modifications 324 | SwitcherPopup.SwitcherList.prototype._addSeparator = newAddSeparator; 325 | SwitcherPopup.SwitcherList.prototype._allocate = newAllocate; 326 | SwitcherPopup.SwitcherList.prototype._getPreferredWidth= newGetPreferredWidth; 327 | } 328 | 329 | function disable() { 330 | AltTab.AppSwitcherPopup.prototype._createSwitcher = oldCreateSwitcherPopup; 331 | AltTab.AppSwitcher.prototype._init = oldInitAppSwitcher; 332 | AltTab.AppSwitcher.prototype._getPreferredHeight = oldGetPreferredHeightAppSwitcher; 333 | AltTab.ThumbnailList.prototype._init = oldInitThumbnailList; 334 | 335 | //SwitcherList modifications 336 | SwitcherPopup.SwitcherList.prototype._addSeparator = oldAddSeparator; 337 | SwitcherPopup.SwitcherList.prototype._allocate = oldAllocate; 338 | SwitcherPopup.SwitcherList.prototype._getPreferredWidth= oldGetPreferredWidth; 339 | } 340 | --------------------------------------------------------------------------------