├── release.sh ├── .editorconfig ├── README.org ├── metadata.desktop ├── CHANGELOG.md ├── contents ├── code │ ├── signal.js │ ├── tests.js │ ├── spirallayout.js │ ├── bladelayout.js │ ├── ignored.js │ ├── i3layout.js │ ├── containerTree.js │ ├── util.js │ ├── layout.js │ ├── halflayout.js │ ├── tilelist.js │ ├── tile.js │ ├── tiling.js │ └── gridlayout.js ├── ui │ ├── main.qml │ ├── layoutosd.qml │ └── config.ui └── config │ └── main.xml ├── commentary.org ├── doc ├── design.txt └── tiling-classdiagram.txt └── LICENSE /release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | git archive --format=zip -o tiling.kwinscript ${1:-master} 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | indent_size = 4 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * kwin-tiling 2 | 3 | Tiling script for kwin 4 | 5 | 6 | This is UNMAINTAINED and I am no longer interested in maintaining it. 7 | 8 | Please use one of the other, very fine, tiling scripts for KWin. 9 | 10 | 11 | -------------------------------------------------------------------------------- /metadata.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Tiling Extension 3 | Icon=preferences-system-windows-script-test 4 | 5 | X-Plasma-API=declarativescript 6 | X-Plasma-MainScript=ui/main.qml 7 | X-KWin-Exclude-Listing=false 8 | 9 | X-KDE-PluginInfo-Author=Fabian Homborg 10 | X-KDE-PluginInfo-Email=FHomborg@gmail.com 11 | X-KDE-PluginInfo-Name=kwin-script-tiling 12 | X-KDE-PluginKeyword=kwin-script-tiling 13 | X-KDE-ParentComponents=kwin-script-tiling 14 | X-KDE-PluginInfo-Version=2.4.0 15 | 16 | X-KDE-PluginInfo-Depends= 17 | X-KDE-PluginInfo-License=GPL 18 | X-KDE-ServiceTypes=KWin/Script,KCModule 19 | X-KDE-Library=kwin/effects/configs/kcm_kwin4_genericscripted 20 | Type=Service 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | version 2.5: 2 | ============ 3 | - On-screen-display for toggling tiling state 4 | - Fix for floating windows going behind the tiled ones (#218) 5 | 6 | version 2.4: 7 | ============ 8 | - Initial support for activities! They're like virtual desktops, but different! I think. 9 | - An off-by-one error in layout configuration was fixed. 10 | - The script no longer attempts to rotate layouts that can't. 11 | - An option to always keep a minimum number of desktops open has been added. 12 | - Desktop compaction will now no longer be triggered when an ignored client is removed. 13 | 14 | version 2.3: 15 | ============ 16 | - Workarounds for clients with empty class, firefox' secondary windows (downloads etc), 17 | kfind, kcalc and evolution's gpg keyphrase entry dialog 18 | - Smart window placement for floating windows should now work better 19 | 20 | version 2.2: 21 | =========== 22 | 23 | - Some shortcuts are now not bound by default, to focus on the core feature set 24 | - The default layout switching shortcuts have been changed to Meta+Shift+PgUp/PgDown 25 | - Floating windows are remembered across layout switches 26 | - Better support for screen resizing and rotation 27 | - Support for rotating layouts 28 | - Improved handling for maximized clients (requires KWin >= 5.16!) 29 | - Move window left/right now moves the client to another screen if necessary 30 | - The default split ratio for HalfLayout is now configurable 31 | 32 | version 2.1: 33 | =========== 34 | 35 | - The resize bindings now resize by a fraction of the screen, not a certain number of pixels 36 | - "wine" is added to the blacklist 37 | - Preexisting clients are tiled again 38 | - The configuration screen has switched a widget that is now unavailable by default for one that is 39 | - Shortcuts for switching to the next/previous tile 40 | - A new i3-like layout 41 | - The blacklist is now case-insensitive 42 | - The long-standing problem where clients freeze up until they are resized has hopefully been fixed 43 | - A new OSD is shown when switching layouts 44 | - Better multimonitor support 45 | - A whole bunch of minor bug fixes 46 | -------------------------------------------------------------------------------- /contents/code/signal.js: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2012 Mathias Gottschlag 6 | 7 | This program is free software; you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation; either version 2 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | *********************************************************************/ 20 | 21 | /** 22 | * Class which manages connections to a signal and allows for signal/slot event 23 | * handling. 24 | * @class 25 | */ 26 | function Signal() { 27 | this.connected = []; 28 | }; 29 | 30 | /** 31 | * Method which connects another handler to the signal. 32 | * 33 | * @param f Function which shall be added to the signal. 34 | */ 35 | Signal.prototype.connect = function(f) { 36 | this.connected.push(f); 37 | }; 38 | 39 | /** 40 | * Method which disconnects a function from the signal which as previously been 41 | * registered with connect(). 42 | * 43 | * @param f Function which shall be removed from the signal. 44 | */ 45 | Signal.prototype.disconnect = function(f) { 46 | var index = this.connected.indexOf(f); 47 | if (index == -1) { 48 | return; 49 | } 50 | this.connected.splice(index, 1); 51 | }; 52 | 53 | /** 54 | * Calls all functions attached to this signals with all parameters passed to 55 | * this function. 56 | */ 57 | Signal.prototype.emit = function() { 58 | var signalArguments = arguments; 59 | this.connected.forEach(function(f) { 60 | f.apply(null, signalArguments); 61 | }); 62 | }; 63 | -------------------------------------------------------------------------------- /contents/code/tests.js: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2012 Mathias Gottschlag 6 | Copyright (C) 2013-2014 Fabian Homborg 7 | 8 | This program is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | *********************************************************************/ 21 | 22 | // Signal tests 23 | 24 | var testSignal = new Signal(); 25 | 26 | var success1 = false; 27 | var success2 = false; 28 | 29 | testSignal.connect(function(a, b, c) { 30 | success1 = a == 1 && b == 2 && c == "test"; 31 | }); 32 | var testSlot2 = function(a, b, c) { 33 | success2 = a == 1 && b == 2 && c == "test"; 34 | }; 35 | testSignal.connect(testSlot2); 36 | testSignal.emit(1, 2, "test"); 37 | print("Signal test 1: " + (success1 && success2 ? "SUCCESS" : "FAILURE")); 38 | 39 | success1 = false; 40 | success2 = false; 41 | testSignal.disconnect(testSlot2); 42 | testSignal.emit(1, 2, "test"); 43 | print("Signal test 2: " + (success1 && !success2 ? "SUCCESS" : "FAILURE")); 44 | 45 | var isConfig = function(name, defaultValue) { 46 | print("Reading", name); 47 | c = KWin.readConfig(name, defaultValue); 48 | print("Read", name); 49 | if (c == null) { 50 | print("Configuration option", name, "not defined"); 51 | } else { 52 | print("Configuration option", name, ": ", c); 53 | } 54 | }; 55 | 56 | print("Testing configuration"); 57 | isConfig("floaters", ""); 58 | -------------------------------------------------------------------------------- /contents/ui/main.qml: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2012 Mathias Gottschlag 6 | 7 | This program is free software; you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation; either version 2 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | *********************************************************************/ 20 | 21 | import QtQuick 2.0; 22 | /* import org.kde.qtextracomponents 2.0 as QtExtra; */ 23 | import org.kde.plasma.core 2.0 as PlasmaCore; 24 | import org.kde.plasma.components 2.0 as Plasma; 25 | import org.kde.kwin 2.0; 26 | import "../code/tilingmanager.js" as Tiling 27 | 28 | Item { 29 | property variant tiling 30 | Component.onCompleted: { 31 | console.log("Starting tiling"); 32 | // Initialize tiling 33 | tiling = new Tiling.TilingManager(timerResize, timerGeometryChanged); 34 | tiling.layoutChanged.connect(function(layout) { 35 | if (KWin.readConfig("showLayoutOsd", true) && !layoutOsdLoader.item) { 36 | layoutOsdLoader.setSource("layoutosd.qml", {"tiling": tiling}); 37 | tiling.layoutChanged.emit(layout); 38 | } 39 | }); 40 | tiling.tilingChanged.connect(function(userActive) { 41 | if (KWin.readConfig("showLayoutOsd", true) && !layoutOsdLoader.item) { 42 | layoutOsdLoader.setSource("layoutosd.qml", {"tiling": tiling}); 43 | tiling.tilingChanged.emit(userActive); 44 | } 45 | }); 46 | } 47 | 48 | Loader { 49 | id: layoutOsdLoader 50 | } 51 | 52 | Timer { 53 | id: timerResize 54 | interval: 500 55 | running: false 56 | repeat: false 57 | property variant screen 58 | onTriggered: tiling.resize(); 59 | } 60 | 61 | Timer { 62 | id: timerGeometryChanged 63 | repeat: false 64 | interval: 1 65 | onTriggered: tiling.tiles.updateGeometry(); 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /commentary.org: -------------------------------------------------------------------------------- 1 | * TODO EDGECASES, EDGECASES, EDGECASES! 2 | ** TODO Doesn't tile properly when screen disappears 3 | ** TODO Resolution changes 4 | Almost possible, but clientArea returns the _old_ resolution 5 | * TODO Testing 6 | ** Weird apps (mplayer, gimp) 7 | Add those to rules by default 8 | ** Weird configurations 9 | ** Multi-monitor 10 | ** Crashes 11 | ** Better release process (i.e. don't just pack up git, TEST) 12 | ** Testcases 13 | *** Client on all desktops 14 | **** There, back, there again 15 | *** Client on last desktop 16 | **** was interpreted as being on all desktops 17 | * TODO Layouts 18 | ** DONE Refactor layouts to share more code 19 | See [[https://github.com/copycat-killer/vain-again][vain]] 20 | This uses a simple "arrange" function as the layout function 21 | ** TODO More/better layouts 22 | ** Implement BladeLayout as HalfLayout with masterCount == tiles.length 23 | ** Multi-master in spirallayout 24 | * TODO Crazy stuff 25 | ** Layout switcher 26 | ** Indicate layout/state (tiling on/off) somehow 27 | ** Per-activity settings 28 | Huge complexity increase (one additional layer on top of desktops/screens) 29 | Investigate how the actual relationship between activities and desktops/clients is 30 | Seems to be the following: 31 | Each client belongs to multiple activities, but is on the same desktop on each 32 | There can only be one activity at the same time 33 | *** Needs changes in tilingmanager (large) and maybe tile and tilelist 34 | ** React on configuration change 35 | Right now kwin restarts are needed every time the configuration is changed 36 | * TODO Report 37 | ** Bugs 38 | *** INPROGRESS ClientArea doesn't update 39 | https://bugs.kde.org/show_bug.cgi?id=330099 40 | *** INPROGRESS no tabGroup 41 | https://bugs.kde.org/show_bug.cgi?id=330100 42 | *** INPROGRESS windowClosed's "deleted" not properly converted 43 | https://bugs.kde.org/show_bug.cgi?id=330102 44 | *** INPROGRESS Pixmap issue 45 | Seems it corrupts when clients are resized before windowShown (when compositing) 46 | There are other cases, though, like firefox resizing after restoring the previous session 47 | https://bugs.kde.org/show_bug.cgi?id=341195 48 | *** KWin maximizes windows automatically on certain conditions 49 | Possibly caused by electricBorderMaximize 50 | *** QML doesn't draw stuff 51 | *** options.useCompositing/compositingMode doesn't keep up 52 | It does not change to false when disabling compositing 53 | *** Crash when wrong desktop is used in workspace.clientArea() 54 | See https://github.com/faho/kwin-tiling/issues/22 55 | *** Stacking order is buggy 56 | i.e. sometimes maximizing a client maximizes it _below_ other clients 57 | or glow is below other clients 58 | *** QTimer is not exported to qml 59 | Either this or import functionality to javascript are needed to properly do resizes 60 | ** Documentation 61 | *** ClientArea options 62 | *** conditions/guarantees (e.g. is windowClosed always executed before FFM selects new activeClient?) 63 | *** Difference between geometryChanged and geometryShapeChanged 64 | geometryChanged fires on geometryShapeChanged and a few other signals 65 | *** windowClosed is only fired when compositing 66 | ** Wishes 67 | *** Integration with rules 68 | *** Some way to draw small window borders 69 | Outline doesn't work as it darkens the window and consumes mouse input 70 | Effect? 71 | *** Handle maximize like minimize in the scripting API 72 | * INPROGRESS Document 73 | ** INPROGRESS A usage section 74 | ** DONE A goals section 75 | * Remove duplicate functionality 76 | * Refactor and beautify code 77 | ** Use "filter" etc more often 78 | ** Remove duplicate code 79 | -------------------------------------------------------------------------------- /doc/design.txt: -------------------------------------------------------------------------------- 1 | 2 | This file is supposed to describe the design and planned development of the 3 | tiling script. What follows is first a list of the goals of the project and then 4 | design decisions and a coarse description of the design. 5 | 6 | Goals: 7 | * A functional tiling window management extension for kwin 8 | * Automatic layouts should be supported: 9 | * Spiral layout (SpiralLayout) 10 | * Two columns/rows (HalfLayout) 11 | * Rows only (BladeLayout) 12 | * Regular grid? 13 | * ... 14 | * Different layouts for different desktops/screens 15 | * A number of different layouts as well as a possibility to disable tiling for 16 | a certain screen/desktop combination 17 | * The possibility to make some windows floating (remove them from the layout) 18 | Both with a keybinding and a menu entry 19 | * Tab groups should be handled correctly (a complete tab group forms one tile) 20 | * Individual windows should be resizable and the whole layout should be updated 21 | accordingly 22 | * Windows should be selected and moved/resized in the layout and between 23 | screens and desktops using the keyboard or the mouse 24 | * Tiled windows should always be drawn below floating ones 25 | and fullscreen windows should always be on top 26 | * Window borders can be disabled when being tiled, 27 | and can be reactivated per-window with a shortcut 28 | * Some windows cannot be tiled properly (dialogs, non resizable windows, etc.), 29 | those should automatically be made floating. 30 | This can be configured (by window-class currently), and there is one app-specific workaround for steam 31 | as it doesn't set the window-class or type (dialog etc) properly 32 | ** Also keep a configurable list with sensible defaults (nobody wants yakuake to float) 33 | * Multiscreen setups should work correctly 34 | * Activities are a distant TODO 35 | 36 | Design decisions: 37 | * tiled windows at the bottom, floating windows over that, fullscreen or maximized windows on top (may add options on request) 38 | * windows that float permanently (because of configuration or type) aren't kept track of in the script - we're not a window manager 39 | * KWin functionality should not be duplicated, even if it's off by default (e.g. "focus window to the right") 40 | 41 | Design description: 42 | * TileList: KWin doesn't give us any information about tab groups, so we have 43 | to figure that out ourselves by setting a property and synchronizing it among 44 | all clients in a tab. This information needs to be kept up-to-date on tab 45 | group changes as well. The code related to this is in tilelist.js which 46 | contains a class which keeps track of all tab groups in the system. 47 | This is where newly opened windows are first added. 48 | * Tile: Various window signals (resizing, moving, maximizing etc.) have to be 49 | reacted to, this class binds handlers to these signals and filters them as 50 | necessary (e.g. intermediate steps in a resize operation are not interesting 51 | for us). 52 | * Layout: This is the class which allocated the areas of the different tiles in 53 | an automatic fashion. It contains functions to get the top/bottom/left/right 54 | neighbour of a tile which are used when the user wants to move the focus to 55 | a different tile. The different kinds of layouts are classes derived from 56 | this class. 57 | Floating windows are _not_ handled in layouts or tiles. 58 | * TilingManager: The main class which contains a list of the current layouts (one per 59 | screen/desktop combination) and which registers global keyboard shortcuts and 60 | some global events (e.g. screen count changes). 61 | * Tiling (also referenced as "Layout" in TilingManager): This is essentially one desktop per screen and makes changing layouts at runtime possible 62 | * There is currently no layout switcher (but it could be implemented in QML). 63 | 64 | The class relationship as cool ASCII-Art: 65 | 66 | TilingManager 67 | / \ 68 | Tiling TileList 69 | / \ / 70 | Layout Tile 71 | -------------------------------------------------------------------------------- /contents/ui/layoutosd.qml: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2012, 2013 Martin Gräßlin 6 | Copyright (C) 2019 David Strobach 7 | 8 | This program is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | *********************************************************************/ 21 | import QtQuick 2.0; 22 | import QtQuick.Window 2.0; 23 | import QtQuick.Layouts 1.12; 24 | import org.kde.plasma.core 2.0 as PlasmaCore; 25 | import org.kde.plasma.extras 2.0 as PlasmaExtras 26 | import org.kde.plasma.components 2.0 as Plasma; 27 | import org.kde.kquickcontrolsaddons 2.0 as KQuickControlsAddons; 28 | import org.kde.kwin 2.0; 29 | 30 | PlasmaCore.Dialog { 31 | id: dialog 32 | location: PlasmaCore.Types.Floating 33 | visible: false 34 | flags: Qt.X11BypassWindowManagerHint | Qt.FramelessWindowHint 35 | outputOnly: true 36 | property variant tiling 37 | 38 | mainItem: Item { 39 | function loadConfig() { 40 | dialogItem.animationDuration = KWin.readConfig("PopupHideDelay", 1000); 41 | } 42 | 43 | function show(text) { 44 | timer.stop(); 45 | textElement.text = text; 46 | var screen = workspace.clientArea(KWin.FullScreenArea, workspace.activeScreen, workspace.currentDesktop); 47 | dialog.visible = true; 48 | dialog.x = screen.x + screen.width/2 - dialogItem.width/2; 49 | dialog.y = screen.y + screen.height/2 - dialogItem.height/2; 50 | timer.start(); 51 | } 52 | 53 | id: dialogItem 54 | property int animationDuration: 1000 55 | 56 | width: Math.ceil(layout.implicitWidth) 57 | height: textElement.height 58 | 59 | RowLayout { 60 | id: layout 61 | anchors.fill: parent 62 | spacing: 10 63 | 64 | PlasmaExtras.Heading { 65 | id: textElement 66 | horizontalAlignment: Text.AlignHCenter 67 | wrapMode: Text.NoWrap 68 | elide: Text.ElideRight 69 | } 70 | PlasmaExtras.Heading { 71 | id: label 72 | horizontalAlignment: Text.AlignHCenter 73 | wrapMode: Text.NoWrap 74 | elide: Text.ElideRight 75 | text: "" 76 | } 77 | } 78 | 79 | Timer { 80 | id: timer 81 | repeat: false 82 | interval: dialogItem.animationDuration 83 | onTriggered: dialog.visible = false 84 | } 85 | 86 | Connections { 87 | target: options 88 | onConfigChanged: dialogItem.loadConfig() 89 | } 90 | Component.onCompleted: { 91 | dialogItem.loadConfig(); 92 | } 93 | } 94 | 95 | Component.onCompleted: { 96 | tiling.layoutChanged.connect(function(layout) { 97 | if (KWin.readConfig("showLayoutOsd", true)) { 98 | dialogItem.show(layout.name + "Layout"); 99 | } 100 | }); 101 | tiling.tilingChanged.connect(function(userActive) { 102 | if (KWin.readConfig("showLayoutOsd", true)) { 103 | dialogItem.show("Tiling " + (userActive ? "enabled" : "disabled")); 104 | } 105 | }); 106 | KWin.registerWindow(dialog); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /contents/config/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | yakuake,plasma,plasma-desktop,krunner,plugin-container,klipper,Wine 11 | 12 | 13 | 14 | false 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | true 23 | 24 | 25 | 26 | 0 27 | 28 | 29 | 30 | 0 31 | 32 | 33 | 34 | 0 35 | 36 | 37 | 38 | 0 39 | 40 | 41 | 42 | 0 43 | 44 | 45 | 46 | 0 47 | 48 | 49 | 50 | 0 51 | 52 | 53 | 54 | false 55 | 56 | 57 | 58 | true 59 | 60 | 61 | 62 | true 63 | 64 | 65 | 66 | 0 67 | 68 | 69 | 70 | false 71 | 72 | 73 | 74 | 1 75 | 76 | 77 | 78 | false 79 | 80 | 81 | 82 | 50 83 | 84 | 85 | 86 | 50 87 | 88 | 89 | 90 | false 91 | 92 | 93 | 94 | false 95 | 96 | 97 | 98 | 0 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /contents/code/spirallayout.js: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2012 Mathias Gottschlag 6 | Copyright (C) 2013-2014 Fabian Homborg 7 | 8 | This program is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | *********************************************************************/ 21 | 22 | Qt.include("layout.js"); 23 | /** 24 | * Class which arranges the windows in a spiral with the largest window filling 25 | * the left half of the screen. 26 | */ 27 | function SpiralLayout(screenRectangle) { 28 | print("Creating SpiralLayout"); 29 | Layout.call(this, screenRectangle); 30 | this.master = 0; 31 | }; 32 | 33 | SpiralLayout.prototype = new Layout(); 34 | SpiralLayout.prototype.constructor = SpiralLayout; 35 | 36 | SpiralLayout.prototype.name = "Spiral"; 37 | SpiralLayout.prototype.supportsRotation = true; 38 | // TODO: Add an image for the layout switcher 39 | //SpiralLayout.image = null; 40 | 41 | SpiralLayout.prototype.addTile = function() { 42 | this._applyGravity(); 43 | if (this.tiles.length == 0) { 44 | // The first tile fills the whole screen 45 | var rect = Qt.rect(this.screenRectangle.x, 46 | this.screenRectangle.y, 47 | this.screenRectangle.width, 48 | this.screenRectangle.height); 49 | this._createTile(rect); 50 | } else { 51 | // Divide the last tile into two halves 52 | var lastRect = this.tiles[this.tiles.length - 1].rectangle; 53 | var newRect = Qt.rect(lastRect.x, 54 | lastRect.y, 55 | lastRect.width, 56 | lastRect.height); 57 | var direction = this.tiles.length % 4; 58 | var splitX = lastRect.width / 2; 59 | var splitY = lastRect.height / 2; 60 | switch (direction) { 61 | case 0: 62 | lastRect.y = lastRect.y + splitY; 63 | lastRect.height = lastRect.height - splitY; 64 | newRect.height = splitY; 65 | break; 66 | case 1: 67 | lastRect.width = splitX; 68 | newRect.x = newRect.x + splitX; 69 | newRect.width = newRect.width - splitX; 70 | break; 71 | case 2: 72 | lastRect.height = splitY; 73 | newRect.y = newRect.y + splitY; 74 | newRect.height = newRect.height - splitY; 75 | break; 76 | case 3: 77 | lastRect.x = lastRect.x + splitX; 78 | lastRect.width = lastRect.width - splitX; 79 | newRect.width = splitX; 80 | break; 81 | } 82 | this.tiles[this.tiles.length - 1].rectangle = lastRect; 83 | this._createTile(newRect); 84 | } 85 | this._unapplyGravity(); 86 | //var lastRect = this.tiles[this.tiles.length - 1].rectangle; 87 | }; 88 | 89 | SpiralLayout.prototype.removeTile = function(tileIndex) { 90 | // Increase the size of the last tile 91 | if (this.tiles.length > 1) { 92 | var tileCount = this.tiles.length - 1; 93 | var rects = [ 94 | this.tiles[tileCount - 1].rectangle, 95 | this.tiles[tileCount].rectangle 96 | ]; 97 | var left = Math.min(rects[0].x, rects[1].x); 98 | var top = Math.min(rects[0].y, rects[1].y); 99 | var right = Math.max(rects[0].x + rects[0].width, 100 | rects[1].x + rects[1].width); 101 | var bottom = Math.max(rects[0].y + rects[0].height, 102 | rects[1].y + rects[1].height); 103 | var lastRect = Qt.rect(left, top, right - left, bottom - top); 104 | this.tiles[tileCount - 1].rectangle = lastRect; 105 | } 106 | // Remove the last array entry 107 | this.tiles.length--; 108 | }; 109 | -------------------------------------------------------------------------------- /doc/tiling-classdiagram.txt: -------------------------------------------------------------------------------- 1 | 2 | @startuml tiling-classdiagram.svg 3 | skinparam classAttributeIconSize 0 4 | 5 | class TilingManager { 6 | -defaultLayout : LayoutType 7 | -availableLayouts : LayoutType 8 | -layouts : Tiling[][] 9 | -desktopCount : int 10 | -screenCount : int 11 | -onTileAdded() 12 | -onTileRemoved() 13 | -onNumberDesktopsChanged() 14 | -onNumberScreensChanged() 15 | -onCurrentDesktopChanged() 16 | -switchLayout(desktop, screen, layoutIndex) 17 | -toggleFloating(tile) 18 | -switchFocus(direction) 19 | -moveTile(direction) 20 | } 21 | 22 | note top of TilingManager 23 | Root class which contains one "Tiling" 24 | instance per screen/desktop, implements 25 | the keyboard shortcuts and keeps track 26 | of windows moving from one screen to the 27 | other. 28 | end note 29 | 30 | class TileList { 31 | +addClient(client : Client) : Tile 32 | +getTile(client : Client) : Tile 33 | +tileAdded : Signal 34 | +tileRemoved : Signal 35 | -onClientAdded(client) 36 | -onClientRemoved(client) 37 | -onClientTabGroupChanged(client) 38 | } 39 | 40 | class Tile { 41 | -savedGeometry : Rect 42 | +clients : Client[] 43 | +floating : bool 44 | +forcedFloating : bool 45 | +tileIndex 46 | +Tile(firstClient, tileIndex) 47 | +setGeometry(geometry : Rect) 48 | +saveGeometry() 49 | +restoreGeometry() 50 | +getActiveClient() 51 | +syncCustomProperties() 52 | -updateForcedFloating() 53 | +movingStarted : Signal 54 | +movingEnded : Signal 55 | +movingStep : Signal 56 | +resizingStarted : Signal 57 | +resizingEnded : Signal 58 | +resizingStep : Signal 59 | +geometryChanged : Signal 60 | +forcedFloatingChanged : Signal 61 | +screenChanged : Signal 62 | +desktopChanged : Signal 63 | +onClientShadeChanged(client) 64 | +onClientGeometryChanged(client) 65 | +onClientKeepAboveChanged(client) 66 | +onClientKeepBelowChanged(client) 67 | +onClientFullScreenChanged(client) 68 | +onClientMinimizedChanged(client) 69 | +onClientMaximizedStateChanged(client, h, v) 70 | +onClientDesktopChanged(client) 71 | +onClientStartUserMovedResized(client) 72 | +onClientStepUserMovedResized(client) 73 | +onClientFinishUserMovedResized(client) 74 | } 75 | 76 | note top of Tile 77 | Manages the windows in one tab group and 78 | tracks and simplifies resize/move events 79 | end note 80 | 81 | class Client { 82 | tiling_tileIndex : int 83 | tiling_floating : bool 84 | } 85 | 86 | abstract class Tiling { 87 | Tiling(screenRectangle : QRect, layoutType) 88 | +setLayoutType(layoutType) 89 | +setLayoutArea(area : QRect) 90 | +addTile(tile : Tile) 91 | +addTile(tile : Tile, x, y) 92 | +removeTile(tile : Tile) 93 | +swapTiles(tile1 : Tile, tile2 : Tile) 94 | +activate() 95 | +deactivate() 96 | +resetTileSizes() 97 | +getTile(x, y) : Tile 98 | +getTileGeometry(x, y) : TileGeometry 99 | +getTiles() : Tile[] 100 | +getAdjacentTile(from : Tile, direction, directOnly : bool) : Tile 101 | } 102 | 103 | note left of Tiling 104 | Resizes the windows according to the 105 | information the class gets from its 106 | Layout instance. 107 | end note 108 | 109 | abstract class Layout { 110 | +screenRectangle 111 | +tiles : TileGeometry[] 112 | +{static}name 113 | +{static}image 114 | +setLayoutArea(area : QRect) 115 | +{abstract}onLayoutAreaChange(oldArea : QRect, newArea : QRect) 116 | +{abstract}resetTileSizes() 117 | +{abstract}addTile() 118 | +{abstract}removeTile(tileIndex : int) 119 | +{abstract}resizeTile(tileIndex : int, rectangle : QRect) 120 | } 121 | 122 | note left of Layout 123 | Partitions the screens into rectangles, the 124 | subclasses implement different layouts. 125 | end note 126 | 127 | class TileGeometry { 128 | +rectangle : QRect 129 | +neighbours : int[4] 130 | +hasDirectNeighbour : bool[4] 131 | } 132 | 133 | class SpiralLayout { 134 | } 135 | 136 | class ZigZagLayout { 137 | } 138 | 139 | class ColumnLayout { 140 | } 141 | 142 | class RowLayout { 143 | } 144 | 145 | class GridLayout { 146 | } 147 | 148 | class MaximizedLayout { 149 | } 150 | 151 | class FloatingLayout { 152 | } 153 | 154 | TilingManager "1" -right-> "1" TileList 155 | TilingManager "1" --> "*" Tiling 156 | 157 | TileList "1" -right-> "*" Tile 158 | 159 | Tile "*" <-- "*" Tiling 160 | Tile "1" --> "*" Client 161 | 162 | Tiling "1" --> "1" Layout 163 | 164 | Layout <|-- SpiralLayout 165 | Layout <|-- ZigZagLayout 166 | Layout <|-- ColumnLayout 167 | Layout <|-- RowLayout 168 | Layout <|-- GridLayout 169 | Layout <|-- MaximizedLayout 170 | Layout <|-- FloatingLayout 171 | Layout -right-> "*" TileGeometry 172 | 173 | @enduml 174 | -------------------------------------------------------------------------------- /contents/code/bladelayout.js: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2013 Fabian Homborg 6 | based on spirallayout.js by Matthias Gottschlag 7 | 8 | This program is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | *********************************************************************/ 21 | 22 | Qt.include("layout.js"); 23 | /** 24 | * Class which arranges the windows in a spiral with the largest window filling 25 | * the left half of the screen. 26 | */ 27 | function BladeLayout(screenRectangle) { 28 | try { 29 | print("Creating BladeLayout"); 30 | Layout.call(this, screenRectangle); 31 | // TODO 32 | } catch(err) { 33 | print(err, "in BladeLayout"); 34 | } 35 | this.master = 0; 36 | print("BladeLayout created"); 37 | }; 38 | 39 | BladeLayout.prototype = new Layout(); 40 | BladeLayout.prototype.constructor = BladeLayout; 41 | 42 | BladeLayout.prototype.name = "Blade"; 43 | BladeLayout.prototype.supportsRotation = true; 44 | 45 | // // TODO: Add an image for the layout switcher 46 | // BladeLayout.image = null; 47 | 48 | BladeLayout.prototype.addTile = function() { 49 | try { 50 | this._applyGravity(); 51 | if (this.tiles.length == 0) { 52 | // The first tile fills the whole screen 53 | var rect = util.copyRect(this.screenRectangle); 54 | this._createTile(rect); 55 | this._unapplyGravity(); 56 | return; 57 | } else { 58 | // Divide the screen width evenly between full-height tiles 59 | var tileWidth = Math.floor(this.screenRectangle.width / (this.tiles.length + 1)); 60 | var newRect = Qt.rect(this.screenRectangle.x + this.tiles.length * tileWidth, 61 | this.screenRectangle.y, 62 | tileWidth, 63 | this.screenRectangle.height); 64 | // FIXME: Try to keep ratio 65 | for (var i = 0; i < this.tiles.length; i++) { 66 | var rect = this.tiles[i].rectangle; 67 | rect.x = this.screenRectangle.x + tileWidth * i; 68 | rect.width = tileWidth; 69 | this.tiles[i].rectangle = rect; 70 | } 71 | // Adjust tile's width for rounding errors 72 | newRect.width = (this.screenRectangle.width + this.screenRectangle.x) - newRect.x; 73 | // TODO: Move this before setting ratio to simplify 74 | this._createTile(newRect); 75 | this._unapplyGravity(); 76 | } 77 | } catch(err) { 78 | print(err, "in BladeLayout.addTile"); 79 | } 80 | }; 81 | 82 | BladeLayout.prototype.removeTile = function(tileIndex) { 83 | try { 84 | this._applyGravity(); 85 | // Remove the array entry 86 | var oldrect = this.tiles[tileIndex].rectangle; 87 | this.tiles.splice(tileIndex, 1); 88 | // Update the other tiles 89 | if (this.tiles.length == 1) { 90 | this.tiles[0].rectangle = util.copyRect(this.screenRectangle); 91 | } 92 | if (this.tiles.length > 1) { 93 | var tileCount = this.tiles.length; 94 | var lastRect = this.tiles[0].rectangle; 95 | var newRect = Qt.rect(this.screenRectangle.x, 96 | this.screenRectangle.y, 97 | this.screenRectangle.width / tileCount, 98 | this.screenRectangle.height); 99 | var lowest = 1; 100 | for (var i = 0; i < this.tiles.length; i++) { 101 | var rect = this.tiles[i].rectangle; 102 | rect.x = newRect.x + newRect.width * i; 103 | rect.width = newRect.width; 104 | this.tiles[i].rectangle = rect; 105 | } 106 | // Adjust rightmost tile's height for rounding errors 107 | this.tiles[this.tiles.length - 1].rectangle.width = (this.screenRectangle.width + this.screenRectangle.x) - this.tiles[this.tiles.length - 1].rectangle.x; 108 | } 109 | this._unapplyGravity(); 110 | } catch(err) { 111 | print(err, "in BladeLayout.removeTile"); 112 | } 113 | }; 114 | -------------------------------------------------------------------------------- /contents/code/ignored.js: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2012 Mathias Gottschlag 6 | Copyright (C) 2013-2014 Fabian Homborg 7 | 8 | This program is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | *********************************************************************/ 21 | 22 | var ignored = {}; 23 | 24 | // A hardcoded list of clients that should never be tiled 25 | ignored._ignoredlist = [ 26 | // If a class is empty, chances are it doesn't behave properly in other ways as well 27 | "", 28 | "lattedock", 29 | "kcalc", 30 | "klipper", 31 | "krunner", 32 | "ksmserver", 33 | "pinentry", 34 | "plasma", 35 | "plasma-desktop", 36 | "plasmashell", 37 | "plugin-container", 38 | "wine", 39 | "yakuake", 40 | "gcr-prompter", 41 | "org.kde.krunner", 42 | "org.kde.yakuake", 43 | "org.kde.kcalc", 44 | "org.kde.klipper", 45 | "org.kde.krunner", 46 | "org.kde.ksmserver", 47 | "org.kde.pinentry", 48 | "org.kde.plasma", 49 | "org.kde.plasma-desktop", 50 | "org.kde.plasmashell", 51 | "urxvt", 52 | ] 53 | 54 | /** 55 | * Returns true for clients which shall never be handled by the tiling script, 56 | * e.g. panels, dialogs or user-defined apps 57 | * Application workarounds should be put here 58 | */ 59 | ignored.isIgnored = function(client) { 60 | if (client.tiling_floating == true) { 61 | return true; 62 | } 63 | // TODO: Add regex and more options (by title/caption, override a floater, maybe even a complete scripting language / code) 64 | // A QLineEdit will backslash-escape ",", so we'll need to split on `\\,`. 65 | // We trim whitespace around commas, and we lowercase it because kwin lowercases the resourceClass. 66 | var floaters = KWin.readConfig("floaters", "").toLowerCase().trim().replace(/\s*,\s*/g,",").split("\\,"); 67 | 68 | if (floaters.indexOf(client.resourceClass.toString()) > -1 69 | || floaters.indexOf(client.resourceName.toString()) > -1 70 | || ignored._ignoredlist.indexOf(client.resourceClass.toString()) > -1) { 71 | print("Ignoring client because of ignoredlist: ", client.resourceClass.toString()); 72 | return true; 73 | } 74 | // HACK: Steam doesn't set the windowtype properly 75 | // Everything that isn't captioned "Steam" should be a dialog - these resize worse than the main window does 76 | // With the exception of course of the class-less update/start dialog with the caption "Steam" (*Sigh*) 77 | if (client.resourceClass.toString() == "steam" && client.caption != "Steam") { 78 | print("Ignoring client because of steam workaround 1: ", client.resourceClass.toString()); 79 | return true; 80 | } else if (client.resourceClass.toString() != "steam" && client.caption == "Steam") { 81 | print("Ignoring client because of steam workaround 2: ", client.resourceClass.toString()); 82 | return true; 83 | } 84 | 85 | // HACK: Firefox' secondary windows, downloads and such, are normal windows with a class of "firefox". 86 | // They have a *name* of "places" though, so we can hopefully detect them that way. 87 | if (client.resourceClass.toString() == "firefox" && client.resourceName == "places") { 88 | print("Ignoring client because of firefox workaround", client.resourceName); 89 | return true; 90 | } 91 | 92 | // KFind is annoying. It sets the window type to dialog (which is arguably wrong) and more importantly sets 93 | // the transient_for property for some bogus "Qt Client Leader Window". 94 | // 95 | // So we whitelist it here - this still allows listing it via class. 96 | if (client.resourceClass.toString() == "kfind") { 97 | return false; 98 | } 99 | 100 | // Transient windows are usually dialogs and such for other windows. 101 | // Usually, they should also have type = dialog, but often (eclipse and inkscape), 102 | // they do not. So we should rather ignore them than display them wrong or have them close when they lose focus because we moved them (and FFM was in effect). 103 | if (client.transient == true) { 104 | print("Ignoring client because it's transient: ", client.resourceClass.toString()); 105 | return true; 106 | } 107 | 108 | // Client has a type that shouldn't be tiled 109 | if (client.specialWindow == true || 110 | client.desktopWindow == true || 111 | client.dock == true || 112 | client.toolbar == true || 113 | client.menu == true || 114 | client.dialog == true || 115 | client.splash == true || 116 | client.utility == true || 117 | client.dropdownMenu == true || 118 | client.popupMenu == true || 119 | client.tooltip == true || 120 | client.notification == true || 121 | client.comboBox == true || 122 | client.dndIcon == true) { 123 | print("Ignoring client because of window type: ", client.resourceClass.toString()); 124 | return true; 125 | } 126 | 127 | return false; 128 | }; 129 | -------------------------------------------------------------------------------- /contents/code/i3layout.js: -------------------------------------------------------------------------------- 1 | 2 | /******************************************************************** 3 | KWin - the KDE window manager 4 | This file is part of the KDE project. 5 | 6 | Copyright (C) 2018-2018 Setzer22 7 | based on bladelayout.js by Fabian Homborg 8 | 9 | This program is free software; you can redistribute it and/or modify 10 | it under the terms of the GNU General Public License as published by 11 | the Free Software Foundation; either version 2 of the License, or 12 | (at your option) any later version. 13 | 14 | This program is distributed in the hope that it will be useful, 15 | but WITHOUT ANY WARRANTY; without even the implied warranty of 16 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 17 | GNU General Public License for more details. 18 | 19 | You should have received a copy of the GNU General Public License 20 | along with this program. If not, see . 21 | *********************************************************************/ 22 | 23 | Qt.include("layout.js"); 24 | Qt.include("containerTree.js"); 25 | 26 | /** 27 | * Class which allows dynamic arrangement of tiles in a similar way as 28 | the i3 window manager. The core logic is implemented in the containerTree 29 | data structure. 30 | */ 31 | function I3Layout(screenRectangle) { 32 | try { 33 | print("Creating I3Layout"); 34 | Layout.call(this, screenRectangle); 35 | 36 | // TODO: Read default layout from config file and don't assume horizontal 37 | this.containerTree = new ContainerNode('horizontal', screenRectangle); 38 | 39 | this.isI3Layout = true; 40 | 41 | /* The current state. 42 | pseudo-enum: 'normal', 'horizontalWrap', 'verticalWrap' 43 | */ 44 | this.state = 'normal'; 45 | 46 | } catch(err) { 47 | print(err, "in I3Layout"); 48 | } 49 | this.master = 0; 50 | print("I3Layout created"); 51 | }; 52 | 53 | I3Layout.prototype = new Layout(); 54 | I3Layout.prototype.constructor = I3Layout; 55 | 56 | I3Layout.prototype.name = "I3"; 57 | 58 | /* 59 | * Gets the tile at position x,y 60 | */ 61 | I3Layout.prototype.getTileAt = function(x, y) { 62 | try { 63 | for (var i = 0; i < this.tiles.length; i++) { 64 | var tile = this.tiles[i]; 65 | if (tile.rectangle.x <= x 66 | && tile.rectangle.y <= y 67 | && tile.rectangle.x + tile.rectangle.width > x 68 | && tile.rectangle.y + tile.rectangle.height > y) { 69 | return tile; 70 | } 71 | } 72 | return null; 73 | } catch(err) { 74 | print(err, "in I3Layout._getTileAt"); 75 | } 76 | }; 77 | 78 | I3Layout.prototype.addTile = function(x, y) { 79 | try { 80 | // Determine the reference container 81 | var selectedContainer = this.containerTree; 82 | var selectedTile = null; 83 | var childIndex = this.tiles.length; 84 | if (x && y) { 85 | selectedTile = this.getTileAt(x,y); 86 | if (selectedTile) { 87 | selectedContainer = this.containerTree.findParentContainer(selectedTile); 88 | childIndex = selectedContainer.children.indexOf(selectedTile) + 1; 89 | } 90 | } 91 | 92 | // We ignore and reset the wrap state if there is no selected tile. 93 | if (!selectedTile) this.state = 'normal'; 94 | 95 | // Also ignore attempts to wrap a container inside a container 96 | if (selectedContainer && this.state !== 'normal' && selectedContainer.children.length <= 1) { 97 | if (selectedContainer === this.containerTree) { 98 | this.containerTree.type = (this.state === 'horizontalWrap' ? 'horizontal' : 'vertical'); 99 | } 100 | this.state = 'normal'; 101 | } 102 | 103 | /* 104 | //NOTE: I'll leave this here just in case someone wants to enable it. 105 | 106 | // Don't want to wrap if the currently selected container is already in the same direction 107 | if (selectedContainer && ((this.state === 'verticalWrap' && selectedContainer.type === 'vertical') || 108 | (this.state === 'horizontalWrap' && selectedContainer.type === 'horizontal'))) { 109 | this.state = 'normal'; 110 | } 111 | */ 112 | 113 | // update all container sizes according to their children's sizes 114 | this.containerTree.updateContainerSizes(); 115 | 116 | // Create the new tile 117 | // TODO: Cleanup: Common parts in both if branches 118 | if (this.state === 'normal') { 119 | // Normal mode: Append to selectedContainer 120 | var leaf = new LeafNode(); 121 | selectedContainer.addNode(leaf, childIndex); 122 | this._createTile(leaf.rectangle); 123 | var tile = this.tiles[this.tiles.length - 1]; 124 | selectedContainer.children[childIndex] = tile; 125 | selectedContainer.recalculateSize(); 126 | } 127 | else if (this.state === 'horizontalWrap' || 128 | this.state === 'verticalWrap') { 129 | 130 | // Wrap mode: wrap selected tile in a new container and append new tile there 131 | var wrapContainer = new ContainerNode(this.state === 'horizontalWrap' ? 'horizontal' : 'vertical',util.copyRect(selectedTile.rectangle)); 132 | selectedContainer.addNode(wrapContainer, childIndex); 133 | selectedContainer.removeNode(selectedTile); 134 | wrapContainer.addNode(selectedTile, 0); 135 | 136 | var leaf = new LeafNode(); 137 | wrapContainer.addNode(leaf, 1); 138 | this._createTile(leaf.rectangle); 139 | var tile = this.tiles[this.tiles.length - 1]; 140 | wrapContainer.children[1] = tile; 141 | wrapContainer.recalculateSize(); 142 | } 143 | 144 | this.state = 'normal'; 145 | 146 | // print(debugPrintTree(this.containerTree)); 147 | 148 | } catch(err) { 149 | print(err, "in I3Layout.addTile"); 150 | } 151 | }; 152 | 153 | function debugPrintTree(node) { 154 | var out = ""; 155 | out += "(" + node.type + " "; 156 | node.children.forEach(function(child) { 157 | if (child.children) out += debugPrintTree(child); 158 | else out += " [] "; 159 | }); 160 | out += ") "; 161 | 162 | return out; 163 | } 164 | 165 | I3Layout.prototype.removeTile = function(tileIndex) { 166 | try { 167 | var toDeleteTile = this.tiles[tileIndex]; 168 | var container = this.containerTree.findParentContainer(toDeleteTile); 169 | 170 | this.containerTree.updateContainerSizes(); 171 | container.removeNode(toDeleteTile); 172 | this.tiles.splice(tileIndex, 1); 173 | container.recalculateSize(); 174 | 175 | this.containerTree.cleanup(); 176 | 177 | print(debugPrintTree(this.containerTree)); 178 | 179 | } catch(err) { 180 | print(err, "in I3Layout.removeTile"); 181 | } 182 | }; 183 | -------------------------------------------------------------------------------- /contents/code/containerTree.js: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2018-2018 Setzer22 6 | 7 | This program is free software; you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation; either version 2 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | *********************************************************************/ 20 | 21 | 22 | /** 23 | * This data structure represents a tile tree with horizontal and vertical layouts. 24 | * the tree is responsible for recomputing tile sizes when adding/removing nodes and 25 | * also implements a cleanup function to ensure the tree shape is always consistent and 26 | * intuitive to the user (e.g. don't allow nested containers with a single child). The 27 | * tree is meant to store two kinds of nodes, `ContainerNode`s and tiles (the ones from 28 | * i3layout.tiles). However, a third node type, LeafNode is implemented as a placeholder 29 | * to allocate new tiles when they are created. 30 | */ 31 | function ContainerNode(type, rect) { 32 | try { 33 | this.type = type; 34 | this.rectangle = rect || {}; 35 | this.children = []; 36 | this.parent = null; 37 | 38 | } catch(err) { 39 | print(err, "in ContainerNode"); 40 | } 41 | } 42 | 43 | /* 44 | * Update sizes for this container and all children-containers as a bottom-up operation 45 | */ 46 | ContainerNode.prototype.updateContainerSizes = function() { 47 | 48 | this.children.forEach(function (child) { 49 | if (child.children) child.updateContainerSizes(); 50 | }); 51 | 52 | if (this.children[0]) { 53 | var rect = this.children[0].rectangle; 54 | this.children.forEach(function (child) { 55 | rect = util.expandRect(rect, child.rectangle); 56 | }); 57 | this.rectangle = util.copyRect(rect); 58 | } 59 | }; 60 | 61 | /* 62 | * Resize this node and its children as a top-down operation 63 | * Keep size ratio of children 64 | */ 65 | ContainerNode.prototype.resizeNode = function(rectAfter) { 66 | 67 | var rectBefore = this.rectangle; 68 | this.rectangle = util.copyRect(rectAfter); 69 | var childrenLength = this.children.length; 70 | 71 | 72 | if (this.type === 'horizontal') { 73 | 74 | var totalWidth = 0; 75 | var newWidth = 0; 76 | 77 | this.children.forEach(function(child, index) { 78 | var newRect = Qt.rect(0, 0, 0, 0); 79 | if (index === childrenLength - 1) { 80 | newWidth = Math.floor(rectAfter.width - totalWidth); 81 | } else { 82 | newWidth = Math.floor((child.rectangle.width / rectBefore.width) * rectAfter.width); 83 | } 84 | 85 | newRect.x = rectAfter.x + totalWidth; 86 | newRect.y = rectAfter.y; 87 | newRect.width = newWidth; 88 | newRect.height = rectAfter.height; 89 | totalWidth += newWidth; 90 | if(child.children) child.resizeNode(util.copyRect(newRect)); 91 | else child.rectangle = util.copyRect(newRect); 92 | }); 93 | 94 | } else if (this.type === 'vertical') { 95 | 96 | var totalHeight = 0; 97 | var newHeight = 0; 98 | 99 | this.children.forEach(function(child, index) { 100 | var newRect = Qt.rect(0, 0, 0, 0); 101 | if (index === childrenLength - 1) { 102 | newHeight = Math.floor(rectAfter.height - totalHeight); 103 | } else { 104 | newHeight = Math.floor((child.rectangle.height / rectBefore.height) * rectAfter.height); 105 | } 106 | 107 | newRect.x = rectAfter.x; 108 | newRect.y = rectAfter.y + totalHeight; 109 | newRect.width = rectAfter.width; 110 | newRect.height = newHeight; 111 | totalHeight += newHeight; 112 | if(child.children) child.resizeNode(util.copyRect(newRect)); 113 | else child.rectangle = util.copyRect(newRect); 114 | }); 115 | } 116 | }; 117 | 118 | /* 119 | * Recalculate sizes for this nodes children as a top-down operation 120 | * give all children the same size 121 | */ 122 | ContainerNode.prototype.recalculateSize = function() { 123 | 124 | var rectBefore = this.rectangle; 125 | var childrenLength = this.children.length; 126 | 127 | 128 | if (this.type === 'horizontal') { 129 | 130 | var totalWidth = 0; 131 | var newWidth = 0; 132 | 133 | this.children.forEach(function(child, index) { 134 | var newRect = Qt.rect(0, 0, 0, 0); 135 | if (index === childrenLength - 1) { 136 | newWidth = Math.floor(rectBefore.width - totalWidth); 137 | } else { 138 | newWidth = Math.floor(rectBefore.width / childrenLength); 139 | } 140 | 141 | newRect.x = rectBefore.x + totalWidth; 142 | newRect.y = rectBefore.y; 143 | newRect.width = newWidth; 144 | newRect.height = rectBefore.height; 145 | totalWidth += newWidth; 146 | if(child.children) child.resizeNode(util.copyRect(newRect)); 147 | else child.rectangle = util.copyRect(newRect); 148 | }); 149 | 150 | } else if (this.type === 'vertical') { 151 | 152 | var totalHeight = 0; 153 | var newHeight = 0; 154 | 155 | this.children.forEach(function(child, index) { 156 | var newRect = Qt.rect(0, 0, 0, 0); 157 | if (index === childrenLength - 1) { 158 | newHeight = Math.floor(rectBefore.height - totalHeight); 159 | } else { 160 | newHeight = Math.floor(rectBefore.height / childrenLength); 161 | } 162 | 163 | newRect.x = rectBefore.x; 164 | newRect.y = rectBefore.y + totalHeight; 165 | newRect.width = rectBefore.width; 166 | newRect.height = newHeight; 167 | totalHeight += newHeight; 168 | if(child.children) child.resizeNode(util.copyRect(newRect)); 169 | else child.rectangle = util.copyRect(newRect); 170 | }); 171 | } 172 | }; 173 | 174 | /* 175 | * Inserts a new node into the ContanierNode at the specified index position 176 | */ 177 | ContainerNode.prototype.addNode = function(node, index) { 178 | this.children.splice(index, 0, node); 179 | node.parent = this; 180 | }; 181 | 182 | /* 183 | * Removes the node from the container node 184 | * @pre the node must be a direct child of the container node. 185 | */ 186 | ContainerNode.prototype.removeNode = function(node) { 187 | this.children = this.children.filter(function (x) {return x !== node;}); 188 | }; 189 | 190 | /* 191 | * Prunes empty containers and un-wraps single-child containers 192 | * NOTE: This code could probably be simplified, but it works. 193 | */ 194 | ContainerNode.prototype.cleanup = function(node) { 195 | // Defer node deletion so we don't delete during loop 196 | var nodesToRemove = []; 197 | 198 | // Cleanup is bottom-up, not top-down 199 | for (var c = 0; c < this.children.length; ++c) { 200 | if (this.children[c].children) { 201 | this.children[c].cleanup(); 202 | } 203 | } 204 | 205 | 206 | if (this.children && this.children.length == 1 && this.children[0].children) { 207 | this.type = this.children[0].type; 208 | this.rectangle = this.children[0].rectangle; 209 | this.children = this.children[0].children; 210 | } 211 | 212 | for (var c = 0; c < this.children.length; ++c) { 213 | if (this.children[c].children) { 214 | if (this.children[c].children.length == 1) { 215 | var grandchild = this.children[c].children[0]; 216 | this.children[c] = grandchild; 217 | } else if (this.children[c].children.length == 0) { 218 | nodesToRemove.push(this.children[c]); 219 | } 220 | } 221 | } 222 | 223 | nodesToRemove.forEach(function(nodeToRemove) { 224 | this.removeNode(nodeToRemove); 225 | }); 226 | }; 227 | 228 | /* 229 | * Search operation on the tree that will recursively locate for a leaf node 230 | * and return its parent container. 231 | */ 232 | ContainerNode.prototype.findParentContainer = function(leafNode) { 233 | var found = null; 234 | 235 | for (var c = 0; c < this.children.length; c++) { 236 | var child = this.children[c]; 237 | 238 | if (child.children ) { 239 | var foundInChild = child.findParentContainer(leafNode); 240 | if (foundInChild) return foundInChild; 241 | } else if (child === leafNode) { 242 | return this; 243 | } 244 | 245 | } 246 | return null; 247 | }; 248 | 249 | /* 250 | * Placeholder class. See description of ContainerNode 251 | */ 252 | function LeafNode() { 253 | try { 254 | this.rectangle = {}; 255 | } catch(err) { 256 | print(err, "in LeafNode"); 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /contents/code/util.js: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2013-2014 Fabian Homborg 6 | 7 | This program is free software; you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation; either version 2 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | *********************************************************************/ 20 | 21 | var util = {}; 22 | util.copyRect = function(rect) { 23 | return Qt.rect(Math.floor(rect.x), 24 | Math.floor(rect.y), 25 | Math.floor(rect.width), 26 | Math.floor(rect.height)); 27 | }; 28 | 29 | // Sets rect1 to rect2 by value 30 | util.setRect = function(rect1,rect2) { 31 | rect1.x = Math.floor(rect2.x); 32 | rect1.y = Math.floor(rect2.y); 33 | rect1.width = Math.floor(rect2.width); 34 | rect1.height = Math.floor(rect2.height); 35 | }; 36 | 37 | // Returns true if rects are equal, false if not 38 | util.compareRect = function(rect1,rect2) { 39 | if (rect1 == null || rect2 == null) { 40 | return rect1 == rect2; 41 | } 42 | return rect1.x == rect2.x && 43 | rect1.y == rect2.y && 44 | rect1.width == rect2.width && 45 | rect1.height == rect2.height; 46 | }; 47 | 48 | util.intersectRect = function(rect1, rect2) { 49 | if (rect1.x + rect1.width < rect2.x || 50 | rect2.x + rect2.width < rect1.x || 51 | rect1.y + rect1.height < rect2.y || 52 | rect2.y + rect2.height < rect1.y) { 53 | return null; // No intersection 54 | } 55 | var newRect = Qt.rect(0,0,0,0); 56 | newRect.x = Math.max(rect1.x, rect2.x); 57 | newRect.y = Math.max(rect1.y, rect2.y); 58 | newRect.width = (Math.min(rect1.x + rect1.width, rect2.x + rect2.width) - newRect.x); 59 | newRect.height = (Math.min(rect1.y + rect1.height, rect2.y + rect2.height) - newRect.y); 60 | return newRect; 61 | }; 62 | 63 | util.expandRect = function(rect1, rect2) { 64 | var newRect = Qt.rect(0,0,0,0); 65 | newRect.x = Math.min(rect1.x, rect2.x); 66 | newRect.y = Math.min(rect1.y, rect2.y); 67 | newRect.width = (Math.max(rect1.x + rect1.width, rect2.x + rect2.width) - newRect.x); 68 | newRect.height = (Math.max(rect1.y + rect1.height, rect2.y + rect2.height) - newRect.y); 69 | return newRect; 70 | }; 71 | 72 | util.setX = function(geom, value) { 73 | geom.width = (geom.width + geom.x) - value; 74 | geom.x = value; 75 | }; 76 | util.getX = function(geom) { 77 | return geom.x; 78 | }; 79 | util.setY = function(geom, value) { 80 | geom.height = (geom.height + geom.y) - value; 81 | geom.y = value; 82 | }; 83 | util.getY = function(geom) { 84 | return geom.y; 85 | }; 86 | util.setR = function(geom, value) { 87 | geom.width = value - geom.x; 88 | }; 89 | // Return right edge 90 | util.getR = function(geom) { 91 | return (geom.x + geom.width); 92 | }; 93 | util.setB = function(geom, value) { 94 | geom.height = value - geom.y; 95 | }; 96 | // Return bottom edge 97 | util.getB = function(geom) { 98 | return (geom.y + geom.height); 99 | }; 100 | 101 | util.rectToMatrix = function(rect) { 102 | return [[rect.x, rect.y],[rect.x + rect.width, rect.y + rect.height]]; 103 | }; 104 | 105 | util.matrixToRect = function(m) { 106 | var newRect = Qt.rect(m[0][0], m[0][1], 0, 0); 107 | newRect.width = m[1][0] - newRect.x; 108 | newRect.height = m[1][1] - newRect.y; 109 | // normalize 110 | if (newRect.width < 0) { 111 | newRect.x += newRect.width; 112 | newRect.width *= -1; 113 | } 114 | if (newRect.height < 0) { 115 | newRect.y += newRect.height; 116 | newRect.height *= -1; 117 | } 118 | return newRect; 119 | }; 120 | 121 | util.multiplyRectMatrices = function(m1, m2) { 122 | var res = new Array(); 123 | for (var i = 0; i < 2; i++) { 124 | res[i] = new Array(); 125 | for (var j = 0; j < 2; j++) { 126 | var val = 0; 127 | for (var k = 0; k < 2; k++) { 128 | val += m1[i][k] * m2[k][j]; 129 | } 130 | res[i][j] = val; 131 | } 132 | } 133 | return res; 134 | }; 135 | 136 | /** 137 | * Utility function which returns the area on the selected screen/desktop which 138 | * is filled by the layout for that screen. 139 | * 140 | * @param desktop Desktop for which the area shall be returned. 141 | * @param screen Screen for which the area shall be returned. 142 | * @return Rectangle which contains the area which shall be used by layouts. 143 | */ 144 | util.getTilingArea = function(screen, desktop) { 145 | var cA = workspace.clientArea(KWin.PlacementArea, screen, desktop); 146 | return util.copyRect(cA); 147 | }; 148 | 149 | util.rectToString = function(rect) { 150 | return "x" + rect.x + "y" + rect.y + "w" + rect.width + "h" + rect.height; 151 | if (rect == null) { 152 | return "null"; 153 | } 154 | } 155 | 156 | util.printRect = function(rect) { 157 | print(util.rectToString(rect)); 158 | } 159 | 160 | util.printTile = function(tile) { 161 | print("Tile ", tile.tileIndex, " on desktop ", tile.desktop, 162 | " screen ", tile.screen, " rect ", util.rectToString(tile.rectangle), 163 | " client ", tile.clients[0].resourceClass.toString()); 164 | } 165 | 166 | util.clientToString = function(c) { 167 | var outp = c.tiling_tileIndex + " " + c.resourceClass.toString() + '- "' + c.caption + '" ' + util.rectToString(c.geometry); 168 | return outp; 169 | }; 170 | 171 | util.printClient = function(client) { 172 | print(util.clientToString(client)); 173 | } 174 | 175 | util.assertRectInScreen = function(rect, screenRectangle) { 176 | util.assertTrue(rect.x >= screenRectangle.x && 177 | rect.y >= screenRectangle.y && 178 | util.getR(rect) <= util.getR(screenRectangle) && 179 | util.getB(rect) <= util.getB(screenRectangle), "Rectangle not in screen"); 180 | }; 181 | 182 | /** 183 | * workspace.clientArea leaves holes for panels. 184 | * Also there could be gaps between screens. 185 | * Therefore I do a search for the screen with minimal distance in given direction. 186 | */ 187 | util.nextScreenInDirection = function(curScreen, desktop, direction) { 188 | var curScreenRect = workspace.clientArea(KWin.ScreenArea, curScreen, desktop); 189 | var targetScreen = null; 190 | 191 | // Limit jump distance 192 | switch (direction) { 193 | case Direction.Left: 194 | case Direction.Right: 195 | var minDist = curScreenRect.width / 2; 196 | break; 197 | case Direction.Up: 198 | case Direction.Down: 199 | var minDist = curScreenRect.height / 2; 200 | break; 201 | default: 202 | print("Wrong direction in util.nextScreenInDirection"); 203 | return; 204 | } 205 | 206 | // assumes a fully horizontal or vertical screen setup 207 | for (var i=0; i 0 ? 1 : -1) * Math.floor(Math.abs(number)); 265 | }; 266 | var maxSafeInteger = Math.pow(2, 53) - 1; 267 | var toLength = function (value) { 268 | var len = toInteger(value); 269 | return Math.min(Math.max(len, 0), maxSafeInteger); 270 | }; 271 | 272 | // The length property of the from method is 1. 273 | return function from(arrayLike/*, mapFn, thisArg */) { 274 | // 1. Let C be the this value. 275 | var C = this; 276 | 277 | // 2. Let items be ToObject(arrayLike). 278 | var items = Object(arrayLike); 279 | 280 | // 3. ReturnIfAbrupt(items). 281 | if (arrayLike == null) { 282 | throw new TypeError('Array.from requires an array-like object - not null or undefined'); 283 | } 284 | 285 | // 4. If mapfn is undefined, then let mapping be false. 286 | var mapFn = arguments.length > 1 ? arguments[1] : void undefined; 287 | var T; 288 | if (typeof mapFn !== 'undefined') { 289 | // 5. else 290 | // 5. a If IsCallable(mapfn) is false, throw a TypeError exception. 291 | if (!isCallable(mapFn)) { 292 | throw new TypeError('Array.from: when provided, the second argument must be a function'); 293 | } 294 | 295 | // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined. 296 | if (arguments.length > 2) { 297 | T = arguments[2]; 298 | } 299 | } 300 | 301 | // 10. Let lenValue be Get(items, "length"). 302 | // 11. Let len be ToLength(lenValue). 303 | var len = toLength(items.length); 304 | 305 | // 13. If IsConstructor(C) is true, then 306 | // 13. a. Let A be the result of calling the [[Construct]] internal method 307 | // of C with an argument list containing the single item len. 308 | // 14. a. Else, Let A be ArrayCreate(len). 309 | var A = isCallable(C) ? Object(new C(len)) : new Array(len); 310 | 311 | // 16. Let k be 0. 312 | var k = 0; 313 | // 17. Repeat, while k < len… (also steps a - h) 314 | var kValue; 315 | while (k < len) { 316 | kValue = items[k]; 317 | if (mapFn) { 318 | A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k); 319 | } else { 320 | A[k] = kValue; 321 | } 322 | k += 1; 323 | } 324 | // 18. Let putStatus be Put(A, "length", len, true). 325 | A.length = len; 326 | // 20. Return A. 327 | return A; 328 | }; 329 | }()); 330 | } 331 | -------------------------------------------------------------------------------- /contents/code/layout.js: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2012 Mathias Gottschlag 6 | Copyright (C) 2013-2014 Fabian Homborg 7 | 8 | This program is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | *********************************************************************/ 21 | 22 | Qt.include("util.js"); 23 | var Direction = { 24 | Up : 0, 25 | Down : 1, 26 | Left : 2, 27 | Right : 3 28 | }; 29 | 30 | var Gravity = { 31 | Bottom : 0, 32 | Right : 1, 33 | Top : 2, 34 | Left : 3 35 | }; 36 | 37 | /** 38 | * Generic rotation matrix is [[cos(x), -sin(x)], [sin(x), cos(x)]]. 39 | * The following are premultiplied rotation matrices we are going to use. 40 | */ 41 | var RotationMatrix = { 42 | rot90 : [[ 0,-1],[ 1, 0]], 43 | rot180 : [[-1, 0],[ 0,-1]], 44 | rot270 : [[ 0, 1],[-1, 0]] 45 | } 46 | 47 | /** 48 | * Base class for all tiling layouts. 49 | * @class 50 | */ 51 | function Layout(screenRectangle) { 52 | this.construct(screenRectangle); 53 | }; 54 | 55 | Layout.prototype.construct = function(screenRectangle) { 56 | try { 57 | /** 58 | * Screen area which is used by the layout. 59 | */ 60 | this.screenRectangle = screenRectangle; 61 | /** 62 | * Geometry of the different tiles. This array stays empty in the case of 63 | * floating layouts. 64 | */ 65 | this.tiles = []; 66 | // TODO 67 | if (KWin.readConfig("autorotatePortrait", false) && 68 | screenRectangle.width < screenRectangle.height) { 69 | this._gravity = Gravity.Right; 70 | } 71 | } catch(err) { 72 | print(err, "in Layout"); 73 | } 74 | }; 75 | Layout.prototype.constructor = Layout; 76 | Layout.prototype.name = "Wurst"; 77 | 78 | Layout.prototype._gravity = Gravity.Bottom; 79 | Layout.prototype._gravityApplied = false; 80 | Layout.prototype._gravityDX = 0; 81 | Layout.prototype._gravityDY = 0; 82 | Layout.prototype.supportsRotation = false; 83 | 84 | Layout.prototype.setLayoutArea = function(newArea) { 85 | try { 86 | var oldArea = this.screenRectangle; 87 | var xscale = newArea.width / oldArea.width; 88 | var yscale = newArea.height / oldArea.height; 89 | var xoffset = newArea.x - oldArea.x; 90 | var yoffset = newArea.y - oldArea.y; 91 | this.tiles.forEach(function(tile) { 92 | var lastrect = tile.rectangle; 93 | var newrect = Qt.rect(Math.floor((lastrect.x + xoffset) * xscale), 94 | Math.floor((lastrect.y + yoffset) * yscale), 95 | Math.floor(lastrect.width * xscale), 96 | Math.floor(lastrect.height * yscale)); 97 | // Stay at the screenedges 98 | // It's better to have roundingerrors in the middle than at the edges 99 | // left screenedge, keep right windowedge (i.e. adjust width) 100 | if (lastrect.x == oldArea.x) { 101 | newrect.width = newrect.width + (newrect.x - newArea.x); 102 | newrect.x = newArea.x; 103 | } 104 | // Top screenedge, keep bottom windowedge (i.e. adjust height) 105 | if (lastrect.y == oldArea.y) { 106 | newrect.height = newrect.height + (newrect.y - newArea.y); 107 | newrect.y = newArea.y; 108 | } 109 | // Right screenedge, keep left windowedge (i.e. don't adjust x) 110 | if (lastrect.x + lastrect.width == oldArea.x + oldArea.width) { 111 | newrect.width = (newArea.width + newArea.x) - newrect.x; 112 | } 113 | // Bottom screenedge, keep top windowedge (i.e. don't adjust y) 114 | if (lastrect.y + lastrect.height == oldArea.y + oldArea.height) { 115 | newrect.height = (newArea.height + newArea.y) - newrect.y; 116 | } 117 | tile.rectangle = newrect; 118 | }); 119 | this.screenRectangle = newArea; 120 | } catch(err) { 121 | print(err, "in Layout.setLayoutArea"); 122 | } 123 | }; 124 | 125 | Layout.prototype.resizeTile = function(tileIndex, rectangle) { 126 | try { 127 | // Sanitize 128 | if (tileIndex < 0 || tileIndex > (this.tiles.length - 1)) { 129 | print("Tileindex invalid", tileIndex, "/", this.tiles.length); 130 | return; 131 | } 132 | if (this.tiles.length == 1) { 133 | return; 134 | } 135 | if (this.tiles[tileIndex] == null) { 136 | print("No tile"); 137 | return; 138 | } 139 | if (rectangle == null){ 140 | print("No rect"); 141 | return; 142 | } 143 | // Cut off parts outside of the screen 144 | var rect = util.intersectRect(rectangle, this.screenRectangle); 145 | if (rect == null) { 146 | print("Rectangle is off screen", util.rectToString(rectangle)); 147 | return; 148 | } 149 | // TODO: Remove overlap 150 | this.doResize(tileIndex, rect, util.setX, util.getX, util.setR, util.getR); 151 | this.doResize(tileIndex, rect, util.setY, util.getY, util.setB, util.getB); 152 | this.doResize(tileIndex, rect, util.setR, util.getR, util.setX, util.getX); 153 | this.doResize(tileIndex, rect, util.setB, util.getB, util.setY, util.getY); 154 | this.tiles[tileIndex].rectangle = util.copyRect(rect); 155 | } catch(err) { 156 | print(err, "in Layout.resizeTile"); 157 | } 158 | }; 159 | 160 | /* 161 | * Resize all rectangles for one edge 162 | * Params: 163 | * set, get: a set/get function for one edge 164 | * setOther, getOther: a set/get function for the opposite edge 165 | */ 166 | Layout.prototype.doResize = function(tileIndex, rectangle, set, get, setOther, getOther) { 167 | var oldD = get(this.tiles[tileIndex].rectangle); 168 | var newD = get(rectangle); 169 | if (oldD == newD) { 170 | return; 171 | } 172 | // Disallow moving away from screenedges 173 | if (oldD == get(this.screenRectangle)) { 174 | set(rectangle, oldD); 175 | return; 176 | } 177 | // Disallow moving to screenedges 178 | // - otherwise we need to check when moving away if this is supposed to be there 179 | if (newD == get(this.screenRectangle)) { 180 | set(rectangle, oldD); 181 | return; 182 | } 183 | /* 184 | * A tile needs to be changed if it 185 | * a) Had the same value as the oldRect 186 | * b) Had the same other edge value as oldRect 187 | */ 188 | for (var i = 0; i < this.tiles.length; i++) { 189 | if (i == tileIndex) { 190 | continue; 191 | } 192 | var dOther = getOther(this.tiles[i].rectangle); 193 | var d = get(this.tiles[i].rectangle); 194 | if (d == oldD) { 195 | set(this.tiles[i].rectangle, newD); 196 | continue; 197 | } 198 | if (dOther == oldD) { 199 | setOther(this.tiles[i].rectangle, newD); 200 | continue; 201 | } 202 | } 203 | }; 204 | 205 | Layout.prototype.resetTileSizes = function() { 206 | try { 207 | // Simply erase all tiles and recreate them to recompute the initial sizes 208 | var tileCount = this.tiles.length; 209 | this.tiles.length = 0; 210 | for (var i = 0; i < tileCount; i++) { 211 | this.addTile(); 212 | } 213 | } catch(err) { 214 | print(err, "in Layout.resetTileSizes"); 215 | } 216 | }; 217 | 218 | Layout.prototype._createTile = function(rect) { 219 | try { 220 | // Create a new tile and add it to the list 221 | var tile = {}; 222 | tile.rectangle = rect; 223 | tile.index = this.tiles.length; 224 | this.tiles.push(tile); 225 | } catch(err) { 226 | print(err, "in Layout._createTile"); 227 | } 228 | }; 229 | 230 | Layout.prototype.setGravity = function(grav) { 231 | if(grav == this._gravity) { 232 | return; 233 | } 234 | this._gravity = grav; 235 | }; 236 | 237 | Layout.prototype.getGravity = function() { 238 | return this._gravity; 239 | } 240 | 241 | Layout.prototype._rotate = function(mRot) { 242 | if(this._gravityApplied == false) { 243 | this.screenRectangle.x -= this._gravityDX; 244 | this.screenRectangle.y -= this._gravityDY; 245 | } 246 | var mScreen = util.rectToMatrix(this.screenRectangle); 247 | var mTransformed = util.multiplyRectMatrices(mScreen, mRot); 248 | var rectScreenTransformed = util.matrixToRect(mTransformed); 249 | if(this._gravityApplied == true) { 250 | rectScreenTransformed.x += this._gravityDX; 251 | rectScreenTransformed.y += this._gravityDY; 252 | } 253 | util.setRect(this.screenRectangle, rectScreenTransformed); 254 | for (var i = 0; i < this.tiles.length; i++) { 255 | var rectTile = this.tiles[i].rectangle; 256 | if(this._gravityApplied == false) { 257 | rectTile.x -= this._gravityDX; 258 | rectTile.y -= this._gravityDY; 259 | } 260 | var mTile = util.rectToMatrix(rectTile); 261 | var mTileTransformed = util.multiplyRectMatrices(mTile, mRot); 262 | var rectTileTransformed = util.matrixToRect(mTileTransformed); 263 | if(this._gravityApplied == true) { 264 | rectTileTransformed.x += this._gravityDX; 265 | rectTileTransformed.y += this._gravityDY; 266 | } 267 | util.setRect(this.tiles[i].rectangle, rectTileTransformed); 268 | } 269 | } 270 | 271 | Layout.prototype._applyGravity = function() { 272 | if (this._gravityApplied == true) { 273 | return; 274 | } 275 | if (this._gravity == Gravity.Bottom) { 276 | this._gravityApplied = true; 277 | return; 278 | } 279 | switch (this._gravity) { 280 | case Gravity.Right: 281 | var mRot = RotationMatrix.rot90; 282 | break; 283 | case Gravity.Top: 284 | var mRot = RotationMatrix.rot180; 285 | break; 286 | case Gravity.Left: 287 | var mRot = RotationMatrix.rot270; 288 | break; 289 | default: 290 | print("Unknown gravity in Layout._applyGravity"); 291 | return; 292 | } 293 | this._gravityDX = Math.floor(this.screenRectangle.x + this.screenRectangle.width / 2); 294 | this._gravityDY = Math.floor(this.screenRectangle.y + this.screenRectangle.height / 2); 295 | this._rotate(mRot); 296 | this._gravityApplied = true; 297 | }; 298 | 299 | Layout.prototype._unapplyGravity = function() { 300 | if (this._gravityApplied == false) { 301 | return; 302 | } 303 | if (this._gravity == Gravity.Bottom) { 304 | this._gravityApplied = false; 305 | return; 306 | } 307 | switch (this._gravity) { 308 | case Gravity.Right: 309 | var mRot = RotationMatrix.rot270; 310 | break; 311 | case Gravity.Top: 312 | var mRot = RotationMatrix.rot180; 313 | break; 314 | case Gravity.Left: 315 | var mRot = RotationMatrix.rot90; 316 | break; 317 | default: 318 | print("Unknown gravity in Layout._unapplyGravity"); 319 | return; 320 | } 321 | this._rotate(mRot); 322 | this._gravityApplied = false; 323 | }; 324 | -------------------------------------------------------------------------------- /contents/code/halflayout.js: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2013 Fabian Homborg 6 | based on spirallayout.js by Matthias Gottschlag 7 | 8 | This program is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | *********************************************************************/ 21 | Qt.include("layout.js"); 22 | /** 23 | * Class which arranges the windows in a spiral with the largest window filling 24 | * the left half of the screen. 25 | */ 26 | function HalfLayout(screenRectangle) { 27 | print("Creating HalfLayout"); 28 | try { 29 | Layout.call(this, screenRectangle); 30 | } catch(err) { 31 | print(err, "in HalfLayout"); 32 | } 33 | this.masterRatio = 100 / KWin.readConfig("halfLayoutMasterRatio", 50); 34 | this.firstWidth = this.screenRectangle.width / this.masterRatio; 35 | this.master = 0; 36 | this.masterCount = 1; 37 | }; 38 | 39 | HalfLayout.prototype = new Layout(); 40 | HalfLayout.prototype.constructor = HalfLayout; 41 | 42 | HalfLayout.prototype.name = "Half"; 43 | HalfLayout.prototype.supportsRotation = true; 44 | // // TODO: Add an image for the layout switcher 45 | // HalfLayout.image = null; 46 | 47 | HalfLayout.prototype.addTile = function() { 48 | try { 49 | this._applyGravity(); 50 | if (this.tiles.length == 0) { 51 | // The first tile fills the whole screen 52 | var rect = util.copyRect(this.screenRectangle); 53 | util.assertRectInScreen(rect, this.screenRectangle); 54 | this._createTile(rect); 55 | this._unapplyGravity(); 56 | return; 57 | } 58 | if (this.tiles.length <= this.masterCount) { 59 | // The second tile fills the right half of the screen 60 | if (this.tiles.length < this.masterCount) { 61 | var newWidth = this.screenRectangle.width / (this.tiles.length + 1); 62 | var newSWidth = newWidth; 63 | } else { 64 | this.firstWidth = this.screenRectangle.width / this.masterRatio; 65 | var newWidth = this.firstWidth / (this.tiles.length); 66 | var newSWidth = this.screenRectangle.width - this.firstWidth; 67 | } 68 | for (var i = 0; i < this.tiles.length; i++) { 69 | this.tiles[i].rectangle.x = this.screenRectangle.x + i * newWidth; 70 | this.tiles[i].rectangle.width = newWidth; 71 | } 72 | 73 | var lastRect = this.tiles[this.tiles.length - 1].rectangle; 74 | var newRect = Qt.rect(lastRect.x + lastRect.width, 75 | this.screenRectangle.y, 76 | newSWidth, 77 | this.screenRectangle.height); 78 | util.assertRectInScreen(newRect, this.screenRectangle); 79 | this._createTile(newRect); 80 | this._unapplyGravity(); 81 | return; 82 | } 83 | if (this.tiles.length > this.masterCount) { 84 | // Every other tile separates the right half 85 | var slaveCount = this.tiles.length - this.masterCount; 86 | var lastRect = this.tiles[this.master + this.masterCount].rectangle; 87 | var newRect = Qt.rect(lastRect.x, 88 | lastRect.y, 89 | this.screenRectangle.width - this.getMasterWidth(), 90 | this.screenRectangle.height / (slaveCount + 1)); 91 | newRect.y = newRect.y + newRect.height * slaveCount; 92 | // FIXME: Try to keep ratio 93 | for (var i = this.master + this.masterCount; i < this.tiles.length; i++) { 94 | var rect = this.tiles[i].rectangle; 95 | rect.x = newRect.x; 96 | var offset = newRect.height * (i - (this.master + this.masterCount)); 97 | rect.y = lastRect.y + offset; 98 | rect.width = newRect.width; 99 | rect.height = newRect.height; 100 | this.tiles[i].rectangle = rect; 101 | } 102 | // Adjust new tile's height for rounding errors 103 | newRect.height = (this.screenRectangle.y + this.screenRectangle.height) - newRect.y; 104 | util.assertRectInScreen(newRect, this.screenRectangle); 105 | this._createTile(newRect); 106 | this._unapplyGravity(); 107 | } 108 | } catch(err) { 109 | print(err, "in HalfLayout.addTile"); 110 | } 111 | }; 112 | 113 | // Save the first tile's width 114 | HalfLayout.prototype.resizeTile = function(tileIndex, rectangle) { 115 | Layout.prototype.resizeTile.call(this, tileIndex, rectangle); 116 | // Fixes bug where firstWidth will be set to the full screen when resizing with just masters 117 | if (this.tiles.length > this.masterCount) { 118 | this.firstWidth = this.getMasterWidth(); 119 | } 120 | }; 121 | 122 | HalfLayout.prototype.getMasterWidth = function() { 123 | var tile = this.tiles[Math.min(this.tiles.length, this.masterCount) - 1]; 124 | if (tile != null) { 125 | var lastMaster = tile.rectangle; 126 | return lastMaster.x + lastMaster.width - this.screenRectangle.x; 127 | } else { 128 | // No masters exist 129 | return 0; 130 | } 131 | }; 132 | 133 | HalfLayout.prototype.removeTile = function(tileIndex) { 134 | try { 135 | // Remove the array entry 136 | var oldrect = this.tiles[tileIndex].rectangle; 137 | if (tileIndex < 0 || tileIndex >= this.tiles.length) { 138 | print("Removing invalid tileindex"); 139 | return; 140 | } 141 | this._applyGravity(); 142 | this.tiles.splice(tileIndex, 1); 143 | // Update the other tiles 144 | if (this.tiles.length == 1) { 145 | this.tiles[0].rectangle = util.copyRect(this.screenRectangle); 146 | } 147 | if (this.tiles.length > 1) { 148 | var mC = Math.min(this.tiles.length, this.masterCount); 149 | if (this.tiles.length > mC) { 150 | // The distance between the right edge of the last master and the left edge of the screen is the width of the master area 151 | if(mC > 0){ 152 | if(tileIndex === mC - 1){ 153 | var mWidth = (oldrect.x + oldrect.width - this.screenRectangle.x) / mC; 154 | } 155 | else if(tileIndex < mC - 1){ 156 | var mWidth = (this.tiles[mC - 2].rectangle.x + this.tiles[mC - 2].rectangle.width - this.screenRectangle.x) / mC; 157 | }else { 158 | var mWidth = (this.tiles[mC - 1].rectangle.x + this.tiles[mC - 1].rectangle.width - this.screenRectangle.x) / mC; 159 | } 160 | } else { 161 | var mWidth = 0; 162 | } 163 | } else { 164 | var mWidth = this.screenRectangle.width / this.tiles.length; 165 | } 166 | for (var i = 0; i < mC; i++) { 167 | this.tiles[i].rectangle.x = i * mWidth + this.screenRectangle.x; 168 | this.tiles[i].rectangle.width = mWidth; 169 | this.tiles[i].rectangle.height = this.screenRectangle.height; 170 | this.tiles[i].rectangle.y = this.screenRectangle.y; 171 | } 172 | // Fallthrough for slaves 173 | } 174 | if (this.tiles.length > this.masterCount) { 175 | var tileCount = this.tiles.length - this.masterCount; 176 | util.assertTrue(tileCount > 0, "Tilecount is zero"); 177 | var lastRect = this.tiles[0].rectangle; 178 | var newRect = Qt.rect(this.screenRectangle.x + this.getMasterWidth(), 179 | this.screenRectangle.y, 180 | this.screenRectangle.width - this.getMasterWidth(), 181 | this.screenRectangle.height / tileCount); 182 | util.assertTrue(newRect.height > 0, "newRect.height is zero"); 183 | var lowest = this.tiles.length - 1; 184 | for (var i = this.masterCount + this.master; i < this.tiles.length; i++) { 185 | var rect = this.tiles[i].rectangle; 186 | rect.y = newRect.y + newRect.height * (i - this.masterCount); 187 | rect.height = newRect.height; 188 | this.tiles[i].rectangle = rect; 189 | } 190 | // Adjust lowest tile's height for rounding errors 191 | this.tiles[lowest].rectangle.height = (this.screenRectangle.y + this.screenRectangle.height) - this.tiles[lowest].rectangle.y; 192 | util.assertTrue(this.tiles[lowest].rectangle.height > 0, "Lowest rect has zero height"); 193 | } 194 | this._unapplyGravity(); 195 | } catch(err) { 196 | print(err, "in HalfLayout.removeTile"); 197 | } 198 | }; 199 | 200 | HalfLayout.prototype.increaseMaster = function() { 201 | var oldC = this.masterCount; 202 | this.masterCount++; 203 | if (this.tiles.length == 0) { 204 | return; 205 | } 206 | this._applyGravity(); 207 | if (this.masterCount > 1) { 208 | if (this.tiles.length >= this.master + oldC && oldC > 0) { 209 | var rightEdgeRect = this.tiles[this.master + oldC - 1].rectangle; 210 | var rightEdge = util.getR(rightEdgeRect); 211 | } 212 | if (this.masterCount < this.tiles.length) { 213 | var newWidth = (rightEdge - this.screenRectangle.x) / (oldC + 1); 214 | } else if (this.masterCount == this.tiles.length) { 215 | var newWidth = (this.screenRectangle.width) / (this.masterCount); 216 | } else { 217 | this._unapplyGravity(); 218 | return; 219 | } 220 | for (var i = this.master; i < Math.min(this.master + oldC,this.tiles.length); i++) { 221 | this.tiles[i].rectangle.x = this.screenRectangle.x + newWidth * (i - this.master); 222 | this.tiles[i].rectangle.y = this.screenRectangle.y; 223 | this.tiles[i].rectangle.width = newWidth; 224 | this.tiles[i].rectangle.height = this.screenRectangle.height; 225 | } 226 | this.tiles[this.master + this.masterCount - 1].rectangle = Qt.rect(rightEdgeRect.x + rightEdgeRect.width, 227 | rightEdgeRect.y, 228 | newWidth, 229 | rightEdgeRect.height); 230 | } else { 231 | this.tiles[this.master + this.masterCount - 1].rectangle = Qt.rect(this.screenRectangle.x, 232 | this.screenRectangle.y, 233 | this.firstWidth, 234 | this.screenRectangle.height); 235 | } 236 | var newHeight = (this.screenRectangle.y + this.screenRectangle.height) / (this.tiles.length - (this.master + this.masterCount)); 237 | for (var i = this.master + this.masterCount; i < this.tiles.length; i++) { 238 | this.tiles[i].rectangle.x = this.screenRectangle.x + this.getMasterWidth(); 239 | this.tiles[i].rectangle.width = this.screenRectangle.width - this.getMasterWidth(); 240 | this.tiles[i].rectangle.height = newHeight; 241 | this.tiles[i].rectangle.y = this.screenRectangle.y + (i - (this.master + this.masterCount)) * newHeight; 242 | } 243 | this.firstWidth = this.getMasterWidth(); 244 | this._unapplyGravity(); 245 | }; 246 | 247 | HalfLayout.prototype.decrementMaster = function() { 248 | var oldC = this.masterCount; 249 | var newC = this.masterCount - 1; 250 | if (this.masterCount == 0) { 251 | return; 252 | } 253 | this._applyGravity(); 254 | // Explicitly allow usage without master - it's effectively a different layout 255 | if (this.masterCount == 1) { 256 | var oldMWidth = 0; 257 | } else { 258 | var oldMWidth = this.getMasterWidth(); 259 | } 260 | if (this.tiles.length > oldC) { 261 | var newMWidth = oldMWidth / newC; 262 | if(newC == 0) { 263 | newMWidth = 0; 264 | } 265 | var newSWidth = this.screenRectangle.width - (newMWidth * newC); 266 | var newSHeight = this.screenRectangle.height / (this.tiles.length - newC); 267 | } else if (this.tiles.length == oldC) { 268 | var newMWidth = (this.screenRectangle.width / this.masterRatio) / newC ; 269 | var newSWidth = this.screenRectangle.width - (newMWidth * newC); 270 | var newSHeight = this.screenRectangle.height / (this.tiles.length - newC); 271 | } else { 272 | var newMWidth = this.screenRectangle.width / this.tiles.length; 273 | } 274 | for (var i = 0; i < Math.min(this.tiles.length,newC); i++) { 275 | this.tiles[i].rectangle.x = this.screenRectangle.x + i * newMWidth; 276 | this.tiles[i].rectangle.width = newMWidth; 277 | this.tiles[i].rectangle.y = this.screenRectangle.y; 278 | this.tiles[i].rectangle.height = this.screenRectangle.height; 279 | util.assertRectInScreen(this.tiles[i].rectangle, this.screenRectangle); 280 | } 281 | for (var i = newC; i < this.tiles.length; i++) { 282 | this.tiles[i].rectangle.y = this.screenRectangle.y + (i - newC) * newSHeight; 283 | this.tiles[i].rectangle.height = newSHeight; 284 | this.tiles[i].rectangle.width = newSWidth; 285 | this.tiles[i].rectangle.x = this.screenRectangle.x + (newMWidth * newC); 286 | util.assertRectInScreen(this.tiles[i].rectangle, this.screenRectangle); 287 | } 288 | this.masterCount--; 289 | if(newC != 0) { 290 | this.firstWidth = this.getMasterWidth(); 291 | } else { 292 | this.firstWidth = this.screenRectangle.width / this.masterRatio; 293 | } 294 | this._unapplyGravity(); 295 | }; 296 | -------------------------------------------------------------------------------- /contents/ui/config.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | KWin::TilingScriptConfigForm 4 | 5 | 6 | 7 | 0 8 | 0 9 | 412 10 | 412 11 | 12 | 13 | 14 | 15 | 16 | 17 | 0 18 | 19 | 20 | 21 | 22 | 23 | 24 | Common options 25 | 26 | 27 | 28 | 29 | 30 | The windows that should be excluded from tiling, by window class. Comma-separated. 31 | 32 | 33 | Excluded window classes 34 | 35 | 36 | Excluded window classes 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | Let KWin decide 45 | 46 | 47 | 48 | 49 | Open new tiles as master 50 | 51 | 52 | 53 | 54 | Open new tiles at the end 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | Show layout OSD 63 | 64 | 65 | true 66 | 67 | 68 | 69 | 70 | 71 | 72 | Excluded window classes 73 | 74 | 75 | 76 | 77 | 78 | 79 | Respect minimum/maximum Size 80 | 81 | 82 | true 83 | 84 | 85 | 86 | 87 | 88 | 89 | Tiling enabled by default 90 | 91 | 92 | true 93 | 94 | 95 | 96 | 97 | 98 | 99 | Placement method 100 | 101 | 102 | 103 | 104 | 105 | 106 | Remove window borders on tiled windows 107 | 108 | 109 | 110 | 111 | 112 | 113 | Tile vertically on portrait oriented displays 114 | 115 | 116 | false 117 | 118 | 119 | 120 | 121 | 122 | 123 | Remove empty desktops 124 | 125 | 126 | false 127 | 128 | 129 | 130 | 131 | 132 | 133 | Layouts for each Desktop <br>desktopNr:layoutname[,...] 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | Qt::Horizontal 146 | 147 | 148 | QSizePolicy::Fixed 149 | 150 | 151 | 152 | 40 153 | 20 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | false 162 | 163 | 164 | Number of desktops to keep: 165 | 166 | 167 | 168 | 169 | 170 | 171 | false 172 | 173 | 174 | 175 | 176 | 177 | 1 178 | 179 | 180 | 12 181 | 182 | 183 | 184 | 185 | 186 | 187 | Qt::Horizontal 188 | 189 | 190 | 191 | 26 192 | 20 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | Gaps 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | <html><head/><body><p><span style=" font-weight:600;">Gaps between screenedges and windows</span></p></body></html> 216 | 217 | 218 | 219 | 220 | 221 | 222 | Right 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | Top 239 | 240 | 241 | 242 | 243 | 244 | 245 | Bottom 246 | 247 | 248 | 249 | 250 | 251 | 252 | <html><head/><body><p><span style=" font-weight:600;">Gaps between windows</span></p></body></html> 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | Width 263 | 264 | 265 | 266 | 267 | 268 | 269 | Height 270 | 271 | 272 | 273 | 274 | 275 | 276 | Left 277 | 278 | 279 | 280 | 281 | 282 | 283 | Also apply for windows that take up the screen 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | Half Layout 292 | 293 | 294 | 295 | 296 | 297 | Qt::Horizontal 298 | 299 | 300 | 301 | 40 302 | 20 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | Qt::Vertical 311 | 312 | 313 | 314 | 20 315 | 40 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | % 324 | 325 | 326 | 10 327 | 328 | 329 | 90 330 | 331 | 332 | 50 333 | 334 | 335 | 336 | 337 | 338 | 339 | Default master width 340 | 341 | 342 | 343 | 344 | 345 | 346 | 347 | Grid Layout 348 | 349 | 350 | 351 | 352 | 353 | Qt::Horizontal 354 | 355 | 356 | 357 | 40 358 | 20 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | Qt::Vertical 367 | 368 | 369 | 370 | 20 371 | 40 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | % 380 | 381 | 382 | 10 383 | 384 | 385 | 90 386 | 387 | 388 | 50 389 | 390 | 391 | 392 | 393 | 394 | 395 | Default master width 396 | 397 | 398 | 399 | 400 | 401 | 402 | Allow Windows to span two columns 403 | 404 | 405 | 406 | 407 | 408 | 409 | Allow Windows to span two rows 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 419 | Default master count 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | 428 | 429 | 430 | tabWidget 431 | kcfg_floaters 432 | kcfg_noBorder 433 | kcfg_userActive 434 | kcfg_respectMinMax 435 | kcfg_showLayoutOsd 436 | kcfg_removeEmptyDesktops 437 | kcfg_minDesktopsToKeep 438 | kcfg_autorotatePortrait 439 | kcfg_placement 440 | kcfg_layouts 441 | kcfg_fullscreenGaps 442 | kcfg_screenGapSizeLeft 443 | kcfg_screenGapSizeRight 444 | kcfg_screenGapSizeTop 445 | kcfg_screenGapSizeBottom 446 | kcfg_windowsGapSizeWidth 447 | kcfg_windowsGapSizeHeight 448 | kcfg_halfLayoutMasterRatio 449 | kcfg_gridLayoutMasterRatio 450 | kcfg_gridLayoutColSpan 451 | kcfg_gridLayoutRowSpan 452 | kcfg_gridLayoutMasterCount 453 | 454 | 455 | 456 | 457 | kcfg_removeEmptyDesktops 458 | toggled(bool) 459 | kcfg_minDesktopsToKeep 460 | setEnabled(bool) 461 | 462 | 463 | 206 464 | 225 465 | 466 | 467 | 267 468 | 254 469 | 470 | 471 | 472 | 473 | kcfg_removeEmptyDesktops 474 | toggled(bool) 475 | label_12 476 | setEnabled(bool) 477 | 478 | 479 | 113 480 | 227 481 | 482 | 483 | 117 484 | 260 485 | 486 | 487 | 488 | 489 | 490 | -------------------------------------------------------------------------------- /contents/code/tilelist.js: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2012 Mathias Gottschlag 6 | Copyright (C) 2013-2014 Fabian Homborg 7 | 8 | This program is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | *********************************************************************/ 21 | Qt.include("ignored.js"); 22 | /** 23 | * Class which keeps track of all tiles in the system. The class automatically 24 | * puts tab groups in one single tile. Tracking of new and removed clients is 25 | * done here as well. 26 | * @class 27 | */ 28 | function TileList(timer) { 29 | /** 30 | * List of currently existing tiles. 31 | */ 32 | this.tiles = []; 33 | /** 34 | * Signal which is triggered whenever a new tile is added to the list. 35 | */ 36 | this.tileAdded = new Signal(); 37 | /** 38 | * Signal which is triggered whenever a tile is removed from the list. 39 | */ 40 | this.tileRemoved = new Signal(); 41 | /** 42 | * Signal which is triggered whenever a new client is activated. 43 | */ 44 | this.activeClientChanged = new Signal(); 45 | 46 | /** 47 | * Stores the current and last focused windows. 48 | * NOTE: We need to keep track of the last focused window because when the addTile 49 | * function is called, the focused tile has already changed to the new client. 50 | */ 51 | this.focusHistory = {}; 52 | 53 | try { 54 | this.noBorder = KWin.readConfig("noBorder", false); 55 | } catch(err) { 56 | print(err, "in TileList"); 57 | } 58 | 59 | // we use this timer and the array to update client geometry asynchronously 60 | this.timer = timer; 61 | this._scheduledUpdates = new Array(); 62 | 63 | // We connect to the global workspace callbacks which are triggered when 64 | // clients are added in order to be able to keep track of the 65 | // new tiles 66 | var self = this; 67 | workspace.clientAdded.connect(function(client) { 68 | self.addClient(client); 69 | // NOTE: When a new client is added, activeChanged will be called before it even appears 70 | // in workspace.clientList(), so we need to keep track of the focus change here as well. 71 | self.trackFocusChanges(client); 72 | }); 73 | }; 74 | 75 | /* 76 | * Connect all signals for a client we need 77 | */ 78 | TileList.prototype.connectSignals = function(client) { 79 | var self = this; 80 | // We have to connect client signals here instead of in Tile 81 | // because the tile of a client might change over time 82 | var getTile = function(client) { 83 | return self.getTile(client); 84 | }; 85 | 86 | if (client.tiling_connected1 != true) { 87 | // First handle fullscreen as clients become interesting when they unfullscreen 88 | // We don't need to remove a client on fullscreen as it may be temporary (vlc) 89 | // and no other client is visible while one is fullscreen (meaning their geom doesn't matter) 90 | // and we can properly restore the geometry this way 91 | client.fullScreenChanged.connect(function() { 92 | if (client.fullScreen == true) { 93 | client.tiling_floating = true; 94 | client.keepBelow = false; 95 | } else { 96 | // If we already have a tile, just reset the geometry 97 | var tile = getTile(client); 98 | if (tile != null) { 99 | client.tiling_floating = false; 100 | tile.onClientGeometryChanged(client); 101 | } else { 102 | self.addClient(client); 103 | } 104 | } 105 | }); 106 | client.shadeChanged.connect(function() { 107 | if (client.shade == true) { 108 | // Untile shaded clients since we get the unshaded geometry, 109 | // so we can't otherwise use them. 110 | self.removeClient(client); 111 | } else { 112 | // If we already have a tile, just reset the geometry 113 | var tile = getTile(client); 114 | if (tile != null) { 115 | client.tiling_floating = false; 116 | tile.onClientGeometryChanged(client); 117 | } else { 118 | self.addClient(client); 119 | } 120 | } 121 | }); 122 | client.tiling_connected1 = true; 123 | } 124 | if (client.tiling_connected2 == true) { 125 | return; 126 | } 127 | // geometryChanged fires also on maximizedStateChanged and stepUserMovedResized 128 | // (from a cursory reading of the KWin source code) 129 | // so use geometryShapeChanged 130 | client.geometryShapeChanged.connect(function() { 131 | // Only fire this if _we_ aren't the ones resizing. 132 | // Otherwise we end up in a loop. 133 | // If we do resize the rectangle again, we fire tile.resizingEnded, which should be enough. 134 | if (!client.tiling_resize) { 135 | var tile = getTile(client); 136 | if (tile != null) { 137 | self.timer.stop(); 138 | self._scheduledUpdates.push({tile: tile, client: client}); 139 | self.timer.start(); 140 | } 141 | } 142 | }); 143 | // Do not use clientRemoved as it is called after FFM selects a new active client 144 | // Instead, connect to client.windowClosed 145 | client.windowClosed.connect(function(cl, deleted) { 146 | self.removeClient(client); 147 | }); 148 | client.clientStartUserMovedResized.connect(function() { 149 | var tile = getTile(client); 150 | if (tile != null) { 151 | tile.onClientStartUserMovedResized(client); 152 | } 153 | }); 154 | client.clientStepUserMovedResized.connect(function() { 155 | var tile = getTile(client); 156 | if (tile != null) { 157 | tile.onClientStepUserMovedResized(client); 158 | } 159 | }); 160 | client.clientFinishUserMovedResized.connect(function() { 161 | var tile = getTile(client); 162 | if (tile != null) { 163 | tile.onClientFinishUserMovedResized(client); 164 | } 165 | }); 166 | client.activitiesChanged.connect(function(client) { 167 | var tile = getTile(client); 168 | if (tile != null) { 169 | tile.onClientActivitiesChanged(client); 170 | } 171 | }); 172 | client.desktopChanged.connect(function() { 173 | var tile = getTile(client); 174 | if (tile != null) { 175 | tile.onClientDesktopChanged(client); 176 | } 177 | }); 178 | client.screenChanged.connect(function() { 179 | var tile = getTile(client); 180 | if (tile != null) { 181 | tile.onClientScreenChanged(client); 182 | } 183 | if (client.active == true) { 184 | self.activeClientChanged.emit(client); 185 | } 186 | }); 187 | client.clientMinimized.connect(function(client) { 188 | try { 189 | self.untileClient(client); 190 | } catch(err) { 191 | print(err, "in mimimized"); 192 | } 193 | }); 194 | client.clientUnminimized.connect(function(client) { 195 | try { 196 | self.addClient(client); 197 | } catch(err) { 198 | print(err, "in Unminimized"); 199 | } 200 | }); 201 | client.activeChanged.connect(function() { 202 | try { 203 | self.trackFocusChanges(); 204 | if (client.active == true) { 205 | self.activeClientChanged.emit(client); 206 | } 207 | } catch(err) { 208 | print(err, "in activeChanged - focus tracking"); 209 | } 210 | }); 211 | if (client.setMaximize != null) { 212 | client.activeChanged.connect(function() { 213 | try { 214 | // Make sure the newly active client is not covered by 215 | // a maximized one. 216 | if (client.active == true) { 217 | var screen = client.screen; 218 | var desktop = client.desktop; 219 | self.tiles.forEach(function(t) { 220 | var tileDesktop = t.getDesktop(); 221 | if (t.maximized == true && t.getScreen() == screen && 222 | (tileDesktop == desktop || tileDesktop == -1)) 223 | { 224 | for (var i = 0; i < t.clients.length; i++) { 225 | if (t.clients[i] === client) { 226 | continue; 227 | } 228 | t.clients[i].setMaximize(false, false); 229 | } 230 | client.setMaximize(true, true); 231 | } 232 | }); 233 | } 234 | } catch(err) { 235 | print(err, "in activeChanged - setMaximize"); 236 | } 237 | }); 238 | } 239 | client.clientMaximizedStateChanged.connect(function(client, h, v) { 240 | var tile = self.getTile(client); 241 | if (tile != null) { 242 | tile.onClientMaximizedStateChanged(client, h, v); 243 | } 244 | }); 245 | client.tiling_connected2 = true; 246 | }; 247 | /** 248 | * Adds another client to the tile list. When this is called, the tile list also 249 | * adds callback functions to the relevant client signals to trigger tile change 250 | * events when necessary. This function might trigger a tileAdded event. 251 | * 252 | * @param client Client which is added to the tile list. 253 | */ 254 | TileList.prototype.addClient = function(client) { 255 | if (client == null) { 256 | return; 257 | } 258 | if (ignored.isIgnored(client)) { 259 | client.tiling_tileIndex = -1; 260 | client.keepBelow = false; 261 | // WARNING: This crashes kwin! 262 | //client.tiling_floating = true; 263 | return; 264 | } 265 | 266 | // Check whether the client is part of an existing tile 267 | if (this._indexWithClient(client) != -1) { 268 | return; 269 | } 270 | 271 | this.connectSignals(client); 272 | 273 | // Ignore fullscreen, shaded or minimized clients, 274 | // but after connecting signals, so 275 | // they'll be added once that changes. 276 | if (client.fullScreen 277 | || client.shade 278 | || client.minimized) { 279 | client.keepBelow = false; 280 | return; 281 | } 282 | 283 | if (client.setMaximize != null) { 284 | // Unmaximize the new client. 285 | client.setMaximize(false, false); 286 | 287 | // Unmaximize the active client if there is already maximized one 288 | // on the same desktop. 289 | var screen = client.screen; 290 | var desktop = client.desktop; 291 | this.tiles.forEach(function(t) { 292 | var tileDesktop = t.getDesktop(); 293 | if (t.maximized == true && t.getScreen() == screen && 294 | (tileDesktop == desktop || tileDesktop == -1)) { 295 | for (var i = 0; i < t.clients.length; i++) { 296 | t.clients[i].setMaximize(false, false); 297 | } 298 | } 299 | }); 300 | } 301 | 302 | this._addTile(client); 303 | client.tiling_floating = false; 304 | }; 305 | 306 | /** 307 | * Returns the tile in which a certain client is located. 308 | * 309 | * @param client Client for which the tile shall be returned. 310 | * @return Tile in which the client is located. 311 | */ 312 | TileList.prototype.getTile = function(client) { 313 | var index = this._indexWithClient(client); 314 | if (index > -1) { 315 | return this.tiles[index]; 316 | } 317 | return null; 318 | }; 319 | 320 | /* 321 | * Untile a client 322 | * This means undoing things we do to tiled clients 323 | * and removing them from the list 324 | */ 325 | TileList.prototype.untileClient = function(client) { 326 | try { 327 | // Unset keepBelow because we set it when tiling 328 | client.keepBelow = false; 329 | 330 | // Don't remove tileIndex, so we can move the window to its position in case it comes back (after minimize etc) 331 | //client.tiling_tileIndex = - 1; 332 | if (client.tiling_floating == true) { 333 | client.noBorder = false; 334 | } 335 | 336 | this.removeClient(client); 337 | } catch(err) { 338 | print(err, "in untileClient with", client.resourceClass.toString()); 339 | } 340 | }; 341 | 342 | /* 343 | * Remove client from the tileList 344 | */ 345 | TileList.prototype.removeClient = function(client) { 346 | // Remove the client from its tile 347 | var tileIndex = this._indexWithClient(client); 348 | var tile = this.tiles[tileIndex]; 349 | if (tile != null) { 350 | if (tile.clients.length == 1) { 351 | // Remove the tile if this was the last client in it 352 | this._removeTile(tileIndex); 353 | } else { 354 | // Remove the client from its tile 355 | tile.removeClient(client); 356 | } 357 | } 358 | }; 359 | 360 | TileList.prototype._addTile = function(client) { 361 | var tileIndex = -1; 362 | if (client.tiling_tileIndex > -1) { 363 | tileIndex = client.tiling_tileIndex; 364 | } 365 | var newTile = new Tile(client, tileIndex); 366 | this.tiles.push(newTile); 367 | this.tileAdded.emit(newTile); 368 | }; 369 | 370 | TileList.prototype._removeTile = function(tileIndex) { 371 | try { 372 | // "tileIndex" here is the index in the tileList, while outside this class 373 | // it's "index in that desktop" 374 | // That means you should _not_ try changing tile.tileIndex here 375 | // TODO: Change to a better name 376 | var tile = this.tiles[tileIndex]; 377 | if (tileIndex > -1) { 378 | tile.active = false; 379 | this.tiles.splice(tileIndex, 1); 380 | } 381 | this.tileRemoved.emit(tile); 382 | } catch(err) { 383 | print(err, "in TileList._removeTile"); 384 | } 385 | }; 386 | 387 | TileList.prototype._indexWithClient = function(client) { 388 | for (var i = 0; i < this.tiles.length; i++) { 389 | if (this.tiles[i].hasClient(client)) { 390 | return i; 391 | } 392 | } 393 | return -1; 394 | }; 395 | 396 | /* 397 | * Set the border for all non-floating managed clients 398 | * This is "noBorder" (i.e. inverted boolean) since that's what kwin uses 399 | */ 400 | TileList.prototype.setNoBorder = function(nB) { 401 | this.noBorder = nB; 402 | this.tiles.forEach(function (t) { 403 | for (var i = 0; i < t.clients.length; i++) { 404 | if (t.clients[i].tiling_floating != true) { 405 | t.clients[i].noBorder = nB; 406 | } 407 | } 408 | }); 409 | }; 410 | 411 | TileList.prototype.toggleNoBorder = function() { 412 | try { 413 | this.setNoBorder(!this.noBorder); 414 | } catch (err) { 415 | print(err, "in TileList.toggleNoBorder"); 416 | } 417 | }; 418 | /** 419 | * Looks for the new focused window when it has changed and updates the 420 | * focusHistory internal variable consistently 421 | * 422 | * @param client Optional. If the newly focused client is passed, it will 423 | * be set directly. Otherwise the function will look for it 424 | */ 425 | TileList.prototype.trackFocusChanges = function(focusedClient) { 426 | try { 427 | if (!focusedClient) { 428 | var clients = workspace.clientList(); 429 | for (var i = 0; i < clients.length; ++i) { 430 | if (clients[i].active) { 431 | focusedClient = clients[i]; 432 | break; 433 | } 434 | } 435 | if (!focusedClient && clients.length > 0) { 436 | focusedClient = clients[0]; 437 | } 438 | } 439 | if (focusedClient && ((focusedClient != this.focusHistory.current) || !this.focusHistory.previous)) { 440 | this.focusHistory.previous = this.focusHistory.current; 441 | this.focusHistory.current = focusedClient; 442 | //print('Focused:' + focusedClient.caption); 443 | } 444 | } catch (err) { 445 | print(err, "in TileList.trackFocusChanges"); 446 | } 447 | }; 448 | 449 | TileList.prototype.updateGeometry = function() { 450 | while (this._scheduledUpdates.length > 0) { 451 | this._scheduledUpdates[0].tile.onClientGeometryChanged( 452 | this._scheduledUpdates[0].client); 453 | this._scheduledUpdates.shift(); 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /contents/code/tile.js: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2012 Mathias Gottschlag 6 | Copyright (C) 2013-2017 Fabian Homborg 7 | 8 | This program is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | *********************************************************************/ 21 | 22 | /** 23 | * Class which manages the windows in one tile and handles resize/move and 24 | * property change events. 25 | * @class 26 | */ 27 | function Tile(firstClient, tileIndex) { 28 | try { 29 | /** 30 | * Signal which is triggered whenever the user starts to move the tile. 31 | */ 32 | this.movingStarted = new Signal(); 33 | /** 34 | * Signal which is triggered whenever the user stops moving the tile. 35 | */ 36 | this.movingEnded = new Signal(); 37 | /** 38 | * Signal which is triggered whenever the geometry changes between 39 | * movingStarted and movingEnded. 40 | */ 41 | this.movingStep = new Signal(); 42 | /** 43 | * Signal which is triggered whenever the user starts to resize the tile. 44 | */ 45 | this.resizingStarted = new Signal(); 46 | /** 47 | * Signal which is triggered whenever the user stops resizing the tile. 48 | */ 49 | this.resizingEnded = new Signal(); 50 | /** 51 | * Signal which is triggered whenever the geometry changes between 52 | * resizingStarted and resizingEnded. 53 | */ 54 | this.resizingStep = new Signal(); 55 | /** 56 | * Signal which is triggered whenever the tile is moved to a different 57 | * screen. Two parameters are passed to the handlers, the old and the new 58 | * screen. 59 | */ 60 | this.screenChanged = new Signal(); 61 | /** 62 | * Signal which is triggered whenever the tile is moved to a different 63 | * desktop. Two parameters are passed to the handlers, the old and the new 64 | * desktop. 65 | */ 66 | this.desktopChanged = new Signal(); 67 | /** 68 | * Signal which is triggered whenever the tile's presence across 69 | * activities changes. Two parameters are passed to the handlers, the 70 | * tile's current desktop and the tile's current screen. 71 | */ 72 | this.activitiesChanged = new Signal(); 73 | /** 74 | * List of the clients in this tile. 75 | */ 76 | this.clients = []; 77 | this.originalx = util.middlex(firstClient.geometry); 78 | this.originaly = util.middley(firstClient.geometry); 79 | /** 80 | * Index of this tile in the TileList to which the tile belongs. 81 | */ 82 | this.tileIndex = tileIndex; 83 | /** 84 | * True if this tile is currently moved by the user. 85 | */ 86 | this._moving = false; 87 | /** 88 | * True if this tile is currently moved by the user. 89 | */ 90 | this._resizing = false; 91 | /** 92 | * Stores the current screen of the tile in order to be able to detect 93 | * movement between screens. 94 | */ 95 | this._currentScreen = firstClient.screen; 96 | 97 | /** 98 | * Stores the current desktop as this is needed as a desktopChanged 99 | * parameter. 100 | */ 101 | this._currentDesktop = firstClient.desktop; 102 | 103 | this._activities = Array.from(firstClient.activities); 104 | 105 | this.rectangle = null; 106 | 107 | this.respectMinMax = KWin.readConfig("respectMinMax", true); 108 | 109 | var gapSize = KWin.readConfig("gapSize", 0); /* stick to old gaps config by default */ 110 | this.windowsGapSizeHeight = KWin.readConfig("windowsGapSizeHeight", gapSize); 111 | this.windowsGapSizeWidth = KWin.readConfig("windowsGapSizeWidth", gapSize); 112 | this.screenGapSizeLeft = KWin.readConfig("screenGapSizeLeft", 0); 113 | this.screenGapSizeRight = KWin.readConfig("screenGapSizeRight", 0); 114 | this.screenGapSizeTop = KWin.readConfig("screenGapSizeTop", 0); 115 | this.screenGapSizeBottom = KWin.readConfig("screenGapSizeBottom", 0); 116 | if (KWin.readConfig("noBorder", false)) { 117 | firstClient.noBorder = true; 118 | } 119 | this.maximized = false; 120 | this._canSetMaximize = (firstClient.setMaximize != null); 121 | this.clients.push(firstClient); 122 | this.syncCustomProperties(); 123 | 124 | // Whether this tile should *do* anything 125 | this.active = true; 126 | } catch(err) { 127 | print(err, "in Tile"); 128 | } 129 | }; 130 | 131 | /** 132 | * Sets the geometry of the tile. geometryChanged events caused by this function 133 | * are suppressed. 134 | * 135 | * @param geometry New tile geometry. 136 | */ 137 | Tile.prototype.setGeometry = function(geometry) { 138 | try { 139 | if (!geometry) { 140 | return; 141 | } 142 | if (!this.rectangle) { 143 | this.rectangle = util.copyRect(geometry); 144 | } else { 145 | util.setRect(this.rectangle, geometry); 146 | } 147 | for (var i = 0; i < this.clients.length; i++) { 148 | this.setClientGeometry(this.clients[i]); 149 | } 150 | } catch(err) { 151 | print(err, "in Tile.setGeometry"); 152 | } 153 | }; 154 | 155 | Tile.prototype.resetGeometry = function() { 156 | this.setGeometry(this.rectangle); 157 | }; 158 | 159 | /** 160 | * Returns the currently active client in the tile. 161 | */ 162 | Tile.prototype.getActiveClient = function() { 163 | try { 164 | var active = null; 165 | this.clients.forEach(function(client) { 166 | active = client; 167 | }); 168 | return active; 169 | } catch(err) { 170 | print(err, "in Tile.getActiveClient"); 171 | } 172 | }; 173 | 174 | /** 175 | * Synchronizes all custom properties (tileIndex, floating between all clients 176 | * in the tile). 177 | */ 178 | Tile.prototype.syncCustomProperties = function() { 179 | try { 180 | var client = this.getActiveClient(); 181 | if (!client) { 182 | client = this.clients[0]; 183 | } 184 | if (client) { 185 | client.tiling_tileIndex = this.tileIndex; 186 | } 187 | } catch(err) { 188 | print(err, "in Tile.syncCustomProperties"); 189 | } 190 | }; 191 | 192 | Tile.prototype.onClientGeometryChanged = function(client) { 193 | this.setClientGeometry(client); 194 | }; 195 | 196 | Tile.prototype.setClientGeometry = function(client) { 197 | try { 198 | if (!client) { 199 | return; 200 | } 201 | if (client.tiling_resize) { 202 | return; 203 | } 204 | if (client.fullScreen) { 205 | return; 206 | } 207 | if (this.maximized) { 208 | return; 209 | } 210 | if (!this.hasClient(client)) { 211 | print("Wrong tile called"); 212 | return; 213 | } 214 | // These two should never be reached 215 | if (client.deleted) { 216 | return; 217 | } 218 | if (client.move 219 | || client.resize 220 | || !client.resizeable 221 | || !client.moveable) { 222 | return; 223 | } 224 | if (this._moving || this._resizing || !this.active) { 225 | return; 226 | } 227 | // This client is bogus 228 | if (client.minSize.width == client.maxSize.width && client.minSize.height == client.maxSize.width) { 229 | return; 230 | } 231 | if (this.rectangle) { 232 | // We set keepBelow here to keep tiling clients below, 233 | // also because that allows us to not set it for floating ones. 234 | client.keepBelow = true; 235 | if (client.screen != this._currentScreen) { 236 | this._currentScreen = client.screen; 237 | } 238 | // Respect min/maxSize 239 | var changedRect = false; 240 | var screenRect = util.getTilingArea(this._currentScreen, this._currentDesktop); 241 | // We cannot accomodate this. The client is wrong. 242 | if (client.minSize.width > screenRect.width 243 | || client.minSize.height > screenRect.height) { 244 | return; 245 | } 246 | if (client.minSize.width > this.rectangle.width) { 247 | if (this.rectangle.x + this.rectangle.width == screenRect.x + screenRect.width - this.screenGapSizeRight) { 248 | this.rectangle.x = (screenRect.x + screenRect.width - this.screenGapSizeRight) - client.minSize.width; 249 | } 250 | this.rectangle.width = client.minSize.width + this.windowsGapSizeWidth; 251 | changedRect = true; 252 | } 253 | if (client.minSize.height > this.rectangle.height) { 254 | if (this.rectangle.y + this.rectangle.height == screenRect.y + screenRect.height - this.screenGapSizeBottom) { 255 | this.rectangle.y = (screenRect.y + screenRect.height - this.screenGapSizeBottom) - client.minSize.height; 256 | } 257 | this.rectangle.height = client.minSize.height + this.windowsGapSizeHeight; 258 | changedRect = true; 259 | } 260 | if (client.maxSize.width < this.rectangle.width && client.maxSize.width > 0) { 261 | if (this.rectangle.x + this.rectangle.width == screenRect.x + screenRect.width - this.screenGapSizeRight) { 262 | this.rectangle.x = (screenRect.x + screenRect.width - this.screenGapSizeRight) - client.maxSize.width; 263 | } 264 | this.rectangle.width = client.maxSize.width + this.windowsGapSizeWidth; 265 | changedRect = true; 266 | } 267 | if (client.maxSize.height < this.rectangle.height && client.maxSize.height > 0) { 268 | if (this.rectangle.y + this.rectangle.height == screenRect.y + screenRect.height - this.screenGapSizeBottom) { 269 | this.rectangle.y = (screenRect.y + screenRect.height - this.screenGapSizeBottom) - client.maxSize.height; 270 | } 271 | this.rectangle.height = client.maxSize.height + this.windowsGapSizeHeight; 272 | changedRect = true; 273 | } 274 | if (client.shade) { 275 | this.rectangle.height = client.geometry.height; 276 | changedRect = true; 277 | } 278 | 279 | client.tiling_resize = true; 280 | client.geometry = util.copyRect(this.rectangle); 281 | 282 | if (changedRect) { 283 | this._resizing = true; 284 | this.resizingEnded.emit(); 285 | this._resizing = false; 286 | } 287 | 288 | client.tiling_resize = false; 289 | } else { 290 | print("No rectangle", client.resourceClass.toString(), client.windowId); 291 | } 292 | } catch(err) { 293 | print(err, "in Tile.setClientGeometry"); 294 | } 295 | }; 296 | 297 | Tile.prototype.onClientActivitiesChanged = function(client) { 298 | try { 299 | var activities = Array.from(client.activities); 300 | var emit = false; 301 | if (this._activities.length == activities.length) { 302 | for (var i = 0; i < activities.length; i++) { 303 | if (!this._activities.includes(activities[i])) { 304 | emit = true; 305 | break; 306 | } 307 | } 308 | } else { 309 | emit = true; 310 | } 311 | if (emit) { 312 | this._activities = activities; 313 | this.activitiesChanged.emit(); 314 | } 315 | } catch(err) { 316 | print(err, "in Tile.onClientActivitiesChanged"); 317 | } 318 | }; 319 | 320 | Tile.prototype.onClientDesktopChanged = function(client) { 321 | try { 322 | var oldDesktop = this._currentDesktop; 323 | this._currentDesktop = client.desktop; 324 | this.desktopChanged.emit(oldDesktop, this._currentDesktop); 325 | } catch(err) { 326 | print(err, "in Tile.onClientDesktopChanged"); 327 | } 328 | }; 329 | 330 | Tile.prototype.onClientScreenChanged = function(client) { 331 | try { 332 | var oldScreen = this._currentScreen; 333 | this._currentScreen = client.screen; 334 | this.screenChanged.emit(oldScreen, this._currentScreen); 335 | } catch(err) { 336 | print(err, "in Tile.onClientScreenChanged"); 337 | } 338 | }; 339 | 340 | Tile.prototype.onClientStartUserMovedResized = function(client) { 341 | if (!this.active) return; 342 | // Let client stay above the other tilers so the user sees the move 343 | client.keepBelow = false; 344 | }; 345 | 346 | Tile.prototype.onClientStepUserMovedResized = function(client) { 347 | if (!this.active) return; 348 | try { 349 | if (client.resize) { 350 | this._resizing = true; 351 | this.resizingStep.emit(); 352 | // This means it gets "animated" 353 | this.resizingEnded.emit(); 354 | return; 355 | } 356 | if (client.move) { 357 | this._moving = true; 358 | this.movingStep.emit(); 359 | this.movingEnded.emit(); 360 | return; 361 | } 362 | } catch(err) { 363 | print(err, "in Tile.onClientStepUserMovedResized"); 364 | } 365 | }; 366 | 367 | Tile.prototype.onClientFinishUserMovedResized = function(client) { 368 | if (!this.active) return; 369 | try { 370 | if (this._moving) { 371 | this._moving = false; 372 | this.movingEnded.emit(); 373 | } else if (this._resizing) { 374 | this._resizing = false; 375 | this.resizingEnded.emit(); 376 | } 377 | // Put the client on the same layer as the other tilers again 378 | client.keepBelow = true; 379 | } catch(err) { 380 | print(err, "in Tile.onClientFinishUserMovedResized"); 381 | } 382 | }; 383 | 384 | Tile.prototype.removeClient = function(client) { 385 | try { 386 | this.clients.splice(this.clients.indexOf(client), 1); 387 | } catch(err) { 388 | print(err, "in Tile.removeClient"); 389 | } 390 | }; 391 | 392 | Tile.prototype.addClient = function(client) { 393 | try { 394 | if (this.clients.indexOf(client) == -1) { 395 | if (KWin.readConfig("noBorder", false)) { 396 | client.noBorder = true; 397 | } 398 | this.clients.push(client); 399 | this.syncCustomProperties(); 400 | this.setClientGeometry(client); 401 | } 402 | } catch(err) { 403 | print(err, "in Tile.addClient"); 404 | } 405 | }; 406 | 407 | Tile.prototype.onClientMaximizedStateChanged = function(client, h, v) { 408 | try { 409 | // Set keepBelow to keep maximized clients over tiled ones 410 | // TODO: We don't distinguish between horizontal and vertical maximization, 411 | // also there's no way to find that the _user_ caused this. 412 | // So we might want to ignore maximization entirely. 413 | if (h || v) { 414 | client.keepBelow = false; 415 | // We might get a geometryChanged signal before this 416 | // so we need to manually maximize the client. 417 | client.tiling_resize = true; 418 | client.geometry = workspace.clientArea(KWin.MaximizeFullArea, this._currentScreen, this._currentDesktop); 419 | client.tiling_resize = false; 420 | this.maximized = true; 421 | } else { 422 | this.maximized = false; 423 | client.keepBelow = true; 424 | } 425 | } catch(err) { 426 | print(err, "in tile.onClientMaximizedStateChanged"); 427 | } 428 | }; 429 | 430 | Tile.prototype.hasClient = function(client) { 431 | return (this.clients.indexOf(client) > -1); 432 | }; 433 | 434 | Tile.prototype.getActivities = function() { 435 | return Array.from(this._activities); 436 | }; 437 | 438 | Tile.prototype.getDesktop = function() { 439 | return this._currentDesktop; 440 | }; 441 | 442 | Tile.prototype.getScreen = function() { 443 | return this._currentScreen; 444 | }; 445 | 446 | Tile.prototype.unmaximize = function() { 447 | if (this._canSetMaximize == true && this.maximized == true) { 448 | this.clients.forEach(function(c) { 449 | c.setMaximize(false, false); 450 | }); 451 | } 452 | }; 453 | 454 | Tile.prototype.setKeepBelow = function(setting) { 455 | this.clients.forEach(function(c) { 456 | c.keepBelow = setting; 457 | }); 458 | }; 459 | 460 | Tile.prototype.onAllActivities = function() { 461 | return this._activities.length == 0; 462 | }; 463 | -------------------------------------------------------------------------------- /contents/code/tiling.js: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2012 Mathias Gottschlag 6 | Copyright (C) 2013-2014 Fabian Homborg 7 | 8 | This program is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | *********************************************************************/ 21 | 22 | /** 23 | * Class which implements tiling for a single screen. 24 | * @class 25 | */ 26 | function Tiling(layoutType, desktop, screen) { 27 | try { 28 | this.desktop = desktop; 29 | this.screen = screen; 30 | this.screenRectangle = util.getTilingArea(this.screen, this.desktop); 31 | /** 32 | * Tiles which have been added to the layout 33 | */ 34 | this.tiles = []; 35 | /** 36 | * Layout type which provided the current layout. 37 | */ 38 | this.layoutType = layoutType; 39 | /** 40 | * Layout which specifies window sizes/positions. 41 | */ 42 | this.layout = new layoutType(this.screenRectangle); 43 | /** 44 | * active: True if the layout is active (i.e. on the current desktop) 45 | * useractive: True if the layout is activated by the user 46 | * False if the user deactivated it 47 | * useractive implies active 48 | */ 49 | this.active = false; 50 | this.userActive = true; 51 | 52 | var gapSize = KWin.readConfig("gapSize", 0); /* stick to old gaps config by default */ 53 | this.windowsGapSizeHeight = KWin.readConfig("windowsGapSizeHeight", gapSize); 54 | this.windowsGapSizeWidth = KWin.readConfig("windowsGapSizeWidth", gapSize); 55 | this.screenGapSizeLeft = KWin.readConfig("screenGapSizeLeft", 0); 56 | this.screenGapSizeRight = KWin.readConfig("screenGapSizeRight", 0); 57 | this.screenGapSizeTop = KWin.readConfig("screenGapSizeTop", 0); 58 | this.screenGapSizeBottom = KWin.readConfig("screenGapSizeBottom", 0); 59 | } catch(err) { 60 | print(err, "in Tiling"); 61 | } 62 | }; 63 | 64 | Tiling.prototype.setLayoutType = function(layoutType) { 65 | try { 66 | var newLayout = new layoutType(this.layout.screenRectangle); 67 | for (var i = 0; i < this.layout.tiles.length; i++) { 68 | newLayout.addTile(); 69 | } 70 | this.layout = newLayout; 71 | this.layoutType = layoutType; 72 | this._updateAllTiles(); 73 | } catch(err) { 74 | print(err, "in Tiling.setLayoutType"); 75 | } 76 | }; 77 | 78 | Tiling.prototype.addTile = function(tile, previouslyFocusedClient, x, y) { 79 | try { 80 | // NOTE: Separate handling is necessary because the 81 | // semantics of passing an x,y value are different in I3Layout. 82 | if (this.layout.isI3Layout) { 83 | var finalX = x; 84 | var finalY = y; 85 | if (!x || !y) { 86 | var focused = previouslyFocusedClient; 87 | if (focused && focused.geometry) { 88 | print('Last focused client: ' + focused.caption); 89 | finalX = focused.geometry.x + focused.geometry.width/2; 90 | finalY = focused.geometry.y + focused.geometry.height/2; 91 | print('Desktop: '+this.desktop + 92 | ', Screen: '+this.screen + 93 | ' FinalX: ' +finalX + 94 | ", FinalY: "+finalY); 95 | } 96 | } 97 | this.layout.addTile(finalX,finalY); 98 | this.tiles.push(tile); 99 | } 100 | // For Gridlayout we want to avoid moving every tile in the grid when adding a new tile 101 | // So we swap the new tile with another one, and append the other one at the end 102 | else if (this.layout.isGridLayout) 103 | { 104 | this.layout.addTile(); 105 | // If a position was specified, we insert the tile at the specified position 106 | if (x != null && y != null) { 107 | var index = this._getTileIndex(x, y); 108 | if (index == -1) { 109 | this.tiles.splice(Math.min(this.layout.masterCount, this.tiles.length - 1), 0, tile); 110 | } else { 111 | if(this.layout.master > -1) 112 | this.tiles.splice(index, 0, tile); 113 | else 114 | this.tiles.splice(0, 0, tile); 115 | } 116 | } else { 117 | if (tile.tileIndex > -1 && tile.tileIndex <= this.tiles.length) { 118 | var removedTile = this.tiles.splice(tile.tileIndex, 1, tile); 119 | if(removedTile.length > 0) 120 | this.tiles.splice(Math.min(this.layout.masterCount, this.tiles.length - 1), 0, removedTile[0]); 121 | } else { 122 | this.tiles.splice(Math.min(this.layout.masterCount, this.tiles.length - 1), 0, tile); 123 | } 124 | } 125 | } 126 | else { 127 | this.layout.addTile(); 128 | // If a position was specified, we insert the tile at the specified position 129 | if (x != null && y != null) { 130 | var index = this._getTileIndex(x, y); 131 | if (index == -1) { 132 | this.tiles.push(tile); 133 | } else { 134 | this.tiles.splice(index, 0, tile); 135 | } 136 | } else { 137 | if (tile.tileIndex > -1 && tile.tileIndex <= this.tiles.length) { 138 | this.tiles.splice(tile.tileIndex, 0, tile); 139 | } else { 140 | this.tiles.push(tile); 141 | } 142 | } 143 | } 144 | for (var i = 0; i < this.tiles.length; i++) { 145 | this.tiles[i].tileIndex = i; 146 | this.tiles[i].active = this.userActive; 147 | this.tiles[i].syncCustomProperties(); 148 | } 149 | this._updateAllTiles(); 150 | } catch(err) { 151 | print(err, "in Tiling.addTile"); 152 | } 153 | }; 154 | 155 | Tiling.prototype.removeTile = function(tile) { 156 | try { 157 | // For Gridlayout we want to avoid moving every tile in the grid when removing a tile 158 | // So we remove the last tile and swap it with the tile that should be removed 159 | if (this.layout.isGridLayout) 160 | { 161 | var tileIndex = this.tiles.indexOf(tile); 162 | if (tileIndex > -1) { 163 | var tempIndex = Math.min(this.layout.masterCount,this.tiles.length - 1); 164 | var temp = this.tiles[tempIndex]; 165 | if(tileIndex < this.tiles.length) 166 | this.tiles.splice(tileIndex, 1,temp); 167 | this.tiles.splice(tempIndex, 1); 168 | this.layout.removeTile(tileIndex); 169 | // Correct tileIndex 170 | for (var i = 0; i < this.tiles.length; i++) { 171 | this.tiles[i].tileIndex = i; 172 | this.tiles[i].syncCustomProperties(); 173 | } 174 | // TODO: Unregister tile callbacks 175 | this._updateAllTiles(); 176 | } else { 177 | print("removeTile: No such tile ", tile._currentDesktop, 178 | tile._currentScreen, tile.clients[0].resourceClass.toString()); 179 | print(this.desktop, this.screen); 180 | } 181 | return; 182 | } 183 | 184 | var tileIndex = this.tiles.indexOf(tile); 185 | if (tileIndex > -1) { 186 | this.tiles.splice(tileIndex, 1); 187 | this.layout.removeTile(tileIndex); 188 | // Correct tileIndex 189 | for (var i = 0; i < this.tiles.length; i++) { 190 | this.tiles[i].tileIndex = i; 191 | this.tiles[i].syncCustomProperties(); 192 | } 193 | // TODO: Unregister tile callbacks 194 | this._updateAllTiles(); 195 | } else { 196 | print("removeTile: No such tile ", tile._currentDesktop, 197 | tile._currentScreen, tile.clients[0].resourceClass.toString()); 198 | print(this.desktop, this.screen); 199 | } 200 | } catch(err) { 201 | print(err, "in Tiling.removeTile"); 202 | } 203 | }; 204 | 205 | Tiling.prototype.swapTiles = function(tile1, tile2) { 206 | try { 207 | // Cut down on updates by not doing them if tile1 is hovering over itself 208 | if (tile1 != tile2) { 209 | var index1 = this.tiles.indexOf(tile1); 210 | var index2 = this.tiles.indexOf(tile2); 211 | this.tiles[index1] = tile2; 212 | this.tiles[index2] = tile1; 213 | this.tiles[index1].tileIndex = index1; 214 | this.tiles[index2].tileIndex = index2; 215 | this.tiles[index1].syncCustomProperties(); 216 | this.tiles[index2].syncCustomProperties(); 217 | this._updateAllTiles(); 218 | // This will only be called if tile1 just stopped moving 219 | } else if (tile1._moving == false) { 220 | this._updateAllTiles(); 221 | } 222 | } catch(err) { 223 | print(err, "in Tiling.swapTiles"); 224 | } 225 | }; 226 | 227 | Tiling.prototype.activate = function() { 228 | if (this.userActive == true) { 229 | this.active = true; 230 | // Resize the tiles like specified by the layout 231 | this._updateAllTiles(); 232 | } 233 | }; 234 | 235 | Tiling.prototype.deactivate = function() { 236 | this.active = false; 237 | // Unset keepBelow so they handle like true floating clients. 238 | for (var i = 0; i < this.layout.tiles.length; i++) { 239 | this.tiles[i].setKeepBelow(false); 240 | this.tiles[i].active = false; 241 | } 242 | }; 243 | 244 | Tiling.prototype.toggleActive = function() { 245 | if (this.active) { 246 | this.deactivate(); 247 | } else { 248 | this.activate(); 249 | } 250 | }; 251 | 252 | Tiling.prototype.toggleUserActive = function() { 253 | if (this.userActive == true) { 254 | this.userActive = false; 255 | this.deactivate(); 256 | } else { 257 | this.userActive = true; 258 | this.activate(); 259 | } 260 | }; 261 | 262 | /** 263 | * Resets tile sizes to their initial size (in case they were resized by the 264 | * user). 265 | */ 266 | Tiling.prototype.resetTileSizes = function() { 267 | this.layout.resetTileSizes(); 268 | this._updateAllTiles(); 269 | }; 270 | 271 | Tiling.prototype.getTile = function(x, y) { 272 | try { 273 | var index = this._getTileIndex(x, y); 274 | if (index != -1) { 275 | return this.tiles[index]; 276 | } else { 277 | return null; 278 | } 279 | } catch(err) { 280 | print(err, "in Tiling.getTile"); 281 | } 282 | }; 283 | 284 | Tiling.prototype._getTileIndex = function(x, y) { 285 | try { 286 | for (var i = 0; i < this.layout.tiles.length; i++) { 287 | var tile = this.layout.tiles[i]; 288 | // Remove gaps 289 | // FIXME: Take screenGaps into account - not important for what we use this for 290 | // but this would break if we'd ask about x=screenRect.x or similar and sG is larger than wG 291 | var realrect = Qt.rect(tile.rectangle.x - this.windowsGapSizeWidth, 292 | tile.rectangle.y - this.windowsGapSizeHeight, 293 | tile.rectangle.width + this.windowsGapSizeWidth * 2, 294 | tile.rectangle.height + this.windowsGapSizeHeight * 2); 295 | realrect = util.intersectRect(this.screenRectangle, realrect); 296 | if (tile.rectangle.x <= x 297 | && tile.rectangle.y <= y 298 | && tile.rectangle.x + tile.rectangle.width > x 299 | && tile.rectangle.y + tile.rectangle.height > y) { 300 | return i; 301 | } 302 | } 303 | return -1; 304 | } catch(err) { 305 | print(err, "in Tiling._getTileIndex"); 306 | } 307 | }; 308 | 309 | Tiling.prototype.resizeTile = function(tile){ 310 | try { 311 | if (tile != null) { 312 | var tileIndex = tile.tileIndex; 313 | var geometry = tile.clients[0].geometry; 314 | var tileRect = Qt.rect(geometry.x - this.windowsGapSizeWidth, 315 | geometry.y - this.windowsGapSizeHeight, 316 | geometry.width + this.windowsGapSizeWidth * 2, 317 | geometry.height + this.windowsGapSizeHeight * 2); 318 | this.layout.resizeTile(tileIndex, tileRect); 319 | this._updateAllTiles(); 320 | } 321 | } catch(err) { 322 | print(err, "in Tiling.resizeTile"); 323 | } 324 | }; 325 | 326 | Tiling.prototype.resizeTileTo = function(tile, geometry) { 327 | try { 328 | if (tile != null && geometry != null) { 329 | var tileIndex = tile.tileIndex; 330 | var tileRect = Qt.rect(geometry.x - this.windowsGapSizeWidth, 331 | geometry.y - this.windowsGapSizeHeight, 332 | geometry.width + this.windowsGapSizeWidth * 2, 333 | geometry.height + this.windowsGapSizeHeight * 2); 334 | this.layout.resizeTile(tileIndex, tileRect); 335 | this._updateAllTiles(); 336 | } 337 | } catch(err) { 338 | print(err, "in Tiling.resizeTileTo"); 339 | } 340 | } 341 | 342 | Tiling.prototype._updateAllTiles = function() { 343 | try { 344 | // Set the position/size of all tiles 345 | if (this.active == true && this.userActive == true) { 346 | this.resizeScreen(); 347 | for (var i = 0; i < this.layout.tiles.length; i++) { 348 | var newRect = this.layout.tiles[i].rectangle; 349 | if (! newRect) { 350 | return; 351 | } 352 | 353 | var geometry = util.intersectRect(newRect, this.screenRectangle); 354 | this.applyGaps(geometry); 355 | this.tiles[i].active = true; 356 | this.tiles[i].setGeometry(geometry); 357 | } 358 | } 359 | } catch(err) { 360 | print(err, "in Tiling._updateAllTiles"); 361 | } 362 | }; 363 | 364 | Tiling.prototype.resizeMaster = function(geometry) { 365 | try { 366 | if (this.layout.master > -1) { 367 | this.layout.resizeTile(this.layout.master, geometry); 368 | this._updateAllTiles(); 369 | } 370 | } catch(err) { 371 | print(err, "in resizeMaster"); 372 | } 373 | }; 374 | 375 | Tiling.prototype.getMaster = function() { 376 | if (this.layout.master > -1) { 377 | return this.tiles[this.layout.master]; 378 | } else { 379 | return null; 380 | } 381 | }; 382 | 383 | Tiling.prototype.resizeScreen = function() { 384 | // FIXME: KWin bug: clientArea returns the _former_ area 385 | // See https://bugs.kde.org/show_bug.cgi?id=330099 386 | var rect = util.getTilingArea(this.screen, this.desktop); 387 | if (util.compareRect(rect,this.screenRectangle) == false) { 388 | this.layout.screenRectangle.x = this.screenRectangle.x; 389 | this.layout.screenRectangle.y = this.screenRectangle.y; 390 | this.layout.screenRectangle.width = this.screenRectangle.width; 391 | this.layout.screenRectangle.height = this.screenRectangle.height; 392 | this.layout.setLayoutArea(rect); 393 | this.screenRectangle = rect; 394 | } 395 | }; 396 | 397 | Tiling.prototype.tile = function() { 398 | var self = this; 399 | this.tiles.forEach(function(tile) { 400 | var geom = util.copyRect(tile.rectangle); 401 | self.applyGaps(geom); 402 | tile.setGeometry(geom); 403 | }); 404 | }; 405 | 406 | Tiling.prototype.increaseMaster = function() { 407 | if (this.layout.increaseMaster != null) { 408 | this.layout.increaseMaster(); 409 | this._updateAllTiles(); 410 | } 411 | }; 412 | 413 | Tiling.prototype.decrementMaster = function() { 414 | if (this.layout.decrementMaster != null) { 415 | this.layout.decrementMaster(); 416 | this._updateAllTiles(); 417 | } 418 | }; 419 | 420 | /* 421 | * Apply gaps to a rectangle, in-place 422 | */ 423 | Tiling.prototype.applyGaps = function(rect) { 424 | if (KWin.readConfig("fullscreenGaps", false) == false && util.compareRect(rect, this.screenRectangle) == true) return; 425 | if (rect.x + rect.width == this.screenRectangle.x + this.screenRectangle.width) { 426 | rect.width -= this.screenGapSizeRight; 427 | } else { 428 | rect.width -= this.windowsGapSizeWidth; 429 | } 430 | if (rect.x == this.screenRectangle.x) { 431 | rect.x += this.screenGapSizeLeft; 432 | rect.width -= this.screenGapSizeLeft; 433 | } else { 434 | rect.x += this.windowsGapSizeWidth; 435 | rect.width -= this.windowsGapSizeWidth; 436 | } 437 | if (rect.y + rect.height == this.screenRectangle.y + this.screenRectangle.height) { 438 | rect.height -= this.screenGapSizeBottom; 439 | } else { 440 | rect.height -= this.windowsGapSizeHeight; 441 | } 442 | if (rect.y == this.screenRectangle.y) { 443 | rect.y += this.screenGapSizeTop; 444 | rect.height -= this.screenGapSizeTop; 445 | } else { 446 | rect.y += this.windowsGapSizeHeight; 447 | rect.height -= this.windowsGapSizeHeight; 448 | } 449 | }; 450 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc. 5 | 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Library General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License 307 | along with this program; if not, write to the Free Software 308 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 309 | 310 | 311 | Also add information on how to contact you by electronic and paper mail. 312 | 313 | If the program is interactive, make it output a short notice like this 314 | when it starts in an interactive mode: 315 | 316 | Gnomovision version 69, Copyright (C) year name of author 317 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 318 | This is free software, and you are welcome to redistribute it 319 | under certain conditions; type `show c' for details. 320 | 321 | The hypothetical commands `show w' and `show c' should show the appropriate 322 | parts of the General Public License. Of course, the commands you use may 323 | be called something other than `show w' and `show c'; they could even be 324 | mouse-clicks or menu items--whatever suits your program. 325 | 326 | You should also get your employer (if you work as a programmer) or your 327 | school, if any, to sign a "copyright disclaimer" for the program, if 328 | necessary. Here is a sample; alter the names: 329 | 330 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 331 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 332 | 333 | , 1 April 1989 334 | Ty Coon, President of Vice 335 | 336 | This General Public License does not permit incorporating your program into 337 | proprietary programs. If your program is a subroutine library, you may 338 | consider it more useful to permit linking proprietary applications with the 339 | library. If this is what you want to do, use the GNU Library General 340 | Public License instead of this License. 341 | -------------------------------------------------------------------------------- /contents/code/gridlayout.js: -------------------------------------------------------------------------------- 1 | /******************************************************************** 2 | KWin - the KDE window manager 3 | This file is part of the KDE project. 4 | 5 | Copyright (C) 2013 Fabian Homborg 6 | based on spirallayout.js by Matthias Gottschlag 7 | 8 | This program is free software; you can redistribute it and/or modify 9 | it under the terms of the GNU General Public License as published by 10 | the Free Software Foundation; either version 2 of the License, or 11 | (at your option) any later version. 12 | 13 | This program is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU General Public License for more details. 17 | 18 | You should have received a copy of the GNU General Public License 19 | along with this program. If not, see . 20 | *********************************************************************/ 21 | 22 | Qt.include("layout.js"); 23 | 24 | /** 25 | * Class which arranges the windows in a grid for slaveTiles and a sideways stack for the masterTiles 26 | * The amount of masterTiles is changeable by shortcut and is 0 by default 27 | */ 28 | function GridLayout(screenRectangle) { 29 | print("Creating GridLayout"); 30 | try { 31 | Layout.call(this, screenRectangle); 32 | } catch (err) { 33 | print(err, "in GridLayout"); 34 | } 35 | this.masterAreaRatio = KWin.readConfig("gridLayoutMasterRatio", 50) / 100; 36 | this.masterCount = KWin.readConfig("gridLayoutMasterCount", 0); 37 | this.master = this.masterCount - 1; 38 | this.masterAreaWidth = 0; 39 | 40 | if(this.masterCount > 0) 41 | { 42 | this.masterAreaWidth = this.screenRectangle.width; 43 | } 44 | 45 | this.isGridLayout = true; 46 | this.spanRows = KWin.readConfig("gridLayoutRowSpan", false); 47 | this.spanCols = KWin.readConfig("gridLayoutColSpan", false); 48 | } 49 | 50 | GridLayout.prototype = new Layout(); 51 | GridLayout.prototype.constructor = GridLayout; 52 | 53 | GridLayout.prototype.name = "Grid"; 54 | GridLayout.prototype.supportsRotation = true; 55 | // // TODO: Add an image for the layout switcher 56 | // GridLayout.image = null; 57 | 58 | GridLayout.prototype.addTile = function () { 59 | try { 60 | this._applyGravity(); 61 | if (this.tiles.length === 0) { 62 | // The first tile fills the whole screen 63 | var rect = util.copyRect(this.screenRectangle); 64 | util.assertRectInScreen(rect, this.screenRectangle); 65 | this._createTile(rect); 66 | if (this.masterCount > 0) 67 | { 68 | this.masterAreaWidth = this.screenRectangle.width; 69 | this.master = 0; 70 | } 71 | this._unapplyGravity(); 72 | return; 73 | } 74 | var newRect = util.copyRect(this.screenRectangle); 75 | this._createTile(newRect); 76 | var tile = this.tiles.pop(); 77 | 78 | if (this.tiles.length < this.masterCount) { 79 | // tile is another master 80 | this.masterAddTile(tile); 81 | this._unapplyGravity(); 82 | } else if (this.tiles.length === this.masterCount) { 83 | // tile is first slave 84 | this.masterAreaWidth = this.screenRectangle.width * this.masterAreaRatio; 85 | this.adjustMastersWidth(this.masterAreaRatio, 86 | 0, 87 | this.masterCount - 1, this.screenRectangle.x, 88 | this.screenRectangle.x + this.masterAreaWidth); 89 | tile.rectangle = Qt.rect(this.screenRectangle.x + this.masterAreaWidth, 90 | this.screenRectangle.y, 91 | this.screenRectangle.width - this.masterAreaWidth, 92 | this.screenRectangle.height); 93 | this.tiles.push(tile); 94 | this._unapplyGravity(); 95 | } else if (this.tiles.length > this.masterCount) { 96 | // Tile is another slave 97 | this.slaveAddTile(tile); 98 | this._unapplyGravity(); 99 | } 100 | } catch (err) { 101 | print(err, "in GridLayout.addTile"); 102 | print(err.stack) 103 | } 104 | }; 105 | 106 | // Save the first tile's width 107 | GridLayout.prototype.resizeTile = function (tileIndex, rectangle) { 108 | Layout.prototype.resizeTile.call(this, tileIndex, rectangle); 109 | // Fixes bug where firstWidth will be set to the full screen when resizing with just masters 110 | if (this.tiles.length > this.masterCount) { 111 | this.masterAreaWidth = this.getMasterWidth(); 112 | } 113 | }; 114 | 115 | GridLayout.prototype.getMasterWidth = function () { 116 | var tile = this.tiles[Math.min(this.tiles.length, this.masterCount) - 1]; 117 | if (tile != null) { 118 | var lastMaster = tile.rectangle; 119 | return lastMaster.x + lastMaster.width - this.screenRectangle.x; 120 | } else { 121 | // No masters exist 122 | return 0; 123 | } 124 | }; 125 | 126 | GridLayout.prototype.removeTile = function (tileIndex) { 127 | try { 128 | // Remove the array entry 129 | if (tileIndex < 0 || tileIndex >= this.tiles.length) { 130 | print("Removing invalid tileindex" + tileindex); 131 | return; 132 | } 133 | this._applyGravity(); 134 | 135 | // second last tile 136 | if (this.tiles.length === 2) { 137 | this.tiles.pop(); 138 | this.tiles[0].rectangle = util.copyRect(this.screenRectangle); 139 | return; 140 | } 141 | // last tile 142 | if (this.tiles.length === 1) { 143 | this.tiles.splice(tileIndex, 1); 144 | if(this.masterCount > 0) 145 | { 146 | this.masterAreaWidth = 0; 147 | this.master = -1; 148 | } 149 | return; 150 | } 151 | //other cases 152 | if (this.tiles.length > 1) { 153 | // tile is master 154 | if (tileIndex < this.masterCount) { 155 | // there is exactly one slave 156 | if (this.tiles.length - this.masterCount === 1) { 157 | this.slaveRemoveTile() 158 | this.adjustMastersWidth(this.screenRectangle.width / this.masterAreaWidth, 159 | 0, 160 | this.masterCount - 1, this.screenRectangle.x, 161 | this.screenRectangle.x + this.screenRectangle.width); 162 | this.masterAreaWidth = this.screenRectangle.width; 163 | } 164 | // there are no slaves 165 | else if (this.tiles.length - this.masterCount < 1) { 166 | this.masterRemoveTile(tileIndex); 167 | } 168 | // there are more than one slave 169 | else if (this.tiles.length - this.masterCount > 1) { 170 | this.slaveRemoveTile(); 171 | } 172 | } 173 | // tile is slave 174 | else if (tileIndex >= this.masterCount) { 175 | // tile is only slave 176 | if (this.masterCount === this.tiles.length - 1) { 177 | this.slaveRemoveTile(); 178 | this.adjustMastersWidth(this.screenRectangle.width / this.masterAreaWidth, 179 | 0, 180 | this.masterCount - 1, this.screenRectangle.x, 181 | this.screenRectangle.x + this.screenRectangle.width); 182 | this.masterAreaWidth = this.screenRectangle.width; 183 | } 184 | // there is more than one slave 185 | else if (this.masterCount < this.tiles.length - 1) { 186 | this.slaveRemoveTile(); 187 | } 188 | } 189 | } 190 | } catch (e) { 191 | print(e, "in GridLayout.removeTile") 192 | print(e.stack) 193 | } 194 | }; 195 | 196 | GridLayout.prototype.increaseMaster = function () { 197 | try { 198 | this._applyGravity(); 199 | // There are no tiles 200 | if (this.tiles.length === 0) { 201 | this.masterCount++; 202 | this._unapplyGravity(); 203 | return; 204 | } 205 | // masterCount 0 -> 1 206 | // There are no MasterTiles 207 | if (this.masterCount === 0) { 208 | // There is one SlaveTile 209 | if (this.tiles.length === this.masterCount + 1) { 210 | this.masterCount++; 211 | this.master = 0; 212 | this.masterAreaWidth = this.screenRectangle.width; 213 | this._unapplyGravity(); 214 | return; 215 | } 216 | // There are multiple SlaveTiles 217 | if (this.tiles.length > this.masterCount + 1) { 218 | var tempTile = this.slaveRemoveTile(); 219 | this.masterAreaWidth = this.screenRectangle.width * this.masterAreaRatio; 220 | tempTile.rectangle = Qt.rect(this.screenRectangle.x, 221 | this.screenRectangle.y, 222 | this.masterAreaWidth, 223 | this.screenRectangle.height); 224 | this.adjustSlavesWidth( 225 | (this.screenRectangle.width - this.masterAreaWidth) / this.screenRectangle.width, 226 | this.masterCount, 227 | this.tiles.length - 1, 228 | this.screenRectangle.x + this.masterAreaWidth, 229 | this.screenRectangle.x + this.screenRectangle.width); 230 | this.tiles.splice(this.masterCount, 0, tempTile) 231 | this.masterCount++; 232 | this.master = 0; 233 | this._unapplyGravity(); 234 | return; 235 | } 236 | } 237 | // There is at least one MasterTile 238 | else if (this.masterCount > 0) { 239 | // There are no SlaveTiles 240 | if (this.tiles.length <= this.masterCount) { 241 | this.masterCount++; 242 | this._unapplyGravity(); 243 | return; 244 | } 245 | // There is one SlaveTile 246 | if (this.tiles.length === this.masterCount + 1) { 247 | var tempTile = this.slaveRemoveTile(); 248 | this.masterAddTile(tempTile); 249 | this.masterCount++; 250 | this.adjustMastersWidth(this.screenRectangle.width / this.masterAreaWidth, 251 | 0, 252 | this.masterCount - 1, this.screenRectangle.x, 253 | this.screenRectangle.x + this.screenRectangle.width); 254 | this.masterAreaWidth = this.screenRectangle.width; 255 | this._unapplyGravity(); 256 | return; 257 | } 258 | // There are multiple SlaveTiles 259 | if (this.tiles.length > this.masterCount + 1) { 260 | var tempTile = this.slaveRemoveTile(); 261 | this.masterAddTile(tempTile); 262 | this.masterCount++; 263 | this._unapplyGravity(); 264 | return; 265 | } 266 | } 267 | } catch (e) { 268 | print(e) 269 | print(e.stack) 270 | } 271 | } 272 | 273 | 274 | GridLayout.prototype.decrementMaster = function () { 275 | try { 276 | this._applyGravity(); 277 | // mastercount is already 0 278 | if (this.masterCount === 0) { 279 | this._unapplyGravity(); 280 | return; 281 | } 282 | // There are no tiles 283 | // Or Fewer Tiles than masterCount 284 | if (this.tiles.length === 0 || this.tiles.length < this.masterCount) { 285 | this.masterCount--; 286 | this._unapplyGravity(); 287 | return; 288 | } 289 | // There is exatly one MasterTile 290 | if (this.masterCount === 1) { 291 | // There are no SlaveTiles 292 | if (this.tiles.length === 1) { 293 | this.masterCount--; 294 | this.master = -1; 295 | this.masterAreaWidth = 0; 296 | this._unapplyGravity(); 297 | return; 298 | } 299 | // There is at least one SlaveTile 300 | if (this.tiles.length > 1) { 301 | var tempTile = this.tiles.splice(0, 1)[0]; 302 | this.masterCount--; 303 | this.master = -1; 304 | this.slaveAddTile(tempTile); 305 | this.adjustSlavesWidth( 306 | this.screenRectangle.width / (this.screenRectangle.width - this.masterAreaWidth), 307 | 0, 308 | this.tiles.length - 1, this.screenRectangle.x, 309 | this.screenRectangle.x + this.screenRectangle.width); 310 | this.masterAreaWidth = 0; 311 | this._unapplyGravity(); 312 | return; 313 | } 314 | } 315 | // There are Multiple MasterTiles 316 | if (this.masterCount > 1) { 317 | // There are no SlaveTiles 318 | if (this.tiles.length === this.masterCount) { 319 | var tempTile = this.masterRemoveTile(this.masterCount - 1); 320 | this.masterCount--; 321 | this.masterAreaWidth = this.masterAreaRatio * this.screenRectangle.width; 322 | this.adjustMastersWidth(this.masterAreaWidth / this.screenRectangle.width, 323 | 0, 324 | this.masterCount - 1, this.screenRectangle.x, 325 | this.screenRectangle.x + this.masterAreaWidth) 326 | tempTile.rectangle = Qt.rect(this.screenRectangle.x + this.masterAreaWidth, 327 | this.screenRectangle.y, 328 | this.screenRectangle.width - this.masterAreaWidth, 329 | this.screenRectangle.height); 330 | this.tiles.push(tempTile); 331 | this._unapplyGravity(); 332 | return; 333 | } 334 | // There is at least one SlaveTile 335 | if (this.tiles.length > this.masterCount) { 336 | var tempTile = this.masterRemoveTile(this.masterCount - 1); 337 | this.masterCount--; 338 | this.slaveAddTile(tempTile); 339 | this._unapplyGravity(); 340 | return; 341 | } 342 | } 343 | 344 | } catch (e) { 345 | print(e) 346 | print(e.stack) 347 | } 348 | } 349 | 350 | // adds a tile to the slave grid 351 | GridLayout.prototype.slaveAddTile = function (tile) { 352 | var slaveAreaWidth = this.screenRectangle.width - this.masterAreaWidth; 353 | this.tiles.splice(this.masterCount, 0, tile); 354 | var cr = this.getGridMeasurements(this.tiles.length - this.masterCount); 355 | var newc = cr.col; 356 | var newr = cr.row; 357 | cr = this.getGridMeasurements(this.tiles.length - this.masterCount - 1); 358 | var oldc = cr.col; 359 | var oldr = cr.row; 360 | var changedc = newc - oldc; 361 | var changedr = newr - oldr; 362 | 363 | // The grid measurements dont change 364 | if (!changedc && !changedr) { 365 | var cr = this.getCoordinatesFromIndex(this.tiles.length - this.masterCount - 2); 366 | var prevc = cr.col; 367 | var prevr = cr.row; 368 | 369 | cr = this.getCoordinatesFromIndex(this.tiles.length - this.masterCount - 1); 370 | var c = cr.col; 371 | var r = cr.row; 372 | var dc = c - prevc; 373 | var dr = r - prevr; 374 | 375 | // The new tile has different column and same row as the previous tile 376 | if (dc > 0) { 377 | if(this.spanRows) 378 | { 379 | var prevRectOld = util.copyRect(this.tiles[this.masterCount + 1].rectangle); 380 | 381 | let it = this.getIndexFromCoordinates(c,r - 1); 382 | let belowRectOld = util.copyRect(this.tiles[this.tiles.length - 1 - it].rectangle); 383 | var belowRectNew = this.tiles[this.tiles.length - 1 - it].rectangle; 384 | belowRectNew.y = prevRectOld.y + prevRectOld.height; 385 | belowRectNew.height = belowRectOld.height - prevRectOld.height; 386 | 387 | var newRect = Qt.rect(belowRectOld.x, 388 | belowRectOld.y, 389 | belowRectOld.width, 390 | prevRectOld.height); 391 | tile.rectangle = newRect; 392 | } 393 | else 394 | { 395 | let it = this.getIndexFromCoordinates(c - 1,r - 1); 396 | let belowPrevRect = util.copyRect(this.tiles[this.tiles.length - 1 - it].rectangle); 397 | 398 | var prevRectOld = util.copyRect(this.tiles[this.masterCount + 1].rectangle); 399 | var prevRectNew = this.tiles[this.masterCount + 1].rectangle; 400 | prevRectNew.x = belowPrevRect.x 401 | prevRectNew.width = belowPrevRect.width 402 | var newRect = Qt.rect(prevRectOld.x, 403 | prevRectNew.y, 404 | prevRectOld.width - prevRectNew.width, 405 | prevRectNew.height); 406 | tile.rectangle = newRect; 407 | } 408 | } 409 | // The new tile has different row and same column as the previous tile 410 | else if (dr > 0) { 411 | if(this.spanCols) 412 | { 413 | var prevRectOld = util.copyRect(this.tiles[this.masterCount + 1].rectangle); 414 | 415 | let it = this.getIndexFromCoordinates(c - 1,r); 416 | let rightRectOld = util.copyRect(this.tiles[this.tiles.length - 1 - it].rectangle); 417 | var rightRectNew = this.tiles[this.tiles.length - 1 - it].rectangle; 418 | rightRectNew.x = prevRectOld.x + prevRectOld.width; 419 | rightRectNew.width = rightRectOld.width - prevRectOld.width; 420 | 421 | var newRect = Qt.rect(prevRectOld.x, 422 | rightRectOld.y, 423 | prevRectOld.width, 424 | rightRectOld.height); 425 | tile.rectangle = newRect; 426 | } 427 | else 428 | { 429 | let it = this.getIndexFromCoordinates(c - 1,r - 1); 430 | let rightPrevRect = util.copyRect(this.tiles[this.tiles.length - 1 - it].rectangle); 431 | 432 | var prevRectOld = util.copyRect(this.tiles[this.masterCount + 1].rectangle); 433 | var prevRectNew = this.tiles[this.masterCount + 1].rectangle; 434 | prevRectNew.y = rightPrevRect.y; 435 | prevRectNew.height = rightPrevRect.height; 436 | var newRect = Qt.rect(prevRectNew.x, 437 | prevRectOld.y, 438 | prevRectNew.width, 439 | prevRectNew.y - prevRectOld.y); 440 | tile.rectangle = newRect; 441 | } 442 | } else { 443 | print("Error in SlaveAddTile"); 444 | } 445 | } 446 | // The number of columns changes 447 | else if (changedc) { 448 | if(this.spanCols) 449 | { 450 | let it = this.getIndexFromCoordinates(oldc, 1); 451 | let tempRect = this.tiles[this.tiles.length - 1 - it].rectangle; 452 | tile.rectangle = Qt.rect(tempRect.x - Math.floor(slaveAreaWidth / oldc), 453 | tempRect.y, 454 | Math.floor(slaveAreaWidth / oldc), 455 | tempRect.height); 456 | 457 | for (let rt = 2; rt <= oldr; rt++) 458 | { 459 | let it = this.getIndexFromCoordinates(oldc, rt); 460 | let tempRect = this.tiles[this.tiles.length - 1 - it].rectangle; 461 | this.tiles[this.tiles.length - 1 - it].rectangle = Qt.rect(tempRect.x - Math.floor(slaveAreaWidth / oldc), 462 | tempRect.y, 463 | tempRect.width + Math.floor(slaveAreaWidth / oldc), 464 | tempRect.height); 465 | } 466 | 467 | var newWidthSum = slaveAreaWidth; 468 | var oldWidthSum = newWidthSum + tile.rectangle.width 469 | var widthRatio = newWidthSum / oldWidthSum; 470 | this.adjustSlavesWidth(widthRatio, 471 | this.masterCount, 472 | this.tiles.length - 1, 473 | this.screenRectangle.x + this.masterAreaWidth, 474 | this.screenRectangle.x + this.screenRectangle.width); 475 | } 476 | else 477 | { 478 | tile.rectangle = Qt.rect(this.screenRectangle.x + this.masterAreaWidth, 479 | this.screenRectangle.y, 480 | Math.floor(slaveAreaWidth / newc), 481 | this.screenRectangle.height); 482 | 483 | var newWidthSum = slaveAreaWidth - Math.floor(slaveAreaWidth / newc); 484 | var oldWidthSum = slaveAreaWidth 485 | var widthRatio = newWidthSum / oldWidthSum; 486 | this.adjustSlavesWidth(widthRatio, 487 | this.masterCount + 1, 488 | this.tiles.length - 1, 489 | this.screenRectangle.x + this.masterAreaWidth + Math.floor(slaveAreaWidth / newc), 490 | this.screenRectangle.x + this.screenRectangle.width); 491 | } 492 | } 493 | // The number of rows changes 494 | else if (changedr) { 495 | if(this.spanRows) 496 | { 497 | let it = this.getIndexFromCoordinates(1,oldr); 498 | let tempRect = this.tiles[this.tiles.length - 1 - it].rectangle; 499 | tile.rectangle = Qt.rect(tempRect.x, 500 | this.screenRectangle.y - Math.floor(this.screenRectangle.height / oldr), 501 | tempRect.width, 502 | Math.floor(this.screenRectangle.height / oldr)); 503 | 504 | for (let ct = 2; ct <= oldc; ct++) 505 | { 506 | let it = this.getIndexFromCoordinates(ct,oldr); 507 | let tempRect = this.tiles[this.tiles.length - 1 - it].rectangle; 508 | this.tiles[this.tiles.length - 1 - it].rectangle = Qt.rect(tempRect.x, 509 | tempRect.y - Math.floor(this.screenRectangle.height / oldr), 510 | tempRect.width, 511 | tempRect.height + Math.floor(this.screenRectangle.height / oldr)); 512 | } 513 | 514 | var newHeightSum = this.screenRectangle.height; 515 | var oldHeightSum = newHeightSum + tile.rectangle.height 516 | var heightRatio = newHeightSum / oldHeightSum; 517 | this.adjustSlavesHeight(heightRatio, 518 | this.masterCount, 519 | this.tiles.length - 1, 520 | this.screenRectangle.y, 521 | this.screenRectangle.y + this.screenRectangle.height); 522 | } 523 | else 524 | { 525 | tile.rectangle = Qt.rect(this.screenRectangle.x + this.masterAreaWidth, 526 | this.screenRectangle.y, 527 | slaveAreaWidth, 528 | Math.floor(this.screenRectangle.height / newr)); 529 | 530 | var newHeightSum = this.screenRectangle.height - Math.floor(this.screenRectangle.height / newr); 531 | var oldHeightSum = this.screenRectangle.height 532 | var heightRatio = newHeightSum / oldHeightSum; 533 | this.adjustSlavesHeight(heightRatio, 534 | this.masterCount + 1, 535 | this.tiles.length - 1, 536 | this.screenRectangle.y + Math.floor(this.screenRectangle.height / newr), 537 | this.screenRectangle.y + this.screenRectangle.height); 538 | } 539 | 540 | } 541 | }; 542 | 543 | // removes a tile from the slave grid 544 | GridLayout.prototype.slaveRemoveTile = function () { 545 | var slaveAreaWidth = this.screenRectangle.width - this.masterAreaWidth; 546 | var removed = this.tiles.splice(this.masterCount, 1)[0]; 547 | var cr = this.getGridMeasurements(this.tiles.length - this.masterCount); 548 | var newc = cr.col; 549 | var newr = cr.row; 550 | cr = this.getGridMeasurements(this.tiles.length - this.masterCount + 1); 551 | var oldc = cr.col; 552 | var oldr = cr.row; 553 | var changedc = oldc - newc; 554 | var changedr = oldr - newr; 555 | 556 | // The new tile has different row and same column as the previous tile 557 | if (!changedc && !changedr) { 558 | var cr = this.getCoordinatesFromIndex(this.tiles.length - this.masterCount); 559 | var prevc = cr.col; 560 | var prevr = cr.row; 561 | 562 | cr = this.getCoordinatesFromIndex(this.tiles.length - this.masterCount - 1); 563 | var c = cr.col; 564 | var r = cr.row; 565 | var dc = prevc - c; 566 | var dr = prevr - r; 567 | 568 | // The removed tile has different column and same row as the new last tile 569 | if (dc > 0) { 570 | if(this.spanRows) 571 | { 572 | var oldRect = removed.rectangle; 573 | let it = this.getIndexFromCoordinates(prevc,prevr - 1); 574 | let belowRectOld = util.copyRect(this.tiles[this.tiles.length - 1 - it].rectangle); 575 | let belowRectNew = this.tiles[this.tiles.length - 1 - it].rectangle; 576 | belowRectNew.y = oldRect.y; 577 | belowRectNew.height = oldRect.height + belowRectOld.height; 578 | } 579 | else 580 | { 581 | var oldRect = removed.rectangle; 582 | var rect = this.tiles[this.masterCount].rectangle; 583 | rect.x = oldRect.x; 584 | rect.width = oldRect.width + rect.width; 585 | } 586 | } 587 | // The removed tile has different row and same column as the new last tile 588 | else if (dr > 0) { 589 | if(this.spanCols) 590 | { 591 | var oldRect = removed.rectangle; 592 | let it = this.getIndexFromCoordinates(prevc - 1,prevr); 593 | let rightRectOld = util.copyRect(this.tiles[this.tiles.length - 1 - it].rectangle); 594 | let rightRectNew = this.tiles[this.tiles.length - 1 - it].rectangle; 595 | rightRectNew.x = oldRect.x; 596 | rightRectNew.width = oldRect.width + rightRectOld.width; 597 | } 598 | else 599 | { 600 | var oldRect = removed.rectangle; 601 | var rect = this.tiles[this.masterCount].rectangle; 602 | rect.y = oldRect.y; 603 | rect.height = oldRect.height + rect.height; 604 | } 605 | } else { 606 | print("Error in SlaveRemoveTile"); 607 | } 608 | } 609 | // The number of columns changes 610 | else if (changedc) { 611 | var newWidthSum = slaveAreaWidth; 612 | var oldWidthSum = newWidthSum - removed.rectangle.width 613 | var widthRatio = newWidthSum / oldWidthSum; 614 | this.adjustSlavesWidth(widthRatio, 615 | this.masterCount, 616 | this.tiles.length - 1, 617 | this.screenRectangle.x + this.masterAreaWidth, 618 | this.screenRectangle.x + this.screenRectangle.width); 619 | } 620 | // The number of rows changes 621 | else if (changedr) { 622 | var newHeightSum = this.screenRectangle.height; 623 | var oldHeightSum = newHeightSum - removed.rectangle.height 624 | var heightRatio = newHeightSum / oldHeightSum; 625 | this.adjustSlavesHeight(heightRatio, 626 | this.masterCount, 627 | this.tiles.length - 1, 628 | this.screenRectangle.y, 629 | this.screenRectangle.y + this.screenRectangle.height); 630 | } 631 | return removed; 632 | }; 633 | 634 | // adds a tile to the master stack 635 | GridLayout.prototype.masterAddTile = function (tile) { 636 | tile.rectangle.width = Math.floor(this.masterAreaWidth / Math.min(this.masterCount, this.tiles.length)); 637 | tile.rectangle.height = this.screenRectangle.height; 638 | tile.rectangle.y = this.screenRectangle.y; 639 | tile.rectangle.x = this.screenRectangle.x + this.masterAreaWidth + tile.rectangle.width; 640 | 641 | this.tiles.splice(Math.min(this.masterCount, this.tiles.length), 0, tile) 642 | var oldWidthSum = this.masterAreaWidth + tile.rectangle.width; 643 | var newWidthSum = this.masterAreaWidth; 644 | var widthRatio = newWidthSum / oldWidthSum; 645 | this.adjustMastersWidth(widthRatio, 646 | 0, Math.min(this.masterCount, this.tiles.length - 1), 647 | this.screenRectangle.x, 648 | this.screenRectangle.x + this.masterAreaWidth); 649 | }; 650 | 651 | // removes a tile from the master stack 652 | GridLayout.prototype.masterRemoveTile = function (tileIndex) { 653 | var removed = this.tiles.splice(tileIndex, 1)[0]; 654 | var newWidthSum = this.masterAreaWidth; 655 | var oldWidthSum = this.masterAreaWidth - removed.rectangle.width; 656 | var widthRatio = newWidthSum / oldWidthSum; 657 | 658 | this.adjustMastersWidth(widthRatio, 659 | 0, 660 | Math.min(this.masterCount - 2, this.tiles.length - 1), 661 | this.screenRectangle.x, 662 | this.screenRectangle.x + this.masterAreaWidth); 663 | return removed; 664 | }; 665 | 666 | // adjusts width of specified slaveTiles keeping size ratio between tiles 667 | // should pass indices for all slaveTiles 668 | GridLayout.prototype.adjustSlavesWidth = function (ratio, firstIndex, lastIndex, leftBorder, rightBorder) { 669 | var cr = this.getGridMeasurements(lastIndex - firstIndex + 1); 670 | var col_nr = cr.col; 671 | var row_nr = cr.row; 672 | 673 | for (let r = 1; r <= row_nr; r++) { 674 | let nextX = rightBorder; 675 | for (let c = 1; c <= col_nr; c++) { 676 | if (c !== col_nr) { 677 | if (this.getIndexFromCoordinates(c, r) <= lastIndex - firstIndex) { 678 | var tile = this.tiles[lastIndex - this.getIndexFromCoordinates(c, r)]; 679 | tile.rectangle.width = Math.floor(ratio * tile.rectangle.width); 680 | tile.rectangle.x = nextX - tile.rectangle.width; 681 | nextX = tile.rectangle.x; 682 | } 683 | } else { 684 | if (this.getIndexFromCoordinates(c, r) <= lastIndex - firstIndex) { 685 | var tile = this.tiles[lastIndex - this.getIndexFromCoordinates(c, r)]; 686 | tile.rectangle.x = leftBorder; 687 | tile.rectangle.width = nextX - leftBorder; 688 | nextX = leftBorder; 689 | } 690 | else if (this.spanCols) 691 | { 692 | var tile = this.tiles[lastIndex - this.getIndexFromCoordinates(c - 1, r)]; 693 | tile.rectangle.width = tile.rectangle.width + (tile.rectangle.x - leftBorder); 694 | tile.rectangle.x = leftBorder; 695 | nextX = leftBorder; 696 | } 697 | } 698 | } 699 | } 700 | }; 701 | 702 | // adjusts height of specified slaveTiles keeping size ratio between tiles 703 | // should pass indices for all slaveTiles 704 | GridLayout.prototype.adjustSlavesHeight = function (ratio, firstIndex, lastIndex, upperBorder, lowerBorder) { 705 | var cr = this.getGridMeasurements(lastIndex - firstIndex + 1); 706 | var col_nr = cr.col; 707 | var row_nr = cr.row; 708 | 709 | for (let c = 1; c <= col_nr; c++) { 710 | let nextY = lowerBorder; 711 | for (let r = 1; r <= row_nr; r++) { 712 | if (r !== row_nr) { 713 | if (this.getIndexFromCoordinates(c, r) <= lastIndex - firstIndex) { 714 | var tile = this.tiles[lastIndex - this.getIndexFromCoordinates(c, r)]; 715 | tile.rectangle.height = Math.floor(ratio * tile.rectangle.height); 716 | tile.rectangle.y = nextY - tile.rectangle.height; 717 | nextY = tile.rectangle.y; 718 | } 719 | } else { 720 | if (this.getIndexFromCoordinates(c, r) <= lastIndex - firstIndex) { 721 | var tile = this.tiles[lastIndex - this.getIndexFromCoordinates(c, r)]; 722 | tile.rectangle.y = upperBorder; 723 | tile.rectangle.height = nextY - upperBorder; 724 | nextY = upperBorder; 725 | } 726 | else if (this.spanRows) 727 | { 728 | var tile = this.tiles[lastIndex - this.getIndexFromCoordinates(c, r - 1)]; 729 | tile.rectangle.height = tile.rectangle.height + (tile.rectangle.y - upperBorder); 730 | tile.rectangle.y = upperBorder; 731 | nextY = upperBorder; 732 | } 733 | } 734 | } 735 | } 736 | }; 737 | 738 | // adjusts width of specified masterTiles keeping size ratio between tiles 739 | // should pass indices for all master tiles 740 | GridLayout.prototype.adjustMastersWidth = function (ratio, firstIndex, lastIndex, leftBorder, rightBorder) { 741 | var nextX = leftBorder; 742 | for (var i = firstIndex; i <= lastIndex; i++) { 743 | if (i !== lastIndex) { 744 | this.tiles[i].rectangle.x = nextX; 745 | this.tiles[i].rectangle.width = Math.floor(ratio * this.tiles[i].rectangle.width); 746 | nextX = this.tiles[i].rectangle.x + this.tiles[i].rectangle.width; 747 | } else { 748 | this.tiles[i].rectangle.x = nextX; 749 | this.tiles[i].rectangle.width = rightBorder - this.tiles[i].rectangle.x; 750 | nextX = rightBorder; 751 | } 752 | } 753 | }; 754 | 755 | // returns column and row count of a grid with slaveTileCount many tiles 756 | GridLayout.prototype.getGridMeasurements = function (slaveTileCount) { 757 | if (slaveTileCount === 0) 758 | return [0, 0]; 759 | 760 | var columns = Math.ceil(Math.sqrt(slaveTileCount)); 761 | var rows = Math.ceil((slaveTileCount / columns)); 762 | return { "col": columns, "row": rows }; 763 | } 764 | 765 | //returns [column,row] for a given index 766 | //col and row starting at 1 767 | //index starting at 0 768 | GridLayout.prototype.getCoordinatesFromIndex = function (i) { 769 | let a = Math.ceil(Math.sqrt(i + 1)) - 1; 770 | let b = a * a - 1; 771 | 772 | var col = 0; 773 | var row = 0; 774 | 775 | if (i - b <= a) { 776 | col = a + 1; 777 | row = i - b; 778 | } else { 779 | row = a + 1; 780 | col = (i - b) - a; 781 | } 782 | return { "col": col, "row": row }; 783 | }; 784 | 785 | //returns index for a given column and row 786 | //col and row starting at 1 787 | //index starting at 0 788 | GridLayout.prototype.getIndexFromCoordinates = function (col, row) { 789 | let a = Math.max(col, row) - 1; 790 | let b = a * a - 1; 791 | let offset = 0; 792 | if (row > a) { 793 | offset += a; 794 | offset += col; 795 | } else { 796 | offset += row; 797 | } 798 | let c = b + offset; 799 | return c; 800 | }; 801 | --------------------------------------------------------------------------------