diff --git a/dark.css b/dark.css index d9a2931c..3256714b 100644 --- a/dark.css +++ b/dark.css @@ -55,3 +55,9 @@ .pop-shell-tab-urgent { background: #D00; } + +.pop-shell-resize-hint { + background: #EDEDED; + padding: 12px; + border-radius: 32px; +} \ No newline at end of file diff --git a/highcontrast.css b/highcontrast.css index 1ecd6851..2a6eecd8 100644 --- a/highcontrast.css +++ b/highcontrast.css @@ -58,4 +58,10 @@ .pop-shell-entry:indeterminate { font-style: italic +} + +.pop-shell-resize-hint { + background: #EDEDED; + padding: 12px; + border-radius: 32px; } \ No newline at end of file diff --git a/keybindings/10-pop-shell-move.xml b/keybindings/10-pop-shell-move.xml index 79cb47cb..8d68d659 100644 --- a/keybindings/10-pop-shell-move.xml +++ b/keybindings/10-pop-shell-move.xml @@ -17,13 +17,10 @@ - - - - + diff --git a/light.css b/light.css index c3ff1806..4320edd6 100644 --- a/light.css +++ b/light.css @@ -55,3 +55,9 @@ .pop-shell-tab-urgent { background: #D00; } + +.pop-shell-resize-hint { + background: #EDEDED; + padding: 12px; + border-radius: 32px; +} \ No newline at end of file diff --git a/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml b/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml index 48958ee2..ac4fc793 100644 --- a/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml +++ b/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml @@ -178,25 +178,10 @@ Toggle tiling orientation - - - Left','KP_Left','h']]]> - Resize window left - - - - Down','KP_Down','j']]]> - Resize window down - - - - Up','KP_Up','k']]]> - Resize window up - - - - Right','KP_Right','l']]]> - Resize window right + + + R']]]> + Toggle resize mode diff --git a/src/extension.ts b/src/extension.ts index c3833e96..5f1e09c8 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1003,7 +1003,7 @@ export class Ext extends Ecs.System { if (!win.is_tilable(this)) { return } - + mon = win.meta.get_monitor() work = win.meta.get_workspace().index() diff --git a/src/fork.ts b/src/fork.ts index 852574ab..6f3de8be 100644 --- a/src/fork.ts +++ b/src/fork.ts @@ -242,7 +242,7 @@ export class Fork { let region = this.area.clone(); - const half = this.area.array[l] / 2; + const half = ~~(this.area.array[l] / 32) * 16 let length; if (this.length_left > half - 32 && this.length_left < half + 32) { diff --git a/src/keybindings.ts b/src/keybindings.ts index b3cebaa4..9d4c1019 100644 --- a/src/keybindings.ts +++ b/src/keybindings.ts @@ -16,7 +16,8 @@ export class Keybindings { this.ext = ext; this.global = { "activate-launcher": () => ext.window_search.open(ext), - "tile-enter": () => ext.tiler.enter(ext) + "resize-mode": () => ext.tiler.resize_mode(ext), + "tile-enter": () => ext.tiler.enter(ext), }; this.window_focus = { diff --git a/src/mod.d.ts b/src/mod.d.ts index 00a5ea46..b49b63a2 100644 --- a/src/mod.d.ts +++ b/src/mod.d.ts @@ -293,7 +293,7 @@ declare namespace St { show(): void; } - interface Bin extends St.Widget { + interface Bin extends Widget { // empty for now } @@ -303,4 +303,8 @@ declare namespace St { get_clutter_text(): Clutter.Text; set_hint_text(hint: string): void; } + + interface Icon extends Widget { + icon_name: string; + } } diff --git a/src/stack.ts b/src/stack.ts index 639de84d..d77a4b52 100644 --- a/src/stack.ts +++ b/src/stack.ts @@ -10,7 +10,7 @@ import * as a from 'arena'; import * as utils from 'utils'; const Arena = a.Arena; -const { St } = imports.gi; +const { Clutter, GObject, St } = imports.gi; const ACTIVE_TAB = 'pop-shell-tab pop-shell-tab-active'; const INACTIVE_TAB = 'pop-shell-tab pop-shell-tab-inactive'; @@ -42,6 +42,78 @@ function stack_widgets_new(): StackWidgets { return { tabs }; } +const ContainerButton = GObject.registerClass({ + Signals: { 'activate': {} }, +}, class ImageButton extends St.Button { + _init(icon: St.Icon) { + super._init({ + child: icon, + x_expand: true, + y_expand: true, + }) + } +}) + +interface TabButton extends St.Button { + set_title: (title: string) => void; +} + +const TabButton = GObject.registerClass({ + Signals: { 'activate': {} }, +}, class TabButton extends St.Button { + _init(window: ShellWindow) { + const icon = window.icon(window.ext, 24) + icon.set_x_align(Clutter.ActorAlign.START) + + const label = new St.Label({ + y_expand: true, + x_align: Clutter.ActorAlign.START, + y_align: Clutter.ActorAlign.CENTER, + style: "padding-left: 8px" + }) + + label.text = window.title() + + const container = new St.BoxLayout({ + y_expand: true, + y_align: Clutter.ActorAlign.CENTER, + }) + + const close_button = new ContainerButton(new St.Icon({ + icon_name: 'window-close-symbolic', + icon_size: 24, + y_align: Clutter.ActorAlign.CENTER, + })) + + close_button.connect('clicked', () => { + window.meta.delete(global.get_current_time()) + }) + + close_button.set_x_align(Clutter.ActorAlign.END) + close_button.set_y_align(Clutter.ActorAlign.CENTER) + + container.add_actor(icon) + container.add_actor(label) + container.add_actor(close_button) + + super._init({ + child: container, + x_expand: true, + y_expand: true, + y_align: Clutter.ActorAlign.CENTER, + }) + + + this._title = label + } + + set_title(text: string) { + if (this._title) { + this._title.text = text + } + } +}) + export class Stack { ext: Ext; @@ -60,7 +132,7 @@ export class Stack { workspace: number; - buttons: a.Arena = new Arena(); + buttons: a.Arena = new Arena(); tabs_height: number = TAB_HEIGHT; @@ -95,15 +167,9 @@ export class Stack { if (!this.widgets) return; const entity = window.entity; - const label = window.title() const active = Ecs.entity_eq(entity, this.active); - const button: St.Button = new St.Button({ - label, - x_expand: true, - style_class: active ? ACTIVE_TAB : INACTIVE_TAB - }); - + const button = new TabButton(window) const id = this.buttons.insert(button); let tab: Tab = { active, entity, signals: [], button: id, button_signal: null }; @@ -443,7 +509,7 @@ export class Stack { } this.watch_signals(this.active_id, c.button, window); - this.buttons.get(c.button)?.set_label(window.title()); + this.buttons.get(c.button)?.set_title(window.title()); this.activate(window.entity); } } @@ -598,7 +664,7 @@ export class Stack { this.tabs[comp].signals = [ window.meta.connect('notify::title', () => { this.window_exec(comp, entity, (window) => { - this.buttons.get(button)?.set_label(window.title()) + this.buttons.get(button)?.set_title(window.title()) }); }), diff --git a/src/tiling.ts b/src/tiling.ts index c5c16dce..da4645f2 100644 --- a/src/tiling.ts +++ b/src/tiling.ts @@ -1,8 +1,6 @@ // @ts-ignore const Me = imports.misc.extensionUtils.getCurrentExtension(); -// 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'; @@ -11,16 +9,19 @@ import * as Tags from 'tags'; import * as window from 'window'; import * as geom from 'geom'; import * as exec from 'executor'; +import * as movement from 'movement'; +import * as stack from 'stack'; +import * as utils from 'utils'; 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 { Clutter, Meta, St } = imports.gi; const Main = imports.ui.main; +const { layoutManager } = Main const { ShellWindow } = window; export enum Direction { @@ -30,8 +31,54 @@ export enum Direction { Down } +const ICON_LEFT_ARROW: string = "go-previous-symbolic" +const ICON_RIGHT_ARROW: string = "go-next-symbolic" +const ICON_UP_ARROW: string = "go-up-symbolic" +const ICON_DOWN_ARROW: string = "go-down-symbolic" + export class Tiler { - private keybindings: Object; + private keybindings: Object + private resize_bindings: Object + private resize_grab: any = null + private resize_keymon: null | number = null + private resize_keymon_release: null | number = null + + private resize_hint: St.Widget = new St.BoxLayout({ + vertical: true, + style: "background: transparent" + }) + + private resize_up: St.Icon = new St.Icon({ + icon_name: ICON_UP_ARROW, + icon_size: 32, + x_align: Clutter.ActorAlign.CENTER, + style_class: "pop-shell-resize-hint", + visible: false, + }) + + private resize_left: St.Icon = new St.Icon({ + icon_name: ICON_LEFT_ARROW, + icon_size: 32, + y_align: Clutter.ActorAlign.CENTER, + style_class: "pop-shell-resize-hint", + visible: false, + }) + + private resize_right: St.Icon = new St.Icon({ + icon_name: ICON_RIGHT_ARROW, + icon_size: 32, + y_align: Clutter.ActorAlign.CENTER, + style_class: "pop-shell-resize-hint", + visible: false, + }) + + private resize_down: St.Icon = new St.Icon({ + icon_name: ICON_DOWN_ARROW, + icon_size: 32, + x_align: Clutter.ActorAlign.CENTER, + style_class: "pop-shell-resize-hint", + visible: false, + }) window: Entity | null = null; @@ -43,16 +90,40 @@ export class Tiler { queue: exec.ChannelExecutor<() => void> = new exec.ChannelExecutor() constructor(ext: Ext) { + this.resize_hint.visible = false; + + const left_box = new St.BoxLayout({ + x_align: Clutter.ActorAlign.START, + x_expand: true, + }) + + left_box.add(this.resize_left) + + const right_box = new St.BoxLayout({ + x_align: Clutter.ActorAlign.END, + }) + + right_box.add(this.resize_right) + + const middle_arrows = new St.BoxLayout({ vertical: false, x_expand: true, y_expand: true }) + + middle_arrows.add(left_box) + middle_arrows.add(right_box) + + this.resize_hint.add(this.resize_up) + this.resize_hint.add(middle_arrows) + this.resize_hint.add(this.resize_down) + this.resize_hint.width = 128 + this.resize_hint.height = 128 + + layoutManager.addChrome(this.resize_hint) + this.keybindings = { "management-orientation": () => this.toggle_orientation(ext), "tile-move-left": () => this.move_left(ext), "tile-move-down": () => this.move_down(ext), "tile-move-up": () => this.move_up(ext), "tile-move-right": () => this.move_right(ext), - "tile-resize-left": () => this.resize(ext, Direction.Left), - "tile-resize-down": () => this.resize(ext, Direction.Down), - "tile-resize-up": () => this.resize(ext, Direction.Up), - "tile-resize-right": () => this.resize(ext, Direction.Right), "tile-swap-left": () => this.swap_left(ext), "tile-swap-down": () => this.swap_down(ext), "tile-swap-up": () => this.swap_up(ext), @@ -61,6 +132,104 @@ export class Tiler { "tile-reject": () => this.exit(ext), "toggle-stacking": () => this.toggle_stacking(ext), }; + + this.resize_bindings = { + "tile-accept": () => this.exit(ext), + "tile-reject": () => this.exit(ext), + } + } + + resize_mode(ext: Ext) { + if (!this.window) { + const win = ext.focus_window(); + if (!win) return; + + if (this.resize_keymon !== null) { + global.stage.disconnect(this.resize_keymon) + } + + if (this.resize_keymon_release !== null) { + global.stage.disconnect(this.resize_keymon_release) + } + + this.resize_keymon = global.stage.connect("key-press-event", (_: any, event: any) => { + const state: number = event.get_state() + + switch (event.get_key_symbol()) { + case Clutter.KEY_Escape: + case Clutter.KEY_Return: + this.exit(ext) + break + case Clutter.KEY_Shift_L: + case Clutter.KEY_Shift_R: + this.reverse_arrows() + break + case Clutter.KEY_Left: + case Clutter.KEY_H: + case Clutter.KEY_h: + this.resize(ext, Direction.Left, state === 1) + break + case Clutter.KEY_Right: + case Clutter.KEY_L: + case Clutter.KEY_l: + this.resize(ext, Direction.Right, state === 1) + break + case Clutter.KEY_Up: + case Clutter.KEY_K: + case Clutter.KEY_k: + this.resize(ext, Direction.Up, state === 1) + break + case Clutter.KEY_Down: + case Clutter.KEY_J: + case Clutter.KEY_j: + this.resize(ext, Direction.Down, state === 1) + break + default: + return Clutter.EVENT_PROPAGATE + } + }) + + this.resize_keymon_release = global.stage.connect("key-release-event", (_: any, event: any) => { + switch (event.get_key_symbol()) { + case Clutter.KEY_Shift_L: + case Clutter.KEY_Shift_R: + this.reset_arrows() + default: + return Clutter.EVENT_PROPAGATE + } + }) + + this.resize_grab = Main.pushModal(global.stage) + + this.window = win.entity; + + ext.keybindings.disable(ext.keybindings.window_focus) + .disable(this.keybindings) + .enable(this.resize_bindings); + + this.update_resize_position(ext) + + const color_value = ext.settings.hint_color_rgba(); + const css = `background: ${color_value}; color: ${utils.is_dark(color_value) ? 'white' : 'black'}`; + this.resize_up.set_style(css) + this.resize_down.set_style(css) + this.resize_left.set_style(css) + this.resize_right.set_style(css) + } + } + + private reverse_arrows() { + this.resize_up.icon_name = ICON_DOWN_ARROW + this.resize_down.icon_name = ICON_UP_ARROW + this.resize_left.icon_name = ICON_RIGHT_ARROW + this.resize_right.icon_name = ICON_LEFT_ARROW + } + + private reset_arrows() { + this.resize_up.icon_name = ICON_UP_ARROW + this.resize_down.icon_name = ICON_DOWN_ARROW + this.resize_left.icon_name = ICON_LEFT_ARROW + this.resize_right.icon_name = ICON_RIGHT_ARROW } toggle_orientation(ext: Ext) { @@ -86,6 +255,34 @@ export class Tiler { return monitor_rect(monitor, columns, rows); } + update_resize_position(ext: Ext) { + ext.register_fn(() => { + if (this.window) { + const window = ext.windows.get(this.window) + if (window) { + const area = window.rect() + this.resize_hint.visible = true + this.resize_hint.x = area.x - 32 + this.resize_hint.y = area.y - 32 + this.resize_hint.width = 64 + area.width + this.resize_hint.height = 64 + area.height + + const work_area = ext.monitor_work_area(window.meta.get_monitor()) + let { x, y, width, height } = this.resize_hint + const wy = work_area.y + ext.gap_outer + const wx = work_area.x + ext.gap_outer + const wh = work_area.height - (ext.gap_outer * 2) + const ww = work_area.width - (ext.gap_outer * 2) + + this.resize_up.visible = y >= wy + this.resize_left.visible = x >= wx + this.resize_down.visible = y + height <= wy + wh + this.resize_right.visible = x + width <= wx + ww + } + } + }) + } + change(overlay: Rectangular, rect: Rectangle, dx: number, dy: number, dw: number, dh: number): Tiler { let changed = new Rect.Rectangle([ overlay.x + dx * rect.width, @@ -394,50 +591,6 @@ export class Tiler { } } - move_auto_(ext: Ext, mov1: Rectangle, mov2: Rectangle, callback: (m: Rectangle, a: Rectangle, mov: Rectangle) => boolean) { - if (ext.auto_tiler && this.window) { - const entity = ext.auto_tiler.attached.get(this.window); - if (entity) { - const fork = ext.auto_tiler.forest.forks.get(entity); - const window = ext.windows.get(this.window); - - if (!fork || !window) return; - - const workspace_id = ext.workspace_id(window); - - const toplevel = ext.auto_tiler.forest.find_toplevel(workspace_id); - - if (!toplevel) return; - - const topfork = ext.auto_tiler.forest.forks.get(toplevel); - - if (!topfork) return; - - const toparea = topfork.area as Rect.Rectangle; - - const before = window.rect(); - - const grab_op = new GrabOp.GrabOp((this.window as Entity), before); - - let crect = grab_op.rect.clone(); - - let resize = (mov: Rectangle, func: (m: Rectangle, a: Rectangle, mov: Rectangle) => boolean) => { - if (func(toparea, crect, mov) || crect.eq(grab_op.rect)) return; - - (ext.auto_tiler as AutoTiler).forest.resize(ext, entity, fork, (this.window as Entity), grab_op.operation(crect), crect); - grab_op.rect = crect.clone(); - }; - - resize(mov1, callback); - resize(mov2, callback); - - ext.auto_tiler.forest.arrange(ext, fork.workspace); - - ext.register_fn(() => ext.set_overlay(window.rect())); - } - } - } - overlay_watch(ext: Ext, window: window.ShellWindow) { ext.register_fn(() => { if (window) { @@ -461,45 +614,6 @@ export class Tiler { } } - resize_auto(ext: Ext, direction: Direction) { - let mov1: [number, number, number, number], mov2: [number, number, number, number]; - - const hrow = 64; - const hcolumn = 64; - - switch (direction) { - case Direction.Left: - mov1 = [hrow, 0, -hrow, 0]; - mov2 = [0, 0, -hrow, 0]; - break; - case Direction.Right: - mov1 = [0, 0, hrow, 0]; - mov2 = [-hrow, 0, hrow, 0]; - break; - case Direction.Up: - mov1 = [0, hcolumn, 0, -hcolumn]; - mov2 = [0, 0, 0, -hcolumn]; - break; - default: - mov1 = [0, 0, 0, hcolumn]; - mov2 = [0, -hcolumn, 0, hcolumn]; - } - - this.move_auto_( - ext, - new Rect.Rectangle(mov1), - new Rect.Rectangle(mov2), - (work_area, crect, mov) => { - crect.apply(mov); - let before = crect.clone(); - crect.clamp(work_area); - const diff = before.diff(crect); - crect.apply(new Rect.Rectangle([0, 0, -diff.x, -diff.y])); - return false; - }, - ); - } - move_auto(ext: Ext, focused: window.ShellWindow, move_to: window.ShellWindow | number, stack_from_left: boolean = true) { let watching: null | window.ShellWindow = null; @@ -590,7 +704,7 @@ export class Tiler { Meta.DisplayDirection.DOWN )); } - + move_up(ext: Ext, window?: Entity) { this.move(ext, window ?? this.window, 0, -1, 0, 0, Direction.Up, move_window_or_monitor( ext, @@ -607,38 +721,86 @@ export class Tiler { )); } - resize(ext: Ext, direction: Direction) { - if (!this.window) return; - this.resizing_window = true + resize(ext: Ext, direction: Direction, inverse: boolean) { + if (!this.window) return - if (ext.auto_tiler && !ext.contains_tag(this.window, Tags.Floating)) { - this.resize_auto(ext, direction); - } else { - let array: [number, number, number, number]; - switch (direction) { - case Direction.Down: - array = [0, 0, 0, 1]; - break - case Direction.Left: - array = [0, 0, -1, 0]; - break - case Direction.Up: - array = [0, 0, 0, -1]; - break - default: - array = [0, 0, 1, 0]; - } + const window = ext.windows.get(this.window) + if (!window) return - const [x, y, w, h] = array; + if (ext.auto_tiler) { + const fork_entity = ext.auto_tiler.attached.get(window.entity) + if (fork_entity) { + const forest = ext.auto_tiler.forest + const fork = forest.forks.get(fork_entity) + if (fork) { + let top_level = forest.find_toplevel(ext.workspace_id()); + if (top_level) { + const work_area = (forest.forks.get(top_level) as Fork).area + const before = window.rect() + + let [x, y, width, height] = before.array + + const step = 64; + + const is_leftmost = x <= work_area.x + const is_topmost = y <= work_area.y + + if (inverse && Direction.Down === direction) { + if (y - step < work_area.y) return + y += step + height -= step + } else if (inverse && Direction.Left === direction) { + if (x + width + step > work_area.x + work_area.width) return + width -= step + if (is_leftmost) width += step / 2 + } else if (inverse && Direction.Up === direction) { + if (y + height + step > work_area.y + work_area.height) return + height -= step + if (is_topmost) height += step / 2 + } else if (inverse) { + if (x - step < work_area.x) return + x += step + width -= step + } else if (Direction.Down === direction) { + if (y + height + step > work_area.y + work_area.height) return + height += step + } else if (Direction.Left === direction) { + if (x - step < work_area.x) return + width += step + x -= step + } else if (Direction.Up === direction) { + if (y - step < work_area.y) return + y -= step + height += step + } else { + if (x + width + step > work_area.x + work_area.width) return + width += step + } - this.swap_window = null; - this.rect_by_active_area(ext, (_monitor, rect) => { - this.change(ext.overlay, rect, x, y, w, h) - .change(ext.overlay, rect, 0, 0, 0, 0); - }); - } + const after = new Rect.Rectangle([x, y, width, height]) + + if (window.stack) { + const tab_dimension = ext.dpi * stack.TAB_HEIGHT; + after.height += tab_dimension; + after.y -= tab_dimension; + } + + after.clamp(work_area); - this.resizing_window = false + const change = movement.calculate(before, after) + window.meta.move_resize_frame(true, after.x, after.y, after.width, after.height) + if (ext.movement_is_valid(window, change)) { + forest.resize(ext, fork_entity, fork, window.entity, change, after); + forest.arrange(ext, fork.workspace); + } else { + forest.tile(ext, fork, fork.area); + } + + this.update_resize_position(ext) + } + } + } + } } swap(ext: Ext, selector: window.ShellWindow | null) { @@ -758,14 +920,26 @@ export class Tiler { exit(ext: Ext) { this.queue.clear() + if (this.resize_keymon !== null) { + global.stage.disconnect(this.resize_keymon) + this.resize_keymon = null + Main.popModal(this.resize_grab) + } + + if (this.resize_keymon_release !== null) { + global.stage.disconnect(this.resize_keymon_release) + } + if (this.window) { this.window = null; + this.resize_hint.visible = false // Disable overlay ext.overlay.visible = false; // Disable tiling keybindings ext.keybindings.disable(this.keybindings) + .disable(this.resize_bindings) .enable(ext.keybindings.window_focus); } } diff --git a/src/window.ts b/src/window.ts index 5f63c145..25a8ee34 100644 --- a/src/window.ts +++ b/src/window.ts @@ -419,7 +419,7 @@ export class ShellWindow { this.restack(); if (this.ext.settings.active_hint()) { - let border = this.border; + const border = this.border; const permitted = () => { return this.actor_exists()