/******************************************************************** KWin - the KDE window manager This file is part of the KDE project. Copyright (C) 2012 Mathias Gottschlag <mgottschlag@gmail.com> Copyright (C) 2013-2014 Fabian Homborg <FHomborg@gmail.com> This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see <http://www.gnu.org/licenses/>. *********************************************************************/ Qt.include("signal.js"); Qt.include("tile.js"); Qt.include("tilelist.js"); Qt.include("layout.js"); Qt.include("spirallayout.js"); Qt.include("halflayout.js"); Qt.include("bladelayout.js"); Qt.include("tiling.js"); Qt.include("tests.js"); Qt.include("util.js"); /** * Class which manages all layouts, connects the various signals and handlers * and implements all keyboard shortcuts. * @class */ function TilingManager() { /** * Default layout type which is selected for new layouts. */ this.defaultLayout = HalfLayout; /** * List of all available layout types. */ this.availableLayouts = [ HalfLayout, BladeLayout, SpiralLayout/*, ZigZagLayout, ColumnLayout, RowLayout, GridLayout, MaximizedLayout, FloatingLayout*/ ]; for (var i = 0; i < this.availableLayouts.length; i++) { this.availableLayouts[i].index = i; } /** * Number of desktops in the system. */ this.desktopCount = workspace.desktops; /** * Number of screens in the system. */ this.screenCount = workspace.numScreens; /** * Array containing a list of layouts for every desktop. Each of the lists * has one element per screen. */ this.layouts = []; /** * List of all tiles in the system. */ this.tiles = new TileList(); /** * Current screen, needed to be able to track screen changes. */ this._currentScreen = workspace.activeScreen; /** * Current desktop, needed to be able to track screen changes. */ this._currentDesktop = workspace.currentDesktop - 1; /** * True if a user moving operation is in progress. */ this._moving = false; /** * The screen where the current window move operation started. */ this._movingStartScreen = 0; /** * Whether tiling is active on all desktops * This is overridden by per-desktop settings */ this.userActive = readConfig("userActive", true); // Read layout configuration // Format: desktop:layoutname[,...] // Negative desktop number deactivates tiling this.layoutConfig = []; var lC = String(readConfig("layouts", "")).replace(/ /g,"").split(","); for (var i = 0; i < lC.length; i++) { var layout = lC[i].split(":"); try { var desktop = parseInt(layout[0]); } catch (err) { continue; } var l = this.defaultLayout; for (var j = 0; j < this.availableLayouts.length; j++) { if (this.availableLayouts[j].name == layout[1]) { l = this.availableLayouts[j]; break; } } if (desktop < 0) { var tiling = false; desktop = desktop * -1; } else { var tiling = true; } if (desktop == 0) { this.defaultLayout = l; } // Subtract 1 because the config is in user-indexed format desktop = desktop - 1; var desktoplayout = {}; desktoplayout.desktop = desktop; desktoplayout.layout = l; desktoplayout.tiling = tiling; this.layoutConfig.push(desktoplayout); } // Create the various layouts, one for every desktop for (var i = 0; i < this.desktopCount; i++) { this._createDefaultLayouts(i); } var self = this; // Connect the tile list signals so that new tiles are added to the layouts this.tiles.tileAdded.connect(function(tile) { self._onTileAdded(tile); }); this.tiles.tileRemoved.connect(function(tile) { self._onTileRemoved(tile); }); var existingClients = workspace.clientList(); existingClients.forEach(function(client) { self.tiles.addClient(client); }); // Activate the visible layouts // Do it after adding the existingClients to prevent unnecessary geometry changes this.layouts[workspace.currentDesktop - 1].forEach(function(layout) { layout.activate(); }); // Register global callbacks workspace.numberDesktopsChanged.connect(function() { self._onNumberDesktopsChanged(); }); workspace.numberScreensChanged.connect(function() { self._onNumberScreensChanged(); }); workspace.screenResized.connect(function(screen) { try { self._onScreenResized(screen); } catch(err) { print(err); } }); workspace.currentDesktopChanged.connect(function() { self._onCurrentDesktopChanged(); }); // Register keyboard shortcuts registerShortcut("Next Tiling Layout", "Next Tiling Layout", "Meta+PgDown", function() { var currentLayout = self._getCurrentLayoutType(); var nextIndex = (currentLayout.index + 1) % self.availableLayouts.length; self._switchLayout(workspace.currentDesktop - 1, workspace.activeScreen, nextIndex); }); registerShortcut("Previous Tiling Layout", "Previous Tiling Layout", "Meta+PgUp", function() { var currentLayout = self._getCurrentLayoutType(); var nextIndex = currentLayout.index - 1; if (nextIndex < 0) { nextIndex += self.availableLayouts.length; } self._switchLayout(workspace.currentDesktop - 1, workspace.activeScreen, nextIndex); }); registerShortcut("Toggle Floating", "Toggle Floating", "Meta+F", function() { var client = workspace.activeClient; if (client == null) { print("No active client"); return; } client.tiling_floating = !client.tiling_floating; if (client.tiling_floating == true) { self.tiles._onClientRemoved(client); } else { self.tiles.addClient(client); } }); registerShortcut("Toggle Border for all", "Toggle Border for all", "Meta+Shift+U", function() { self.tiles.toggleNoBorder(); }); registerShortcut("Move Window Left", "Move Window Left", "Meta+Shift+H", function() { self._moveTile(Direction.Left); }); registerShortcut("Move Window Right", "Move Window Right", "Meta+Shift+L", function() { self._moveTile(Direction.Right); }); registerShortcut("Move Window Up", "Move Window Up", "Meta+Shift+K", function() { self._moveTile(Direction.Up); }); registerShortcut("Move Window Down", "Move Window Down", "Meta+Shift+J", function() { self._moveTile(Direction.Down); }); registerShortcut("Toggle Tiling", "Toggle Tiling", "Meta+Shift+f11", function() { var currentScreen = workspace.activeScreen; var currentDesktop = workspace.currentDesktop - 1; self.layouts[currentDesktop][currentScreen].toggleUserActive(); }); registerShortcut("Tile now", "Tile now", "Meta+t", function() { var currentScreen = workspace.activeScreen; var currentDesktop = workspace.currentDesktop - 1; self.layouts[currentDesktop][currentScreen].toggleUserActive(); self.layouts[currentDesktop][currentScreen].toggleUserActive(); }); registerShortcut("Swap Window With Master", "Swap Window With Master", "Meta+Shift+M", function() { try { var layout = self.layouts[workspace.currentDesktop - 1][workspace.activeScreen]; if (layout != null) { var client = workspace.activeClient; if (client != null) { var tile = layout.getTile(client.x, client.y); } } if (tile != null) { layout.swapTiles(tile, layout.tiles[0]); } } catch(err) { print(err, "in swap-window-with-master"); } }); registerShortcut("Resize Active Window To The Left", "Resize Active Window To The Left", "Meta+Alt+H", function() { try { var client = workspace.activeClient; if (client == null) { return; } var tile = self.tiles.getTile(client); if (tile == null) { return; } geom = new Qt.rect(tile.rectangle.x - 10, tile.rectangle.y, tile.rectangle.width + 10, tile.rectangle.height); var screenRectangle = util.getTilingArea(client.screen, client.desktop); if (geom.x < screenRectangle.x) { geom.x = screenRectangle.x; geom.width = geom.width - 20; } self.layouts[tile._currentDesktop - 1][tile._currentScreen].resizeTileTo(tile, geom); } catch(err) { print(err, "in resize-window-to-the-left"); } }); registerShortcut("Resize Active Window To The Right", "Resize Active Window To The Right", "Meta+Alt+L", function() { try { var client = workspace.activeClient; if (client == null) { return; } var tile = self.tiles.getTile(client); if (tile == null) { return; } var geom = new Qt.rect(tile.rectangle.x, tile.rectangle.y, tile.rectangle.width + 10, tile.rectangle.height); var screenRectangle = util.getTilingArea(client.screen, client.desktop); if (geom.x + geom.width > screenRectangle.x + screenRectangle.width) { geom.x = geom.x + 10; geom.width = (screenRectangle.x + screenRectangle.width) - geom.x; } self.layouts[tile._currentDesktop - 1][tile._currentScreen].resizeTileTo(tile, geom); } catch(err) { print(err, "in resize-window-to-the-left"); } }); registerShortcut("Resize Active Window To The Top", "Resize Active Window To The Top", "Meta+Alt+K", function() { try { var client = workspace.activeClient; if (client == null) { return; } var tile = self.tiles.getTile(client); if (tile == null) { return; } var geom = new Qt.rect(tile.rectangle.x, tile.rectangle.y - 10, tile.rectangle.width, tile.rectangle.height + 10); var screenRectangle = util.getTilingArea(client.screen, client.desktop); if (geom.y < screenRectangle.y) { geom.y = screenRectangle.y; geom.height = geom.height - 20; } self.layouts[tile._currentDesktop - 1][tile._currentScreen].resizeTileTo(tile, geom); } catch(err) { print(err, "in resize-window-to-the-left"); } }); registerShortcut("Resize Active Window To The Bottom", "Resize Active Window To The Bottom", "Meta+Alt+J", function() { try { var client = workspace.activeClient; if (client == null) { return; } var tile = self.tiles.getTile(client); if (tile == null) { return; } var geom = new Qt.rect(tile.rectangle.x, tile.rectangle.y, tile.rectangle.width, tile.rectangle.height + 10); var screenRectangle = util.getTilingArea(client.screen, client.desktop); if (geom.y + geom.height > screenRectangle.y + screenRectangle.height) { geom.y = geom.y + 10; geom.height = (screenRectangle.y + screenRectangle.height) - geom.y; } self.layouts[tile._currentDesktop - 1][tile._currentScreen].resizeTileTo(tile, geom); } catch(err) { print(err, "in resize-window-to-the-left"); } }); registerShortcut("Increase Number Of Masters", "Increase Number Of Masters", "Meta+*", function() { try { self.layouts[self._currentDesktop][self._currentScreen].increaseMaster(); } catch(err) { print(err, "in Increase-Number-Of-Masters"); } }); registerShortcut("Decrease Number Of Masters", "Decrease Number Of Masters", "Meta+_", function() { try { self.layouts[self._currentDesktop][self._currentScreen].decrementMaster(); } catch(err) { print(err, "in Decrease-Number-Of-Masters"); } }); registerUserActionsMenu(function(client) { return { text : "Toggle floating", triggered: function () { client.tiling_floating = ! client.tiling_floating; if (client.tiling_floating == true) { self.tiles._onClientRemoved(client); } else { self.tiles.addClient(client); } } }; }); }; TilingManager.prototype._createDefaultLayouts = function(desktop) { var screenLayouts = []; var layout = this.defaultLayout; var tiling = false; var userConfig = false; for (var i = 0; i < this.layoutConfig.length; i++) { if (this.layoutConfig[i].desktop == desktop) { userConfig = true; layout = this.layoutConfig[i].layout; tiling = this.layoutConfig[i].tiling; this.layoutConfig.splice(i,1); } } for (var j = 0; j < this.screenCount; j++) { screenLayouts[j] = new Tiling(layout, desktop, j); // Either the default is to tile and the desktop hasn't been configured, // or the desktop has been set to tile (in which case the default is irrelevant) screenLayouts[j].userActive = (this.userActive == true && userConfig == false) || (tiling == true); } this.layouts[desktop] = screenLayouts; }; TilingManager.prototype._getCurrentLayoutType = function() { var currentLayout = this.layouts[this._currentDesktop][this._currentScreen]; return currentLayout.layoutType; }; TilingManager.prototype._onTileAdded = function(tile) { // Add tile callbacks which are needed to move the tile between different // screens/desktops var self = this; tile.screenChanged.connect(function(oldScreen, newScreen) { self._onTileScreenChanged(tile, oldScreen, newScreen); }); tile.desktopChanged.connect(function(oldDesktop, newDesktop) { self._onTileDesktopChanged(tile, oldDesktop, newDesktop); }); tile.movingStarted.connect(function() { self._onTileMovingStarted(tile); }); tile.movingEnded.connect(function() { self._onTileMovingEnded(tile); }); tile.resizingEnded.connect(function() { self._onTileResized(tile); }); // Add the tile to the layouts var tileLayouts = this._getLayouts(tile._currentDesktop, tile._currentScreen); tileLayouts.forEach(function(layout) { layout.addTile(tile); }); }; TilingManager.prototype._onTileResized = function(tile) { var tileLayouts = this._getLayouts(tile._currentDesktop, tile._currentScreen); tileLayouts.forEach(function(layout) { layout.resizeTile(tile); }); }; TilingManager.prototype._getMaster = function(screen, desktop) { try { return this.layouts[desktop][screen].getMaster(); } catch(err) { print(err, "in _getMaster"); } }; TilingManager.prototype._onTileRemoved = function(tile) { try { var tileLayouts = this._getLayouts(tile._currentDesktop, tile._currentScreen); tileLayouts.forEach(function(layout) { layout.removeTile(tile); }); } catch(err) { print(err, "in TilingManager._onTileRemoved"); } }; TilingManager.prototype._onNumberDesktopsChanged = function() { var newDesktopCount = workspace.desktopGridWidth * workspace.desktopGridHeight; var onAllDesktops = tiles.tiles.filter(function(tile) { return tile.desktop == -1; }); // Remove tiles from desktops which do not exist any more (we only have to // care about tiles shown on all desktops as all others have been moved away // from the desktops by kwin before) for (var i = newDesktopCount; i < this.desktopCount; i++) { onAllDesktops.forEach(function(tile) { this.layouts[i][tile.screen].removeTile(tile); }); } // Add new desktops for (var i = this.desktopCount; i < newDesktopCount; i++) { this._createDefaultLayouts(i); onAllDesktops.forEach(function(tile) { this.layouts[i][tile.screen].addTile(tile); }); } // Remove deleted desktops if (this.desktopCount > newDesktopCount) { layouts.length = newDesktopCount; } this.desktopCount = newDesktopCount; }; TilingManager.prototype._onNumberScreensChanged = function() { // Add new screens if (this.screenCount < workspace.numScreens) { for (var i = 0; i < this.desktopCount; i++) { for (var j = this.screenCount; j < workspace.numScreens; j++) { this.layouts[i][j] = new Tiling(this.defaultLayout, i, j); // Activate the new layout if necessary if (i == workspace.currentDesktop - 1) { this.layouts[i][j].activate(); } } } } // Remove deleted screens if (this.screenCount > workspace.numScreens) { for (var i = 0; i < this.desktopCount; i++) { this.layouts[i].length = workspace.numScreens; } } this.screenCount = workspace.numScreens; }; TilingManager.prototype._onScreenResized = function(screen) { if (screen < this.screenCount) { for (var i = 0; i < this.desktopCount; i++) { this.layouts[i][screen].activate(); } } }; TilingManager.prototype._onTileScreenChanged = function(tile, oldScreen, newScreen) { // If a tile is moved by the user, screen changes are handled in the move // callbacks below if (this._moving) { return; } var client = tile.clients[0]; var oldLayouts = this._getLayouts(client.desktop, oldScreen); var newLayouts = this._getLayouts(client.desktop, newScreen); this._changeTileLayouts(tile, oldLayouts, newLayouts); }; TilingManager.prototype._onTileDesktopChanged = function(tile, oldDesktop, newDesktop) { try { var client = tile.clients[0]; var oldLayouts = this._getLayouts(oldDesktop, client.screen); var newLayouts = this._getLayouts(newDesktop, client.screen); if (oldDesktop == -1) { oldLayouts.splice(newDesktop - 1, 1); newLayouts.splice(newDesktop - 1, 1); } this._changeTileLayouts(tile, oldLayouts, newLayouts); } catch(err) { print(err, "in TilingManager._onTileDesktopChanged"); } }; TilingManager.prototype._onTileMovingStarted = function(tile) { // NOTE: This supports only one moving window, breaks with multitouch input this._moving = true; this._movingStartScreen = tile.clients[0].screen; }; TilingManager.prototype._onTileMovingEnded = function(tile) { try { var client = tile.clients[0]; this._moving = false; var movingEndScreen = client.screen; var windowRect = client.geometry; if (client.tiling_tileIndex >= 0) { if (this._movingStartScreen != movingEndScreen) { // Transfer the tile from one layout to another layout var startLayout = this.layouts[this._currentDesktop][this._movingStartScreen]; var endLayout = this.layouts[this._currentDesktop][client.screen]; startLayout.removeTile(tile); endLayout.addTile(tile, windowRect.x + windowRect.width / 2, windowRect.y + windowRect.height / 2); } else { // Transfer the tile to a different location in the same layout var layout = this.layouts[this._currentDesktop][client.screen]; var targetTile = layout.getTile(windowRect.x + windowRect.width / 2, windowRect.y + windowRect.height / 2); // In case no tile is found (e.g. middle of the window is offscreen), move the client back if (targetTile == null) { targetTile = tile; } // swapTiles() works correctly even if tile == targetTile layout.swapTiles(tile, targetTile); } } } catch(err) { print(err, "in TilingManager._onTileMovingEnded"); } }; TilingManager.prototype._changeTileLayouts = function(tile, oldLayouts, newLayouts) { try { oldLayouts.forEach(function(layout) { layout.removeTile(tile); }); newLayouts.forEach(function(layout) { layout.addTile(tile); }); } catch(err) { print(err, "in TilingManager._changeTileLayouts"); } }; TilingManager.prototype._onCurrentDesktopChanged = function() { // TODO: This is wrong, we need to activate *all* visible layouts if (this.layouts[this._currentDesktop][this._currentScreen].active) { this.layouts[this._currentDesktop][this._currentScreen].deactivate(); } if (this._currentDesktop === workspace.currentDesktop -1) { return; } this._currentDesktop = workspace.currentDesktop - 1; if (! this.layouts[this._currentDesktop][this._currentScreen].active) { this.layouts[this._currentDesktop][this._currentScreen].activate(); } }; TilingManager.prototype._switchLayout = function(desktop, screen, layoutIndex) { // TODO: Show the layout switcher dialog var layoutType = this.availableLayouts[layoutIndex]; this.layouts[desktop][screen].setLayoutType(layoutType); }; TilingManager.prototype._moveTile = function(direction) { var client = workspace.activeClient; if (client == null) { return; } var activeTile = this.tiles.getTile(client); if (activeTile == null) { return; } if (direction == Direction.Left) { var x = activeTile.rectangle.x - 1; var y = activeTile.rectangle.y + 1; } else if (direction == Direction.Right) { var x = activeTile.rectangle.x + activeTile.rectangle.width + 1; var y = activeTile.rectangle.y + 1; } else if (direction == Direction.Up) { var x = activeTile.rectangle.x + 1; var y = activeTile.rectangle.y - 1; } else if (direction == Direction.Down) { var x = activeTile.rectangle.x + 1; var y = activeTile.rectangle.y + activeTile.rectangle.height + 1; } else { print("Wrong direction in _moveTile"); return; } var layout = this.layouts[client.desktop - 1][this._currentScreen]; var nextTile = layout.getTile(x, y); if (nextTile != null) { layout.swapTiles(activeTile, nextTile); } }; TilingManager.prototype._getLayouts = function(desktop, screen) { if (desktop > 0) { return [this.layouts[desktop - 1][screen]]; } else if (desktop == 0) { return []; } else if (desktop == -1) { var result = []; for (var i = 0; i < this.desktopCount; i++) { result.push(this.layouts[i][screen]); } return result; } return null; };