diff --git a/dark.css b/dark.css
index 5ec69604..6d13d03b 100644
--- a/dark.css
+++ b/dark.css
@@ -30,4 +30,22 @@
.pop-shell-gaps-entry {
/* Seems to get the width just right to fit 3 digits */
width: 75px;
-}
\ No newline at end of file
+}
+
+.pop-shell-tab {
+ border: 1px solid #333;
+ color: #000;
+ padding: 0 1em;
+}
+
+.pop-shell-tab-active {
+ background: #FBB86C;
+}
+
+.pop-shell-tab-inactive {
+ background: #9B8E8A;
+}
+
+.pop-shell-tab-urgent {
+ background: #D00;
+}
diff --git a/light.css b/light.css
index 43fb8f23..397d05ea 100644
--- a/light.css
+++ b/light.css
@@ -29,4 +29,22 @@
.pop-shell-gaps-entry {
/* Seems to get the width just right to fit 3 digits */
width: 75px;
-}
\ No newline at end of file
+}
+
+.pop-shell-tab {
+ border: 1px solid #333;
+ color: #000;
+ padding: 0 1em;
+}
+
+.pop-shell-tab-active {
+ background: #FFAD00;
+}
+
+.pop-shell-tab-inactive {
+ background: #9B8E8A;
+}
+
+.pop-shell-tab-urgent {
+ background: #D00;
+}
diff --git a/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml b/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml
index f36c46cf..63999f0b 100644
--- a/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml
+++ b/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml
@@ -76,6 +76,16 @@
+
+
+ Toggle stacking mode inside management mode
+
+
+
+ s']]]>
+ Toggle stacking mode outside management mode
+
+
Toggle tiling orientation
diff --git a/src/auto_tiler.ts b/src/auto_tiler.ts
index e88381c3..c7342d11 100644
--- a/src/auto_tiler.ts
+++ b/src/auto_tiler.ts
@@ -5,6 +5,7 @@ import * as lib from 'lib';
import * as log from 'log';
import * as node from 'node';
import * as result from 'result';
+import * as stack from 'stack';
import type {Entity} from 'ecs';
import type {Ext} from 'extension';
@@ -12,9 +13,10 @@ import type {Forest} from 'forest';
import type {Fork} from 'fork';
import type {Rectangle} from 'rectangle';
import type {Result} from 'result';
-import type {ShellWindow} from 'window';
+import type { ShellWindow } from 'window';
-const {Ok, Err, ERR} = result;
+const { Stack } = stack;
+const { Ok, Err, ERR } = result;
const {NodeKind} = node;
const Tags = Me.imports.tags;
@@ -32,19 +34,63 @@ export class AutoTiler {
* Call this when a window has swapped positions with another, so that we
* may update the associations in the auto-tiler world.
*/
- attach_swap(a: Entity, b: Entity) {
- const a_ent = this.attached.remove(a);
- const b_ent = this.attached.remove(b);
+ attach_swap(ext: Ext, a: Entity, b: Entity) {
+ const a_ent = this.attached.get(a),
+ b_ent = this.attached.get(b);
- if (a_ent) {
- this.forest.forks.with(a_ent, (fork) => fork.replace_window(a, b));
- this.attached.insert(b, a_ent);
- }
+ let a_win = ext.windows.get(a),
+ b_win = ext.windows.get(b);
+
+ if (!a_ent || !b_ent || !a_win || !b_win) return;
+
+ const a_fork = this.forest.forks.get(a_ent),
+ b_fork = this.forest.forks.get(b_ent);
+
+ if (!a_fork || !b_fork) return;
- if (b_ent) {
- this.forest.forks.with(b_ent, (fork) => fork.replace_window(b, a));
- this.attached.insert(a, b_ent);
+ const a_stack = a_win.stack, b_stack = b_win.stack;
+
+ if (ext.auto_tiler) {
+ if (a_win.stack !== null) {
+ const stack = ext.auto_tiler.forest.stacks.get(a_win.stack);
+ if (stack) {
+ a = stack.active;
+ a_win = ext.windows.get(a);
+ if (!a_win) return;
+
+ stack.deactivate(a_win);
+ }
+ }
+
+ if (b_win.stack !== null) {
+ const stack = ext.auto_tiler.forest.stacks.get(b_win.stack);
+ if (stack) {
+ b = stack.active;
+ b_win = ext.windows.get(b);
+ if (!b_win) return;
+
+ stack.deactivate(b_win);
+ }
+ }
}
+
+ const a_fn = a_fork.replace_window(ext, a_win, b_win);
+ this.attached.insert(b, a_ent);
+
+ const b_fn = b_fork.replace_window(ext, b_win, a_win);
+ this.attached.insert(a, b_ent);
+
+ if (a_fn) a_fn();
+ if (b_fn) b_fn();
+
+ a_win.stack = b_stack;
+ b_win.stack = a_stack;
+
+ a_win.meta.get_compositor_private()?.show();
+ b_win.meta.get_compositor_private()?.show();
+
+ this.tile(ext, a_fork, a_fork.area);
+ this.tile(ext, b_fork, b_fork.area);
}
update_toplevel(ext: Ext, fork: Fork, monitor: number, smart_gaps: boolean) {
@@ -93,8 +139,8 @@ export class AutoTiler {
}
/** Tiles a window into another */
- attach_to_window(ext: Ext, attachee: ShellWindow, attacher: ShellWindow, cursor: Rectangle) {
- let attached = this.forest.attach_window(ext, attachee.entity, attacher.entity, cursor);
+ attach_to_window(ext: Ext, attachee: ShellWindow, attacher: ShellWindow, cursor: Rectangle, stack_from_left: boolean = true) {
+ let attached = this.forest.attach_window(ext, attachee.entity, attacher.entity, cursor, stack_from_left);
if (attached) {
const [, fork] = attached;
@@ -155,10 +201,17 @@ export class AutoTiler {
}
}
+ /** Destroy all widgets owned by this object. Call before dropping. */
+ destroy() {
+ for (const stack of this.forest.stacks.values()) stack.destroy();
+
+ this.forest.stacks.truncate(0);
+ }
+
/** Detaches the window from a tiling branch, if it is attached to one. */
detach_window(ext: Ext, win: Entity) {
this.attached.take_with(win, (prev_fork: Entity) => {
- const reflow_fork = this.forest.detach(prev_fork, win);
+ const reflow_fork = this.forest.detach(ext, prev_fork, win);
if (reflow_fork) {
const fork = reflow_fork[1];
@@ -187,21 +240,21 @@ export class AutoTiler {
const fork = this.forest.forks.get(fork_entity);
if (fork) {
- if (fork.left.kind == NodeKind.WINDOW && fork.right && fork.right.kind == NodeKind.WINDOW) {
+ if (fork.left.inner.kind === 2 && fork.right && fork.right.inner.kind === 2) {
if (fork.left.is_window(win)) {
- const sibling = ext.windows.get(fork.right.entity);
+ const sibling = ext.windows.get(fork.right.inner.entity);
if (sibling && sibling.rect().contains(cursor)) {
- fork.left.entity = fork.right.entity;
- fork.right.entity = win;
+ fork.left.inner.entity = fork.right.inner.entity;
+ fork.right.inner.entity = win;
this.tile(ext, fork, fork.area);
return true;
}
} else if (fork.right.is_window(win)) {
- const sibling = ext.windows.get(fork.left.entity);
+ const sibling = ext.windows.get(fork.left.inner.entity);
if (sibling && sibling.rect().contains(cursor)) {
- fork.right.entity = fork.left.entity;
- fork.left.entity = win;
+ fork.right.inner.entity = fork.left.inner.entity;
+ fork.left.inner.entity = win;
this.tile(ext, fork, fork.area);
return true;
@@ -214,6 +267,30 @@ export class AutoTiler {
return false;
}
+ find_stack(entity: Entity): null | [Fork, node.Node, boolean] {
+ const att = this.attached.get(entity);
+ if (att) {
+ const fork = this.forest.forks.get(att);
+ if (fork) {
+ if (fork.left.is_in_stack(entity)) {
+ return [fork, fork.left, true];
+ } else if (fork.right?.is_in_stack(entity)) {
+ return [fork, fork.right, false];
+ }
+ }
+ }
+
+ return null;
+ }
+
+ /** Find the fork that belongs to a window */
+ get_parent_fork(window: Entity): null | Fork {
+ const entity = this.attached.get(window);
+ if (!entity) return null;
+
+ return this.forest.forks.get(entity);
+ }
+
/** Performed when a window that has been dropped is destined to be tiled
*
* ## Implementation Notes
@@ -288,8 +365,85 @@ export class AutoTiler {
const result = this.toggle_orientation_(ext, window);
if (result.kind == ERR) {
log.warn(`toggle_orientation: ${result.value}`);
+ }
+ }
+
+ toggle_stacking(ext: Ext) {
+ const focused = ext.focus_window();
+ if (!focused) return;
+
+ // Disable floating if floating is enabled
+ if (ext.contains_tag(focused.entity, Tags.Floating)) {
+ ext.delete_tag(focused.entity, Tags.Floating);
+ this.auto_tile(ext, focused, false);
+ }
+
+ let stack = null, fork = null;
+ const fork_entity = this.attached.get(focused.entity);
+
+ if (fork_entity) {
+ fork = this.forest.forks.get(fork_entity);
+ if (fork) {
+ const stack_toggle = (fork: Fork, branch: node.Node) => {
+ // If the stack contains 1 item, unstack it
+ const stack = branch.inner as node.NodeStack;
+ if (stack.entities.length === 1) {
+ focused.stack = null;
+ this.forest.stacks.remove(stack.idx)?.destroy();
+ fork.measure(this.forest, ext, fork.area, this.forest.on_record());
+ return node.Node.window(focused.entity);
+ }
+
+ return null;
+ };
+
+ if (fork.left.is_window(focused.entity)) {
+ // Assign left window as stack.
+ focused.stack = this.forest.stacks.insert(new Stack(ext, focused.entity, fork.workspace));
+ fork.left = node.Node.stacked(focused.entity, focused.stack);
+ stack = fork.left.inner as node.NodeStack;
+ fork.measure(this.forest, ext, fork.area, this.forest.on_record());
+ } else if (fork.left.is_in_stack(focused.entity)) {
+ const node = stack_toggle(fork, fork.left);
+ if (node) fork.left = node;
+ } else if (fork.right?.is_window(focused.entity)) {
+ // Assign right window as stack
+ focused.stack = this.forest.stacks.insert(new Stack(ext, focused.entity, fork.workspace));
+ fork.right = node.Node.stacked(focused.entity, focused.stack);
+ stack = fork.right.inner as node.NodeStack;
+ fork.measure(this.forest, ext, fork.area, this.forest.on_record());
+ } else if (fork.right?.is_in_stack(focused.entity)) {
+ const node = stack_toggle(fork, fork.right);
+ if (node) fork.right = node;
+ }
+ }
+ }
+
+ if (stack) this.update_stack(ext, stack);
+
+ if (fork) this.tile(ext, fork, fork.area);
+ }
+
+ update_stack(ext: Ext, stack: node.NodeStack) {
+ if (stack.rect) {
+ const container = this.forest.stacks.get(stack.idx);
+ if (container) {
+ container.clear();
+
+ // Collect names of each entity in the stack
+ for (const entity of stack.entities) {
+ const window = ext.windows.get(entity);
+ if (window) {
+ window.stack = stack.idx;
+ container.add(window);
+ }
+ }
+
+ container.update_positions(stack.rect);
+ container.auto_activate();
+ }
} else {
- log.info('toggled orientation');
+ log.warn('stack rect was null');
}
}
@@ -366,7 +520,7 @@ export class AutoTiler {
this.forest.measure(ext, fork, fork.area);
for (const child of this.forest.iter(fork_entity, NodeKind.FORK)) {
- const child_fork = this.forest.forks.get(child.entity);
+ const child_fork = this.forest.forks.get((child.inner as node.NodeFork).entity);
if (child_fork) {
child_fork.rebalance_orientation();
this.forest.measure(ext, child_fork, child_fork.area);
diff --git a/src/extension.ts b/src/extension.ts
index 4063d5f8..0ff8a923 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -20,6 +20,7 @@ import * as node from 'node';
import * as utils from 'utils';
import * as Executor from 'executor';
import * as movement from 'movement';
+import * as stack from 'stack';
import type { Entity } from 'ecs';
import type { ExtEvent } from 'events';
@@ -224,13 +225,8 @@ export class Ext extends Ecs.System {
actor.remove_all_transitions();
- event.window.meta.move_resize_frame(
- true,
- event.kind.rect.x,
- event.kind.rect.y,
- event.kind.rect.width,
- event.kind.rect.height
- );
+ const r = event.kind.rect;
+ event.window.meta.move_resize_frame(true, r.x, r.y, r.width, r.height);
this.monitors.insert(event.window.entity, [
win.meta.get_monitor(),
@@ -339,6 +335,11 @@ export class Ext extends Ecs.System {
return global.workspace_manager.get_active_workspace_index();
}
+ actor_of(entity: Entity): null | Clutter.Actor {
+ const window = this.windows.get(entity);
+ return window ? window.meta.get_compositor_private() : null;
+ }
+
/// Connects a callback signal to a GObject, and records the signal.
connect(object: GObject.Object, property: string, callback: (...args: any) => boolean | void): SignalID {
const signal = object.connect(property, callback);
@@ -441,6 +442,16 @@ export class Ext extends Ecs.System {
this.show_border_on_focused();
this.exit_modes();
this.last_focused = null;
+
+ // Hide / Show Stacks
+ this.register_fn(() => {
+ if (this.auto_tiler) {
+ for (const container of this.auto_tiler.forest.stacks.values()) {
+ container.set_visible(container.workspace === this.active_workspace());
+ container.restack();
+ }
+ }
+ });
}
on_destroy(win: Entity) {
@@ -461,7 +472,11 @@ export class Ext extends Ecs.System {
if (entity) {
const fork = this.auto_tiler.forest.forks.get(entity);
if (fork?.right?.is_window(win)) {
- this.windows.with(fork.right.entity, (sibling) => sibling.activate())
+ const entity = fork.right.inner.kind === 3
+ ? fork.right.inner.entities[0]
+ : fork.right.inner.entity;
+
+ this.windows.with(entity, (sibling) => sibling.activate())
}
}
}
@@ -500,17 +515,28 @@ export class Ext extends Ecs.System {
fork.workspace = new_work_id;
- for (const child of forest.iter(entity, node.NodeKind.FORK)) {
- if (child.kind === node.NodeKind.FORK) {
- const cfork = forest.forks.get(child.entity);
- if (!cfork) continue;
- cfork.workspace = new_work_id;
- } else {
- let window = this.windows.get(child.entity);
- if (window) {
- this.size_signals_block(window);
- blocked.push(window);
- }
+ for (const child of forest.iter(entity)) {
+ switch (child.inner.kind) {
+ case 1:
+ const cfork = forest.forks.get(child.inner.entity);
+ if (!cfork) continue;
+ cfork.workspace = new_work_id;
+ break
+ case 2:
+ let window = this.windows.get(child.inner.entity);
+ if (window) {
+ this.size_signals_block(window);
+ blocked.push(window);
+ }
+ break
+ case 3:
+ for (const entity of child.inner.entities) {
+ let window = this.windows.get(entity);
+ if (window) {
+ this.size_signals_block(window);
+ blocked.push(window);
+ }
+ }
}
}
@@ -525,7 +551,6 @@ export class Ext extends Ecs.System {
/** Triggered when a window has been focused */
on_focused(win: Window.ShellWindow) {
-
this.exit_modes();
this.size_signals_unblock(win);
@@ -533,8 +558,25 @@ export class Ext extends Ecs.System {
this.prev_focused = this.last_focused;
this.last_focused = win.entity;
+ function activate_in_stack(ext: Ext, stack: node.NodeStack, win: Window.ShellWindow) {
+ ext.auto_tiler?.forest.stacks.get(stack.idx)?.activate(win.entity);
+ }
+
if (this.auto_tiler) {
win.meta.raise();
+
+ // Update the active tab in the stack.
+ const attached = this.auto_tiler.attached.get(win.entity);
+ if (attached) {
+ const fork = this.auto_tiler.forest.forks.get(attached);
+ if (fork) {
+ if (fork.left.is_in_stack(win.entity)) {
+ activate_in_stack(this, (fork.left.inner as node.NodeStack), win);
+ } else if (fork.right?.is_in_stack(win.entity)) {
+ activate_in_stack(this, (fork.right.inner as node.NodeStack), win);
+ }
+ }
+ }
}
this.show_border_on_focused();
@@ -574,16 +616,28 @@ export class Ext extends Ecs.System {
show_border_on_focused() {
this.hide_all_borders();
-
- let focusedWindow = this.focus_window();
- if (focusedWindow) {
- focusedWindow.show_border();
+ const ignore_stack = this.grab_op !== null;
+
+ const focus = this.focus_window();
+ if (focus) {
+ if (!ignore_stack && focus.stack !== null) {
+ const stack = ext?.auto_tiler?.forest.stacks.get(focus.stack);
+ if (stack) {
+ stack.show_border();
+ }
+ } else {
+ focus.show_border();
+ }
}
}
hide_all_borders() {
- for (const extWindow of this.windows.values()) {
- extWindow.hide_border();
+ for (const win of this.windows.values()) {
+ win.hide_border();
+ }
+
+ if (this.auto_tiler) for (const stack of this.auto_tiler.forest.stacks.values()) {
+ stack.hide_border();
}
}
@@ -676,11 +730,17 @@ export class Ext extends Ecs.System {
}
}
} else {
- const fork = this.auto_tiler.attached.get(win.entity);
- if (fork) {
+ const fork_entity = this.auto_tiler.attached.get(win.entity);
+ if (fork_entity) {
const forest = this.auto_tiler.forest;
- const node = forest.forks.get(fork);
- if (node) {
+ const fork = forest.forks.get(fork_entity);
+ if (fork) {
+ if (win.stack) {
+ const tab_dimension = this.dpi * stack.TAB_HEIGHT;
+ crect.height += tab_dimension;
+ crect.y -= tab_dimension;
+ }
+
let top_level = forest.find_toplevel(this.workspace_id());
if (top_level) {
crect.clamp((forest.forks.get(top_level) as Fork).area);
@@ -689,10 +749,10 @@ export class Ext extends Ecs.System {
const movement = this.grab_op.operation(crect);
if (this.movement_is_valid(win, movement)) {
- forest.resize(this, fork, node, win.entity, movement, crect);
- forest.arrange(this, node.workspace);
+ forest.resize(this, fork_entity, fork, win.entity, movement, crect);
+ forest.arrange(this, fork.workspace);
} else {
- forest.tile(this, node, node.area);
+ forest.tile(this, fork, fork.area);
}
} else {
Log.error(`no fork component found`);
@@ -742,17 +802,17 @@ export class Ext extends Ecs.System {
/** Triggered when a grab operation has been started */
on_grab_start(meta: Meta.Window) {
let win = this.get_window(meta);
- if (!win) return;
-
- if (win.is_tilable(this)) {
- let entity = win.entity;
- let rect = win.rect();
+ if (win) {
+ if (win.is_tilable(this)) {
+ let entity = win.entity;
+ let rect = win.rect();
- this.unset_grab_op();
+ this.unset_grab_op();
- this.grab_op = new GrabOp.GrabOp(entity, rect);
+ this.grab_op = new GrabOp.GrabOp(entity, rect);
- this.size_signals_block(win);
+ this.size_signals_block(win);
+ }
}
}
@@ -767,6 +827,10 @@ export class Ext extends Ecs.System {
/** Handle window maximization notifications */
on_maximize(win: Window.ShellWindow) {
if (win.is_maximized()) {
+ // Raise maximized to top so stacks won't appear over them.
+ const actor = win.meta.get_compositor_private();
+ if (actor) global.window_group.set_child_above_sibling(actor, null);
+
if (win.meta.is_fullscreen()) {
this.size_changed_block();
win.meta.unmake_fullscreen();
@@ -936,7 +1000,7 @@ export class Ext extends Ecs.System {
if (fork) {
fork.workspace = value;
for (const child of this.auto_tiler.forest.iter(entity, node.NodeKind.FORK)) {
- fork = this.auto_tiler.forest.forks.get(child.entity);
+ fork = this.auto_tiler.forest.forks.get((child.inner as node.NodeFork).entity);
if (fork) fork.workspace = value;
}
}
@@ -1201,6 +1265,7 @@ export class Ext extends Ecs.System {
toggle_tiling() {
if (this.auto_tiler) {
this.unregister_storage(this.auto_tiler.attached);
+ this.auto_tiler.destroy();
this.auto_tiler = null;
this.settings.set_tile_by_default(false);
this.tiling_toggle_switch.setToggleState(false);
diff --git a/src/forest.ts b/src/forest.ts
index 37029cf6..2fdac05e 100644
--- a/src/forest.ts
+++ b/src/forest.ts
@@ -1,6 +1,7 @@
// @ts-ignore
const Me = imports.misc.extensionUtils.getCurrentExtension();
+import * as arena from 'arena';
import * as Ecs from 'ecs';
import * as Lib from 'lib';
import * as Log from 'log';
@@ -13,7 +14,9 @@ import type { Entity } from 'ecs';
import type { Rectangle } from './rectangle';
import type { ShellWindow } from './window';
import type { Ext } from './extension';
+import { Stack } from './stack';
+const { Arena } = arena;
const { Meta } = imports.gi;
const { Movement } = movement;
@@ -47,6 +50,9 @@ export class Forest extends Ecs.World {
/** Stores window positions that have been requested. */
requested: Map = new Map();
+ /** Stores stacks which must have their containers redrawn */
+ stack_updates: Array<[Node.NodeStack, Entity]> = new Array();
+
/** The storage for holding all fork associations. */
forks: Ecs.Storage = this.register_storage();
@@ -56,8 +62,10 @@ export class Forest extends Ecs.World {
/** Needed when we're storing the entities in a map, because JS limitations. */
string_reps: Ecs.Storage = this.register_storage();
+ stacks: arena.Arena = new Arena();
+
/** The callback to execute when a window has been attached to a fork. */
- private on_attach: (parent: Entity, child: Entity) => void = () => { };
+ on_attach: (parent: Entity, child: Entity) => void = () => { };
constructor() {
super();
@@ -74,8 +82,7 @@ export class Forest extends Ecs.World {
}
/** Place all windows into their calculated positions. */
- arrange(ext: Ext, _workspace: number, ignore_reset: boolean = false) {
- // const new_positions = new Array();
+ arrange(ext: Ext, _workspace: number, _ignore_reset: boolean = false) {
for (const [entity, r] of this.requested) {
const window = ext.windows.get(entity);
if (!window) continue;
@@ -93,28 +100,9 @@ export class Forest extends Ecs.World {
this.requested.clear();
- if (ignore_reset) return;
-
- // let reset = false;
-
- // outer:
- // for (const [, , new_area] of new_positions) {
- // for (const [, , other] of new_positions) {
- // if (!other.eq(new_area) && other.intersects(new_area)) {
- // reset = true;
- // break outer;
- // }
- // }
- // }
-
- // if (reset) {
- // for (const [window, origin] of new_positions) {
- // const signals = ext.size_signals.get(window.entity);
- // if (signals) {
- // move_window(window, origin, signals);
- // }
- // }
- // }
+ for (const [stack,] of this.stack_updates.splice(0)) {
+ ext.auto_tiler?.update_stack(ext, stack);
+ }
}
attach_fork(ext: Ext, fork: Fork.Fork, window: Entity, is_left: boolean) {
@@ -141,8 +129,40 @@ export class Forest extends Ecs.World {
this.on_attach(fork.entity, window);
}
+ attach_stack(ext: Ext, stack: Node.NodeStack, fork: Fork.Fork, new_entity: Entity, stack_from_left: boolean): [Entity, Fork.Fork] | null {
+ const container = this.stacks.get(stack.idx);
+ if (container) {
+ const window = ext.windows.get(new_entity);
+ if (window) {
+ window.stack = stack.idx;
+
+ if (stack_from_left) {
+ stack.entities.push(new_entity);
+ } else {
+ stack.entities.unshift(new_entity);
+ }
+
+ this.on_attach(fork.entity, new_entity);
+
+ ext.auto_tiler?.update_stack(ext, stack);
+
+ if (window.meta.has_focus()) {
+ container.activate(new_entity);
+ }
+
+ return [fork.entity, fork];
+ } else {
+ Log.warn('attempted to attach window to stack that does not exist');
+ }
+ } else {
+ Log.warn('attempted to attach to stack that does not exist');
+ }
+
+ return null;
+ }
+
/** Attaches a `new` window to the fork which `onto` is attached to. */
- attach_window(ext: Ext, onto_entity: Entity, new_entity: Entity, cursor: Rectangle): [Entity, Fork.Fork] | null {
+ attach_window(ext: Ext, onto_entity: Entity, new_entity: Entity, cursor: Rectangle, stack_from_left: boolean): [Entity, Fork.Fork] | null {
const right_node = Node.Node.window(new_entity);
for (const [entity, fork] of this.forks.iter()) {
@@ -169,23 +189,31 @@ export class Forest extends Ecs.World {
fork.set_ratio(fork.length() / 2);
return this._attach(onto_entity, new_entity, this.on_attach, entity, fork, null);
}
- } else if (fork.right && fork.right.is_window(onto_entity)) {
- const area = fork.area_of_right(ext);
- const [fork_entity, new_fork] = this.create_fork(fork.right, right_node, area, fork.workspace);
-
- const inner_left = new_fork.is_horizontal()
- ? new Rect.Rectangle([new_fork.area.x, new_fork.area.y, new_fork.area.width / 2, new_fork.area.height])
- : new Rect.Rectangle([new_fork.area.x, new_fork.area.y, new_fork.area.width, new_fork.area.height / 2]);
-
- if (inner_left.contains(cursor)) {
- const temp = new_fork.left;
- new_fork.left = new_fork.right as Node.Node;
- new_fork.right = temp;
- }
+ } else if (fork.left.is_in_stack(onto_entity)) {
+ const stack = fork.left.inner as Node.NodeStack;
+ return this.attach_stack(ext, stack, fork, new_entity, stack_from_left);
+ } else if (fork.right) {
+ if (fork.right.is_window(onto_entity)) {
+ const area = fork.area_of_right(ext);
+ const [fork_entity, new_fork] = this.create_fork(fork.right, right_node, area, fork.workspace);
+
+ const inner_left = new_fork.is_horizontal()
+ ? new Rect.Rectangle([new_fork.area.x, new_fork.area.y, new_fork.area.width / 2, new_fork.area.height])
+ : new Rect.Rectangle([new_fork.area.x, new_fork.area.y, new_fork.area.width, new_fork.area.height / 2]);
- fork.right = Node.Node.fork(fork_entity);
- this.parents.insert(fork_entity, entity);
- return this._attach(onto_entity, new_entity, this.on_attach, entity, fork, [fork_entity, new_fork]);
+ if (inner_left.contains(cursor)) {
+ const temp = new_fork.left;
+ new_fork.left = new_fork.right as Node.Node;
+ new_fork.right = temp;
+ }
+
+ fork.right = Node.Node.fork(fork_entity);
+ this.parents.insert(fork_entity, entity);
+ return this._attach(onto_entity, new_entity, this.on_attach, entity, fork, [fork_entity, new_fork]);
+ } else if (fork.right.is_in_stack(onto_entity)) {
+ const stack = fork.right.inner as Node.NodeStack;
+ return this.attach_stack(ext, stack, fork, new_entity, stack_from_left);
+ }
}
}
@@ -215,7 +243,6 @@ export class Forest extends Ecs.World {
const entity = this.create_entity();
let orient = area.width > area.height ? Lib.Orientation.HORIZONTAL : Lib.Orientation.VERTICAL;
let fork = new Fork.Fork(entity, left, right, area, workspace, orient);
-
this.forks.insert(entity, fork);
return [entity, fork];
}
@@ -249,7 +276,7 @@ export class Forest extends Ecs.World {
}
/** Detaches an entity from the a fork, re-arranging the fork's tree as necessary */
- detach(fork_entity: Entity, window: Entity): [Entity, Fork.Fork] | null {
+ detach(ext: Ext, fork_entity: Entity, window: Entity): [Entity, Fork.Fork] | null {
const fork = this.forks.get(fork_entity);
if (!fork) return null;
@@ -263,30 +290,62 @@ export class Forest extends Ecs.World {
reflow_fork = [parent, pfork];
} else if (fork.right) {
reflow_fork = [fork_entity, fork];
- if (fork.right.kind == Node.NodeKind.WINDOW) {
- const detached = fork.right;
- fork.left = detached;
- fork.right = null;
- } else {
- this.reassign_children_to_parent(fork_entity, fork.right.entity, fork);
+ switch (fork.right.inner.kind) {
+ case 1:
+ this.reassign_children_to_parent(fork_entity, (fork.right.inner as Node.NodeFork).entity, fork);
+ break
+ default:
+ const detached = fork.right;
+ fork.left = detached;
+ fork.right = null;
}
} else {
this.delete_entity(fork_entity);
}
- } else if (fork.right && fork.right.is_window(window)) {
- // Same as the `fork.left` branch.
- if (parent) {
- const pfork = this.reassign_child_to_parent(fork_entity, parent, fork.left);
- if (!pfork) return null;
- reflow_fork = [parent, pfork];
- } else {
- reflow_fork = [fork_entity, fork];
-
- if (fork.left.kind == Node.NodeKind.FORK) {
- this.reassign_children_to_parent(fork_entity, fork.left.entity, fork);
+ } else if (fork.left.is_in_stack(window)) {
+ reflow_fork = [fork_entity, fork];
+
+ this.remove_from_stack(
+ ext,
+ fork.left.inner as Node.NodeStack,
+ window,
+ () => {
+ if (fork.right) {
+ fork.left = fork.right;
+ fork.right = null;
+ } else {
+ this.delete_entity(fork.entity);
+ }
+ }
+ );
+ } else if (fork.right) {
+ if (fork.right.is_window(window)) {
+ // Same as the `fork.left` branch.
+ if (parent) {
+ const pfork = this.reassign_child_to_parent(fork_entity, parent, fork.left);
+ if (!pfork) return null;
+ reflow_fork = [parent, pfork];
} else {
- fork.right = null;
+ reflow_fork = [fork_entity, fork];
+
+ switch (fork.left.inner.kind) {
+ case 1:
+ this.reassign_children_to_parent(fork_entity, fork.left.inner.entity, fork);
+ break
+ default:
+ fork.right = null;
+ break
+ }
}
+ } else if (fork.right.is_in_stack(window)) {
+ reflow_fork = [fork_entity, fork];
+
+ this.remove_from_stack(
+ ext,
+ fork.right.inner as Node.NodeStack,
+ window,
+ () => fork.right = null,
+ );
}
}
@@ -374,20 +433,20 @@ export class Forest extends Ecs.World {
let forks = new Array(2);
while (fork) {
- if (fork.left.kind == Node.NodeKind.FORK) {
- forks.push(this.forks.get(fork.left.entity));
+ if (fork.left.inner.kind === 1) {
+ forks.push(this.forks.get(fork.left.inner.entity));
}
- if (kind === null || fork.left.kind == kind) {
+ if (kind === null || fork.left.inner.kind === kind) {
yield fork.left
}
if (fork.right) {
- if (fork.right.kind == Node.NodeKind.FORK) {
- forks.push(this.forks.get(fork.right.entity));
+ if (fork.right.inner.kind === 1) {
+ forks.push(this.forks.get(fork.right.inner.entity));
}
- if (kind === null || fork.right.kind == kind) {
+ if (kind === null || fork.right.inner.kind == kind) {
yield fork.right;
}
}
@@ -401,9 +460,8 @@ export class Forest extends Ecs.World {
let largest_window = null;
let largest_size = 0;
- for (const win of this.iter(entity, Node.NodeKind.WINDOW)) {
- const window = ext.windows.get(win.entity);
-
+ let window_compare = (entity: Entity) => {
+ const window = ext.windows.get(entity);
if (window) {
const rect = window.rect();
const size = rect.width * rect.height;
@@ -412,6 +470,16 @@ export class Forest extends Ecs.World {
largest_window = window;
}
}
+ };
+
+ for (const node of this.iter(entity)) {
+ switch (node.inner.kind) {
+ case 2:
+ window_compare(node.inner.entity);
+ break
+ case 3:
+ window_compare(node.inner.entities[0]);
+ }
}
return largest_window;
@@ -419,7 +487,7 @@ export class Forest extends Ecs.World {
/** Resize a window from a given fork based on a supplied movement. */
resize(ext: Ext, fork_e: Entity, fork_c: Fork.Fork, win_e: Entity, movement: movement.Movement, crect: Rectangle) {
- const is_left = fork_c.left.is_window(win_e);
+ const is_left = fork_c.left.is_window(win_e) || fork_c.left.is_in_stack(win_e);
((movement & Movement.SHRINK) != 0 ? this.shrink_sibling : this.grow_sibling)
.call(this, ext, fork_e, fork_c, is_left, movement, crect);
@@ -465,8 +533,18 @@ export class Forest extends Ecs.World {
* - If it is a window, simply call on_attach
*/
private reassign_sibling(sibling: Node.Node, parent: Entity) {
- (sibling.kind == Node.NodeKind.FORK ? this.reassign_parent : this.on_attach)
- .call(this, parent, sibling.entity);
+ switch (sibling.inner.kind) {
+ case 1:
+ this.reassign_parent(parent, sibling.inner.entity);
+ break;
+ case 2:
+ this.on_attach(parent, sibling.inner.entity);
+ break;
+ case 3:
+ for (const entity of sibling.inner.entities) {
+ this.on_attach(parent, entity);
+ }
+ }
}
/**
@@ -516,6 +594,29 @@ export class Forest extends Ecs.World {
this.readjust_fork_ratio_by_left(ext, fork_length - right_length, fork);
}
+ /** Removes window from stack, destroying the stack if it was the last window. */
+ private remove_from_stack(ext: Ext, stack: Node.NodeStack, window: Entity, on_last: () => void) {
+ if (stack.entities.length === 1) {
+ this.stacks.remove(stack.idx)?.destroy();
+ on_last();
+ } else {
+ const idx = Node.stack_remove(this, stack, window);
+
+ // Activate the next window in the stack if the window was destroyed.
+ if (idx !== null && idx > 0) {
+ const focused = ext.focus_window();
+ if (focused && !focused.meta.get_compositor_private() && Ecs.entity_eq(window, focused.entity)) {
+ ext.windows.get(stack.entities[idx - 1])?.activate();
+ }
+ }
+ }
+
+ const shell_window = ext.windows.get(window);
+ if (shell_window) {
+ shell_window.stack = null;
+ }
+ }
+
/** Resizes a fork in the direction that a movement requests */
private resize_fork_(ext: Ext, child_e: Entity, crect: Rectangle, mov: movement.Movement, shrunk: boolean) {
let parent = this.parents.get(child_e),
@@ -655,12 +756,22 @@ export class Forest extends Ecs.World {
}
private display_branch(ext: Ext, branch: Node.Node, scope: number): string {
- if (branch.kind == Node.NodeKind.WINDOW) {
- const window = ext.windows.get(branch.entity);
- return `Window(${branch.entity}) (${window ? window.rect().fmt() : "unknown area"})`;
- } else {
- const fork = this.forks.get(branch.entity);
- return fork ? this.display_fork(ext, branch.entity, fork, scope + 1) : "Missing Fork";
+ switch (branch.inner.kind) {
+ case 1:
+ const fork = this.forks.get(branch.inner.entity);
+ return fork ? this.display_fork(ext, branch.inner.entity, fork, scope + 1) : "Missing Fork";
+ case 2:
+ const window = ext.windows.get(branch.inner.entity);
+ return `Window(${branch.inner.entity}) (${window ? window.rect().fmt() : "unknown area"})`;
+ case 3:
+ let fmt = 'Stack(';
+
+ for (const entity of branch.inner.entities) {
+ const window = ext.windows.get(entity);
+ fmt += `Window(${entity}) (${window ? window.rect().fmt() : "unknown area"}), `;
+ }
+
+ return fmt + ')';
}
}
diff --git a/src/fork.ts b/src/fork.ts
index 861c7683..8d032663 100644
--- a/src/fork.ts
+++ b/src/fork.ts
@@ -7,9 +7,11 @@ import type { Ext } from 'extension';
import type { Rectangle } from 'rectangle';
import type { Node } from 'node';
+import * as Ecs from 'ecs';
import * as Lib from 'lib';
import * as node from 'node';
import * as Rect from 'rectangle';
+import { ShellWindow } from './window';
const XPOS = 0;
const YPOS = 1;
@@ -80,6 +82,32 @@ export class Fork {
return this.is_horizontal() ? this.area.height : this.area.width;
}
+ find_branch(entity: Entity): Node | null {
+ const locate = (branch: Node): Node | null => {
+ switch (branch.inner.kind) {
+ case 2:
+ if (Ecs.entity_eq(branch.inner.entity, entity)) {
+ return branch;
+ }
+
+ break
+ case 3:
+ for (const e of branch.inner.entities) {
+ if (Ecs.entity_eq(e, entity)) {
+ return branch;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ const node = locate(this.left);
+ if (node) return node;
+
+ return this.right ? locate(this.right) : null;
+ }
+
/** If this fork has a horizontal orientation */
is_horizontal(): boolean {
return Lib.Orientation.HORIZONTAL == this.orientation;
@@ -90,16 +118,61 @@ export class Fork {
}
/** Replaces the association of a window in a fork with another */
- replace_window(a: Entity, b: Entity): boolean {
- if (this.left.is_window(a)) {
- this.left.entity = b;
- } else if (this.right) {
- this.right.entity = b;
- } else {
- return false;
+ replace_window(ext: Ext, a: ShellWindow, b: ShellWindow): null | (() => void) {
+ let closure = null;
+
+ let check_right = () => {
+ if (this.right) {
+ const inner = this.right.inner;
+ if (inner.kind === 2) {
+ closure = () => {
+ inner.entity = b.entity;
+ };
+ } else if (inner.kind === 3) {
+ const idx = node.stack_find(inner, a.entity);
+ if (idx === null) {
+ closure = null;
+ return;
+ }
+
+ closure = () => {
+ node.stack_replace(ext, inner, b);
+ inner.entities[idx] = b.entity;
+ };
+ }
+ }
+ }
+
+ switch (this.left.inner.kind) {
+ case 1:
+ check_right();
+ break;
+ case 2:
+ const inner = this.left.inner;
+ if (Ecs.entity_eq(inner.entity, a.entity)) {
+ closure = () => {
+ inner.entity = b.entity;
+ }
+ } else {
+ check_right();
+ }
+
+ break
+ case 3:
+ const inner_s = this.left.inner as node.NodeStack;
+ let idx = node.stack_find(inner_s, a.entity);
+ if (idx !== null) {
+ const id = idx;
+ closure = () => {
+ node.stack_replace(ext, inner_s, b);
+ inner_s.entities[id] = b.entity;
+ }
+ } else {
+ check_right();
+ }
}
- return true;
+ return closure;
}
/** Sets a new area for this fork */
@@ -108,12 +181,6 @@ export class Fork {
return this.area;
}
- /** Sets the orientation of this fork */
- set_orientation(orientation: Lib.Orientation): Fork {
- this.orientation = orientation;
- return this;
- }
-
/** Sets the ratio of this fork
*
* Ensures that the ratio is never smaller or larger than the constraints.
@@ -196,7 +263,7 @@ export class Fork {
if (this.workspace !== workspace) {
this.workspace = workspace;
for (const child_node of forest.iter(this.entity, node.NodeKind.FORK)) {
- let child = forest.forks.get(child_node.entity);
+ let child = forest.forks.get((child_node.inner as node.NodeFork).entity);
if (child) child.workspace = workspace;
}
}
@@ -210,12 +277,14 @@ export class Fork {
}
rebalance_orientation() {
- let new_orientation = this.area.height > this.area.width
+ this.set_orientation(this.area.height > this.area.width
? Lib.Orientation.VERTICAL
- : Lib.Orientation.HORIZONTAL;
+ : Lib.Orientation.HORIZONTAL)
+ }
- if (new_orientation !== this.orientation) {
- this.orientation = new_orientation;
+ set_orientation(o: Lib.Orientation) {
+ if (o !== this.orientation) {
+ this.orientation = o;
this.orientation_changed = true;
}
}
diff --git a/src/keybindings.ts b/src/keybindings.ts
index fe670993..c0353a2b 100644
--- a/src/keybindings.ts
+++ b/src/keybindings.ts
@@ -1,8 +1,15 @@
+// @ts-ignore
+const Me = imports.misc.extensionUtils.getCurrentExtension();
+
+import type { Entity } from './ecs';
import type { Ext } from "./extension";
const { wm } = imports.ui.main;
const { Meta, Shell } = imports.gi;
+import * as Node from 'node';
+import { Stack } from "./stack";
+
export class Keybindings {
global: Object;
window_focus: Object;
@@ -21,19 +28,76 @@ export class Keybindings {
};
this.window_focus = {
- "focus-left": () => ext.activate_window(ext.focus_selector.left(ext, null)),
+ "focus-left": () => {
+ this.stack_select(
+ ext,
+ (id, stack) => id === 0 ? null : stack.components[id - 1].entity,
+ () => ext.activate_window(ext.focus_selector.left(ext, null))
+ );
+ },
+
"focus-down": () => ext.activate_window(ext.focus_selector.down(ext, null)),
"focus-up": () => ext.activate_window(ext.focus_selector.up(ext, null)),
- "focus-right": () => ext.activate_window(ext.focus_selector.right(ext, null)),
+
+ "focus-right": () => {
+ this.stack_select(
+ ext,
+ (id, stack) => stack.components.length > id + 1 ? stack.components[id + 1].entity : null,
+ () => ext.activate_window(ext.focus_selector.right(ext, null))
+ );
+ },
"tile-orientation": () => {
const win = ext.focus_window();
if (win) ext.auto_tiler?.toggle_orientation(ext, win);
},
"toggle-floating": () => ext.auto_tiler?.toggle_floating(ext),
"toggle-tiling": () => ext.toggle_tiling(),
+ "toggle-stacking-global": () => ext.auto_tiler?.toggle_stacking(ext),
};
}
+ stack_select(
+ ext: Ext,
+ select: (id: number, stack: Stack) => Entity | null,
+ focus_shift: () => void,
+ ) {
+ const switched = this.stack_switch(ext, (stack) => {
+ if (!stack) return false;
+
+ const stack_con = ext.auto_tiler?.forest.stacks.get(stack.idx);
+ if (stack_con) {
+ const id = stack_con.active_id;
+ if (id !== -1) {
+ const next = select(id, stack_con);
+ if (next) {
+ stack_con.activate(next);
+ const window = ext.windows.get(next)
+ if (window) {
+ window.activate();
+ return true;
+ }
+ }
+ }
+ }
+
+ return false;
+ });
+
+ if (!switched) {
+ focus_shift();
+ }
+ }
+
+ stack_switch(ext: Ext, apply: (stack: Node.NodeStack) => boolean) {
+ const window = ext.focus_window();
+ if (window) {
+ if (ext.auto_tiler) {
+ const node = ext.auto_tiler.find_stack(window.entity);
+ return node ? apply(node[1].inner as Node.NodeStack) : false;
+ }
+ }
+ }
+
enable(keybindings: any) {
for (const name in keybindings) {
wm.addKeybinding(
diff --git a/src/mod.d.ts b/src/mod.d.ts
index f8e7ec9e..8fe0b4a9 100644
--- a/src/mod.d.ts
+++ b/src/mod.d.ts
@@ -23,9 +23,10 @@ declare interface GLib {
signal_handler_block(object: GObject.Object, signal: SignalID): void;
signal_handler_unblock(object: GObject.Object, signal: SignalID): void;
+ source_remove(id: SignalID): void;
spawn_command_line_sync(cmd: string): ProcessResult;
- timeout_add(priority: any, ms: number, callback: () => Boolean): number;
+ timeout_add(priority: number, ms: number, callback: () => Boolean): number;
}
declare namespace GObject {
@@ -82,6 +83,7 @@ declare namespace Clutter {
add(child: Actor): void;
add_child(child: Actor): void;
destroy(): void;
+ destroy_all_children(): void;
ease(params: Object): void;
hide(): void;
get_child_at_index(nth: number): Clutter.Actor | null;
@@ -94,15 +96,21 @@ declare namespace Clutter {
remove_all_children(): void;
remove_all_transitions(): void;
remove_child(child: Actor): void;
+ set_child_above_sibling(child: Actor, sibling: Actor | null): void;
set_child_below_sibling(child: Actor, sibling: Actor | null): void;
set_easing_duration(msecs: number | null): void;
set_opacity(value: number): void;
+ set_size(width: number, height: number): void;
set_y_align(align: ActorAlign): void;
set_position(x: number, y: number): void;
set_size(width: number, height: number): void;
show(): void;
}
+ interface ActorBox {
+ new(x: number, y: number, width: number, height: number): ActorBox;
+ }
+
interface Text extends Actor {
get_text(): Readonly;
set_text(text: string | null): void;
@@ -142,6 +150,7 @@ declare namespace Meta {
get_transient_for(): Window | null;
get_wm_class(): string | null;
get_workspace(): Workspace | null;
+ has_focus(): boolean;
is_client_decorated(): boolean;
is_fullscreen(): boolean;
is_skip_taskbar(): boolean;
@@ -179,8 +188,13 @@ declare namespace Shell {
}
declare namespace St {
+ interface Button extends Widget {
+ set_label(label: string): void;
+ }
+
interface Widget extends Clutter.Actor {
get_theme_node(): any
+ add(child: St.Widget): void;
hide(): void;
set_style_class_name(name: string): void;
add_style_class_name(name: string): void
@@ -195,7 +209,7 @@ declare namespace St {
}
interface Bin extends St.Widget {
-
+
}
interface Entry extends Widget {
diff --git a/src/node.ts b/src/node.ts
index da5272b0..32e60b6c 100644
--- a/src/node.ts
+++ b/src/node.ts
@@ -7,11 +7,14 @@ import type { Forest } from './forest';
import type { Entity } from 'ecs';
import type { Ext } from 'extension';
import type { Rectangle } from 'rectangle';
+import type { Stack } from 'stack';
+import { ShellWindow } from './window';
/** A node is either a fork a window */
export enum NodeKind {
FORK = 1,
WINDOW = 2,
+ STACK = 3,
}
/** Fetch the string representation of this value */
@@ -20,60 +23,205 @@ function node_variant_as_string(value: NodeKind): string {
}
/** Identifies this node as a fork */
-interface NodeFork {
+export interface NodeFork {
kind: 1;
entity: Entity;
}
/** Identifies this node as a window */
-interface NodeWindow {
+export interface NodeWindow {
kind: 2;
entity: Entity;
}
-type NodeADT = NodeFork | NodeWindow;
+export interface NodeStack {
+ kind: 3;
+ idx: number;
+ entities: Array;
+ rect: Rectangle | null;
+}
+
+function stack_detach(node: NodeStack, stack: Stack, idx: number) {
+ node.entities.splice(idx, 1);
+ const c = stack.components[idx];
+ for (const s of c.signals) c.meta.disconnect(s);
+ stack.buttons.remove(c.button)?.destroy();
+ c.meta.get_compositor_private()?.show();
+ stack.components.splice(idx, 1);
+}
+
+export function stack_find(node: NodeStack, entity: Entity): null | number {
+ let idx = 0;
+ while (idx < node.entities.length) {
+ if (Ecs.entity_eq(entity, node.entities[idx])) {
+ return idx;
+ }
+ idx += 1
+ }
+
+ return null;
+}
+
+/** Move the window in a stack to the left, and detach if it it as the end. */
+export function stack_move_left(ext: Ext, forest: Forest, node: NodeStack, entity: Entity): boolean {
+ const stack = forest.stacks.get(node.idx);
+ if (!stack) return false;
+
+ let moved = false;
+ let idx = 0;
+ for (const cmp of node.entities) {
+ if (Ecs.entity_eq(cmp, entity)) {
+ if (idx === 0) {
+ stack_detach(node, stack, 0);
+ moved = false;
+ } else {
+ stack_swap(node, idx - 1, idx)
+ stack.active_id -= 1;
+ ext.auto_tiler?.update_stack(ext, node);
+ moved = true;
+ }
+ break
+ }
+
+ idx += 1;
+ }
+
+ return moved;
+}
+
+/** Move the window in a stack to the right, and detach if it is at the end. */
+export function stack_move_right(ext: Ext, forest: Forest, node: NodeStack, entity: Entity): boolean {
+ const stack = forest.stacks.get(node.idx);
+ if (!stack) return false;
+
+ let moved = false;
+ let idx = 0;
+ const max = node.entities.length - 1;
+ for (const cmp of node.entities) {
+ if (Ecs.entity_eq(cmp, entity)) {
+ if (idx === max) {
+ stack_detach(node, stack, idx);
+ moved = false;
+ } else {
+ stack_swap(node, idx + 1, idx);
+ stack.active_id += 1;
+ ext.auto_tiler?.update_stack(ext, node);
+ moved = true;
+ }
+ break
+ }
+
+ idx += 1;
+ }
+
+ return moved;
+}
+
+export function stack_replace(ext: Ext, node: NodeStack, window: ShellWindow) {
+ if (!ext.auto_tiler) return;
+
+ const stack = ext.auto_tiler.forest.stacks.get(node.idx);
+ if (!stack) return;
+
+ stack.replace(window)
+}
+
+/** Removes a window from a stack */
+export function stack_remove(forest: Forest, node: NodeStack, entity: Entity): null | number {
+ const stack = forest.stacks.get(node.idx);
+ if (!stack) return null;
+
+ let idx = 0;
+
+ for (const cmp of node.entities) {
+ if (Ecs.entity_eq(cmp, entity)) {
+ node.entities.splice(idx, 1);
+ stack.buttons.remove(stack.components[idx].button)?.destroy();
+ stack.components.splice(idx, 1);
+ return idx;
+ }
+ idx += 1;
+ }
+
+ return null;
+}
+
+function stack_swap(node: NodeStack, from: number, to: number) {
+ const tmp = node.entities[from];
+ node.entities[from] = node.entities[to];
+ node.entities[to] = tmp;
+}
+
+export type NodeADT = NodeFork | NodeWindow | NodeStack;
/** A tiling node may either refer to a window entity, or another fork entity */
export class Node {
/** The actual data for this node */
- private inner: NodeADT;
+ inner: NodeADT;
- constructor(kind: NodeKind, entity: Entity) {
- this.inner = { kind: kind, entity: entity };
+ constructor(inner: NodeADT) {
+ this.inner = inner;
}
/** Create a fork variant of a `Node` */
- static fork(fork: Entity): Node {
- return new Node(NodeKind.FORK, fork);
+ static fork(entity: Entity): Node {
+ return new Node({ kind: NodeKind.FORK, entity });
}
/** Create the window variant of a `Node` */
- static window(window: Entity): Node {
- return new Node(NodeKind.WINDOW, window);
+ static window(entity: Entity): Node {
+ return new Node({ kind: NodeKind.WINDOW, entity });
}
- get entity(): Entity { return this.inner.entity; }
-
- set entity(entity: Entity) { this.inner.entity = entity; }
+ static stacked(window: Entity, idx: number): Node {
+ const node = new Node({
+ kind: NodeKind.STACK,
+ entities: [window],
+ idx,
+ rect: null
+ });
- get kind(): NodeKind { return this.inner.kind; }
-
- set kind(kind: NodeKind) { this.inner.kind = kind; }
+ return node;
+ }
/** Generates a string representation of the this value. */
display(fmt: string): string {
- fmt += `{\n kind: ${node_variant_as_string(this.kind)},\n entity: (${this.entity})\n }`;
- return fmt;
+ fmt += `{\n kind: ${node_variant_as_string(this.inner.kind)},\n `;
+
+ switch (this.inner.kind) {
+ // Fork + Window
+ case 1:
+ case 2:
+ fmt += `entity: (${this.inner.entity})\n }`;
+ return fmt;
+ // Stack
+ case 3:
+ fmt += `entities: ${this.inner.entities}\n }`;
+ return fmt;
+ }
+
+
+ }
+
+ /** Check if the entity exists as a child of this stack */
+ is_in_stack(entity: Entity): boolean {
+ if (this.inner.kind === 3) {
+ for (const compare of this.inner.entities) {
+ if (Ecs.entity_eq(entity, compare)) return true;
+ }
+ }
+
+ return false;
}
/** Asks if this fork is the fork we are looking for */
is_fork(entity: Entity): boolean {
- return NodeKind.FORK == this.kind && Ecs.entity_eq(this.entity, entity);
+ return this.inner.kind === 1 && Ecs.entity_eq(this.inner.entity, entity);
}
/** Asks if this window is the window we are looking for */
is_window(entity: Entity): boolean {
- return NodeKind.WINDOW == this.kind && Ecs.entity_eq(this.entity, entity);
+ return this.inner.kind === 2 && Ecs.entity_eq(this.inner.entity, entity);
}
/** Calculates the future arrangement of windows in this node */
@@ -84,13 +232,35 @@ export class Node {
area: Rectangle,
record: (win: Entity, parent: Entity, area: Rectangle) => void
) {
- if (NodeKind.FORK == this.kind) {
- const fork = tiler.forks.get(this.entity);
- if (fork) {
- fork.measure(tiler, ext, area, record);
- }
- } else {
- record(this.entity, parent, area.clone());
+ switch (this.inner.kind) {
+ // Fork
+ case 1:
+ const fork = tiler.forks.get(this.inner.entity);
+ if (fork) {
+ record
+ fork.measure(tiler, ext, area, record);
+ }
+
+ break
+ // Window
+ case 2:
+ record(this.inner.entity, parent, area.clone());
+ break
+ // Stack
+ case 3:
+ const size = ext.dpi * 4;
+
+ this.inner.rect = area.clone();
+ this.inner.rect.y += size * 6;
+ this.inner.rect.height -= size * 6;
+
+ for (const entity of this.inner.entities) {
+ record(entity, parent, this.inner.rect);
+ }
+
+ if (ext.auto_tiler) {
+ ext.auto_tiler.forest.stack_updates.push([this.inner, parent]);
+ }
}
}
}
diff --git a/src/panel_settings.ts b/src/panel_settings.ts
index d0e9889e..a0da3de8 100644
--- a/src/panel_settings.ts
+++ b/src/panel_settings.ts
@@ -83,7 +83,6 @@ function menu_separator(text: any): any {
}
function settings_button(menu: any): any {
-
let item = new PopupMenuItem(_('View All'));
item.connect('activate', () => {
let path: string | null = GLib.find_program_in_path('pop-shell-shortcuts');
diff --git a/src/stack.ts b/src/stack.ts
new file mode 100644
index 00000000..08be9e85
--- /dev/null
+++ b/src/stack.ts
@@ -0,0 +1,469 @@
+// @ts-ignore
+const Me = imports.misc.extensionUtils.getCurrentExtension();
+
+import type { Entity } from './ecs';
+import type { Ext } from './extension';
+import type { ShellWindow } from './window';
+
+import * as Ecs from 'ecs';
+import * as a from 'arena';
+
+const Arena = a.Arena;
+const { St } = imports.gi;
+
+const ACTIVE_TAB = 'pop-shell-tab pop-shell-tab-active';
+const INACTIVE_TAB = 'pop-shell-tab pop-shell-tab-inactive';
+const URGENT_TAB = 'pop-shell-tab pop-shell-tab-urgent';
+
+export var TAB_HEIGHT: number = 24
+
+interface Component {
+ entity: Entity;
+ button: number;
+ meta: Meta.Window;
+ signals: Array;
+}
+
+interface StackWidgets {
+ tabs: St.Widget;
+}
+
+function stack_widgets_new(): StackWidgets {
+ let tabs = new St.BoxLayout({
+ style_class: 'pop-shell-stack',
+ x_expand: true
+ });
+
+ tabs.get_layout_manager()?.set_homogeneous(true);
+
+ return { tabs };
+}
+
+export class Stack {
+ ext: Ext;
+
+ widgets: null | StackWidgets = null;
+
+ active: Entity;
+
+ active_id: number = 0
+
+ components: Array = new Array();
+
+ workspace: number;
+
+ buttons: a.Arena = new Arena();
+
+ stack_rect: Rectangular = { width: 0, height: 0, x: 0, y: 0 };
+
+ private border: St.Bin = new St.Bin({ style_class: 'pop-shell-active-hint' });
+
+ private border_size: number = 0;
+
+ private active_meta: Meta.Window | null = null;
+
+ private active_signals: [SignalID, SignalID, SignalID] | null = null;
+
+ private rect: Rectangular = { width: 0, height: 0, x: 0, y: 0 };
+
+ private restacker: SignalID = (global.display as GObject.Object).connect('restacked', () => this.restack());
+
+ constructor(ext: Ext, active: Entity, workspace: number) {
+ this.ext = ext;
+ this.active = active;
+ this.workspace = workspace;
+
+ this.widgets = stack_widgets_new();
+
+ global.window_group.add_child(this.widgets.tabs);
+ global.window_group.add_child(this.border);
+
+ this.reposition();
+
+ this.border.connect('style-changed', () => {
+ this.on_style_changed();
+ });
+
+ this.border.hide();
+
+ this.widgets.tabs.connect('destroy', () => this.recreate_widgets());
+ }
+
+ /** Adds a new window to the stack */
+ add(window: ShellWindow) {
+ if (!this.widgets) return;
+
+ if (this.border_size === 0 && window.meta.get_compositor_private()?.get_stage()) {
+ this.on_style_changed();
+ }
+
+ const entity = window.entity;
+ const label = window.meta.get_title();
+
+ const button: St.Button = new St.Button({
+ label,
+ x_expand: true,
+ style_class: Ecs.entity_eq(entity, this.active) ? ACTIVE_TAB : INACTIVE_TAB
+ });
+
+ const id = this.buttons.insert(button);
+ this.components.push({ entity, signals: [], button: id, meta: window.meta });
+
+ this.watch_signals(this.components.length - 1, id, window);
+
+ this.widgets.tabs.add(button);
+ }
+
+ /** Activates a tab based on the previously active entry */
+ auto_activate(): null | Entity {
+ if (this.components.length === 0) return null;
+
+ let id = this.components.length <= this.active_id ? this.components.length - 1 : this.active_id;
+
+ const c = this.components[id];
+
+ this.activate(c.entity);
+ return c.entity;
+ }
+
+ /** Activates the tab of this entity */
+ activate(entity: Entity) {
+ const win = this.ext.windows.get(entity);
+ if (!win) return;
+
+ this.active_disconnect();
+
+ this.active_meta = win.meta;
+ this.active = entity;
+ this.active_connect();
+
+ let id = 0;
+
+ for (const component of this.components) {
+ let name;
+
+ const actor = component.meta.get_compositor_private();
+
+ if (Ecs.entity_eq(entity, component.entity)) {
+ this.active_id = id;
+ name = ACTIVE_TAB;
+ if (actor) actor.show()
+ } else {
+ name = INACTIVE_TAB;
+ if (actor) actor.hide();
+ }
+
+ this.buttons.get(component.button)?.set_style_class_name(name);
+
+ id += 1;
+ }
+
+ this.restack();
+ }
+
+ private active_connect() {
+ if (!this.active_meta) return;
+ const window = this.active_meta;
+
+ this.active_signals = [
+ window.connect('size-changed', () => {
+ if (this.ext.grab_op !== null) return;
+ this.update_positions(window.get_frame_rect());
+ }),
+ window.connect('size-changed', () => {
+ if (this.ext.grab_op !== null) return;
+ this.window_changed();
+ }),
+ window.connect('position-changed', () => {
+ if (this.ext.grab_op !== null) return;
+ this.window_changed();
+ }),
+ ]
+ }
+
+ private active_disconnect() {
+ if (this.active_signals && this.active_meta) {
+ for (const s of this.active_signals) this.active_meta.disconnect(s);
+ }
+ this.active_signals = null;
+ }
+
+ /** Clears watched components and removes all tabs */
+ clear() {
+ this.buttons.truncate(0);
+ this.widgets?.tabs.destroy_all_children();
+
+ for (const c of this.components.splice(0)) {
+ for (const s of c.signals) c.meta.disconnect(s);
+ }
+ }
+
+ /** Deactivate the signals belonging to an entity */
+ deactivate(w: ShellWindow) {
+ for (const c of this.components) if (Ecs.entity_eq(c.entity, w.entity)) {
+ for (const s of c.signals) c.meta.disconnect(s);
+ c.signals = [];
+ }
+
+ if (this.active_signals && Ecs.entity_eq(this.active, w.entity)) {
+ this.active_disconnect();
+ }
+ }
+
+ /** Disconnects this stack's signal, and destroys its widgets */
+ destroy() {
+ global.display.disconnect(this.restacker);
+ this.border.destroy();
+
+ // Disconnect stack signals from each window, and unhide them.
+ for (const c of this.components) {
+ for (const s of c.signals) c.meta.disconnect(s);
+ c.meta.get_compositor_private()?.show();
+ }
+
+ for (const b of this.buttons.values()) b.destroy();
+
+ if (this.widgets) {
+ const tabs = this.widgets.tabs;
+ this.widgets = null;
+ tabs.destroy();
+ }
+ }
+
+ hide_border() {
+ this.border.hide();
+ }
+
+ private on_style_changed() {
+ this.border_size = this.border.get_theme_node().get_border_width(St.Side.TOP);
+ }
+
+ /** Workaround for when GNOME Shell destroys our widgets when they're reparented
+ * in an active workspace change. */
+ recreate_widgets() {
+ if (this.widgets !== null) {
+ this.widgets = stack_widgets_new();
+
+ global.window_group.add_child(this.widgets.tabs);
+
+ this.widgets.tabs.connect('destroy', () => this.recreate_widgets());
+
+ for (const c of this.components.splice(0)) {
+ for (const s of c.signals) c.meta.disconnect(s);
+ const window = this.ext.windows.get(c.entity);
+ if (window) this.add(window);
+ }
+
+ this.update_positions(this.rect);
+ this.restack();
+ }
+ }
+
+ /** Removes the tab associated with the entity */
+ remove_tab(entity: Entity) {
+ if (!this.widgets) return;
+
+ let idx = 0;
+ for (const c of this.components) {
+ if (Ecs.entity_eq(c.entity, entity)) {
+ const b = this.buttons.remove(c.button);
+ if (b) this.widgets.tabs.remove_child(b);
+ for (const s of c.signals) c.meta.disconnect(s);
+ this.components.splice(idx, 1);
+ break
+ }
+ }
+ }
+
+ replace(window: ShellWindow) {
+ if (!this.widgets) return;
+ const c = this.components[this.active_id];
+ if (c) {
+ for (const s of c.signals) c.meta.disconnect(s);
+
+ const actor = window.meta.get_compositor_private();
+
+ if (Ecs.entity_eq(window.entity, this.active)) {
+ this.active_disconnect();
+ this.active_meta = window.meta;
+ this.active = window.entity;
+ this.active_connect();
+ actor?.show();
+ } else {
+ actor?.hide();
+ }
+
+ c.meta = window.meta;
+ this.watch_signals(this.active_id, c.button, window);
+ this.buttons.get(c.button)?.set_label(window.meta.get_title());
+ this.activate(window.entity);
+ }
+ }
+
+ /** Repositions the stack, arranging the stack's actors around the active window */
+ reposition() {
+ if (!this.widgets) return;
+
+ const window = this.ext.windows.get(this.active);
+ if (!window) return;
+
+ const actor = window.meta.get_compositor_private();
+ if (!actor) return;
+
+ actor.show();
+
+ const parent = actor.get_parent();
+
+ if (!parent) {
+ return;
+ }
+
+ let restack = false;
+ const stack_parent = this.widgets.tabs.get_parent();
+ if (!stack_parent) {
+ parent.add_child(this.widgets.tabs);
+ restack = true;
+ } else if (stack_parent != parent) {
+ stack_parent.remove_child(this.widgets.tabs);
+ restack = true;
+ }
+
+ if (restack) {
+ parent.add_child(this.widgets.tabs);
+ for (const c of this.components) {
+ if (Ecs.entity_eq(c.entity, this.active)) continue;
+ const actor = c.meta.get_compositor_private();
+ if (!actor) continue
+ actor.hide();
+ }
+ }
+
+ if (!window.meta.is_fullscreen() && !window.is_maximized()) {
+ parent.set_child_above_sibling(this.widgets.tabs, actor);
+ parent.set_child_above_sibling(this.border, this.widgets.tabs);
+ } else {
+ parent.set_child_below_sibling(this.widgets.tabs, actor);
+ }
+ }
+
+ /** Repositions the stack, and hides all but the active window in the stack */
+ restack() {
+ if (!this.widgets) return;
+ if (this.ext.grab_op !== null) return;
+
+ if (global.workspace_manager.get_active_workspace_index() !== this.workspace) {
+ this.widgets.tabs.visible = false;
+ for (const c of this.components) {
+ c.meta.get_compositor_private()?.hide();
+ }
+ this.hide_border();
+
+ } else if (this.widgets.tabs.visible) {
+ for (const c of this.components) {
+ c.meta.get_compositor_private()?.hide();
+ }
+
+ this.reposition();
+ }
+
+ this.update_border_layout();
+ }
+
+ show_border() {
+ if (this.ext.settings.active_hint()) {
+ this.border.show();
+ this.restack();
+ }
+ }
+
+ /** Changes visibility of the stack's actors */
+ set_visible(visible: boolean) {
+ if (!this.widgets) return;
+
+ if (visible) {
+ this.widgets.tabs.show();
+ this.widgets.tabs.visible = true;
+ } else {
+ this.widgets.tabs.visible = false;
+ this.widgets.tabs.hide();
+ }
+ }
+
+ private update_border_layout() {
+ if (!this.active_meta) return;
+
+ const rect = this.active_meta.get_frame_rect(),
+ size = this.border_size,
+ border = this.border;
+
+ border.set_position(rect.x - size, rect.y - TAB_HEIGHT - size);
+ border.set_size(rect.width + 2 * size, rect.height + TAB_HEIGHT + 2 * size);
+ }
+
+ /** Updates the dimensions and positions of the stack's actors */
+ update_positions(rect: Rectangular) {
+ if (!this.widgets) return;
+
+ this.rect = rect;
+
+ const tabs_height = TAB_HEIGHT * this.ext.dpi;
+
+ this.stack_rect = {
+ x: rect.x,
+ y: rect.y - tabs_height,
+ width: rect.width,
+ height: tabs_height + rect.height,
+ };
+
+ this.widgets.tabs.x = rect.x;
+ this.widgets.tabs.y = this.stack_rect.y;
+ this.widgets.tabs.height = tabs_height;
+ this.widgets.tabs.width = rect.width;
+ }
+
+ watch_signals(comp: number, button: number, window: ShellWindow) {
+ const entity = window.entity;
+ const widget = this.buttons.get(button);
+ if (widget) widget.connect('clicked', () => {
+ global.log(`clicked ${entity}`);
+ this.activate(entity);
+ const window = this.ext.windows.get(entity);
+ if (window) {
+ const actor = window.meta.get_compositor_private();
+ if (actor) {
+ actor.show();
+ window.meta.raise();
+ window.meta.unminimize();
+ window.meta.activate(global.get_current_time());
+
+ this.reposition();
+
+ for (const comp of this.components) {
+ this.buttons.get(comp.button)?.set_style_class_name(INACTIVE_TAB);
+ }
+
+ widget.set_style_class_name(ACTIVE_TAB);
+ } else {
+ this.remove_tab(entity);
+ window.stack = null;
+ }
+ }
+ });
+
+ this.components[comp].signals = [
+ window.meta.connect('notify::title', () => {
+ this.buttons.get(button)?.set_label(window.meta.get_title());
+ }),
+
+ window.meta.connect('notify::urgent', () => {
+ if (!window.meta.has_focus()) {
+ this.buttons.get(button)?.set_style_class_name(URGENT_TAB);
+ }
+ })
+ ];
+ }
+
+ private window_changed() {
+ this.ext.show_border_on_focused();
+ }
+}
diff --git a/src/tiling.ts b/src/tiling.ts
index 1dea5793..53835736 100644
--- a/src/tiling.ts
+++ b/src/tiling.ts
@@ -1,18 +1,23 @@
// @ts-ignore
const Me = imports.misc.extensionUtils.getCurrentExtension();
-import * as Lib from 'lib';
-import * as Tags from 'tags';
+// import * as Ecs from 'ecs';
import * as GrabOp from 'grab_op';
+import * as Lib from 'lib';
+import * as Log from 'log';
+import * as Node from 'node';
import * as Rect from 'rectangle';
-import * as window from 'window';
import * as shell from 'shell';
+import * as Tags from 'tags';
import * as Tweener from 'tweener';
+import * as window from 'window';
import type { Entity } from './ecs';
import type { Rectangle } from './rectangle';
import type { Ext } from './extension';
+import type { NodeStack } from './node';
import { AutoTiler } from './auto_tiler';
+import { Fork } from './fork';
const { Meta } = imports.gi;
const Main = imports.ui.main;
@@ -57,6 +62,11 @@ export class Tiler {
"tile-swap-right": () => this.swap_right(ext),
"tile-accept": () => this.accept(ext),
"tile-reject": () => this.exit(ext),
+ "toggle-stacking": () => {
+ ext.auto_tiler?.toggle_stacking(ext);
+ const win = ext.focus_window();
+ if (win) this.overlay_watch(ext, win);
+ },
};
}
@@ -140,10 +150,112 @@ export class Tiler {
return this;
}
- move(ext: Ext, x: number, y: number, w: number, h: number, focus: () => window.ShellWindow | number | null) {
+ unstack_from_fork(ext: Ext, stack: NodeStack, focused: window.ShellWindow, fork: Fork, left: Node.Node, right: Node.Node, is_left: boolean) {
+ if (!ext.auto_tiler) return;
+
+ const forest = ext.auto_tiler.forest;
+ const new_fork = forest.create_fork(
+ left,
+ right,
+ fork.area,
+ fork.workspace
+ );
+
+ if (is_left) {
+ fork.left = Node.Node.fork(new_fork[0]);
+ } else {
+ fork.right = Node.Node.fork(new_fork[0]);
+ }
+
+ // Update parent assignments
+ forest.on_attach(new_fork[0], focused.entity);
+ for (const e of stack.entities) {
+ forest.on_attach(new_fork[0], e);
+ }
+ }
+
+ move(ext: Ext, x: number, y: number, w: number, h: number, direction: Direction, focus: () => window.ShellWindow | number | null) {
if (!this.window) return;
if (ext.auto_tiler && !ext.contains_tag(this.window, Tags.Floating)) {
- this.move_auto(ext, focus());
+ const focused = ext.focus_window();
+ if (focused) {
+ const move_to = focus();
+
+ // Check if the focused window is in a stack first.
+ if (ext.auto_tiler) {
+ const s = ext.auto_tiler.find_stack(focused.entity);
+ if (s) {
+ const [fork, branch, is_left] = s;
+ const stack = branch.inner as NodeStack;
+
+ if (stack.entities.length === 1) {
+ ext.auto_tiler.toggle_stacking(ext);
+ this.overlay_watch(ext, focused);
+ return;
+ }
+
+ const detach = (orientation: Lib.Orientation, reverse: boolean) => {
+ if (!ext.auto_tiler) return;
+ focused.stack = null;
+
+ if (fork.right) {
+ let left, right;
+ if (reverse) {
+ left = branch;
+ right = Node.Node.window(focused.entity);
+ } else {
+ left = Node.Node.window(focused.entity);
+ right = branch;
+ }
+
+ this.unstack_from_fork(ext, stack, focused, fork, left, right, is_left);
+ } else if (reverse) {
+ fork.right = Node.Node.window(focused.entity);
+ } else {
+ fork.right = fork.left;
+ fork.left = Node.Node.window(focused.entity);
+ }
+
+ fork.set_orientation(orientation);
+
+ ext.auto_tiler.tile(ext, fork, fork.area);
+ this.overlay_watch(ext, focused);
+ }
+
+ switch (direction) {
+ case Direction.Left:
+ if (!Node.stack_move_left(ext, ext.auto_tiler.forest, stack, focused.entity)) {
+ detach(Lib.Orientation.HORIZONTAL, false);
+ }
+
+ ext.auto_tiler.update_stack(ext, stack);
+
+ return;
+
+ case Direction.Right:
+ if (!Node.stack_move_right(ext, ext.auto_tiler.forest, stack, focused.entity)) {
+ detach(Lib.Orientation.HORIZONTAL, true);
+ }
+
+ ext.auto_tiler.update_stack(ext, stack);
+
+ return;
+
+ case Direction.Up:
+ Node.stack_remove(ext.auto_tiler.forest, stack, focused.entity);
+ detach(Lib.Orientation.VERTICAL, false);
+ return;
+
+ case Direction.Down:
+ Node.stack_remove(ext.auto_tiler.forest, stack, focused.entity);
+ detach(Lib.Orientation.VERTICAL, true);
+ return;
+ }
+ }
+ }
+
+ if (move_to !== null) this.move_auto(ext, focused, move_to, direction === Direction.Left);
+ }
} else {
this.swap_window = null;
this.rect_by_active_area(ext, (_monitor, rect) => {
@@ -199,6 +311,19 @@ export class Tiler {
}
}
+ overlay_watch(ext: Ext, window: window.ShellWindow) {
+ Tweener.on_window_tweened(window.meta, () => {
+ ext.register_fn(() => {
+ if (window) {
+ ext.set_overlay(window.rect());
+ window.meta.raise();
+ window.meta.unminimize();
+ window.meta.activate(global.get_current_time());
+ }
+ });
+ });
+ }
+
rect_by_active_area(ext: Ext, callback: (monitor: Rectangle, area: Rectangle) => void) {
if (this.window) {
const monitor_id = ext.monitors.get(this.window);
@@ -252,30 +377,57 @@ export class Tiler {
);
}
- move_auto(ext: Ext, move_to: window.ShellWindow | number | null) {
- if (move_to === null) return;
-
- const focused = ext.focus_window();
+ move_auto(ext: Ext, focused: window.ShellWindow, move_to: window.ShellWindow | number, stack_from_left: boolean = true) {
let watching: null | window.ShellWindow = null;
- if (ext.auto_tiler && focused) {
+ if (ext.auto_tiler) {
if (move_to instanceof ShellWindow) {
- const parent = ext.auto_tiler.windows_are_siblings(focused.entity, move_to.entity);
- if (parent) {
- const fork = ext.auto_tiler.forest.forks.get(parent);
- if (fork) {
- const temp = fork.left.entity;
- fork.left.entity = (fork.right as any).entity;
- (fork.right as any).entity = temp;
- ext.auto_tiler.tile(ext, fork, fork.area as any);
- watching = focused
- }
- }
+ // Check if we are moving onto a stack, and if so, move into the stack.
+ const stack_info = ext.auto_tiler.find_stack(move_to.entity);
+ if (stack_info) {
+ const [stack_fork, branch,] = stack_info;
+ const stack = branch.inner as NodeStack;
+
+ ext.auto_tiler.detach_window(ext, focused.entity);
+
+ ext.auto_tiler.forest.on_attach(stack_fork.entity, focused.entity);
+ ext.auto_tiler.update_stack(ext, stack);
+
+ ext.auto_tiler.tile(ext, stack_fork, stack_fork.area);
- if (!watching) {
ext.auto_tiler.detach_window(ext, focused.entity);
- ext.auto_tiler.attach_to_window(ext, move_to, focused, Lib.cursor_rect());
+ ext.auto_tiler.attach_to_window(ext, move_to, focused, Lib.cursor_rect(), stack_from_left);
watching = focused;
+ } else {
+ const parent = ext.auto_tiler.windows_are_siblings(focused.entity, move_to.entity);
+ if (parent) {
+ const fork = ext.auto_tiler.forest.forks.get(parent);
+ if (fork) {
+ if (!fork.right) {
+ Log.error('move_auto: detected as sibling, but fork lacks right branch');
+ return;
+ }
+
+ if (fork.left.inner.kind === 3) {
+ Node.stack_remove(ext.auto_tiler.forest, fork.left.inner, focused.entity);
+ focused.stack = null;
+ } else {
+ const temp = fork.right;
+
+ fork.right = fork.left;
+ fork.left = temp;
+
+ ext.auto_tiler.tile(ext, fork, fork.area);
+ watching = focused;
+ }
+ }
+ }
+
+ if (!watching) {
+ ext.auto_tiler.detach_window(ext, focused.entity);
+ ext.auto_tiler.attach_to_window(ext, move_to, focused, Lib.cursor_rect());
+ watching = focused;
+ }
}
} else {
ext.auto_tiler.detach_window(ext, focused.entity);
@@ -285,19 +437,14 @@ export class Tiler {
}
if (watching) {
- Tweener.on_window_tweened(watching.meta, () => {
- ext.register_fn(() => {
- if (watching) {
- ext.set_overlay(watching.rect());
- watching.activate();
- }
- });
- });
+ this.overlay_watch(ext, watching);
+ } else {
+ ext.set_overlay(focused.rect());
}
}
move_left(ext: Ext) {
- this.move(ext, -1, 0, 0, 0, move_window_or_monitor(
+ this.move(ext, -1, 0, 0, 0, Direction.Left, move_window_or_monitor(
ext,
ext.focus_selector.left,
Meta.DisplayDirection.LEFT
@@ -305,7 +452,7 @@ export class Tiler {
}
move_down(ext: Ext) {
- this.move(ext, 0, 1, 0, 0, move_window_or_monitor(
+ this.move(ext, 0, 1, 0, 0, Direction.Down, move_window_or_monitor(
ext,
ext.focus_selector.down,
Meta.DisplayDirection.DOWN
@@ -313,7 +460,7 @@ export class Tiler {
}
move_up(ext: Ext) {
- this.move(ext, 0, -1, 0, 0, move_window_or_monitor(
+ this.move(ext, 0, -1, 0, 0, Direction.Up, move_window_or_monitor(
ext,
ext.focus_selector.up,
Meta.DisplayDirection.UP
@@ -321,7 +468,7 @@ export class Tiler {
}
move_right(ext: Ext) {
- this.move(ext, 1, 0, 0, 0, move_window_or_monitor(
+ this.move(ext, 1, 0, 0, 0, Direction.Right, move_window_or_monitor(
ext,
ext.focus_selector.right,
Meta.DisplayDirection.RIGHT
@@ -437,27 +584,32 @@ export class Tiler {
if (this.window) {
const meta = ext.windows.get(this.window);
if (meta) {
+ let tree_swapped = false;
+
if (this.swap_window) {
const meta_swap = ext.windows.get(this.swap_window);
if (meta_swap) {
if (ext.auto_tiler) {
- ext.auto_tiler.attach_swap(this.swap_window, this.window);
+ tree_swapped = true;
+ ext.auto_tiler.attach_swap(ext, this.swap_window, this.window);
+ } else {
+ ext.size_signals_block(meta_swap);
+
+ meta_swap.move(ext, meta.rect(), () => {
+ ext.size_signals_unblock(meta_swap);
+ });
}
-
- ext.size_signals_block(meta);
- ext.size_signals_block(meta_swap);
-
- meta_swap.move(ext, meta.rect(), () => {
- ext.size_signals_unblock(meta_swap);
- });
}
}
- const meta_entity = this.window;
- meta.move(ext, ext.overlay, () => {
- ext.size_signals_unblock(meta);
- ext.add_tag(meta_entity, Tags.Tiled);
- });
+ if (!tree_swapped) {
+ ext.size_signals_block(meta);
+ const meta_entity = this.window;
+ meta.move(ext, ext.overlay, () => {
+ ext.size_signals_unblock(meta);
+ ext.add_tag(meta_entity, Tags.Tiled);
+ });
+ }
}
}
diff --git a/src/widgets.ts b/src/widgets.ts
index 4dc994d4..f406b4ac 100644
--- a/src/widgets.ts
+++ b/src/widgets.ts
@@ -30,4 +30,4 @@ export class ApplicationBox extends Box {
.add(app_icon)
.add(label);
}
-}
\ No newline at end of file
+}
diff --git a/src/window.ts b/src/window.ts
index 5e9bfb0e..446bb8da 100644
--- a/src/window.ts
+++ b/src/window.ts
@@ -37,9 +37,12 @@ export class ShellWindow {
entity: Entity;
meta: Meta.Window;
ext: Ext;
+ stack: number | null = null;
was_attached_to?: [Entity, boolean];
+ border: St.Bin = new St.Bin({ style_class: 'pop-shell-active-hint' });
+
private was_hidden: boolean = false;
private window_app: any;
@@ -50,9 +53,7 @@ export class ShellWindow {
xid_: new OnceCell()
};
- private _border: St.Bin = new St.Bin({ style_class: 'pop-shell-active-hint' });
-
- private _border_size = 0;
+ private border_size = 0;
constructor(entity: Entity, window: Meta.Window, window_app: any, ext: Ext) {
this.window_app = window_app;
@@ -75,27 +76,23 @@ export class ShellWindow {
}
}
- this._bind_window_events();
+ this.bind_window_events();
- this._border.connect('style-changed', () => {
- this._on_style_changed();
+ this.border.connect('style-changed', () => {
+ this.on_style_changed();
});
- this._border.hide();
+ this.border.hide();
- global.window_group.add_child(this._border);
+ global.window_group.add_child(this.border);
this.restack();
if (this.meta.get_compositor_private()?.get_stage()) {
- this._on_style_changed();
+ this.on_style_changed();
}
}
- get border() {
- return this._border;
- }
-
activate(): void {
activate(this.meta);
}
@@ -104,26 +101,39 @@ export class ShellWindow {
return this.meta.get_compositor_private() !== null;
}
- private decoration(_ext: Ext, callback: (xid: string) => void): void {
- if (this.may_decorate()) {
- const xid = this.xid();
- if (xid) callback(xid);
- }
+ private bind_window_events() {
+ this.ext.window_signals.get_or(this.entity, () => new Array())
+ .push(
+ this.meta.connect('size-changed', () => {
+ this.window_changed();
+ }),
+ this.meta.connect('position-changed', () => {
+ this.window_changed();
+ }),
+ );
}
cmdline(): string | null {
- let pid = this.meta.get_pid();
- if (-1 === pid) return null;
+ let pid = this.meta.get_pid(), out = null;
+ if (-1 === pid) return out;
const path = '/proc/' + pid + '/cmdline';
- if (!utils.exists(path)) return null;
+ if (!utils.exists(path)) return out;
const result = utils.read_to_string(path);
if (result.kind == 1) {
- return result.value.trim();
+ out = result.value.trim();
} else {
log.error(`failed to fetch cmdline: ${result.value.format()}`);
- return null;
+ }
+
+ return out;
+ }
+
+ private decoration(_ext: Ext, callback: (xid: string) => void): void {
+ if (this.may_decorate()) {
+ const xid = this.xid();
+ if (xid) callback(xid);
}
}
@@ -161,11 +171,6 @@ export class ShellWindow {
return WM_TITLE_BLACKLIST.findIndex((n) => name.startsWith(n)) !== -1;
}
- may_decorate(): boolean {
- const xid = this.xid();
- return xid ? xprop.may_decorate(xid) : false;
- }
-
is_maximized(): boolean {
return this.meta.get_maximized() !== 0;
}
@@ -188,6 +193,15 @@ export class ShellWindow {
return this.meta.get_transient_for() !== null;
}
+ hide_border() {
+ this.border.hide();
+ }
+
+ may_decorate(): boolean {
+ const xid = this.xid();
+ return xid ? xprop.may_decorate(xid) : false;
+ }
+
move(ext: Ext, rect: Rectangular, on_complete?: () => void) {
const clone = Rect.Rectangle.from_meta(rect);
const actor = this.meta.get_compositor_private();
@@ -215,16 +229,15 @@ export class ShellWindow {
if (slot !== undefined) {
const [signal, callback] = slot;
Tweener.remove(actor);
+
utils.source_remove(signal);
callback();
}
- Tweener.add(actor, {
- x: clone.x - dx,
- y: clone.y - dy,
- duration: 149,
- mode: null,
- });
+ const x = clone.x - dx;
+ const y = clone.y - dy;
+
+ Tweener.add(actor, { x, y, duration: 149, mode: null });
ext.tween_signals.set(entity_string, [
Tweener.on_window_tweened(this.meta, onComplete),
@@ -240,10 +253,53 @@ export class ShellWindow {
return ext.names.get_or(this.entity, () => "unknown");
}
+ private on_style_changed() {
+ this.border_size = this.border.get_theme_node().get_border_width(St.Side.TOP);
+ }
+
rect(): Rectangle {
return Rect.Rectangle.from_meta(this.meta.get_frame_rect());
}
+ /**
+ * This current does not work properly on Workspace change when single window
+ * because GNOME Shell puts the Window Actor at the top of the border.
+ *
+ * The update_border_layout() adds a padding outside instead to compensate.
+ */
+ restack() {
+ let border = this.border;
+ let actor = this.meta.get_compositor_private();
+ let win_group = global.window_group;
+
+ if (actor && actor.get_parent() === border.get_parent()) {
+ win_group.set_child_above_sibling(border, actor);
+ } else {
+ win_group.set_child_above_sibling(border, null);
+ }
+ this.update_border_layout();
+ }
+
+ private same_workspace() {
+ const workspace = this.meta.get_workspace();
+ if (workspace) {
+ let workspace_id = workspace.index();
+ return workspace_id === global.workspace_manager.get_active_workspace_index()
+ }
+ return false;
+ }
+
+
+ show_border() {
+ if (this.ext.settings.active_hint()) {
+ let border = this.border;
+ if (!this.is_maximized() && !this.meta.minimized && this.same_workspace()) {
+ border.show();
+ }
+ this.restack();
+ }
+ }
+
size_hint(): lib.SizeHint | null {
return this.extra.normal_hints.get_or_init(() => {
const xid = this.xid();
@@ -259,6 +315,21 @@ export class ShellWindow {
this.move(ext, br, () => place_pointer_on(this.meta));
}
+ private update_border_layout() {
+ let frameRect = this.meta.get_frame_rect();
+ let [frameX, frameY, frameWidth, frameHeight] = [frameRect.x, frameRect.y, frameRect.width, frameRect.height];
+
+ let border = this.border;
+ let borderSize = this.border_size;
+
+ border.set_position(frameX - borderSize, frameY - borderSize);
+ border.set_size(frameWidth + 2 * borderSize, frameHeight + 2 * borderSize);
+ }
+
+ private window_changed() {
+ this.ext.show_border_on_focused();
+ }
+
wm_role(): string | null {
return this.extra.wm_role_.get_or_init(() => {
const xid = this.xid();
@@ -282,84 +353,6 @@ export class ShellWindow {
return xprop.get_xid(this.meta);
})
}
-
- show_border() {
- if (this.ext.settings.active_hint()) {
- let border = this._border;
- if (!this.is_maximized() && !this.meta.minimized && this.same_workspace()) {
- border.show();
- }
- this.restack();
- }
- }
-
- same_workspace() {
- const workspace = this.meta.get_workspace();
- if (workspace) {
- let workspace_id = workspace.index();
- return workspace_id === global.workspace_manager.get_active_workspace_index()
- }
- return false;
- }
-
- /**
- * This current does not work properly on Workspace change when single window
- * because GNOME Shell puts the Window Actor at the top of the border.
- *
- * The update_border_layout() adds a padding outside instead to compensate.
- */
- restack() {
- let border = this._border;
- let actor = this.meta.get_compositor_private();
- let win_group = global.window_group;
-
- if (actor && actor.get_parent() === border.get_parent()) {
- win_group.set_child_above_sibling(border, actor);
- } else {
- win_group.set_child_above_sibling(border, null);
- }
- this._update_border_layout();
- }
-
- hide_border() {
- let border = this._border;
- border.hide();
- }
-
- private _update_border_layout() {
- let frameRect = this.meta.get_frame_rect();
- let [frameX, frameY, frameWidth, frameHeight] = [frameRect.x, frameRect.y, frameRect.width, frameRect.height];
-
- let border = this._border;
- let borderSize = this._border_size;
-
- border.set_position(frameX - borderSize, frameY - borderSize);
- border.set_size(frameWidth + 2 * borderSize, frameHeight + 2 * borderSize);
- }
-
- private _bind_window_events() {
- let windowSignals = [
- this.meta.connect('size-changed', () => {
- this._window_changed();
- }),
- this.meta.connect('position-changed', () => {
- this._window_changed();
- }),
- ];
-
- let extWinSignals = this.ext.window_signals.get_or(this.entity, () => new Array());
- Array.prototype.push.apply(extWinSignals, windowSignals);
- }
-
- private _window_changed() {
- this.ext.show_border_on_focused();
- }
-
- private _on_style_changed() {
- let border = this._border;
- let borderNode = border.get_theme_node();
- this._border_size = borderNode.get_border_width(St.Side.TOP);
- }
}
/// Activates a window, and moves the mouse point to the center of it.