diff --git a/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml b/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml index fc766da7..91a29e4f 100644 --- a/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml +++ b/schemas/org.gnome.shell.extensions.pop-shell.gschema.xml @@ -64,6 +64,11 @@ Hide the outer gap when a tree contains only one window + + false + Destroy stacks when separated by the mouse + + false Snaps windows to the tiling grid on drop diff --git a/src/auto_tiler.ts b/src/auto_tiler.ts index d193b9c6..109848ac 100644 --- a/src/auto_tiler.ts +++ b/src/auto_tiler.ts @@ -232,9 +232,9 @@ export class AutoTiler { } /** Detaches the window from a tiling branch, if it is attached to one. */ - detach_window(ext: Ext, win: Entity) { + detach_window(ext: Ext, win: Entity, destroy_stack: boolean = true) { this.attached.take_with(win, (prev_fork: Entity) => { - const reflow_fork = this.forest.detach(ext, prev_fork, win); + const reflow_fork = this.forest.detach(ext, prev_fork, win, destroy_stack); if (reflow_fork) { const fork = reflow_fork[1]; diff --git a/src/forest.ts b/src/forest.ts index cf88d526..bbb797b7 100644 --- a/src/forest.ts +++ b/src/forest.ts @@ -350,7 +350,7 @@ export class Forest extends Ecs.World { } /** Detaches an entity from the a fork, re-arranging the fork's tree as necessary */ - detach(ext: Ext, fork_entity: Entity, window: Entity): [Entity, Fork.Fork] | null { + detach(ext: Ext, fork_entity: Entity, window: Entity, destroy_stack: boolean = false): [Entity, Fork.Fork] | null { const fork = this.forks.get(fork_entity); if (!fork) return null; @@ -387,8 +387,11 @@ export class Forest extends Ecs.World { ext, fork.left.inner as Node.NodeStack, window, - () => { - if (fork.right) { + destroy_stack, + (window: undefined | Entity) => { + if (window) + fork.left = Node.Node.window(window); + else if (fork.right) { fork.left = fork.right fork.right = null if (parent) { @@ -428,9 +431,14 @@ export class Forest extends Ecs.World { ext, fork.right.inner as Node.NodeStack, window, - () => { - fork.right = null + destroy_stack, + (window) => { + if (window) + fork.right = Node.Node.window(window); + else { + fork.right = null; this.reassign_to_parent(fork, fork.left) + } }, ); } @@ -714,7 +722,7 @@ export class Forest extends Ecs.World { } /** 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) { + private remove_from_stack(ext: Ext, stack: Node.NodeStack, window: Entity, destroy_stack: boolean, on_last: (win?: Entity) => void) { if (stack.entities.length === 1) { this.stacks.remove(stack.idx)?.destroy(); on_last(); @@ -723,6 +731,10 @@ export class Forest extends Ecs.World { if (s) { Node.stack_remove(this, stack, window) } + if (destroy_stack && stack.entities.length === 1) { + on_last(stack.entities[0]) + this.stacks.remove(stack.idx)?.destroy() + } } const win = ext.windows.get(window); diff --git a/src/prefs.ts b/src/prefs.ts index 7d326283..4db828be 100644 --- a/src/prefs.ts +++ b/src/prefs.ts @@ -1,4 +1,4 @@ -export { } +export { }; const ExtensionUtils = imports.misc.extensionUtils; // @ts-ignore @@ -8,9 +8,9 @@ const { Gtk } = imports.gi; const { Settings } = imports.gi.Gio; -import * as settings from 'settings'; -import * as log from 'log'; import * as focus from 'focus'; +import * as log from 'log'; +import * as settings from 'settings'; interface AppWidgets { fullscreen_launcher: any, @@ -20,6 +20,7 @@ interface AppWidgets { outer_gap: any, show_skip_taskbar: any, smart_gaps: any, + auto_unstack: any, snap_to_grid: any, window_titles: any, mouse_cursor_focus_position: any, @@ -52,6 +53,12 @@ function settings_dialog_new(): Gtk.Container { Settings.sync(); }) + app.auto_unstack.set_active(ext.auto_unstack()); + app.auto_unstack.connect('state-set', (_widget: any, state: boolean) => { + ext.set_auto_unstack(state); + Settings.sync(); + }); + app.outer_gap.set_text(String(ext.gap_outer())); app.outer_gap.connect('activate', (widget: any) => { let parsed = parseInt((widget.get_text() as string).trim()); @@ -71,7 +78,7 @@ function settings_dialog_new(): Gtk.Container { }); app.log_level.set_active(ext.log_level()); - app.log_level.connect("changed", () => { + app.log_level.connect('changed', () => { let active_id = app.log_level.get_active_id(); ext.set_log_level(active_id); }); @@ -89,20 +96,20 @@ function settings_dialog_new(): Gtk.Container { }); app.mouse_cursor_focus_position.set_active(ext.mouse_cursor_focus_location()); - app.mouse_cursor_focus_position.connect("changed", () => { + app.mouse_cursor_focus_position.connect('changed', () => { let active_id = app.mouse_cursor_focus_position.get_active_id(); ext.set_mouse_cursor_focus_location(active_id); }); - app.fullscreen_launcher.set_active(ext.fullscreen_launcher()) - app.fullscreen_launcher.connect('state-set', (_widget: any, state: boolean) => { - ext.set_fullscreen_launcher(state) + app.fullscreen_launcher.set_active(ext.fullscreen_launcher()); + app.fullscreen_launcher.connect('state-set',(_widget: any, state: boolean) => { + ext.set_fullscreen_launcher(state); Settings.sync() }); - app.stacking_with_mouse.set_active(ext.stacking_with_mouse()) + app.stacking_with_mouse.set_active(ext.stacking_with_mouse()); app.stacking_with_mouse.connect('state-set', (_widget: any, state: boolean) => { - ext.set_stacking_with_mouse(state) + ext.set_stacking_with_mouse(state); Settings.sync() }); @@ -135,6 +142,11 @@ function settings_dialog_view(): [AppWidgets, Gtk.Container] { xalign: 0.0 }) + const unstack_label = new Gtk.Label({ + label: "Destroy stacks when separated by the mouse", + xalign: 0.0 + }) + const show_skip_taskbar_label = new Gtk.Label({ label: "Show Minimize to Tray Windows", xalign: 0.0 @@ -155,7 +167,7 @@ function settings_dialog_view(): [AppWidgets, Gtk.Container] { xalign: 0.0 }) - const [inner_gap, outer_gap] = gaps_section(grid, 9); + const [inner_gap, outer_gap] = gaps_section(grid, 10); const settings = { inner_gap, @@ -163,23 +175,24 @@ function settings_dialog_view(): [AppWidgets, Gtk.Container] { fullscreen_launcher: new Gtk.Switch({ halign: Gtk.Align.END }), stacking_with_mouse: new Gtk.Switch({ halign: Gtk.Align.END }), smart_gaps: new Gtk.Switch({ halign: Gtk.Align.END }), + auto_unstack: new Gtk.Switch({ halign: Gtk.Align.END }), snap_to_grid: new Gtk.Switch({ halign: Gtk.Align.END }), window_titles: new Gtk.Switch({ halign: Gtk.Align.END }), show_skip_taskbar: new Gtk.Switch({ halign: Gtk.Align.END }), mouse_cursor_follows_active_window: new Gtk.Switch({ halign: Gtk.Align.END }), mouse_cursor_focus_position: build_combo( grid, - 7, + 8, focus.FocusPosition, - 'Mouse Cursor Focus Position', + "Mouse Cursor Focus Position" ), log_level: build_combo( - grid, - 8, - log.LOG_LEVELS, - 'Log Level', - ) - } + grid, + 9, + log.LOG_LEVELS, + "Log Level" + ), + }; grid.attach(win_label, 0, 0, 1, 1) grid.attach(settings.window_titles, 1, 0, 1, 1) @@ -190,19 +203,22 @@ function settings_dialog_view(): [AppWidgets, Gtk.Container] { grid.attach(smart_label, 0, 2, 1, 1) grid.attach(settings.smart_gaps, 1, 2, 1, 1) - grid.attach(fullscreen_launcher_label, 0, 3, 1, 1) - grid.attach(settings.fullscreen_launcher, 1, 3, 1, 1) + grid.attach(unstack_label, 0, 3, 1, 1) + grid.attach(settings.auto_unstack, 1, 3, 1, 1) + + grid.attach(fullscreen_launcher_label, 0, 4, 1, 1) + grid.attach(settings.fullscreen_launcher, 1, 4, 1, 1) - grid.attach(stacking_with_mouse, 0, 4, 1, 1) - grid.attach(settings.stacking_with_mouse, 1, 4, 1, 1) + grid.attach(stacking_with_mouse, 0, 5, 1, 1) + grid.attach(settings.stacking_with_mouse, 1, 5, 1, 1) - grid.attach(show_skip_taskbar_label, 0, 5, 1, 1) - grid.attach(settings.show_skip_taskbar, 1, 5, 1, 1) + grid.attach(show_skip_taskbar_label, 0, 6, 1, 1) + grid.attach(settings.show_skip_taskbar, 1, 6, 1, 1) - grid.attach(mouse_cursor_follows_active_window_label, 0, 6, 1, 1) - grid.attach(settings.mouse_cursor_follows_active_window, 1, 6, 1, 1) + grid.attach(mouse_cursor_follows_active_window_label, 0, 7, 1, 1) + grid.attach(settings.mouse_cursor_follows_active_window, 1, 7, 1, 1) - return [settings, grid] + return [settings, grid]; } function gaps_section(grid: any, top: number): [any, any] { diff --git a/src/settings.ts b/src/settings.ts index 39a6a784..28767a14 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -2,7 +2,7 @@ const Me = imports.misc.extensionUtils.getCurrentExtension(); const { Gio, Gdk } = imports.gi; -const DARK = ["dark", "adapta", "plata", "dracula"] +const DARK = ["dark", "adapta", "plata", "dracula"]; interface Settings extends GObject.Object { get_boolean(key: string): boolean; @@ -14,7 +14,7 @@ interface Settings extends GObject.Object { get_string(key: string): string; set_string(key: string, value: string): void; - bind(key: string, object: GObject.Object, property: string, flags: any): void + bind(key: string, object: GObject.Object, property: string, flags: any): void; } function settings_new_id(schema_id: string): Settings | null { @@ -22,10 +22,10 @@ function settings_new_id(schema_id: string): Settings | null { return new Gio.Settings({ schema_id }); } catch (why) { if (schema_id !== "org.gnome.shell.extensions.user-theme") { - global.log(`failed to get settings for ${schema_id}: ${why}`) + global.log(`failed to get settings for ${schema_id}: ${why}`); } - return null + return null; } } @@ -33,15 +33,15 @@ function settings_new_schema(schema: string): Settings { const GioSSS = Gio.SettingsSchemaSource; const schemaDir = Me.dir.get_child("schemas"); - let schemaSource = schemaDir.query_exists(null) ? - GioSSS.new_from_directory(schemaDir.get_path(), GioSSS.get_default(), false) : + let schemaSource = schemaDir.query_exists(null) + ? GioSSS.new_from_directory(schemaDir.get_path(), GioSSS.get_default(), false) : GioSSS.get_default(); const schemaObj = schemaSource.lookup(schema, true); if (!schemaObj) { throw new Error("Schema " + schema + " could not be found for extension " - + Me.metadata.uuid + ". Please check your installation.") + + Me.metadata.uuid + ". Please check your installation."); } return new Gio.Settings({ settings_schema: schemaObj }); @@ -52,19 +52,20 @@ const ACTIVE_HINT_BORDER_RADIUS = "active-hint-border-radius"; const STACKING_WITH_MOUSE = "stacking-with-mouse"; const COLUMN_SIZE = "column-size"; const EDGE_TILING = "edge-tiling"; -const FULLSCREEN_LAUNCHER = "fullscreen-launcher" +const FULLSCREEN_LAUNCHER = "fullscreen-launcher"; const GAP_INNER = "gap-inner"; const GAP_OUTER = "gap-outer"; const ROW_SIZE = "row-size"; const SHOW_TITLE = "show-title"; const SMART_GAPS = "smart-gaps"; +const AUTO_UNSTACK = "auto-unstack"; const SNAP_TO_GRID = "snap-to-grid"; const TILE_BY_DEFAULT = "tile-by-default"; const HINT_COLOR_RGBA = "hint-color-rgba"; const DEFAULT_RGBA_COLOR = "rgba(251, 184, 108, 1)"; //pop-orange const LOG_LEVEL = "log-level"; const SHOW_SKIPTASKBAR = "show-skip-taskbar"; -const MOUSE_CURSOR_FOLLOWS_ACTIVE_WINDOW = "mouse-cursor-follows-active-window" +const MOUSE_CURSOR_FOLLOWS_ACTIVE_WINDOW = "mouse-cursor-follows-active-window"; const MOUSE_CURSOR_FOCUS_LOCATION = "mouse-cursor-focus-location"; export class ExtensionSettings { @@ -96,7 +97,7 @@ export class ExtensionSettings { } fullscreen_launcher(): boolean { - return this.ext.get_boolean(FULLSCREEN_LAUNCHER) + return this.ext.get_boolean(FULLSCREEN_LAUNCHER); } gap_inner(): number { @@ -120,19 +121,19 @@ export class ExtensionSettings { theme(): string { return this.shell - ? this.shell.get_string("name") - : this.int - ? this.int.get_string("gtk-theme") - : "Adwaita" + ? this.shell.get_string("name") + : this.int + ? this.int.get_string("gtk-theme") + : "Adwaita" } is_dark(): boolean { - const theme = this.theme().toLowerCase() - return DARK.some(dark => theme.includes(dark)) + const theme = this.theme().toLowerCase(); + return DARK.some(dark => theme.includes(dark)); } is_high_contrast(): boolean { - return this.theme().toLowerCase() === "highcontrast" + return this.theme().toLowerCase() === "highcontrast"; } row_size(): number { @@ -147,6 +148,10 @@ export class ExtensionSettings { return this.ext.get_boolean(SMART_GAPS); } + auto_unstack(): boolean { + return this.ext.get_boolean(AUTO_UNSTACK); + } + snap_to_grid(): boolean { return this.ext.get_boolean(SNAP_TO_GRID); } @@ -196,11 +201,11 @@ export class ExtensionSettings { } set_edge_tiling(enable: boolean) { - this.mutter?.set_boolean(EDGE_TILING, enable) + this.mutter?.set_boolean(EDGE_TILING, enable); } set_fullscreen_launcher(enable: boolean) { - this.ext.set_boolean(FULLSCREEN_LAUNCHER, enable) + this.ext.set_boolean(FULLSCREEN_LAUNCHER, enable); } set_gap_inner(gap: number) { @@ -233,6 +238,10 @@ export class ExtensionSettings { this.ext.set_boolean(SMART_GAPS, set); } + set_auto_unstack(set: boolean) { + this.ext.set_boolean(AUTO_UNSTACK, set); + } + set_snap_to_grid(set: boolean) { this.ext.set_boolean(SNAP_TO_GRID, set); } diff --git a/src/tiling.ts b/src/tiling.ts index c5c16dce..6717f9b2 100644 --- a/src/tiling.ts +++ b/src/tiling.ts @@ -392,6 +392,15 @@ export class Tiler { detach(Lib.Orientation.VERTICAL, true) break } + if (ext.moved_by_mouse && inner.entities.length === 1 && ext.settings.auto_unstack()) { + const ent = inner.entities[0] + const win = ext.windows.get(ent) + const fork = ext.auto_tiler.get_parent_fork(ent) + if (fork && win) { + ext.auto_tiler.unstack(ext, fork, win) + ext.auto_tiler.tile(ext, fork, fork.area) + } + } } move_auto_(ext: Ext, mov1: Rectangle, mov2: Rectangle, callback: (m: Rectangle, a: Rectangle, mov: Rectangle) => boolean) {