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..599e7192 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(); @@ -575,15 +617,26 @@ 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 focus = this.focus_window(); + if (focus) { + if (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 +729,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 +748,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 +801,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 +826,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 +999,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 +1264,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..c8e02d50 --- /dev/null +++ b/src/stack.ts @@ -0,0 +1,466 @@ +// @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', () => { + this.update_positions(window.get_frame_rect()); + }), + window.connect('size-changed', () => { + this.window_changed(); + }), + window.connect('position-changed', () => { + 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) { + global.log(`removing ${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 (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.