33 Commits
v0.4 ... v0.5

Author SHA1 Message Date
Peter Fajdiga
43f425c868 config: set overscroll default to 0 2023-09-22 17:45:11 +02:00
Peter Fajdiga
5c5127f7ce bump version to 0.5 2023-09-22 17:42:16 +02:00
Peter Fajdiga
8664f05998 ClientManager: prevent glitchy placement when maximizing a pinned client 2023-09-22 17:36:54 +02:00
Peter Fajdiga
e4f6a32d42 make pinning work with kwin-tiled windows of any frameGeometry 2023-09-22 17:25:32 +02:00
Peter Fajdiga
2882bb8d5d base pinning on KwinClient.tile instead of frameGeometry 2023-09-22 14:40:25 +02:00
Peter Fajdiga
5ef71c92ce Column: prevent stacking columns with unshadeable windows 2023-09-22 11:15:35 +02:00
Peter Fajdiga
de59979a6e Tiled: restructure branches in moveResizedChanged handler 2023-09-22 10:54:12 +02:00
Peter Fajdiga
71d7d60837 ClientManager: use kwinClient parameters 2023-09-20 20:14:30 +02:00
Peter Fajdiga
7f44d23dd0 kwin.d.ts: merge AbstractClient and TopLevel 2023-09-20 20:13:23 +02:00
Peter Fajdiga
aceffae5f9 config.ui: add tooltips 2023-09-20 19:53:33 +02:00
Peter Fajdiga
09e5eaca88 add re-maximize setting 2023-09-19 21:15:01 +02:00
Peter Fajdiga
734dd8a4cc fix re-full-screen bug with Kate on Wayland 2023-09-19 21:05:59 +02:00
Peter Fajdiga
aeabb396f9 Revert "remove re-maximize and re-fullscreen functionality" (fixes #18)
This reverts commit 4f06f17b
2023-09-19 21:04:27 +02:00
Peter Fajdiga
05b9ebc325 handle grid changes during a full-screen window 2023-09-19 18:58:50 +02:00
Peter Fajdiga
4728afb5ea Grid: set preferred width in increaseColumnWidth and decreaseColumnWidth 2023-09-17 19:17:50 +02:00
Peter Fajdiga
653cb20e43 ClientWrapper: remove obsolete TODO 2023-09-17 19:17:50 +02:00
Peter Fajdiga
0cb2b68bf6 implement workaround that fixes #19 2023-09-17 19:17:50 +02:00
Peter Fajdiga
b8b8900754 Tiled: call World.do only if condition satisfied 2023-09-17 08:00:32 +02:00
Peter Fajdiga
14c006b5b1 Window.onFullScreenChanged: remove check for isFocused 2023-09-16 15:34:12 +02:00
Peter Fajdiga
9fe6be9b91 implement pinning (resolves #8) 2023-09-16 14:55:38 +02:00
Peter Fajdiga
97d1592318 add option for setting keepAbove on floating windows (fixes #15) 2023-09-16 14:51:46 +02:00
Peter Fajdiga
cc8cc04b05 WindowRuleEnforcer.shouldTile: don't tile transient windows 2023-09-10 18:18:48 +02:00
Peter Fajdiga
cc74d3610a WindowRuleEnforcer.shouldTile: explode condition 2023-09-10 18:17:25 +02:00
Peter Fajdiga
088725402e WindowRuleEnforcer: refactor function -> private static 2023-09-10 17:31:54 +02:00
Peter Fajdiga
25b7507b30 fix incorrect movement of transient windows (bug introduced in f4e9822f29) 2023-09-09 15:01:59 +02:00
Peter Fajdiga
9b32caafdc config/loader.ts: declare return type of loadConfig 2023-09-09 12:58:56 +02:00
Peter Fajdiga
668d579d63 prevent resizing new floating windows 2023-09-09 11:08:01 +02:00
Peter Fajdiga
f4e9822f29 ClientManager.addClient: avoid constructing state twice 2023-09-09 09:19:37 +02:00
Peter Fajdiga
bdb0a4aeb0 ClientWrapper: refactor constructor parameter initialState -> constructInitialState 2023-09-09 09:11:01 +02:00
Peter Fajdiga
dde1a12fce ClientManager.addClient: use variable kwinClient 2023-09-09 09:01:01 +02:00
Peter Fajdiga
c57c8391fb ClientWrapper: move tiling- and floating-specific functions to ClientState.Floating and ClientState.Tiled 2023-09-08 15:40:16 +02:00
Peter Fajdiga
ec64b47ceb Tiled: add comment regarding kwinClient.fullScreen 2023-09-06 22:53:36 +02:00
Peter Fajdiga
b055345e48 readme: update key bindings 2023-09-04 20:24:17 +02:00
26 changed files with 723 additions and 262 deletions

View File

@@ -28,9 +28,9 @@ Here's the default ones:
| --- | --- |
| Meta+Space | Toggle floating |
| Meta+A | Move focus left |
| Meta+D | Move focus right |
| Meta+W | Move focus up |
| Meta+S | Move focus down |
| Meta+D | Move focus right (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+W | Move focus up (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+S | Move focus down (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+Home | Move focus to start |
| Meta+End | Move focus to end |
| Meta+Shift+A | Move window left (Moves window out of and into columns) |
@@ -53,7 +53,7 @@ Here's the default ones:
| Meta+Alt+PgDown | Scroll right |
| Meta+Alt+Home | Scroll to start |
| Meta+Alt+End | Scroll to end |
| Meta+[N] | Move focus to column N |
| Meta+[N] | Move focus to column N (Clashes with default KDE shortcuts, may require manual remapping) |
| Meta+Shift+[N] | Move window to column N (Requires manual remapping according to your keyboard layout, e.g. Meta+Shift+1 -> Meta+!) |
| Meta+Ctrl+Shift+[N] | Move column to position N (Requires manual remapping according to your keyboard layout, e.g. Meta+Ctrl+Shift+1 -> Meta+Ctrl+!) |
| Meta+Ctrl+Shift+F[N] | Move column to desktop N |

View File

@@ -15,15 +15,13 @@
<attribute name="title">
<string>General</string>
</attribute>
<layout class="QGridLayout" name="layout_tab_general" columnstretch="0,1">
<layout class="QFormLayout" name="layout_tab_general">
<item row="0" column="0">
<widget class="QLabel" name="label_gapsOuterTop">
<property name="text">
<string>Top margin:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="0" column="1">
@@ -45,9 +43,6 @@
<property name="text">
<string>Bottom margin:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
@@ -69,9 +64,6 @@
<property name="text">
<string>Left margin:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="2" column="1">
@@ -93,9 +85,6 @@
<property name="text">
<string>Right margin:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="3" column="1">
@@ -117,9 +106,6 @@
<property name="text">
<string>Horizontal gaps between windows:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="4" column="1">
@@ -141,9 +127,6 @@
<property name="text">
<string>Vertical gaps between windows:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="5" column="1">
@@ -165,9 +148,6 @@
<property name="text">
<string>Overscroll amount:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="6" column="1">
@@ -189,9 +169,6 @@
<property name="text">
<string>Manual scroll step size:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="7" column="1">
@@ -209,36 +186,104 @@
</item>
<item row="8" column="1">
<spacer name="separator_behavior">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>12</height>
</size>
</property>
</spacer>
</item>
<item row="9" column="0">
<widget class="QLabel" name="label_behavior">
<property name="text">
<string>Behavior:</string>
</property>
</widget>
</item>
<item row="9" column="1">
<widget class="QCheckBox" name="kcfg_untileOnDrag">
<property name="text">
<string>Un-tile windows by dragging them</string>
</property>
</widget>
</item>
<item row="9" column="1">
<item row="10" column="1">
<widget class="QCheckBox" name="kcfg_stackColumnsByDefault">
<property name="text">
<string>Stack columns by default</string>
</property>
<property name="toolTip">
<string>New columns start in stacked mode</string>
</property>
</widget>
</item>
<item row="10" column="1">
<item row="11" column="1">
<widget class="QCheckBox" name="kcfg_resizeNeighborColumn">
<property name="text">
<string>Resize neighbor column on edge resize</string>
</property>
<property name="toolTip">
<string>When resizing a column by dragging its edge, also inversely resize the column on the other side of the edge</string>
</property>
</widget>
</item>
<item row="12" column="1">
<widget class="QCheckBox" name="kcfg_reMaximize">
<property name="text">
<string>Re-maximize tiled windows</string>
</property>
<property name="toolTip">
<string>Restore maximized and full-screen states of tiled windows on focus</string>
</property>
</widget>
</item>
<item row="11" column="0" colspan="2">
<spacer name="bottomSpacer_tab_general">
<item row="13" column="1">
<spacer name="separator_layering">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>12</height>
</size>
</property>
</spacer>
</item>
<item row="14" column="0">
<widget class="QLabel" name="label_layering">
<property name="text">
<string>Layering mode:</string>
</property>
</widget>
</item>
<item row="14" column="1">
<widget class="QRadioButton" name="kcfg_tiledKeepBelow">
<property name="text">
<string>Keep tiled windows below</string>
</property>
</widget>
</item>
<item row="15" column="1">
<widget class="QRadioButton" name="kcfg_floatingKeepAbove">
<property name="text">
<string>Keep floating windows above</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_windowRules">

View File

@@ -9,7 +9,7 @@
}],
"Id": "karousel",
"ServiceTypes": ["KWin/Script"],
"Version": "0.4",
"Version": "0.5",
"License": "GPLv3",
"Website": "https://github.com/peterfajdiga/karousel",
"BugReportUrl": "https://github.com/peterfajdiga/karousel/issues"

View File

@@ -10,5 +10,8 @@ type Config = {
untileOnDrag: boolean,
stackColumnsByDefault: boolean,
resizeNeighborColumn: boolean,
reMaximize: boolean,
tiledKeepBelow: boolean,
floatingKeepAbove: boolean,
windowRules: string,
};

View File

@@ -85,7 +85,7 @@ const configDef = [
{
"name": "overscroll",
"type": "UInt",
"default": 18
"default": 0
},
{
"name": "manualScrollStep",
@@ -107,6 +107,21 @@ const configDef = [
"type": "Bool",
"default": false
},
{
"name": "reMaximize",
"type": "Bool",
"default": false
},
{
"name": "tiledKeepBelow",
"type": "Bool",
"default": true
},
{
"name": "floatingKeepAbove",
"type": "Bool",
"default": false
},
{
"name": "windowRules",
"type": "String",

View File

@@ -1,4 +1,4 @@
function loadConfig() {
function loadConfig(): Config {
const config: any = {};
for (const entry of configDef) {
config[entry.name] = KWin.readConfig(entry.name, entry.default);

64
src/extern/kwin.d.ts vendored
View File

@@ -16,56 +16,32 @@ declare const workspace: {
// Signals
currentDesktopChanged: QSignal<[oldDesktopNumber: number]>
clientAdded: QSignal<[KwinClient]>;
clientRemoved: QSignal<[AbstractClient]>;
clientMinimized: QSignal<[AbstractClient]>;
clientUnminimized: QSignal<[AbstractClient]>;
clientMaximizeSet: QSignal<[AbstractClient, horizontally: boolean, vertically: boolean]>;
clientActivated: QSignal<[AbstractClient]>;
clientRemoved: QSignal<[KwinClient]>;
clientMinimized: QSignal<[KwinClient]>;
clientUnminimized: QSignal<[KwinClient]>;
clientMaximizeSet: QSignal<[KwinClient, horizontally: boolean, vertically: boolean]>;
clientActivated: QSignal<[KwinClient]>;
numberDesktopsChanged: QSignal<[oldNumberOfVirtualDesktops: number]>;
currentActivityChanged: QSignal<[newActivity: string]>;
virtualScreenSizeChanged: QSignal<[void]>;
// Functions
clientArea(option: ClientAreaOption, screenNumber: number, desktopNumber: number);
clientList(): TopLevel[];
clientList(): KwinClient[];
};
type Tile = any;
interface AbstractClient {
interface KwinClient {
// Read-only Properties
readonly shadeable: boolean;
readonly caption: string;
readonly minSize: QSize;
readonly transient: boolean;
readonly transientFor: AbstractClient;
readonly transientFor: KwinClient;
readonly move: boolean;
readonly resize: boolean;
readonly resizeable: boolean;
// Read-write Properties
fullScreen: boolean;
activities: string[]; // empty array means all activities
keepBelow: boolean;
shade: boolean;
minimized: boolean;
tile: Tile;
// Signals
fullScreenChanged: QSignal<[void]>;
desktopChanged: QSignal<[void]>;
activitiesChanged: QSignal<[AbstractClient]>;
captionChanged: QSignal<[void]>;
tileChanged: QSignal<[Tile]>;
moveResizedChanged: QSignal<[void]>;
moveResizeCursorChanged: QSignal<[void]>;
clientStartUserMovedResized: QSignal<[void]>;
// Functions
setMaximize(vertically: boolean, horizontally: boolean): void;
}
interface TopLevel extends AbstractClient {
// Read-only Properties
readonly screen: number;
readonly resourceClass: QByteArray;
readonly dock: boolean;
@@ -73,11 +49,27 @@ interface TopLevel extends AbstractClient {
readonly managed: boolean;
// Read-write Properties
fullScreen: boolean;
activities: string[]; // empty array means all activities
keepAbove: boolean;
keepBelow: boolean;
shade: boolean;
minimized: boolean;
frameGeometry: QRect;
desktop: number; // -1 means all desktops
tile: Tile;
// Signals
frameGeometryChanged: QSignal<[TopLevel, oldGeometry: QRect]>;
}
fullScreenChanged: QSignal<[void]>;
desktopChanged: QSignal<[void]>;
activitiesChanged: QSignal<[KwinClient]>;
captionChanged: QSignal<[void]>;
tileChanged: QSignal<[Tile]>;
moveResizedChanged: QSignal<[void]>;
moveResizeCursorChanged: QSignal<[void]>;
clientStartUserMovedResized: QSignal<[void]>;
frameGeometryChanged: QSignal<[KwinClient, oldGeometry: QRect]>;
interface KwinClient extends TopLevel {}
// Functions
setMaximize(vertically: boolean, horizontally: boolean): void;
}

View File

@@ -181,7 +181,7 @@ class Column {
}
public arrange(x: number) {
if (this.stacked && this.windows.length() >= 2) {
if (this.stacked && this.windows.length() >= 2 && this.canStack()) {
this.arrangeStacked(x);
return;
}
@@ -228,6 +228,15 @@ class Column {
this.grid.desktop.onLayoutChanged();
}
private canStack() {
for (const window of this.windows.iterator()) {
if (!window.client.kwinClient.shadeable) {
return false;
}
}
return true;
}
public isVisible(scrollPos: Desktop.ScrollPos, fullyVisible: boolean) {
if (fullyVisible) {
return this.getLeft() >= scrollPos.getLeft() &&

View File

@@ -1,30 +1,35 @@
class Desktop {
public readonly grid: Grid;
public readonly desktopNumber: number;
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
private scrollX: number;
private dirty: boolean;
private dirtyPins: boolean;
public clientArea: QRect;
public tilingArea: QRect;
constructor(desktopNumber: number, config: Desktop.Config, layoutConfig: LayoutConfig) {
constructor(desktopNumber: number, pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig) {
this.pinManager = pinManager;
this.config = config;
this.scrollX = 0;
this.dirty = false;
this.dirty = true;
this.dirtyPins = true;
this.desktopNumber = desktopNumber;
this.grid = new Grid(this, layoutConfig);
this.clientArea = Desktop.getClientArea(desktopNumber);
this.tilingArea = Desktop.getTilingArea(this.clientArea, config);
this.tilingArea = Desktop.getTilingArea(this.clientArea, desktopNumber, pinManager, config);
}
private updateArea() {
const newClientArea = Desktop.getClientArea(this.desktopNumber);
if (newClientArea === this.clientArea) {
if (newClientArea === this.clientArea && !this.dirtyPins) {
return;
}
this.clientArea = newClientArea;
this.tilingArea = Desktop.getTilingArea(newClientArea, this.config);
this.tilingArea = Desktop.getTilingArea(newClientArea, this.desktopNumber, this.pinManager, this.config);
this.dirty = true;
this.dirtyPins = false;
this.grid.onScreenSizeChanged();
this.autoAdjustScroll();
}
@@ -33,12 +38,17 @@ class Desktop {
return workspace.clientArea(ClientAreaOption.PlacementArea, 0, desktopNumber);
}
private static getTilingArea(clientArea: QRect, config: Desktop.Config) {
private static getTilingArea(clientArea: QRect, desktopNumber: number, pinManager: PinManager, config: Desktop.Config) {
const availableSpace = pinManager.getAvailableSpace(desktopNumber, clientArea);
const top = availableSpace.top + config.marginTop;
const bottom = availableSpace.bottom - config.marginBottom;
const left = availableSpace.left + config.marginLeft;
const right = availableSpace.right - config.marginRight;
return Qt.rect(
clientArea.x + config.marginLeft,
clientArea.y + config.marginTop,
clientArea.width - config.marginLeft - config.marginRight,
clientArea.height - config.marginTop - config.marginBottom,
left,
top,
right - left + 1,
bottom - top + 1,
)
}
@@ -149,6 +159,11 @@ class Desktop {
this.dirty = true;
}
public onPinsChanged() {
this.dirty = true;
this.dirtyPins = true;
}
public destroy() {
this.grid.destroy();
}

View File

@@ -149,7 +149,7 @@ class Grid {
public increaseColumnWidth(column: Column) {
const scrollPos = this.desktop.getScrollPosForColumn(column);
if (this.width < scrollPos.width) {
column.adjustWidth(scrollPos.width - this.width, false);
column.adjustWidth(scrollPos.width - this.width, true);
return;
}
@@ -178,7 +178,7 @@ class Grid {
public decreaseColumnWidth(column: Column) {
const scrollPos = this.desktop.getScrollPosForColumn(column);
if (this.width <= scrollPos.width) {
column.setWidth(Math.round(column.getWidth() / 2), false);
column.setWidth(Math.round(column.getWidth() / 2), true);
return;
}

View File

@@ -3,4 +3,7 @@ type LayoutConfig = {
gapsInnerVertical: number,
stackColumnsByDefault: boolean,
resizeNeighborColumn: boolean,
reMaximize: boolean,
tiledKeepBelow: boolean,
maximizedKeepAbove: boolean,
};

View File

@@ -2,11 +2,17 @@ class Window {
public column: Column;
public readonly client: ClientWrapper;
public height: number;
public readonly focusedState: WindowState;
private skipArrange: boolean;
constructor(client: ClientWrapper, column: Column) {
this.client = client;
this.height = client.kwinClient.frameGeometry.height;
this.focusedState = {
fullScreen: false,
maximizedHorizontally: false,
maximizedVertically: false,
};
this.skipArrange = false;
this.column = column;
column.onWindowAdded(this);
@@ -26,7 +32,23 @@ class Window {
// window is maximized, fullscreen, or being manually resized, prevent fighting with the user
return;
}
this.client.place(x, y, width, height);
let maximized = false;
if (this.column.grid.config.reMaximize && this.isFocused()) {
// do this here rather than in `onFocused` to ensure it happens after placement
// (otherwise placement may not happen at all)
if (this.focusedState.maximizedVertically || this.focusedState.maximizedHorizontally) {
this.client.setMaximize(this.focusedState.maximizedVertically, this.focusedState.maximizedHorizontally);
maximized = true;
}
if (this.focusedState.fullScreen) {
this.client.setFullScreen(true);
maximized = true;
}
}
if (!maximized) {
this.client.place(x, y, width, height);
}
}
public focus() {
@@ -57,14 +79,31 @@ class Window {
public onMaximizedChanged(horizontally: boolean, vertically: boolean) {
const maximized = horizontally || vertically;
this.skipArrange = maximized;
this.client.kwinClient.keepBelow = !maximized;
if (this.column.grid.config.tiledKeepBelow) {
this.client.kwinClient.keepBelow = !maximized;
}
if (this.column.grid.config.maximizedKeepAbove) {
this.client.kwinClient.keepAbove = maximized;
}
if (this.isFocused()) {
this.focusedState.maximizedHorizontally = horizontally;
this.focusedState.maximizedVertically = vertically;
}
this.column.grid.desktop.onLayoutChanged();
}
public onFullScreenChanged(fullScreen: boolean) {
this.skipArrange = fullScreen;
if (this.isFocused()) {
if (this.column.grid.config.tiledKeepBelow) {
this.client.kwinClient.keepBelow = !fullScreen;
}
if (this.column.grid.config.maximizedKeepAbove) {
this.client.kwinClient.keepAbove = fullScreen;
}
if (this.isFocused()) {
this.focusedState.fullScreen = fullScreen;
}
this.column.grid.desktop.onLayoutChanged();
}
public onUserResize(oldGeometry: QRect, resizeNeighborColumn: boolean) {
@@ -102,3 +141,9 @@ class Window {
this.column.onWindowRemoved(this, passFocus);
}
}
type WindowState = {
fullScreen: boolean,
maximizedHorizontally: boolean,
maximizedVertically: boolean,
}

View File

@@ -5,7 +5,7 @@ class ClientMatcher {
this.rules = rules;
}
public matches(kwinClient: TopLevel) {
public matches(kwinClient: KwinClient) {
const rule = this.rules.get(kwinClient.resourceClass);
if (rule === undefined) {
return false;

View File

@@ -4,20 +4,24 @@ class WindowRuleEnforcer {
private readonly followCaption: Set<string>;
constructor(windowRules: WindowRule[]) {
const [mapFloat, mapTile] = createWindowRuleMaps(windowRules);
const [mapFloat, mapTile] = WindowRuleEnforcer.createWindowRuleMaps(windowRules);
this.preferFloating = new ClientMatcher(mapFloat);
this.preferTiling = new ClientMatcher(mapTile);
this.followCaption = new Set([...mapFloat.keys(), ...mapTile.keys()]);
}
public shouldTile(kwinClient: TopLevel) {
public shouldTile(kwinClient: KwinClient) {
return Clients.canTileNow(kwinClient) && (
this.preferTiling.matches(kwinClient) ||
kwinClient.normalWindow && kwinClient.managed && !this.preferFloating.matches(kwinClient)
this.preferTiling.matches(kwinClient) || (
kwinClient.normalWindow &&
!kwinClient.transient &&
kwinClient.managed &&
!this.preferFloating.matches(kwinClient)
)
);
}
public initClientSignalManager(world: World, kwinClient: TopLevel) {
public initClientSignalManager(world: World, kwinClient: KwinClient) {
if (!this.followCaption.has(kwinClient.resourceClass)) {
return null;
}
@@ -36,50 +40,50 @@ class WindowRuleEnforcer {
});
return manager;
}
}
function createWindowRuleMaps(windowRules: WindowRule[]) {
const mapFloat = new Map<string, string[]>();
const mapTile = new Map<string, string[]>();
for (const windowRule of windowRules) {
const map = windowRule.tile ? mapTile : mapFloat;
let captions = map.get(windowRule.class);
if (captions === undefined) {
captions = [];
map.set(windowRule.class, captions);
private static createWindowRuleMaps(windowRules: WindowRule[]) {
const mapFloat = new Map<string, string[]>();
const mapTile = new Map<string, string[]>();
for (const windowRule of windowRules) {
const map = windowRule.tile ? mapTile : mapFloat;
let captions = map.get(windowRule.class);
if (captions === undefined) {
captions = [];
map.set(windowRule.class, captions);
}
if (windowRule.caption !== undefined) {
captions.push(windowRule.caption);
}
}
if (windowRule.caption !== undefined) {
captions.push(windowRule.caption);
return [
WindowRuleEnforcer.createWindowRuleRegexMap(mapFloat),
WindowRuleEnforcer.createWindowRuleRegexMap(mapTile),
];
}
private static createWindowRuleRegexMap(windowRuleMap: Map<string, string[]>) {
const regexMap = new Map<string, RegExp>;
for (const [k, v] of windowRuleMap) {
regexMap.set(k, WindowRuleEnforcer.joinRegexes(v));
}
return regexMap;
}
return [
createWindowRuleRegexMap(mapFloat),
createWindowRuleRegexMap(mapTile),
];
}
private static joinRegexes(regexes: string[]) {
if (regexes.length == 0) {
return new RegExp("");
}
function createWindowRuleRegexMap(windowRuleMap: Map<string, string[]>) {
const regexMap = new Map<string, RegExp>;
for (const [k, v] of windowRuleMap) {
regexMap.set(k, joinRegexes(v));
}
return regexMap;
}
if (regexes.length == 1) {
return new RegExp("^" + regexes[0] + "$");
}
function joinRegexes(regexes: string[]) {
if (regexes.length == 0) {
return new RegExp("");
const joinedRegexes = regexes.map(WindowRuleEnforcer.wrapParens).join("|");
return new RegExp("^" + joinedRegexes + "$");
}
if (regexes.length == 1) {
return new RegExp("^" + regexes[0] + "$");
private static wrapParens(str: string) {
return "(" + str + ")";
}
const joinedRegexes = regexes.map(wrapParens).join("|");
return new RegExp("^" + joinedRegexes + "$");
}
function wrapParens(str: string) {
return "(" + str + ")";
}

View File

@@ -7,3 +7,8 @@ function clamp(value: number, min: number, max: number) {
}
return value;
}
function union<T>(array0: T[], array1: T[]) {
const set = new Set([...array0, ...array1]);
return [...set];
}

View File

@@ -16,25 +16,25 @@ function initWorkspaceSignalHandlers(world: World) {
});
});
manager.connect(workspace.clientRemoved, (kwinClient: AbstractClient) => {
manager.connect(workspace.clientRemoved, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.removeClient(kwinClient, true);
});
});
manager.connect(workspace.clientMinimized, (kwinClient: AbstractClient) => {
manager.connect(workspace.clientMinimized, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.minimizeClient(kwinClient);
});
});
manager.connect(workspace.clientUnminimized, (kwinClient: AbstractClient) => {
manager.connect(workspace.clientUnminimized, (kwinClient: KwinClient) => {
world.do((clientManager, desktopManager) => {
clientManager.unminimizeClient(kwinClient);
});
});
manager.connect(workspace.clientMaximizeSet, (kwinClient: AbstractClient, horizontally: boolean, vertically: boolean) => {
manager.connect(workspace.clientMaximizeSet, (kwinClient: KwinClient, horizontally: boolean, vertically: boolean) => {
if ((horizontally || vertically) && kwinClient.tile !== null) {
kwinClient.tile = null;
}
@@ -43,7 +43,7 @@ function initWorkspaceSignalHandlers(world: World) {
});
});
manager.connect(workspace.clientActivated, (kwinClient: AbstractClient) => {
manager.connect(workspace.clientActivated, (kwinClient: KwinClient) => {
if (kwinClient === null) {
return;
}

View File

@@ -1,13 +1,17 @@
class ClientManager {
private readonly world: World;
private readonly config: ClientManager.Config;
private readonly desktopManager: DesktopManager;
private readonly clientMap: Map<AbstractClient, ClientWrapper>;
private lastFocusedClient: AbstractClient|null;
private readonly pinManager: PinManager;
private readonly clientMap: Map<KwinClient, ClientWrapper>;
private lastFocusedClient: KwinClient|null;
private readonly windowRuleEnforcer: WindowRuleEnforcer;
constructor(config: Config, world: World, desktopManager: DesktopManager) {
constructor(config: Config, world: World, desktopManager: DesktopManager, pinManager: PinManager) {
this.world = world;
this.config = { keepAbove: config.floatingKeepAbove };
this.desktopManager = desktopManager;
this.pinManager = pinManager;
this.clientMap = new Map();
this.lastFocusedClient = null;
@@ -21,25 +25,29 @@ class ClientManager {
this.windowRuleEnforcer = new WindowRuleEnforcer(parsedWindowRules);
}
public addClient(kwinClient: TopLevel) {
public addClient(kwinClient: KwinClient) {
console.assert(!this.hasClient(kwinClient));
let constructState: (client: ClientWrapper) => ClientState.State;
if (kwinClient.dock) {
constructState = () => new ClientState.Docked(this.world, kwinClient);
} else if (this.windowRuleEnforcer.shouldTile(kwinClient)) {
const grid = this.desktopManager.getDesktopForClient(kwinClient).grid;
constructState = (client: ClientWrapper) => new ClientState.Tiled(this.world, client, grid);
} else {
constructState = (client: ClientWrapper) => new ClientState.Floating(this.world, client, this.config, false);
}
const client = new ClientWrapper(
kwinClient,
new ClientState.Floating(null),
constructState,
this.findTransientFor(kwinClient),
this.windowRuleEnforcer.initClientSignalManager(this.world, kwinClient),
);
this.clientMap.set(kwinClient, client);
if (kwinClient.dock) {
client.stateManager.setState(() => new ClientState.Docked(this.world, kwinClient), false);
} else if (this.windowRuleEnforcer.shouldTile(kwinClient)) {
const grid = this.desktopManager.getDesktopForClient(client.kwinClient).grid;
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
}
}
public removeClient(kwinClient: AbstractClient, passFocus: boolean) {
public removeClient(kwinClient: KwinClient, passFocus: boolean) {
console.assert(this.hasClient(kwinClient));
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
@@ -49,7 +57,7 @@ class ClientManager {
this.clientMap.delete(kwinClient);
}
private findTransientFor(kwinClient: AbstractClient) {
private findTransientFor(kwinClient: KwinClient) {
if (!kwinClient.transient) {
return null;
}
@@ -62,7 +70,7 @@ class ClientManager {
return transientFor;
}
public minimizeClient(kwinClient: AbstractClient) {
public minimizeClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
@@ -72,18 +80,18 @@ class ClientManager {
}
}
public unminimizeClient(kwinClient: AbstractClient) {
public unminimizeClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientState.TiledMinimized) {
const grid = this.desktopManager.getDesktopForClient(client.kwinClient).grid;
const grid = this.desktopManager.getDesktopForClient(kwinClient).grid;
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
}
}
public tileClient(kwinClient: AbstractClient) {
public tileClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
@@ -91,41 +99,66 @@ class ClientManager {
if (client.stateManager.getState() instanceof ClientState.Tiled) {
return;
}
const grid = this.desktopManager.getDesktopForClient(client.kwinClient).grid;
const grid = this.desktopManager.getDesktopForClient(kwinClient).grid;
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
}
public untileClient(kwinClient: AbstractClient) {
public untileClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
if (client.stateManager.getState() instanceof ClientState.Tiled) {
client.stateManager.setState(() => new ClientState.Floating(client), false);
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
}
}
public toggleFloatingClient(kwinClient: TopLevel) {
public pinClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
client.stateManager.setState(() => new ClientState.Pinned(this.world, this.pinManager, this.desktopManager, kwinClient, this.config), false);
this.pinManager.addClient(kwinClient);
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
desktop.onPinsChanged();
}
}
public unpinClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
console.assert(client.stateManager.getState() instanceof ClientState.Pinned);
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, false), false);
this.pinManager.removeClient(kwinClient);
for (const desktop of this.desktopManager.getDesktopsForClient(kwinClient)) {
desktop.onPinsChanged();
}
}
public toggleFloatingClient(kwinClient: KwinClient) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return;
}
const clientState = client.stateManager.getState();
if (clientState instanceof ClientState.Floating && Clients.canTileEver(kwinClient)) {
if ((clientState instanceof ClientState.Floating || clientState instanceof ClientState.Pinned) && Clients.canTileEver(kwinClient)) {
Clients.makeTileable(kwinClient);
const grid = this.desktopManager.getDesktopForClient(client.kwinClient).grid;
const grid = this.desktopManager.getDesktopForClient(kwinClient).grid;
client.stateManager.setState(() => new ClientState.Tiled(this.world, client, grid), false);
} else if (clientState instanceof ClientState.Tiled) {
client.stateManager.setState(() => new ClientState.Floating(client), false);
client.stateManager.setState(() => new ClientState.Floating(this.world, client, this.config, true), false);
}
}
public hasClient(kwinClient: AbstractClient) {
public hasClient(kwinClient: KwinClient) {
return this.clientMap.has(kwinClient);
}
public onClientFocused(kwinClient: AbstractClient) {
public onClientFocused(kwinClient: KwinClient) {
this.lastFocusedClient = kwinClient;
const window = this.findTiledWindow(kwinClient, true);
if (window !== null) {
@@ -133,7 +166,7 @@ class ClientManager {
}
}
public findTiledWindow(kwinClient: AbstractClient, followTransient: boolean) {
public findTiledWindow(kwinClient: KwinClient, followTransient: boolean) {
const client = this.clientMap.get(kwinClient);
if (client === undefined) {
return null;
@@ -163,3 +196,9 @@ class ClientManager {
this.removeAllClients();
}
}
namespace ClientManager {
export type Config = {
keepAbove: boolean,
}
}

View File

@@ -1,30 +1,30 @@
class ClientWrapper {
public readonly kwinClient: TopLevel;
public readonly kwinClient: KwinClient;
public readonly stateManager: ClientState.Manager;
public transientFor: ClientWrapper | null;
private readonly transients: ClientWrapper[];
private readonly signalManager: SignalManager;
private readonly rulesSignalManager: SignalManager | null;
public preferredWidth: number;
private readonly manipulatingGeometry: Doer;
private lastPlacement: QRect | null; // workaround for issue #19
constructor(
kwinClient: TopLevel,
initialState: ClientState.State,
kwinClient: KwinClient,
constructInitialState: (client: ClientWrapper) => ClientState.State,
transientFor: ClientWrapper | null,
rulesSignalManager: SignalManager | null,
) {
this.kwinClient = kwinClient;
this.stateManager = new ClientState.Manager(initialState);
this.transientFor = transientFor;
this.transients = [];
if (transientFor !== null) {
transientFor.addTransient(this);
}
this.signalManager = ClientWrapper.initSignalManager(this);
this.rulesSignalManager = rulesSignalManager;
this.preferredWidth = kwinClient.frameGeometry.width;
this.manipulatingGeometry = new Doer();
this.lastPlacement = null;
this.stateManager = new ClientState.Manager(constructInitialState(this));
}
public place(x: number, y: number, width: number, height: number) {
@@ -33,12 +33,12 @@ class ClientWrapper {
// window is being manually resized, prevent fighting with the user
return;
}
this.kwinClient.frameGeometry = Qt.rect(x, y, width, height);
this.lastPlacement = Qt.rect(x, y, width, height);
this.kwinClient.frameGeometry = this.lastPlacement;
});
}
private moveTransient(dx: number, dy: number, desktopNumber: number) {
// TODO: prevent moving off the grid
if (this.stateManager.getState() instanceof ClientState.Floating) {
if (this.kwinClient.desktop === desktopNumber) {
const frame = this.kwinClient.frameGeometry;
@@ -56,6 +56,12 @@ class ClientWrapper {
}
}
public moveTransients(dx: number, dy: number) {
for (const transient of this.transients) {
transient.moveTransient(dx, dy, this.kwinClient.desktop);
}
}
public focus() {
workspace.activeClient = this.kwinClient;
}
@@ -86,41 +92,13 @@ class ClientWrapper {
return this.kwinClient.shade;
}
public isManipulatingGeometry() {
public isManipulatingGeometry(newGeometry: QRect | null) {
if (newGeometry !== null && newGeometry === this.lastPlacement) {
return true;
}
return this.manipulatingGeometry.isDoing();
}
public prepareForTiling() {
this.kwinClient.keepBelow = true;
this.setFullScreen(false);
if (this.kwinClient.tile !== null) {
this.setMaximize(false, true); // disable quick tile mode
}
this.setMaximize(false, false);
}
public restoreAfterTiling(screenSize: QRect) {
this.kwinClient.keepBelow = false;
this.setShade(false);
this.setFullScreen(false);
if (this.kwinClient.tile === null) {
this.setMaximize(false, false);
}
this.ensureVisible(screenSize);
}
public prepareForFloating() {
const placementArea = workspace.clientArea(ClientAreaOption.PlacementArea, this.kwinClient.screen, this.kwinClient.desktop);
const clientRect = this.kwinClient.frameGeometry;
const width = this.preferredWidth;
this.place(
clientRect.x,
clientRect.y,
width,
Math.min(clientRect.height, Math.round(placementArea.height / 2)),
);
}
private addTransient(transient: ClientWrapper) {
this.transients.push(transient);
}
@@ -153,7 +131,6 @@ class ClientWrapper {
public destroy(passFocus: boolean) {
this.stateManager.destroy(passFocus);
this.signalManager.destroy();
if (this.rulesSignalManager !== null) {
this.rulesSignalManager.destroy();
}
@@ -164,23 +141,4 @@ class ClientWrapper {
transient.transientFor = null;
}
}
private static initSignalManager(client: ClientWrapper) {
const manager = new SignalManager();
manager.connect(client.kwinClient.frameGeometryChanged, (kwinClient: TopLevel, oldGeometry: QRect) => {
if (client.stateManager.getState() instanceof ClientState.Tiled) {
const newGeometry = client.kwinClient.frameGeometry;
const oldCenterX = oldGeometry.x + oldGeometry.width/2;
const oldCenterY = oldGeometry.y + oldGeometry.height/2;
const newCenterX = newGeometry.x + newGeometry.width/2;
const newCenterY = newGeometry.y + newGeometry.height/2;
const dx = Math.round(newCenterX - oldCenterX);
const dy = Math.round(newCenterY - oldCenterY);
for (const transient of client.transients) {
transient.moveTransient(dx, dy, client.kwinClient.desktop);
}
}
});
return manager;
}
}

View File

@@ -1,13 +1,13 @@
namespace Clients {
export function canTileEver(kwinClient: AbstractClient) {
export function canTileEver(kwinClient: KwinClient) {
return kwinClient.resizeable;
}
export function canTileNow(kwinClient: TopLevel) {
export function canTileNow(kwinClient: KwinClient) {
return canTileEver(kwinClient) && !kwinClient.minimized && kwinClient.desktop > 0 && kwinClient.activities.length === 1;
}
export function makeTileable(kwinClient: TopLevel) {
export function makeTileable(kwinClient: KwinClient) {
if (kwinClient.minimized) {
kwinClient.minimized = false;
}
@@ -19,13 +19,17 @@ namespace Clients {
}
}
export function isMaximizedGeometry(kwinClient: TopLevel) {
export function isMaximizedGeometry(kwinClient: KwinClient) {
const maximizeArea = workspace.clientArea(ClientAreaOption.MaximizeArea, kwinClient.screen, kwinClient.desktop);
return kwinClient.frameGeometry === maximizeArea;
}
export function isFullScreenGeometry(kwinClient: TopLevel) {
export function isFullScreenGeometry(kwinClient: KwinClient) {
const fullScreenArea = workspace.clientArea(ClientAreaOption.FullScreenArea, kwinClient.screen, kwinClient.desktop);
return kwinClient.frameGeometry === fullScreenArea;
}
export function isOnVirtualDesktop(kwinClient: KwinClient, desktopNumber: number) {
return kwinClient.desktop === desktopNumber || kwinClient.desktop === -1;
}
}

View File

@@ -1,10 +1,12 @@
class DesktopManager {
private readonly pinManager: PinManager;
private readonly config: Desktop.Config;
public readonly layoutConfig: LayoutConfig;
private readonly desktopsPerActivity: Map<string, Desktop[]>;
private nVirtualDesktops: number;
constructor(config: Desktop.Config, layoutConfig: LayoutConfig, currentActivity: string) {
constructor(pinManager: PinManager, config: Desktop.Config, layoutConfig: LayoutConfig, currentActivity: string) {
this.pinManager = pinManager;
this.config = config;
this.layoutConfig = layoutConfig;
this.desktopsPerActivity = new Map();
@@ -36,8 +38,8 @@ class DesktopManager {
return this.getDesktop(workspace.currentActivity, desktopNumber);
}
public getDesktopForClient(kwinClient: TopLevel) {
console.assert(kwinClient.activities.length === 1);
public getDesktopForClient(kwinClient: KwinClient) {
console.assert(kwinClient.activities.length === 1 && kwinClient.desktop > 0);
return this.getDesktop(kwinClient.activities[0], kwinClient.desktop);
}
@@ -60,7 +62,7 @@ class DesktopManager {
const nStart = desktops.length;
for (let i = 0; i < n; i++) {
const desktopNumber = nStart + i + 1;
desktops.push(new Desktop(desktopNumber, this.config, this.layoutConfig));
desktops.push(new Desktop(desktopNumber, this.pinManager, this.config, this.layoutConfig));
}
}
@@ -103,4 +105,43 @@ class DesktopManager {
}
}
}
public *getDesktopsForClient(kwinClient: KwinClient) {
const activities = kwinClient.activities.length > 0 ? kwinClient.activities : this.desktopsPerActivity.keys();
for (const activity of activities) {
if (!this.desktopsPerActivity.has(activity)) {
this.addActivity(activity);
}
const activityDesktops = this.desktopsPerActivity.get(activity)!;
if (kwinClient.desktop === -1) {
for (const desktop of activityDesktops) {
yield desktop;
}
} else {
const desktopIndex = kwinClient.desktop - 1;
yield activityDesktops[desktopIndex];
}
}
}
// empty array means all
public *getDesktops(desktopNumbers: number[], inputActivities: string[]) {
const activities = inputActivities.length > 0 ? inputActivities : this.desktopsPerActivity.keys();
for (const activity of activities) {
if (!this.desktopsPerActivity.has(activity)) {
this.addActivity(activity);
}
const activityDesktops = this.desktopsPerActivity.get(activity)!;
if (desktopNumbers.length === 0) {
for (const desktop of activityDesktops) {
yield desktop;
}
} else {
for (const desktopNumber of desktopNumbers) {
const desktopIndex = desktopNumber - 1;
yield activityDesktops[desktopIndex];
}
}
}
}
}

86
src/world/PinManager.ts Normal file
View File

@@ -0,0 +1,86 @@
class PinManager {
private readonly pinnedClients: Set<KwinClient>;
constructor() {
this.pinnedClients = new Set();
}
public addClient(kwinClient: KwinClient) {
this.pinnedClients.add(kwinClient);
}
public removeClient(kwinClient: KwinClient) {
this.pinnedClients.delete(kwinClient);
}
public getAvailableSpace(desktopNumber: number, screen: QRect) {
const baseLot = new PinManager.Lot(screen.top, screen.bottom, screen.left, screen.right);
let lots = [baseLot];
for (const client of this.pinnedClients) {
if (!Clients.isOnVirtualDesktop(client, desktopNumber)) {
continue;
}
const newLots: PinManager.Lot[] = [];
for (const lot of lots) {
lot.split(newLots, client.frameGeometry);
}
lots = newLots;
}
let largestLot = baseLot;
let largestArea = 0;
for (const lot of lots) {
const area = lot.area();
if (area > largestArea) {
largestArea = area;
largestLot = lot;
}
}
return largestLot;
}
}
namespace PinManager {
export class Lot {
private static readonly minWidth = 200;
private static readonly minHeight = 200;
constructor(
public readonly top: number,
public readonly bottom: number,
public readonly left: number,
public readonly right: number,
) {}
public split(destLots: Lot[], obstacle: QRect) {
if (!this.contains(obstacle)) {
// don't split
destLots.push(this);
return;
}
if (obstacle.top - this.top >= Lot.minHeight) {
destLots.push(new Lot(this.top, obstacle.top, this.left, this.right));
}
if (this.bottom - obstacle.bottom >= Lot.minHeight) {
destLots.push(new Lot(obstacle.bottom, this.bottom, this.left, this.right));
}
if (obstacle.left - this.left >= Lot.minWidth) {
destLots.push(new Lot(this.top, this.bottom, this.left, obstacle.left));
}
if (this.right - obstacle.right >= Lot.minWidth) {
destLots.push(new Lot(this.top, this.bottom, obstacle.right, this.right));
}
}
private contains(obstacle: QRect) {
return obstacle.right >= this.left && obstacle.left <= this.right &&
obstacle.bottom >= this.top && obstacle.top <= this.bottom;
}
public area() {
return (this.bottom - this.top) * (this.right - this.left);
}
}
}

View File

@@ -2,6 +2,7 @@ class World {
public readonly untileOnDrag: boolean;
private readonly desktopManager: DesktopManager;
public readonly clientManager: ClientManager;
private readonly pinManager: PinManager;
private readonly workspaceSignalManager: SignalManager;
private readonly screenResizedDelayer: Delayer;
@@ -18,7 +19,10 @@ class World {
this.update();
});
this.pinManager = new PinManager();
this.desktopManager = new DesktopManager(
this.pinManager,
{
marginTop: config.gapsOuterTop,
marginBottom: config.gapsOuterBottom,
@@ -26,10 +30,18 @@ class World {
marginRight: config.gapsOuterRight,
overscroll: config.overscroll,
},
config,
{
gapsInnerHorizontal: config.gapsInnerHorizontal,
gapsInnerVertical: config.gapsInnerVertical,
stackColumnsByDefault: config.stackColumnsByDefault,
resizeNeighborColumn: config.resizeNeighborColumn,
reMaximize: config.reMaximize,
tiledKeepBelow: config.tiledKeepBelow,
maximizedKeepAbove: config.floatingKeepAbove,
},
workspace.currentActivity,
);
this.clientManager = new ClientManager(config, this, this.desktopManager);
this.clientManager = new ClientManager(config, this, this.desktopManager, this.pinManager);
this.addExistingClients();
this.update();
}
@@ -56,7 +68,7 @@ class World {
}
public doIfTiled(
kwinClient: AbstractClient,
kwinClient: KwinClient,
followTransient: boolean,
f: (clientManager: ClientManager, desktopManager: DesktopManager, window: Window, column: Column, grid: Grid) => void,
) {

View File

@@ -3,7 +3,7 @@ namespace ClientState {
private readonly world: World;
private readonly signalManager: SignalManager;
constructor(world: World, kwinClient: TopLevel) {
constructor(world: World, kwinClient: KwinClient) {
this.world = world;
this.signalManager = Docked.initSignalManager(world, kwinClient);
world.onScreenResized();
@@ -14,9 +14,9 @@ namespace ClientState {
this.world.onScreenResized();
}
private static initSignalManager(world: World, kwinClient: TopLevel) {
private static initSignalManager(world: World, kwinClient: KwinClient) {
const manager = new SignalManager();
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: TopLevel, oldGeometry: QRect) => {
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: KwinClient, oldGeometry: QRect) => {
world.onScreenResized();
});
return manager;

View File

@@ -1,11 +1,62 @@
namespace ClientState {
export class Floating implements State {
constructor(client: ClientWrapper | null) {
if (client !== null && client.kwinClient.tile === null) {
client.prepareForFloating();
private readonly client: ClientWrapper;
private readonly config: ClientManager.Config;
private readonly signalManager: SignalManager;
constructor(world: World, client: ClientWrapper, config: ClientManager.Config, limitHeight: boolean) {
this.client = client;
this.config = config;
if (config.keepAbove) {
client.kwinClient.keepAbove = true;
}
if (limitHeight && client.kwinClient.tile === null) {
Floating.limitHeight(client);
}
this.signalManager = Floating.initSignalManager(world, client.kwinClient);
}
public destroy(passFocus: boolean) {
this.signalManager.destroy();
if (this.config.keepAbove) {
this.client.kwinClient.keepAbove = false;
}
}
public destroy(passFocus: boolean) {}
private static limitHeight(client: ClientWrapper) {
const placementArea = workspace.clientArea(ClientAreaOption.PlacementArea, client.kwinClient.screen, client.kwinClient.desktop);
const clientRect = client.kwinClient.frameGeometry;
const width = client.preferredWidth;
client.place(
clientRect.x,
clientRect.y,
width,
Math.min(clientRect.height, Math.round(placementArea.height / 2)),
);
}
private static initSignalManager(world: World, kwinClient: KwinClient) {
const manager = new SignalManager();
manager.connect(kwinClient.tileChanged, () => {
// on X11, this fires after `frameGeometryChanged`
if (kwinClient.tile !== null) {
world.do((clientManager, desktopManager) => {
clientManager.pinClient(kwinClient);
});
}
});
manager.connect(kwinClient.frameGeometryChanged, () => {
// on Wayland, this fires after `tileChanged`
if (kwinClient.tile !== null) {
world.do((clientManager, desktopManager) => {
clientManager.pinClient(kwinClient);
});
}
})
return manager;
}
}
}

View File

@@ -0,0 +1,87 @@
namespace ClientState {
export class Pinned implements State {
private readonly kwinClient: KwinClient;
private readonly pinManager: PinManager;
private readonly desktopManager: DesktopManager;
private readonly config: ClientManager.Config;
private readonly signalManager: SignalManager;
constructor(world: World, pinManager: PinManager, desktopManager: DesktopManager, kwinClient: KwinClient, config: ClientManager.Config) {
this.kwinClient = kwinClient;
this.pinManager = pinManager;
this.desktopManager = desktopManager;
this.config = config;
if (config.keepAbove) {
kwinClient.keepAbove = true;
}
this.signalManager = Pinned.initSignalManager(world, pinManager, kwinClient);
}
public destroy(passFocus: boolean) {
this.signalManager.destroy();
if (this.config.keepAbove) {
this.kwinClient.keepAbove = true;
}
this.pinManager.removeClient(this.kwinClient);
for (const desktop of this.desktopManager.getDesktopsForClient(this.kwinClient)) {
desktop.onPinsChanged();
}
}
private static initSignalManager(world: World, pinManager: PinManager, kwinClient: KwinClient) {
const manager = new SignalManager();
let oldDesktopNumber = kwinClient.desktop;
let oldActivities = kwinClient.activities;
manager.connect(kwinClient.tileChanged, () => {
if (kwinClient.tile === null) {
world.do((clientManager, desktopManager) => {
clientManager.unpinClient(kwinClient);
});
}
});
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: KwinClient, oldGeometry: QRect) => {
if (kwinClient.tile === null) {
world.do((clientManager, desktopManager) => {
clientManager.unpinClient(kwinClient);
});
return;
}
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktopsForClient(kwinClient)) {
desktop.onPinsChanged();
}
})
});
manager.connect(kwinClient.desktopChanged, () => {
const changedDesktops = oldDesktopNumber === -1 || kwinClient.desktop === -1 ?
[] :
[oldDesktopNumber, kwinClient.desktop];
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktops(changedDesktops, kwinClient.activities)) {
desktop.onPinsChanged();
}
});
oldDesktopNumber = kwinClient.desktop;
});
manager.connect(kwinClient.activitiesChanged, (kwinClient: KwinClient) => {
const desktops = kwinClient.desktop === -1 ? [] : [kwinClient.desktop];
const changedActivities = oldActivities.length === 0 || kwinClient.activities.length === 0 ?
[] :
union(oldActivities, kwinClient.activities);
world.do((clientManager, desktopManager) => {
for (const desktop of desktopManager.getDesktops(desktops, changedActivities)) {
desktop.onPinsChanged();
}
});
oldActivities = kwinClient.activities;
});
return manager;
}
}
}

View File

@@ -4,7 +4,7 @@ namespace ClientState {
private readonly signalManager: SignalManager;
constructor(world: World, client: ClientWrapper, grid: Grid) {
client.prepareForTiling();
Tiled.prepareClientForTiling(client, grid.config);
const column = new Column(grid, grid.getLastFocusedColumn() ?? grid.getLastColumn());
const window = new Window(client, column);
@@ -21,7 +21,7 @@ namespace ClientState {
const client = window.client;
window.destroy(passFocus);
client.restoreAfterTiling(grid.desktop.clientArea);
Tiled.restoreClientAfterTiling(client, grid.config, grid.desktop.clientArea);
}
private static initSignalManager(world: World, window: Window) {
@@ -40,7 +40,7 @@ namespace ClientState {
});
});
manager.connect(kwinClient.activitiesChanged, (kwinClient: AbstractClient) => {
manager.connect(kwinClient.activitiesChanged, () => {
world.do((clientManager, desktopManager) => {
if (kwinClient.activities.length !== 1) {
// windows on multiple activities are not supported
@@ -54,8 +54,10 @@ namespace ClientState {
let lastResize = false;
manager.connect(kwinClient.moveResizedChanged, () => {
world.do((clientManager, desktopManager) => {
if (world.untileOnDrag && kwinClient.move) {
clientManager.untileClient(kwinClient);
if (kwinClient.move) {
if (world.untileOnDrag) {
clientManager.untileClient(kwinClient);
}
return;
}
@@ -79,25 +81,47 @@ namespace ClientState {
cursorChangedAfterResizeStart = false;
});
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: TopLevel, oldGeometry: QRect) => {
world.do((clientManager, desktopManager) => {
if (kwinClient.resize) {
window.onUserResize(oldGeometry, !cursorChangedAfterResizeStart);
} else if (!client.isManipulatingGeometry() && !Clients.isMaximizedGeometry(kwinClient) && !Clients.isFullScreenGeometry(kwinClient)) {
window.onFrameGeometryChanged();
}
});
manager.connect(kwinClient.frameGeometryChanged, (kwinClient: KwinClient, oldGeometry: QRect) => {
// on Wayland, this fires after `tileChanged`
if (kwinClient.tile !== null) {
world.do((clientManager, desktopManager) => {
clientManager.pinClient(kwinClient);
});
return;
}
const newGeometry = client.kwinClient.frameGeometry;
const oldCenterX = oldGeometry.x + oldGeometry.width/2;
const oldCenterY = oldGeometry.y + oldGeometry.height/2;
const newCenterX = newGeometry.x + newGeometry.width/2;
const newCenterY = newGeometry.y + newGeometry.height/2;
const dx = Math.round(newCenterX - oldCenterX);
const dy = Math.round(newCenterY - oldCenterY);
if (dx !== 0 || dy !== 0) {
client.moveTransients(dx, dy);
}
if (kwinClient.resize) {
world.do(() => window.onUserResize(oldGeometry, !cursorChangedAfterResizeStart));
} else if (
!client.isManipulatingGeometry(newGeometry) &&
!Clients.isMaximizedGeometry(kwinClient) &&
!Clients.isFullScreenGeometry(kwinClient) // not using `kwinClient.fullScreen` because it may not be set yet at this point
) {
world.do(() => window.onFrameGeometryChanged());
}
});
manager.connect(kwinClient.fullScreenChanged, () => {
window.onFullScreenChanged(kwinClient.fullScreen);
world.do(() => window.onFullScreenChanged(kwinClient.fullScreen));
});
manager.connect(kwinClient.tileChanged, (tile: Tile) => {
if (tile !== null) {
manager.connect(kwinClient.tileChanged, () => {
// on X11, this fires after `frameGeometryChanged`
if (kwinClient.tile !== null) {
world.do((clientManager, desktopManager) => {
clientManager.untileClient(kwinClient);
})
clientManager.pinClient(kwinClient);
});
}
});
@@ -117,5 +141,28 @@ namespace ClientState {
const newColumn = new Column(newGrid, newGrid.getLastFocusedColumn() ?? newGrid.getLastColumn());
window.moveToColumn(newColumn);
}
private static prepareClientForTiling(client: ClientWrapper, config: LayoutConfig) {
if (config.tiledKeepBelow) {
client.kwinClient.keepBelow = true;
}
client.setFullScreen(false);
if (client.kwinClient.tile !== null) {
client.setMaximize(false, true); // disable quick tile mode
}
client.setMaximize(false, false);
}
private static restoreClientAfterTiling(client: ClientWrapper, config: LayoutConfig, screenSize: QRect) {
if (config.tiledKeepBelow) {
client.kwinClient.keepBelow = false;
}
client.setShade(false);
client.setFullScreen(false);
if (client.kwinClient.tile === null) {
client.setMaximize(false, false);
}
client.ensureVisible(screenSize);
}
}
}