diff --git a/README.md b/README.md index 1299ef6..b2d212d 100644 --- a/README.md +++ b/README.md @@ -127,9 +127,13 @@ You can select your favorite layout for each workspace of each monitor. When a window is created, it is automatically moved to the best tile according to where other windows are tiled and the current layout. This is disabled by default and can be enabled in the preferences. - [automatic_tiling](https://github.com/user-attachments/assets/76abc53f-2c6d-47ab-bee3-bbcdd946f2a1) +### Export and import layouts ### + +*Tiling Shell* supports importing and exporting its layouts to a JSON file. With this you can create your own custom layouts without the built-in graphical editor, or share your layouts with others! If you are interested into knowing more about the contents of the layout file check the official [documentation](./doc/json-internal-documentation.md). + +

Go to Usage ⬆️

## Installation diff --git a/doc/example-layouts.json b/doc/example-layouts.json new file mode 100644 index 0000000..900b084 --- /dev/null +++ b/doc/example-layouts.json @@ -0,0 +1,60 @@ +[ + { + "id": "split-half", + "tiles": [ + { + "x": 0, + "y": 0, + "width": 0.5, + "height": 1, + "groups": [ + 2 + ] + }, + { + "x": 0.5, + "y": 0, + "width": 0.5, + "height": 1, + "groups": [ + 1 + ] + } + ] + }, + { + "id": "split-thirds", + "tiles": [ + { + "x": 0, + "y": 0, + "width": 0.333, + "height": 1, + "groups": [ + 2, + 3 + ] + }, + { + "x": 0.333, + "y": 0, + "width": 0.333, + "height": 1, + "groups": [ + 3, + 1 + ] + }, + { + "x": 0.666, + "y": 0, + "width": 0.333, + "height": 1, + "groups": [ + 2, + 1 + ] + } + ] + } +] diff --git a/doc/json-internal-documentation.md b/doc/json-internal-documentation.md new file mode 100644 index 0000000..7a62e29 --- /dev/null +++ b/doc/json-internal-documentation.md @@ -0,0 +1,79 @@ +# Documentation for JSON exported layouts + +*Tiling Shell* supports importing and exporting its layouts as a JSON file. With this you can create your own custom layouts, or fine-tune already existing layouts. + +The exported layouts (from the preferences) are a collection of `Layout` objects. A `Layout` object is an object with two (2) properties: + +- identifier as a `string` +- a list of `Tile` objects + +Example JSON of a `Layout` object would look like + +```json +{ + "id": "The identifier", + "tiles": [ + ... + ] +} +``` + +A `Tile` object has five (5) properties: + +- The X (`x`) axis as a `float` +- The Y (`y`) axis as a `float` +- The width (`width`) as a `float` +- The height (`height`) as a `float` +- A list of identifiers `groups` + +The `x`, `y`, `width` and `height` are percentages relative to the screen size. Both `x` and `y` start from the top left of a `Tile`. + +So a `Tile` with `x` = 0.5 and `y` = 0.5, on a screen with a resolution of 1920x1080 pixels is placed at `x = 0.5 * 1920 = 960px` and `y = 0.5 * 1080 = 540px`. For example, if the `width` and `height` of the `Tile` are set to `0.25`, this gives a `Tile` of `width = 0.25 * 1920 = 480px` and `height = 0.25 * 1080 = 270px`. + +The `group` attribute is mainly used in the layout editor where it determines which `Tile`(s) are "linked": if you resize a single `Tile` it's linked neighbour(s) are also updated. + +For more in depth information you can look at an [in depth explanation](https://github.com/domferr/tilingshell/issues/177#issuecomment-2458322208) of `group`(s). + +Example JSON of a `Tile` object would look like this + +```json +{ + "x": 0, + "y": 0, + "width": 1, + "height": 1, + "groups": [ + 1 + ] +} +``` + +## Example JSON file + +Finally, an example JSON file describing one Layout with two tiles. + +```json +{ + "id": "Equal split", + "tiles": [ + { + "x": 0, + "y": 0, + "width": 0.5, + "height": 1, + "groups": [ + 1 + ] + }, + { + "x": 0.5, + "y": 0, + "width": 0.5, + "height": 1, + "groups": [ + 1 + ] + } + ] +} +``` diff --git a/esbuild.mjs b/esbuild.mjs index f646039..86c3775 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -18,6 +18,10 @@ class Extension { getSettings() { return imports.misc.extensionUtils.getSettings(); } + + static openPrefs() { + return imports.misc.extensionUtils.openPrefs(); + } } class Mtk { Rectangle } diff --git a/package.json b/package.json index e412197..57be3e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tilingshell", - "version": "15.0", + "version": "15.1", "author": "Domenico Ferraro ", "private": true, "license": "GPL v2.0", diff --git a/resources/icons/prefs-symbolic.svg b/resources/icons/prefs-symbolic.svg new file mode 100644 index 0000000..114370e --- /dev/null +++ b/resources/icons/prefs-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/resources/locale/it/LC_MESSAGES/tilingshell.mo b/resources/locale/it/LC_MESSAGES/tilingshell.mo index d686285..b339723 100644 Binary files a/resources/locale/it/LC_MESSAGES/tilingshell.mo and b/resources/locale/it/LC_MESSAGES/tilingshell.mo differ diff --git a/resources/metadata.json b/resources/metadata.json index 5d8e03a..3cc116c 100644 --- a/resources/metadata.json +++ b/resources/metadata.json @@ -11,7 +11,7 @@ "47" ], "version": 99, - "version-name": "15.0", + "version-name": "15.1", "url": "https://github.com/domferr/tilingshell", "settings-schema": "org.gnome.shell.extensions.tilingshell", "gettext-domain": "tilingshell", diff --git a/resources/schemas/org.gnome.shell.extensions.tilingshell.gschema.xml b/resources/schemas/org.gnome.shell.extensions.tilingshell.gschema.xml index f9462a0..7dce347 100644 --- a/resources/schemas/org.gnome.shell.extensions.tilingshell.gschema.xml +++ b/resources/schemas/org.gnome.shell.extensions.tilingshell.gschema.xml @@ -67,6 +67,11 @@ Restore window size Restore the windows to their original size when untiled. + + true + Enable next/previous window focus to wrap around + When focusing next or previous window, wrap around at the window edge + true Enable auto-resize of the complementing tiled windows @@ -137,6 +142,11 @@ Focused window border width The width of the focused window's border. + + true + Enable smart window border radius + Dinamically adapt to the window's border radius. + 180 Snap assistant animation time (milliseconds) @@ -209,6 +219,14 @@ Focus the window below the current focused window + + + Focus the window next to the current focused window + + + + Focus the window prior to the current focused window + diff --git a/src/components/editor/editableTilePreview.ts b/src/components/editor/editableTilePreview.ts index fa0e7b4..d57b1c7 100644 --- a/src/components/editor/editableTilePreview.ts +++ b/src/components/editor/editableTilePreview.ts @@ -18,7 +18,6 @@ export default class EditableTilePreview extends TilePreview { public static MIN_TILE_SIZE: number = 140; private readonly _btn: St.Button; - private readonly _tile: Tile; private readonly _containerRect: Mtk.Rectangle; private _sliders: (Slider | null)[]; @@ -51,10 +50,6 @@ export default class EditableTilePreview extends TilePreview { this.connect('destroy', this._onDestroy.bind(this)); } - public get tile(): Tile { - return this._tile; - } - public getSlider(side: St.Side): Slider | null { return this._sliders[side]; } diff --git a/src/components/editor/layoutEditor.ts b/src/components/editor/layoutEditor.ts index df1f63c..e74a5ff 100644 --- a/src/components/editor/layoutEditor.ts +++ b/src/components/editor/layoutEditor.ts @@ -72,6 +72,7 @@ export default class LayoutEditor extends St.Widget { this._layout = layout; this._drawEditor(); + this.grab_key_focus(); this.connect('destroy', this._onDestroy.bind(this)); } diff --git a/src/components/snapassist/snapAssist.ts b/src/components/snapassist/snapAssist.ts index 7a3d6aa..e28ddb3 100644 --- a/src/components/snapassist/snapAssist.ts +++ b/src/components/snapassist/snapAssist.ts @@ -36,7 +36,7 @@ class SnapAssistContent extends St.BoxLayout { 'Distance from the snap assistant to trigger its opening/closing', GObject.ParamFlags.READWRITE, 0, - 240, + 2000, 16, ), snapAssistantAnimationTime: GObject.ParamSpec.uint( diff --git a/src/components/snapassist/snapAssistTile.ts b/src/components/snapassist/snapAssistTile.ts index b4bde3b..ecc46f5 100644 --- a/src/components/snapassist/snapAssistTile.ts +++ b/src/components/snapassist/snapAssistTile.ts @@ -6,7 +6,6 @@ import { getScalingFactorOf } from '@utils/ui'; @registerGObjectClass export default class SnapAssistTile extends TilePreview { - protected _tile: Tile; private _styleChangedSignalID: number | undefined; constructor(params: { @@ -53,10 +52,6 @@ export default class SnapAssistTile extends TilePreview { this.set_style_class_name('snap-assist-tile button'); } - public get tile() { - return this._tile; - } - _applyStyle() { // the tile will be light or dark, following the text color const [hasColor, { red, green, blue }] = diff --git a/src/components/tilepreview/popupTilePreview.ts b/src/components/tilepreview/popupTilePreview.ts new file mode 100644 index 0000000..1de8b49 --- /dev/null +++ b/src/components/tilepreview/popupTilePreview.ts @@ -0,0 +1,95 @@ +import { registerGObjectClass } from '@/utils/gjs'; +import { GObject, St, Clutter, Gio, Mtk } from '@gi.ext'; +import TilePreview from './tilePreview'; +import Settings from '@settings/settings'; +import { buildBlurEffect } from '@utils/ui'; +import Tile from '@components/layout/Tile'; + +@registerGObjectClass +export default class PopupTilePreview extends TilePreview { + static metaInfo: GObject.MetaInfo = { + GTypeName: 'PopupTilePreview', + Properties: { + blur: GObject.ParamSpec.boolean( + 'blur', + 'blur', + 'Enable or disable the blur effect', + GObject.ParamFlags.READWRITE, + false, + ), + }, + }; + + private _blur: boolean; + + constructor(params: { + parent: Clutter.Actor; + tile?: Tile; + rect?: Mtk.Rectangle; + gaps?: Clutter.Margin; + }) { + super(params); + + this._blur = false; + + // blur not supported due to GNOME shell known bug + /* Settings.bind( + Settings.KEY_ENABLE_BLUR_SELECTED_TILEPREVIEW, + this, + 'blur', + Gio.SettingsBindFlags.GET, + );*/ + + this._recolor(); + const styleChangedSignalID = St.ThemeContext.get_for_stage( + global.get_stage(), + ).connect('changed', () => { + this._recolor(); + }); + this.connect('destroy', () => + St.ThemeContext.get_for_stage(global.get_stage()).disconnect( + styleChangedSignalID, + ), + ); + } + + set blur(value: boolean) { + if (this._blur === value) return; + + this._blur = value; + // blur not supported due to GNOME shell known bug + /* this.get_effect('blur')?.set_enabled(value); + if (this._blur) this.add_style_class_name('blur-tile-preview'); + else this.remove_style_class_name('blur-tile-preview'); + + this._recolor();*/ + } + + _init() { + super._init(); + + const effect = buildBlurEffect(48); + effect.set_name('blur'); + effect.set_enabled(this._blur); + this.add_effect(effect); + + this.add_style_class_name('selection-tile-preview'); + } + + _recolor() { + this.set_style(null); + + const backgroundColor = this.get_theme_node() + .get_background_color() + .copy(); + // since an alpha value lower than 160 is not so much visible, enforce a minimum value of 160 + const newAlpha = Math.max( + Math.min(backgroundColor.alpha + 35, 255), + 160, + ); + // The final alpha value is divided by 255 since CSS needs a value from 0 to 1, but ClutterColor expresses alpha from 0 to 255 + this.set_style(` + background-color: rgba(${backgroundColor.red}, ${backgroundColor.green}, ${backgroundColor.blue}, ${newAlpha / 255}) !important; + `); + } +} diff --git a/src/components/tilepreview/selectionTilePreview.ts b/src/components/tilepreview/selectionTilePreview.ts index 770c085..1047bf7 100644 --- a/src/components/tilepreview/selectionTilePreview.ts +++ b/src/components/tilepreview/selectionTilePreview.ts @@ -1,8 +1,9 @@ import { registerGObjectClass } from '@/utils/gjs'; -import { GObject, St, Clutter, Gio } from '@gi.ext'; +import { GObject, St, Clutter, Gio, Mtk } from '@gi.ext'; import TilePreview from './tilePreview'; import Settings from '@settings/settings'; import { buildBlurEffect } from '@utils/ui'; +import Tile from '@components/layout/Tile'; @registerGObjectClass export default class SelectionTilePreview extends TilePreview { @@ -21,8 +22,14 @@ export default class SelectionTilePreview extends TilePreview { private _blur: boolean; - constructor(params: { parent: Clutter.Actor }) { - super({ parent: params.parent, name: 'SelectionTilePreview' }); + constructor(params: { + parent: Clutter.Actor; + tile?: Tile; + containerRect?: Mtk.Rectangle; + rect?: Mtk.Rectangle; + gaps?: Clutter.Margin; + }) { + super(params); this._blur = false; diff --git a/src/components/tilepreview/tilePreview.ts b/src/components/tilepreview/tilePreview.ts index b2e7da8..ced81bc 100644 --- a/src/components/tilepreview/tilePreview.ts +++ b/src/components/tilepreview/tilePreview.ts @@ -2,6 +2,7 @@ import { St, Clutter, Mtk, Meta } from '@gi.ext'; import { registerGObjectClass } from '@/utils/gjs'; import { buildRectangle, getScalingFactorOf } from '@utils/ui'; import GlobalState from '@utils/globalState'; +import Tile from '@components/layout/Tile'; // export module TilePreview { export interface TilePreviewConstructorProperties @@ -9,6 +10,7 @@ export interface TilePreviewConstructorProperties parent: Clutter.Actor; rect: Mtk.Rectangle; gaps: Clutter.Margin; + tile: Tile; } // } @@ -16,6 +18,7 @@ export interface TilePreviewConstructorProperties export default class TilePreview extends St.Widget { protected _rect: Mtk.Rectangle; protected _showing: boolean; + protected _tile: Tile; private _gaps: Clutter.Margin; @@ -27,6 +30,9 @@ export default class TilePreview extends St.Widget { this._rect = params.rect || buildRectangle({}); this._gaps = new Clutter.Margin(); this.gaps = params.gaps || new Clutter.Margin(); + this._tile = + params.tile || + new Tile({ x: 0, y: 0, width: 0, height: 0, groups: [] }); } public set gaps(gaps: Clutter.Margin) { @@ -50,6 +56,10 @@ export default class TilePreview extends St.Widget { return this._gaps; } + public get tile() { + return this._tile; + } + _init() { super._init(); this.set_style_class_name('tile-preview custom-tile-preview'); diff --git a/src/components/tilingsystem/popupWindowPreview.ts b/src/components/tilingsystem/popupWindowPreview.ts new file mode 100644 index 0000000..4fb1641 --- /dev/null +++ b/src/components/tilingsystem/popupWindowPreview.ts @@ -0,0 +1,417 @@ +import { registerGObjectClass } from '@utils/gjs'; +import { + GObject, + Clutter, + Shell, + Meta, + St, + Graphene, + Atk, + Pango, + GLib, +} from '@gi.ext'; + +const WINDOW_OVERLAY_FADE_TIME = 200; + +const WINDOW_SCALE_TIME = 200; +const WINDOW_ACTIVE_SIZE_INC = 5; // in each direction + +const ICON_SIZE = 36; +const ICON_OVERLAP = 0.7; + +const ICON_TITLE_SPACING = 6; + +/* +This class is heavily based on Gnome Shell's WindowPreview class +*/ +@registerGObjectClass +export default class PopupWindowPreview extends Shell.WindowPreview { + static metaInfo: GObject.MetaInfo = { + GTypeName: 'PopupWindowPreview', + }; + + private _overlayShown: boolean; + private _icon: St.Widget; + private _metaWindow: Meta.Window; + private _windowActor: Meta.WindowActor; + private _title: St.Label; + private _previewContainer: St.Widget; + + constructor(metaWindow: Meta.Window) { + super({ + reactive: true, + can_focus: true, + accessible_role: Atk.Role.PUSH_BUTTON, + offscreen_redirect: Clutter.OffscreenRedirect.AUTOMATIC_FOR_OPACITY, + }); + this._metaWindow = metaWindow; + this._windowActor = metaWindow.get_compositor_private(); + + this._previewContainer = new St.Widget({ + style: 'background-color: rgba(255, 255, 255, 0.3); border-radius: 8px; padding: 6px;', + pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }), + layoutManager: new Clutter.BinLayout(), + xAlign: Clutter.ActorAlign.CENTER, + }); + const windowContainer = new Clutter.Actor({ + pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }), + }); + this.window_container = windowContainer; + + windowContainer.connect('notify::scale-x', () => + this._adjustOverlayOffsets(), + ); + // gjs currently can't handle setting an actors layout manager during + // the initialization of the actor if that layout manager keeps track + // of its container, so set the layout manager after creating the + // container + windowContainer.layout_manager = new Shell.WindowPreviewLayout(); + this.add_child(this._previewContainer); + this._previewContainer.add_child(windowContainer); + + this._addWindow(metaWindow); + + this._stackAbove = null; + + this._windowActor.connectObject('destroy', () => this.destroy(), this); + + this._updateAttachedDialogs(); + + this.connect('destroy', this._onDestroy.bind(this)); + + // this._overlayEnabled = true; + this._overlayShown = false; + // this._idleHideOverlayId = 0; + + const tracker = Shell.WindowTracker.get_default(); + const app = tracker.get_window_app(this._metaWindow); + this._icon = app.create_icon_texture(ICON_SIZE) as St.Widget; + this._icon.add_style_class_name('window-icon'); + this._icon.add_style_class_name('icon-dropshadow'); + this._icon.set({ + reactive: true, + pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }), + }); + this._icon.add_constraint( + new Clutter.BindConstraint({ + source: this._previewContainer, + coordinate: Clutter.BindCoordinate.POSITION, + }), + ); + this._icon.add_constraint( + new Clutter.AlignConstraint({ + source: this._previewContainer, + align_axis: Clutter.AlignAxis.X_AXIS, + factor: 0.5, + }), + ); + this._icon.add_constraint( + new Clutter.AlignConstraint({ + source: this._previewContainer, + align_axis: Clutter.AlignAxis.Y_AXIS, + pivot_point: new Graphene.Point({ x: -1, y: ICON_OVERLAP }), + factor: 1, + }), + ); + + const { scaleFactor } = St.ThemeContext.get_for_stage( + global.stage as Clutter.Stage, + ); + this._title = new St.Label({ + visible: false, + style_class: 'window-caption', + text: this._getCaption(), + reactive: true, + }); + this._title.clutter_text.single_line_mode = true; + this._title.add_constraint( + new Clutter.BindConstraint({ + source: this._previewContainer, + coordinate: Clutter.BindCoordinate.X, + }), + ); + const iconBottomOverlap = ICON_SIZE * (1 - ICON_OVERLAP); + this._title.add_constraint( + new Clutter.BindConstraint({ + source: this._previewContainer, + coordinate: Clutter.BindCoordinate.Y, + offset: scaleFactor * (iconBottomOverlap + ICON_TITLE_SPACING), + }), + ); + this._title.add_constraint( + new Clutter.AlignConstraint({ + source: this._previewContainer, + align_axis: Clutter.AlignAxis.X_AXIS, + factor: 0.5, + }), + ); + this._title.add_constraint( + new Clutter.AlignConstraint({ + source: this._previewContainer, + align_axis: Clutter.AlignAxis.Y_AXIS, + pivot_point: new Graphene.Point({ x: -1, y: 0 }), + factor: 1, + }), + ); + this._title.clutter_text.ellipsize = Pango.EllipsizeMode.END; + this.label_actor = this._title; + this._metaWindow.connectObject( + 'notify::title', + () => (this._title.text = this._getCaption()), + this, + ); + + this._previewContainer.add_child(this._title); + this._previewContainer.add_child(this._icon); + + this.connect('notify::realized', () => { + if (!this.realized) return; + + this._title.ensure_style(); + this._icon.ensure_style(); + }); + } + + _getCaption() { + if (this._metaWindow.title) return this._metaWindow.title; + + const tracker = Shell.WindowTracker.get_default(); + const app = tracker.get_window_app(this._metaWindow); + return app.get_name(); + } + + showOverlay(animate: boolean) { + // if (!this._overlayEnabled) return; + + if (this._overlayShown) return; + + this._overlayShown = true; + this._restack(); + + // If we're supposed to animate and an animation in our direction + // is already happening, let that one continue + const ongoingTransition = this._title.get_transition('opacity'); + if ( + animate && + ongoingTransition && + ongoingTransition.get_interval().peek_final_value() === 255 + ) + return; + + [this._title].forEach((a) => { + a.opacity = 0; + a.show(); + a.ease({ + opacity: 255, + duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + }); + + const [width, height] = this.windowContainer.get_size(); + const { scaleFactor } = St.ThemeContext.get_for_stage( + global.stage as Clutter.Stage, + ); + const activeExtraSize = WINDOW_ACTIVE_SIZE_INC * 2 * scaleFactor; + const origSize = Math.max(width, height); + const scale = (origSize + activeExtraSize) / origSize; + + this._previewContainer.ease({ + scaleX: scale, + scaleY: scale, + duration: animate ? WINDOW_SCALE_TIME : 0, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + + hideOverlay(animate: boolean) { + if (!this._overlayShown) return; + + this._overlayShown = false; + this._restack(); + + // If we're supposed to animate and an animation in our direction + // is already happening, let that one continue + const ongoingTransition = this._title.get_transition('opacity'); + if ( + animate && + ongoingTransition && + ongoingTransition.get_interval().peek_final_value() === 0 + ) + return; + + [this._title].forEach((a) => { + a.opacity = 255; + a.ease({ + opacity: 0, + duration: animate ? WINDOW_OVERLAY_FADE_TIME : 0, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => a.hide(), + }); + }); + + this._previewContainer.ease({ + scaleX: 1, + scaleY: 1, + duration: animate ? WINDOW_SCALE_TIME : 0, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + }); + } + + _adjustOverlayOffsets() { + // Assume that scale-x and scale-y update always set + // in lock-step; that allows us to not use separate + // handlers for horizontal and vertical offsets + const previewScale = this._previewContainer.scale_x; + const [previewWidth, previewHeight] = + this._previewContainer.allocation.get_size(); + + const heightIncrease = Math.floor( + (previewHeight * (previewScale - 1)) / 2, + ); + + this._icon.translation_y = heightIncrease; + this._title.translation_y = heightIncrease; + } + + _addWindow(metaWindow: Meta.Window) { + const clone = + this.window_container.layout_manager.add_window(metaWindow); + // if (!clone) return; + + /* // We expect this to be used for all interaction rather than + // the ClutterClone; as the former is reactive and the latter + // is not, this just works for most cases. However, for DND all + // actors are picked, so DND operations would operate on the clone. + // To avoid this, we hide it from pick. + Shell.util_set_hidden_from_pick(clone, true);*/ + } + + vfunc_has_overlaps() { + return this._hasAttachedDialogs() || this._icon.visible; + } + + addDialog(win: Meta.Window) { + let parent = win.get_transient_for(); + while (parent && parent.is_attached_dialog()) + parent = parent.get_transient_for(); + + // Display dialog if it is attached to our metaWindow + if (win.is_attached_dialog() && parent === this._metaWindow) + this._addWindow(win); + } + + _hasAttachedDialogs() { + return this.window_container.layout_manager.get_windows().length > 1; + } + + _updateAttachedDialogs() { + const iter = (win) => { + const actor = win.get_compositor_private(); + + if (!actor) return false; + if (!win.is_attached_dialog()) return false; + + this._addWindow(win); + win.foreach_transient(iter); + return true; + }; + this._metaWindow.foreach_transient(iter); + } + + /* get overlayEnabled() { + return this._overlayEnabled; + } + + set overlayEnabled(enabled) { + if (this._overlayEnabled === enabled) return; + + this._overlayEnabled = enabled; + this.notify('overlay-enabled'); + + if (!enabled) this.hideOverlay(false); + else if (this['has-pointer'] || global.stage.key_focus === this) + this.showOverlay(true); + }*/ + + // Find the actor just below us, respecting reparenting done by DND code + _getActualStackAbove() { + if (this._stackAbove == null) return null; + + return this._stackAbove; + } + + setStackAbove(actor) { + this._stackAbove = actor; + + const parent = this.get_parent(); + const actualAbove = this._getActualStackAbove(); + if (actualAbove == null) parent.set_child_below_sibling(this, null); + else parent.set_child_above_sibling(this, actualAbove); + } + + _onDestroy() { + this._destroyed = true; + + if (this._idleHideOverlayId > 0) { + GLib.source_remove(this._idleHideOverlayId); + this._idleHideOverlayId = 0; + } + } + + vfunc_enter_event(event) { + this.showOverlay(true); + return super.vfunc_enter_event(event); + } + + vfunc_leave_event(event) { + if (this._destroyed) return super.vfunc_leave_event(event); + + /* if ((event.get_flags() & Clutter.EventFlags.FLAG_GRAB_NOTIFY) !== 0 && + global.stage.get_grab_actor() === this._closeButton) + return super.vfunc_leave_event(event);*/ + + if (!this['has-pointer']) this.hideOverlay(true); + /* if (this._idleHideOverlayId > 0) + GLib.source_remove(this._idleHideOverlayId); + + this._idleHideOverlayId = GLib.timeout_add( + GLib.PRIORITY_DEFAULT, + WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT, () => { + if (!this['has-pointer']) + this.hideOverlay(true); + + this._idleHideOverlayId = 0; + return GLib.SOURCE_REMOVE; + }); + + GLib.Source.set_name_by_id(this._idleHideOverlayId, '[gnome-shell] this._idleHideOverlayId');*/ + + return super.vfunc_leave_event(event); + } + + vfunc_key_focus_in() { + super.vfunc_key_focus_in(); + this.showOverlay(true); + } + + vfunc_key_focus_out() { + super.vfunc_key_focus_out(); + + this.hideOverlay(true); + } + + _restack() { + // We may not have a parent if DnD completed successfully, in + // which case our clone will shortly be destroyed and replaced + // with a new one on the target workspace. + const parent = this.get_parent(); + if (parent !== null) { + if (this._overlayShown) parent.set_child_above_sibling(this, null); + else if (this._stackAbove === null) + parent.set_child_below_sibling(this, null); + else if (!this._stackAbove._overlayShown) + parent.set_child_above_sibling(this, this._stackAbove); + } + } +} diff --git a/src/components/tilingsystem/tilingLayout.ts b/src/components/tilingsystem/tilingLayout.ts index 5086dcb..e7f9167 100644 --- a/src/components/tilingsystem/tilingLayout.ts +++ b/src/components/tilingsystem/tilingLayout.ts @@ -6,7 +6,12 @@ import TilePreview, { import LayoutWidget from '../layout/LayoutWidget'; import Layout from '../layout/Layout'; import Tile from '../layout/Tile'; -import { buildRectangle, buildTileGaps, isPointInsideRect } from '@utils/ui'; +import { + buildRectangle, + buildTileGaps, + isPointInsideRect, + squaredEuclideanDistance, +} from '@utils/ui'; import TileUtils from '@components/layout/TileUtils'; import { logger } from '@utils/logger'; import GlobalState from '@utils/globalState'; @@ -14,25 +19,18 @@ import { KeyBindingsDirection } from '@keybindings'; const debug = logger('TilingLayout'); -export interface DynamicTilePreviewConstructorProperties - extends Partial { - tile: Tile; -} - @registerGObjectClass class DynamicTilePreview extends TilePreview { private _originalRect: Mtk.Rectangle; private _canRestore: boolean; - private _tile: Tile; constructor( - params: DynamicTilePreviewConstructorProperties, + params: Partial, canRestore?: boolean, ) { super(params); this._canRestore = canRestore || false; this._originalRect = this.rect.copy(); - this._tile = params.tile; } public get originalRect(): Mtk.Rectangle { @@ -43,10 +41,6 @@ class DynamicTilePreview extends TilePreview { return this._canRestore; } - public get tile(): Tile { - return this._tile; - } - public restore(ease: boolean = false): boolean { if (!this._canRestore) return false; @@ -378,31 +372,29 @@ export default class TilingLayout extends LayoutWidget { switch (direction) { case KeyBindingsDirection.RIGHT: - sourceCoords.x = Math.min( - this._containerRect.width + this._containerRect.x, - source.x + source.width + enlarge, - ); + sourceCoords.x = source.x + source.width + enlarge; break; case KeyBindingsDirection.LEFT: - sourceCoords.x = Math.max( - this._containerRect.x, - source.x - enlarge, - ); + sourceCoords.x = source.x - enlarge; break; case KeyBindingsDirection.DOWN: - sourceCoords.y = Math.min( - this._containerRect.height + this._containerRect.y, - source.y + source.height + enlarge, - ); + sourceCoords.y = source.y + source.height + enlarge; break; case KeyBindingsDirection.UP: - sourceCoords.y = Math.max( - this._containerRect.y, - source.y - enlarge, - ); + sourceCoords.y = source.y - enlarge; break; } + // if the point to search is outside the container we can already return undefined + if ( + sourceCoords.x < this._containerRect.x || + sourceCoords.x > + this._containerRect.width + this._containerRect.x || + sourceCoords.y < this._containerRect.y || + sourceCoords.y > this._containerRect.height + this._containerRect.y + ) + return undefined; + // uncomment to show debugging /* global.windowGroup .get_children() @@ -435,4 +427,47 @@ export default class TilingLayout extends LayoutWidget { return undefined; } + + public findNearestTile( + source: Mtk.Rectangle, + ): { rect: Mtk.Rectangle; tile: Tile } | undefined { + let previewFound: DynamicTilePreview | undefined; + let bestDistance = -1; + + const sourceCenter = { + x: source.x + source.width / 2, + y: source.x + source.height / 2, + }; + + for (let i = 0; i < this._previews.length; i++) { + const preview = this._previews[i]; + + const previewCenter = { + x: preview.innerX + preview.innerWidth / 2, + y: preview.innerY + preview.innerHeight / 2, + }; + + const euclideanDistance = squaredEuclideanDistance( + previewCenter, + sourceCenter, + ); + + if (!previewFound || euclideanDistance < bestDistance) { + previewFound = preview; + bestDistance = euclideanDistance; + } + } + + if (!previewFound) return undefined; + + return { + rect: buildRectangle({ + x: previewFound.innerX, + y: previewFound.innerY, + width: previewFound.innerWidth, + height: previewFound.innerHeight, + }), + tile: previewFound.tile, + }; + } } diff --git a/src/components/tilingsystem/tilingManager.ts b/src/components/tilingsystem/tilingManager.ts index 85d2ceb..4da75f1 100644 --- a/src/components/tilingsystem/tilingManager.ts +++ b/src/components/tilingsystem/tilingManager.ts @@ -26,6 +26,7 @@ import EdgeTilingManager from './edgeTilingManager'; import TouchPointer from './touchPointer'; import { KeyBindingsDirection } from '@keybindings'; import TilingShellWindowManager from '@components/windowManager/tilingShellWindowManager'; +import TilingPopup from './tilingPopup'; const MINIMUM_DISTANCE_TO_RESTORE_ORIGINAL_SIZE = 90; @@ -323,7 +324,7 @@ export class TilingManager { } // find the nearest tile - // direction is CENTER -> move to the center of the screen + // direction is NODIRECTION -> move to the center of the screen if (direction === KeyBindingsDirection.NODIRECTION) { const rect = buildRectangle({ x: @@ -341,15 +342,18 @@ export class TilingManager { rect, tile: TileUtils.build_tile(rect, this._workArea), }; - } else { + } else if (window.get_monitor() === this._monitor.index) { destination = tilingLayout.findNearestTileDirection( windowRectCopy, direction, ); + } else { + destination = tilingLayout.findNearestTile(windowRectCopy); } // if the window is already on the desired tile if ( + window.get_monitor() === this._monitor.index && destination && (window as ExtendedWindow).assignedTile && (window as ExtendedWindow).assignedTile?.x === destination.tile.x && @@ -750,6 +754,9 @@ export class TilingManager { return; // disable snap assistance + const showPopup = + !this._isSnapAssisting && + !this._edgeTilingManager.isPerformingEdgeTiling(); this._isSnapAssisting = false; if ( @@ -779,6 +786,21 @@ export class TilingManager { ...TileUtils.build_tile(selectedTilesRect, this._workArea), }); this._easeWindowRect(window, desiredWindowRect); + + if (tilingLayout && showPopup) { + const layout = GlobalState.get().getSelectedLayoutOfMonitor( + this._monitor.index, + window.get_workspace().index(), + ); + new TilingPopup( + layout, + tilingLayout.innerGaps, + tilingLayout.outerGaps, + this._workArea, + tilingLayout.scalingFactor, + window as ExtendedWindow, + ); + } } private _easeWindowRect( @@ -882,11 +904,16 @@ export class TilingManager { const [x, y] = TouchPointer.get().isTouchDeviceActive() ? TouchPointer.get().get_pointer(window) : global.get_pointer(); + + const monitorWidth = + this._workArea.x - this._monitor.x + this._workArea.width; + const monitorHeight = + this._workArea.y - this._monitor.y + this._workArea.height; return ( x >= this._monitor.x && - x <= this._monitor.x + this._monitor.width && + x <= this._monitor.x + monitorWidth && y >= this._monitor.y && - y <= this._monitor.y + this._monitor.height + y <= this._monitor.y + monitorHeight ); } @@ -1047,8 +1074,6 @@ export class TilingManager { if (windowCreated) { const windowActor = window.get_compositor_private() as Meta.WindowActor; - // the window won't be visible when will open on its position (e.g. the center of the screen) - windowActor.set_opacity(0); const id = windowActor.connect('first-frame', () => { // while we restore the opacity, making the window visible // again, we perform easing of movement too @@ -1060,15 +1085,8 @@ export class TilingManager { !window.maximizedVertically && window.get_transient_for() === null && !window.is_attached_dialog() - ) { - windowActor.ease({ - opacity: 255, - duration: 200, - }); + ) this._easeWindowRectFromTile(vacantTile, window, true); - } else { - windowActor.set_opacity(255); - } windowActor.disconnect(id); }); diff --git a/src/components/tilingsystem/tilingPopup.ts b/src/components/tilingsystem/tilingPopup.ts new file mode 100644 index 0000000..960b9aa --- /dev/null +++ b/src/components/tilingsystem/tilingPopup.ts @@ -0,0 +1,536 @@ +import { registerGObjectClass } from '@/utils/gjs'; +import { Clutter, Mtk, Meta, St, Graphene } from '@gi.ext'; +import Layout from '../layout/Layout'; +import { getWindows } from '@utils/ui'; +import TileUtils from '@components/layout/TileUtils'; +import { logger } from '@utils/logger'; +import GlobalState from '@utils/globalState'; +import ExtendedWindow from './extendedWindow'; +import PopupWindowPreview from './popupWindowPreview'; +import Tile from '@components/layout/Tile'; +import TilePreview from '@components/tilepreview/tilePreview'; +import LayoutWidget from '@components/layout/LayoutWidget'; +import SignalHandling from '@utils/signalHandling'; +import PopupTilePreview from '@components/tilepreview/popupTilePreview'; + +const debug = logger('TilingPopup'); + +const MASONRY_LAYOUT_SPACING = 32; +const ANIMATION_SPEED = 200; +const MASONRY_ROW_MIN_HEIGHT_PERCENTAGE = 0.3; + +interface ContainerWithAllocationCache extends Clutter.Actor { + _allocationCache: + | Map< + Clutter.Actor, + { x: number; y: number; width: number; height: number } + > + | undefined; +} + +@registerGObjectClass +class MasonryLayout extends Clutter.LayoutManager { + private _rowCount: number; + private _spacing: number; + private _maxRowHeight: number; + private _rowHeight: number; + + constructor(spacing: number, rowHeight: number, maxRowHeight: number) { + super(); + this._rowCount = 0; // Number of rows + this._spacing = spacing; // Spacing between items + this._maxRowHeight = maxRowHeight; + this._rowHeight = rowHeight; + } + + vfunc_allocate(container: Clutter.Actor, box: Clutter.ActorBox) { + const children = container.get_children(); + if (children.length === 0) return; + + this._rowCount = Math.ceil(Math.sqrt(children.length)) + 1; + let rowHeight = 0; + while ( + this._rowCount > 1 && + rowHeight < box.get_height() * MASONRY_ROW_MIN_HEIGHT_PERCENTAGE + ) { + this._rowCount--; + rowHeight = + (box.get_height() - this._spacing * (this._rowCount - 1)) / + this._rowCount; + } + rowHeight = Math.min(rowHeight, this._maxRowHeight); + rowHeight = this._rowHeight; + const rowWidths = Array(this._rowCount).fill(0); // Tracks the width of each row + + // Calculate total content height and width + const contentHeight = + rowHeight * this._rowCount + this._spacing * (this._rowCount - 1); + + // Store placements and cache + const placements = []; + const allocationCache = + (container as ContainerWithAllocationCache)._allocationCache ?? + new Map(); + + for (const child of children) { + // Retrieve the preferred height and width to calculate the aspect ratio + const [minHeight, naturalHeight] = child.get_preferred_height(-1); + const [minWidth, naturalWidth] = + child.get_preferred_width(naturalHeight); + + // Maintain the aspect ratio + const aspectRatio = naturalWidth / naturalHeight; + const width = rowHeight * aspectRatio; + + // Find the shortest row + const shortestRow = rowWidths.indexOf(Math.min(...rowWidths)); + placements.push({ + child, + row: shortestRow, + width, + x: rowWidths[shortestRow], + rowWidth: 0, + }); + + // Update row height + rowWidths[shortestRow] += width + this._spacing; + } + for (const placement of placements) + placement.rowWidth = rowWidths[placement.row]; + + const sortedRowWidths: number[][] = [...rowWidths].map((v, i) => [ + v, + i, + ]); + sortedRowWidths.sort((a, b) => b[0] - a[0]); + const rowsOrdering = new Map(); + sortedRowWidths.forEach((row, newIndex) => { + const index = row[1]; + rowsOrdering.set( + index, + (newIndex + Math.floor(this._rowCount / 2)) % this._rowCount, + ); + }); + for (const placement of placements) + placement.row = rowsOrdering.get(placement.row) ?? placement.row; + + // Calculate offsets for centering the entire grid within the available space + const verticalOffset = (box.get_height() - contentHeight) / 2; + // Determine the largest row and center the content around it + const largestRowWidth = sortedRowWidths[0][0]; + const horizontalOffset = (box.get_width() - largestRowWidth) / 2; + + // Reset row heights for actual allocation + rowWidths.fill(0); + + // Allocate children with preserved proportions + for (const placement of placements) { + const { child, row, width, x, rowWidth } = placement; + const y = + box.y1 + row * (rowHeight + this._spacing) + verticalOffset; + const rowOffset = (largestRowWidth - rowWidth) / 2; + const xPosition = + box.x1 + x + horizontalOffset + rowOffset + this._spacing / 2; + + // Check if this child has a cached allocation + const cachedAlloc = allocationCache.get(child); + if (cachedAlloc) { + child.allocate( + new Clutter.ActorBox({ + x1: cachedAlloc.x, + y1: cachedAlloc.y, + x2: cachedAlloc.x + width, + y2: cachedAlloc.y + rowHeight, + }), + ); + continue; // Skip reallocation + } + + // If the allocation has changed or no cache exists, perform new allocation + child.allocate( + new Clutter.ActorBox({ + x1: xPosition, + y1: y, + x2: xPosition + width, + y2: y + rowHeight, + }), + ); + + // Update cache with the new allocation + allocationCache.set(child, { + x: xPosition, + y, + height: rowHeight, + width, + }); + } + + // Store the updated cache for future allocation passes + (container as ContainerWithAllocationCache)._allocationCache = + allocationCache; + } + + vfunc_get_preferred_width( + container: Clutter.Actor, + forHeight: number, + ): [number, number] { + const children = container.get_children(); + if (children.length === 0) return [0, 0]; + + const rowWidths = Array(this._rowCount).fill(0); + const rowWidth = + (forHeight - this._spacing * (this._rowCount - 1)) / this._rowCount; + + for (const child of children) { + const preferredWidth = child.get_preferred_width(rowWidth)[1]; + const shortestRow = rowWidths.indexOf(Math.min(...rowWidths)); + rowWidths[shortestRow] += preferredWidth + this._spacing; + } + + const totalWidth = Math.max(...rowWidths); + return [totalWidth, totalWidth]; + } + + vfunc_get_preferred_height( + container: Clutter.Actor, + forWidth: number, + ): [number, number] { + const children = container.get_children(); + if (children.length === 0) return [0, 0]; + + const childHeights = children.map( + (child) => child.get_preferred_height(forWidth)[1], + ); + const maxChildHeights = Math.max(...childHeights); + + const totalHeight = + this._rowCount * maxChildHeights + + (this._rowCount - 1) * this._spacing; + return [totalHeight, totalHeight]; + } +} + +@registerGObjectClass +export default class TilingPopup extends LayoutWidget { + private _signals: SignalHandling; + private _lastTiledWindow: Meta.Window | null; + private _showing: boolean; + + constructor( + layout: Layout, + innerGaps: Clutter.Margin, + outerGaps: Clutter.Margin, + workarea: Mtk.Rectangle, + scalingFactor: number, + window: ExtendedWindow, + ) { + super({ + containerRect: workarea, + parent: global.windowGroup, + layout: new Layout([], ''), + innerGaps, + outerGaps, + scalingFactor, + }); + this.canFocus = true; + this.reactive = true; + this._signals = new SignalHandling(); + this._lastTiledWindow = global.display.focusWindow; + this._showing = true; + const tiledWindows: ExtendedWindow[] = []; + const nontiledWindows: Meta.Window[] = []; + getWindows().forEach((extWin) => { + if ( + extWin && + !extWin.minimized && + (extWin as ExtendedWindow).assignedTile + ) + tiledWindows.push(extWin as ExtendedWindow); + else nontiledWindows.push(extWin); + }); + // TODO: let's make this available in the future + const enabled = false; + if (nontiledWindows.length === 0 || !enabled) { + this.destroy(); + return; + } + + this._relayoutVacantTiles(layout, tiledWindows, window); + + this.show(); + this._recursivelyShowPopup(nontiledWindows, window.get_monitor()); + + this.connect('key-focus-out', () => this.close()); + + this._signals.connect( + global.stage, + 'button-press-event', + (_: Clutter.Actor, event: Clutter.Event) => { + const isDescendant = this.contains(event.get_source()); + if ( + !isDescendant || + event.get_source() === this || + event.get_source().get_layout_manager() instanceof + MasonryLayout + ) + this.close(); + }, + ); + this._signals.connect( + global.stage, + 'key-press-event', + (_: Clutter.Actor, event: Clutter.Event) => { + const symbol = event.get_key_symbol(); + if (symbol === Clutter.KEY_Escape) this.close(); + + return Clutter.EVENT_PROPAGATE; + }, + ); + this.connect('destroy', () => this._signals.disconnect()); + } + + private _relayoutVacantTiles( + layout: Layout, + tiledWindows: ExtendedWindow[], + window: ExtendedWindow, + ) { + const tiles = layout.tiles; + const windowDesiredRect = window.assignedTile + ? TileUtils.apply_props(window.assignedTile, this._containerRect) + : window.get_frame_rect(); + const vacantTiles = tiles.filter((t) => { + if ( + window.assignedTile && + t.x === window.assignedTile.x && + t.y === window.assignedTile.y && + t.width === window.assignedTile.width && + t.height === window.assignedTile.height + ) + return false; + const tileRect = TileUtils.apply_props(t, this._containerRect); + return !tiledWindows.find((win) => + tileRect.overlap( + win !== window ? win.get_frame_rect() : windowDesiredRect, + ), + ); + }); + this.relayout({ layout: new Layout(vacantTiles, 'popup') }); + } + + protected override buildTile( + parent: Clutter.Actor, + rect: Mtk.Rectangle, + gaps: Clutter.Margin, + tile: Tile, + ): TilePreview { + const preview = new PopupTilePreview({ parent, rect, gaps, tile }); + + const layoutManager = new MasonryLayout( + MASONRY_LAYOUT_SPACING, + this._containerRect.height * 0.2, + this._containerRect.height * 0.3, + ); + const container = new St.Widget({ + reactive: true, + x_expand: true, + y_expand: true, + pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }), + layout_manager: layoutManager, + style: 'padding: 32px;', + }); + preview.layout_manager = new Clutter.BinLayout(); + preview.add_child(container); + + return preview; + } + + private _recursivelyShowPopup( + nontiledWindows: Meta.Window[], + monitorIndex: number, + ): void { + if (this._previews.length === 0 || nontiledWindows.length === 0) { + this.close(); + return; + } + + // find the leftmost preview + let preview = this._previews[0]; + let container = this._previews[0].firstChild; + this._previews.forEach((prev) => { + if (prev.x < container.x) { + container = prev.firstChild; + preview = prev; + } + }); + + nontiledWindows.forEach((nonTiledWin) => { + const winClone = new PopupWindowPreview(nonTiledWin); + const winActor = + nonTiledWin.get_compositor_private() as Meta.WindowActor; + + container.add_child(winClone); + // fade out and unscale by 10% the window actor + winActor.set_pivot_point(0.5, 0.5); + winActor.ease({ + opacity: 0, + duration: ANIMATION_SPEED, + scaleX: 0.9, + scaleY: 0.9, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onComplete: () => { + winActor.hide(); + winActor.set_pivot_point(0, 0); + }, + }); + // fade in and upscale by 3% the window preview (i.e. the clone) + winClone.set_opacity(0); + winClone.set_pivot_point(0.5, 0.5); + winClone.set_scale(0.6, 0.6); + winClone.ease({ + opacity: 255, + duration: Math.floor(ANIMATION_SPEED * 1.8), + scaleX: 1.03, + scaleY: 1.03, + mode: Clutter.AnimationMode.EASE_IN_OUT, + onComplete: () => { + // scale back to 100% the window preview (i.e the clone) + winClone.ease({ + delay: 60, + duration: Math.floor(ANIMATION_SPEED * 2.1), + scaleX: 1, + scaleY: 1, + mode: Clutter.AnimationMode.EASE_IN_OUT, + // finally hide the window actor when the whole animation completes + onComplete: () => winActor.hide(), + }); + }, + }); + + // when the clone is destroyed, fade in the window actor + winClone.connect('destroy', () => { + if (winActor.visible) return; + + winActor.set_pivot_point(0.5, 0.5); + winActor.show(); + winActor.ease({ + opacity: 255, + duration: ANIMATION_SPEED, + scaleX: 1, + scaleY: 1, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onStopped: () => winActor.set_pivot_point(0, 0), + }); + }); + + // when the clone is selected by the user + winClone.connect('button-press-event', () => { + // finally move the window + // the actor has opacity = 0, so this is not seen by the user + // place the actor with a scale 4% lower, to perform scaling and fading animation later + winActor.set_pivot_point(0.5, 0.5); + winActor.set_scale(0.96, 0.96); + winActor.set_position(preview.innerX, preview.innerY); + winActor.set_size(preview.innerWidth, preview.innerHeight); + + this._lastTiledWindow = nonTiledWin; + // place this window on TOP of everyone (we will focus it later, after the animation) + global.windowGroup.set_child_above_sibling( + this._lastTiledWindow.get_compositor_private(), + null, + ); + if ( + nonTiledWin.maximizedHorizontally || + nonTiledWin.maximizedVertically + ) + nonTiledWin.unmaximize(Meta.MaximizeFlags.BOTH); + if (nonTiledWin.is_fullscreen()) + nonTiledWin.unmake_fullscreen(); + if (nonTiledWin.minimized) nonTiledWin.unminimize(); + + (nonTiledWin as ExtendedWindow).originalSize = nonTiledWin + .get_frame_rect() + .copy(); + + // create a static clone and hide the live clone + // then we can change the actual window size + // without showing that to the user + /* const staticClone = new Clutter.Clone({ + source: winClone, + reactive: false, + });*/ + // hide the live clone, so we can change the actual window size + // without showing that to the user + winClone.opacity = 0; + preview.ease({ + opacity: 0, + duration: ANIMATION_SPEED, + onStopped: () => { + this._previews.splice( + this._previews.indexOf(preview), + 1, + ); + preview.destroy(); + nontiledWindows.splice( + nontiledWindows.indexOf(nonTiledWin), + 1, + ); + this._recursivelyShowPopup( + nontiledWindows, + monitorIndex, + ); + }, + }); + const user_op = false; + nonTiledWin.move_to_monitor(monitorIndex); + nonTiledWin.move_frame(user_op, preview.innerX, preview.innerY); + nonTiledWin.move_resize_frame( + user_op, + preview.innerX, + preview.innerY, + preview.innerWidth, + preview.innerHeight, + ); + (nonTiledWin as ExtendedWindow).assignedTile = new Tile({ + ...preview.tile, + }); + // while we hide the preview, show the actor to the new position, + // fade in and scale back to 100% size + winActor.show(); + winActor.ease({ + opacity: 255, + scaleX: 1, + scaleY: 1, + duration: ANIMATION_SPEED * 0.8, + delay: 100, + onStopped: () => { + winActor.set_pivot_point(0, 0); + if ( + this._previews.length === 0 && + this._lastTiledWindow + ) { + this._lastTiledWindow.focus( + global.get_current_time(), + ); + } + }, + }); + }); + }); + + this.grab_key_focus(); + } + + public close() { + if (!this._showing) return; + + this._showing = false; + this.ease({ + opacity: 0, + duration: GlobalState.get().tilePreviewAnimationTime, + mode: Clutter.AnimationMode.EASE_OUT_QUAD, + onStopped: () => { + this.destroy(); + }, + }); + } +} diff --git a/src/components/windowBorderManager.ts b/src/components/windowBorderManager.ts index fa5f043..470face 100644 --- a/src/components/windowBorderManager.ts +++ b/src/components/windowBorderManager.ts @@ -1,36 +1,72 @@ -import { GObject, Meta, St, Clutter } from '@gi.ext'; +import { GObject, Meta, St, Clutter, Shell, Gio, GLib } from '@gi.ext'; import SignalHandling from '@utils/signalHandling'; import { logger } from '@utils/logger'; import { registerGObjectClass } from '@utils/gjs'; import Settings from '@settings/settings'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; +import { + buildRectangle, + enableScalingFactorSupport, + getMonitorScalingFactor, + getScalingFactorOf, + getScalingFactorSupportString, +} from '@utils/ui'; + +Gio._promisify(Shell.Screenshot, 'composite_to_stream'); + +const DEFAULT_BORDER_RADIUS = 11; +const SMART_BORDER_RADIUS_DELAY = 460; +const SMART_BORDER_RADIUS_FIRST_FRAME_DELAY = 240; const debug = logger('WindowBorderManager'); +interface WindowWithCachedRadius extends Meta.Window { + __ts_cached_radius: [number, number, number, number] | undefined; +} + @registerGObjectClass class WindowBorder extends St.Bin { private readonly _signals: SignalHandling; private _window: Meta.Window; + private _windowMonitor: number; private _bindings: GObject.Binding[]; + private _enableScaling: boolean; + private _borderRadiusValue: [number, number, number, number]; + private _timeout: GLib.Source | undefined; + private _delayedSmartBorderRadius: boolean; + private _borderWidth: number; - constructor(win: Meta.Window) { + constructor(win: Meta.Window, enableScaling: boolean) { super({ - style_class: 'window-border full-radius', + style_class: 'window-border', }); this._signals = new SignalHandling(); this._bindings = []; - - this.updateStyle(); + this._borderWidth = 1; this._window = win; + this._windowMonitor = win.get_monitor(); + this._enableScaling = enableScaling; + this._delayedSmartBorderRadius = false; + const smartRadius = Settings.ENABLE_SMART_WINDOW_BORDER_RADIUS; + this._borderRadiusValue = [ + DEFAULT_BORDER_RADIUS, + DEFAULT_BORDER_RADIUS, + smartRadius ? 0 : DEFAULT_BORDER_RADIUS, + smartRadius ? 0 : DEFAULT_BORDER_RADIUS, + ]; // default value + this.close(); global.windowGroup.add_child(this); + this.trackWindow(win, true); this.connect('destroy', () => { this._bindings.forEach((b) => b.unbind()); this._bindings = []; this._signals.disconnect(); + if (this._timeout) clearTimeout(this._timeout); + this._timeout = undefined; }); } @@ -46,46 +82,46 @@ class WindowBorder extends St.Bin { this._window.get_compositor_private() as Meta.WindowActor; // scale and translate like the window actor - this._bindings.push( - // @ts-expect-error "For some reason GObject.Binding is not recognized" + // @ts-expect-error "For some reason GObject.Binding is not recognized" + this._bindings = [ + 'scale-x', + 'scale-y', + 'translation_x', + 'translation_y', + ].map((prop) => winActor.bind_property( - 'scale-x', + prop, this, - 'scale-x', + prop, GObject.BindingFlags.DEFAULT, // if winActor changes, this will change ), ); - this._bindings.push( - // @ts-expect-error "For some reason GObject.Binding is not recognized" - winActor.bind_property( - 'scale-y', - this, - 'scale-y', - GObject.BindingFlags.DEFAULT, // if winActor changes, this will change - ), - ); - this._bindings.push( - // @ts-expect-error "For some reason GObject.Binding is not recognized" - winActor.bind_property( - 'translation_x', - this, - 'translation_x', - GObject.BindingFlags.DEFAULT, // if winActor changes, this will change - ), + + const winRect = this._window.get_frame_rect(); + this.set_position( + winRect.x - this._borderWidth, + winRect.y - this._borderWidth, ); - this._bindings.push( - // @ts-expect-error "For some reason GObject.Binding is not recognized" - winActor.bind_property( - 'translation_y', - this, - 'translation_y', - GObject.BindingFlags.DEFAULT, // if winActor changes, this will change - ), + this.set_size( + winRect.width + 2 * this._borderWidth, + winRect.height + 2 * this._borderWidth, ); - const winRect = this._window.get_frame_rect(); - this.set_position(winRect.x, winRect.y); - this.set_size(winRect.width, winRect.height); + if (Settings.ENABLE_SMART_WINDOW_BORDER_RADIUS) { + const cached_radius = (this._window as WindowWithCachedRadius) + .__ts_cached_radius; + if (cached_radius) { + this._borderRadiusValue[St.Corner.TOPLEFT] = + cached_radius[St.Corner.TOPLEFT]; + this._borderRadiusValue[St.Corner.TOPRIGHT] = + cached_radius[St.Corner.TOPRIGHT]; + this._borderRadiusValue[St.Corner.BOTTOMLEFT] = + cached_radius[St.Corner.BOTTOMLEFT]; + this._borderRadiusValue[St.Corner.BOTTOMRIGHT] = + cached_radius[St.Corner.BOTTOMRIGHT]; + } + } + this.updateStyle(); const isMaximized = this._window.maximizedVertically && @@ -99,6 +135,9 @@ class WindowBorder extends St.Bin { this.close(); else this.open(); + this._signals.connect(global.display, 'restacked', () => { + global.windowGroup.set_child_above_sibling(this, null); + }); this._signals.connect(this._window, 'position-changed', () => { if ( this._window.maximizedVertically || @@ -111,8 +150,24 @@ class WindowBorder extends St.Bin { return; } + if ( + this._delayedSmartBorderRadius && + Settings.ENABLE_SMART_WINDOW_BORDER_RADIUS + ) { + this._delayedSmartBorderRadius = false; + this._runComputeBorderRadiusTimeout(winActor); + } + const rect = this._window.get_frame_rect(); - this.set_position(rect.x, rect.y); + this.set_position( + rect.x - this._borderWidth, + rect.y - this._borderWidth, + ); + // if the window changes monitor, we may have a different scaling factor + if (this._windowMonitor !== win.get_monitor()) { + this._windowMonitor = win.get_monitor(); + this.updateStyle(); + } this.open(); }); @@ -128,16 +183,183 @@ class WindowBorder extends St.Bin { return; } + if ( + this._delayedSmartBorderRadius && + Settings.ENABLE_SMART_WINDOW_BORDER_RADIUS + ) { + this._delayedSmartBorderRadius = false; + this._runComputeBorderRadiusTimeout(winActor); + } + const rect = this._window.get_frame_rect(); - this.set_size(rect.width, rect.height); + this.set_size( + rect.width + 2 * this._borderWidth, + rect.height + 2 * this._borderWidth, + ); + // if the window changes monitor, we may have a different scaling factor + if (this._windowMonitor !== win.get_monitor()) { + this._windowMonitor = win.get_monitor(); + this.updateStyle(); + } this.open(); }); + + if (Settings.ENABLE_SMART_WINDOW_BORDER_RADIUS) { + const firstFrameId = winActor.connect_after('first-frame', () => { + if ( + this._window.maximizedHorizontally || + this._window.maximizedVertically || + this._window.is_fullscreen() + ) { + this._delayedSmartBorderRadius = true; + return; + } + this._runComputeBorderRadiusTimeout(winActor); + + winActor.disconnect(firstFrameId); + }); + } + } + + private _runComputeBorderRadiusTimeout(winActor: Meta.WindowActor) { + if (this._timeout) clearTimeout(this._timeout); + this._timeout = undefined; + + this._timeout = setTimeout(() => { + this._computeBorderRadius(winActor).then(() => this.updateStyle()); + if (this._timeout) clearTimeout(this._timeout); + this._timeout = undefined; + }, SMART_BORDER_RADIUS_FIRST_FRAME_DELAY); + } + + private async _computeBorderRadius(winActor: Meta.WindowActor) { + // we are only interested into analyze the leftmost pixels (i.e. the whole left border) + const width = 3; + const height = winActor.metaWindow.get_frame_rect().height; + if (height <= 0) return; + const content = winActor.paint_to_content( + buildRectangle({ + x: winActor.metaWindow.get_frame_rect().x, + y: winActor.metaWindow.get_frame_rect().y, + height, + width, + }), + ); + if (!content) return; + + /* for debugging purposes + const elem = new St.Widget({ + x: 100, + y: 100, + width, + height, + content, + name: 'elem', + }); + global.windowGroup + .get_children() + .find((el) => el.get_name() === 'elem') + ?.destroy(); + global.windowGroup.add_child(elem);*/ + // @ts-expect-error "content has get_texture() method" + const texture = content.get_texture(); + const stream = Gio.MemoryOutputStream.new_resizable(); + const x = 0; + const y = 0; + const pixbuf = await Shell.Screenshot.composite_to_stream( + texture, + x, + y, + width, + height, + 1, + null, + 0, + 0, + 1, + stream, + ); + // @ts-expect-error "pixbuf has get_pixels() method" + const pixels = pixbuf.get_pixels(); + + const alphaThreshold = 240; // 255 would be the best value, however, some windows may still have a bit of transparency + // iterate pixels from top to bottom + for (let i = 0; i < height; i++) { + if (pixels[i * width * 4 + 3] > alphaThreshold) { + this._borderRadiusValue[St.Corner.TOPLEFT] = i; + this._borderRadiusValue[St.Corner.TOPRIGHT] = + this._borderRadiusValue[St.Corner.TOPLEFT]; + break; + } + } + // iterate pixels from bottom to top + // eslint-disable-next-line prettier/prettier + for (let i = height - 1; i >= height - this._borderRadiusValue[St.Corner.TOPLEFT] - 2; i--) { + if (pixels[i * width * 4 + 3] > alphaThreshold) { + this._borderRadiusValue[St.Corner.BOTTOMLEFT] = height - i - 1; + this._borderRadiusValue[St.Corner.BOTTOMRIGHT] = + this._borderRadiusValue[St.Corner.BOTTOMLEFT]; + break; + } + } + stream.close(null); + + const cached_radius: [number, number, number, number] = [ + DEFAULT_BORDER_RADIUS, + DEFAULT_BORDER_RADIUS, + 0, + 0, + ]; + cached_radius[St.Corner.TOPLEFT] = + this._borderRadiusValue[St.Corner.TOPLEFT]; + cached_radius[St.Corner.TOPRIGHT] = + this._borderRadiusValue[St.Corner.TOPRIGHT]; + cached_radius[St.Corner.BOTTOMLEFT] = + this._borderRadiusValue[St.Corner.BOTTOMLEFT]; + cached_radius[St.Corner.BOTTOMRIGHT] = + this._borderRadiusValue[St.Corner.BOTTOMRIGHT]; + (this._window as WindowWithCachedRadius).__ts_cached_radius = + cached_radius; } public updateStyle(): void { + // handle scale factor of the monitor + const monitorScalingFactor = this._enableScaling + ? getMonitorScalingFactor(this._window.get_monitor()) + : undefined; + // CAUTION: this overrides the CSS style + enableScalingFactorSupport(this, monitorScalingFactor); + + const [alreadyScaled, scalingFactor] = getScalingFactorOf(this); + // the value is already scaled if the border is on primary monitor + const borderWidth = + (alreadyScaled ? 1 : scalingFactor) * + (Settings.WINDOW_BORDER_WIDTH / + (alreadyScaled ? scalingFactor : 1)); + const radius = this._borderRadiusValue.map((val) => { + const valWithBorder = val === 0 ? val : val + borderWidth; + return ( + (alreadyScaled ? 1 : scalingFactor) * + (valWithBorder / (alreadyScaled ? scalingFactor : 1)) + ); + }); + + const scalingFactorSupportString = monitorScalingFactor + ? `${getScalingFactorSupportString(monitorScalingFactor)};` + : ''; this.set_style( - `border-color: ${Settings.WINDOW_BORDER_COLOR}; border-width: ${Settings.WINDOW_BORDER_WIDTH}px;`, + `border-color: ${Settings.WINDOW_BORDER_COLOR}; border-width: ${borderWidth}px; border-radius: ${radius[St.Corner.TOPLEFT]}px ${radius[St.Corner.TOPRIGHT]}px ${radius[St.Corner.BOTTOMRIGHT]}px ${radius[St.Corner.BOTTOMLEFT]}px; ${scalingFactorSupportString}`, ); + + if (this._borderWidth !== borderWidth) { + const diff = this._borderWidth - borderWidth; + this._borderWidth = borderWidth; + this.set_size( + this.get_width() - 2 * diff, + this.get_height() - 2 * diff, + ); + this.set_position(this.get_x() + diff, this.get_y() + diff); + } } public open() { @@ -148,7 +370,7 @@ class WindowBorder extends St.Bin { opacity: 255, duration: 200, mode: Clutter.AnimationMode.EASE, - delay: 100, + delay: 130, }); } @@ -162,10 +384,12 @@ export class WindowBorderManager { private readonly _signals: SignalHandling; private _border: WindowBorder | null; + private _enableScaling: boolean; - constructor() { + constructor(enableScaling: boolean) { this._signals = new SignalHandling(); this._border = null; + this._enableScaling = enableScaling; } public enable(): void { @@ -223,7 +447,22 @@ export class WindowBorderManager { return; } - if (!this._border) this._border = new WindowBorder(metaWindow); + if (!this._border) + this._border = new WindowBorder(metaWindow, this._enableScaling); else this._border.trackWindow(metaWindow); } } + +/* +If in the future we want to have MULTIPLE borders visible AT THE SAME TIME, +when the windows are restacked we have to restack the borders as well. + +display.connect('restacked', (display) => { + let wg = Meta.get_window_group_for_display(display); + forEachWindowInTheWindowGroup((win) => { + winBorder = getWindowBorder(win) + winActor = win.get_compositor_private() + wg.set_child_above_sibling(winBorder, winActor); + }); +}); +*/ diff --git a/src/extension.ts b/src/extension.ts index c8ff789..1d81bb5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -2,23 +2,31 @@ import './styles/stylesheet.scss'; import { Gio, GLib, Meta } from '@gi.ext'; import { logger } from '@utils/logger'; -import { getMonitors, squaredEuclideanDistance } from '@/utils/ui'; +import { + filterUnfocusableWindows, + getMonitors, + squaredEuclideanDistance, +} from '@/utils/ui'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import { TilingManager } from '@/components/tilingsystem/tilingManager'; import Settings from '@settings/settings'; import SignalHandling from './utils/signalHandling'; import GlobalState from './utils/globalState'; import Indicator from './indicator/indicator'; -import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js'; import { ExtensionMetadata } from 'resource:///org/gnome/shell/extensions/extension.js'; import DBus from './dbus'; -import KeyBindings, { KeyBindingsDirection } from './keybindings'; +import KeyBindings, { + KeyBindingsDirection, + FocusSwitchDirection, +} from './keybindings'; import SettingsOverride from '@settings/settingsOverride'; import { ResizingManager } from '@components/tilingsystem/resizeManager'; import OverriddenWindowMenu from '@components/window_menu/overriddenWindowMenu'; import Tile from '@components/layout/Tile'; import { WindowBorderManager } from '@components/windowBorderManager'; import TilingShellWindowManager from '@components/windowManager/tilingShellWindowManager'; +import ExtendedWindow from '@components/tilingsystem/extendedWindow'; +import { Extension } from '@polyfill'; const debug = logger('extension'); @@ -107,7 +115,9 @@ export default class TilingShellExtension extends Extension { this._resizingManager.enable(); if (this._windowBorderManager) this._windowBorderManager.destroy(); - this._windowBorderManager = new WindowBorderManager(); + this._windowBorderManager = new WindowBorderManager( + !this._fractionalScalingEnabled, + ); this._windowBorderManager.enable(); this.createIndicator(); @@ -172,6 +182,12 @@ export default class TilingShellExtension extends Extension { this._indicator.enableScaling = !this._fractionalScalingEnabled; } + if (this._windowBorderManager) + this._windowBorderManager.destroy(); + this._windowBorderManager = new WindowBorderManager( + this._fractionalScalingEnabled, + ); + this._windowBorderManager.enable(); }, ); @@ -230,11 +246,22 @@ export default class TilingShellExtension extends Extension { ( kb: KeyBindings, dp: Meta.Display, - dir: KeyBindingsDirection, + dir: FocusSwitchDirection, ) => { this._onKeyboardFocusWin(dp, dir); }, ); + this._signals.connect( + this._keybindings, + 'focus-window-direction', + ( + kb: KeyBindings, + dp: Meta.Display, + dir: KeyBindingsDirection, + ) => { + this._onKeyboardFocusWinDirection(dp, dir); + }, + ); } // when Tiling Shell's edge-tiling is enabled/disable @@ -381,11 +408,17 @@ export default class TilingShellExtension extends Extension { return; // if the window is maximized, it cannot be spanned - if (focus_window.get_maximized() && spanFlag) return; + if ( + (focus_window.maximizedHorizontally || + focus_window.maximizedVertically) && + spanFlag + ) + return; // handle unmaximize of maximized window if ( - focus_window.get_maximized() && + (focus_window.maximizedHorizontally || + focus_window.maximizedVertically) && direction === KeyBindingsDirection.DOWN ) { focus_window.unmaximize(Meta.MaximizeFlags.BOTH); @@ -396,7 +429,11 @@ export default class TilingShellExtension extends Extension { this._tilingManagers[focus_window.get_monitor()]; if (!monitorTilingManager) return; - if (Settings.ENABLE_AUTO_TILING && focus_window.get_maximized()) { + if ( + Settings.ENABLE_AUTO_TILING && + (focus_window.maximizedHorizontally || + focus_window.maximizedVertically) + ) { focus_window.unmaximize(Meta.MaximizeFlags.BOTH); return; } @@ -428,12 +465,14 @@ export default class TilingShellExtension extends Extension { // if the window is maximized, direction is UP and there is a monitor above, minimize the window if ( - focus_window.get_maximized() && + (focus_window.maximizedHorizontally || + focus_window.maximizedVertically) && direction === KeyBindingsDirection.UP ) { // @ts-expect-error "Main.wm has skipNextEffect function" Main.wm.skipNextEffect(focus_window.get_compositor_private()); focus_window.unmaximize(Meta.MaximizeFlags.BOTH); + (focus_window as ExtendedWindow).assignedTile = undefined; } const neighborTilingManager = @@ -448,14 +487,17 @@ export default class TilingShellExtension extends Extension { ); } - private _onKeyboardFocusWin( + private _onKeyboardFocusWinDirection( display: Meta.Display, - direction: KeyBindingsDirection, + direction: KeyBindingsDirection | FocusSwitchDirection, ) { const focus_window = display.get_focus_window(); + const focusParent = focus_window.get_transient_for() || focus_window; + if ( !focus_window || !focus_window.has_focus() || + focusParent.windowType !== Meta.WindowType.NORMAL || (focus_window.get_wm_class() && focus_window.get_wm_class() === 'gjs') ) @@ -469,16 +511,14 @@ export default class TilingShellExtension extends Extension { x: focusWindowRect.x + focusWindowRect.width / 2, y: focusWindowRect.y + focusWindowRect.height / 2, }; - focus_window - .get_workspace() - .list_windows() + + const windowList = filterUnfocusableWindows( + focus_window.get_workspace().list_windows(), + ); + + windowList .filter((win) => { - if ( - win === focus_window || - (win.get_wm_class() && win.get_wm_class() === 'gjs') || - win.minimized - ) - return false; + if (win === focus_window || win.minimized) return false; const winRect = win.get_frame_rect(); switch (direction) { @@ -521,11 +561,57 @@ export default class TilingShellExtension extends Extension { bestWindow.activate(global.get_current_time()); } + private _onKeyboardFocusWin( + display: Meta.Display, + direction: FocusSwitchDirection, + ) { + const focus_window = display.get_focus_window(); + const focusParent = focus_window.get_transient_for() || focus_window; + + if ( + !focus_window || + !focus_window.has_focus() || + focusParent.windowType !== Meta.WindowType.NORMAL || + (focus_window.get_wm_class() && + focus_window.get_wm_class() === 'gjs') + ) + return; + + const windowList = filterUnfocusableWindows( + focus_window.get_workspace().list_windows(), + ); + const focusedIdx = windowList.findIndex((win) => { + // in case we are iterating over a modal dialog for our focused window + return win === focusParent; + }); + + let nextIndex = -1; + switch (direction) { + case FocusSwitchDirection.PREV: + if (focusedIdx === 0 && Settings.WRAPAROUND_FOCUS) { + windowList[windowList.length - 1].activate( + global.get_current_time(), + ); + } else { + windowList[focusedIdx - 1].activate( + global.get_current_time(), + ); + } + break; + case FocusSwitchDirection.NEXT: + nextIndex = (focusedIdx + 1) % windowList.length; + if (nextIndex > 0 || Settings.WRAPAROUND_FOCUS) + windowList[nextIndex].activate(global.get_current_time()); + break; + } + } + private _onKeyboardUntileWindow(kb: KeyBindings, display: Meta.Display) { const focus_window = display.get_focus_window(); if ( !focus_window || !focus_window.has_focus() || + focus_window.windowType !== Meta.WindowType.NORMAL || (focus_window.get_wm_class() && focus_window.get_wm_class() === 'gjs') ) diff --git a/src/gi.ext.ts b/src/gi.ext.ts index 14bddc6..fda2045 100644 --- a/src/gi.ext.ts +++ b/src/gi.ext.ts @@ -4,5 +4,20 @@ import Meta from 'gi://Meta'; import Mtk from 'gi://Mtk'; import Shell from 'gi://Shell'; import St from 'gi://St'; +import Graphene from 'gi://Graphene'; +import Atk from 'gi://Atk'; +import Pango from 'gi://Pango'; -export { Clutter, Gio, GLib, GObject, Meta, Mtk, Shell, St }; +export { + Clutter, + Gio, + GLib, + GObject, + Meta, + Mtk, + Shell, + St, + Graphene, + Atk, + Pango, +}; diff --git a/src/indicator/defaultMenu.ts b/src/indicator/defaultMenu.ts index 2a82fff..069945c 100644 --- a/src/indicator/defaultMenu.ts +++ b/src/indicator/defaultMenu.ts @@ -19,6 +19,7 @@ import { registerGObjectClass } from '@utils/gjs'; import { Monitor } from 'resource:///org/gnome/shell/ui/layout.js'; import Layout from '@components/layout/Layout'; import { _ } from '../translations'; +import { openPrefs } from '@polyfill'; const debug = logger('DefaultMenu'); @@ -326,6 +327,16 @@ export default class DefaultMenu implements CurrentMenu { ); buttonsBoxLayout.add_child(newLayoutBtn); + const prefsBtn = IndicatorUtils.createIconButton( + 'prefs-symbolic', + this._indicator.path, + ); + prefsBtn.connect('clicked', () => { + openPrefs(); + this._indicator.menu.toggle(); + }); + buttonsBoxLayout.add_child(prefsBtn); + const buttonsPopupMenu = new PopupMenu.PopupBaseMenuItem({ style_class: 'indicator-menu-item', }); diff --git a/src/indicator/indicator.ts b/src/indicator/indicator.ts index 634eaa4..c64efd2 100644 --- a/src/indicator/indicator.ts +++ b/src/indicator/indicator.ts @@ -12,6 +12,8 @@ import EditorDialog from '../components/editor/editorDialog'; import CurrentMenu from './currentMenu'; import { registerGObjectClass } from '@utils/gjs'; import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js'; +import { getWindows } from '@utils/ui'; +import ExtendedWindow from '@components/tilingsystem/extendedWindow'; enum IndicatorState { DEFAULT = 1, @@ -80,10 +82,40 @@ export default class Indicator extends PanelMenu.Button { } public selectLayoutOnClick(monitorIndex: number, layoutToSelectId: string) { + // get the currently selected layouts const selected = Settings.get_selected_layouts(); + // select the layout for the given monitor selected[global.workspaceManager.get_active_workspace_index()][ monitorIndex ] = layoutToSelectId; + + // if there are 2 or more workspaces, if the last workspace is empty + // it must follow the layout of the second-last workspace + // if we changed the second-last workspace we take care of changing + // the last workspace as well, if there aren't tiled windows (is empty) + const n_workspaces = global.workspaceManager.get_n_workspaces(); + if ( + global.workspaceManager.get_active_workspace_index() === + n_workspaces - 2 + ) { + const lastWs = global.workspaceManager.get_workspace_by_index( + n_workspaces - 1, + ); + if (!lastWs) return; + + // check if there are tiled windows on that monitor and in the last workspace + const tiledWindows = getWindows(lastWs).find( + (win) => + (win as ExtendedWindow).assignedTile && + win.get_monitor() === monitorIndex, + ); + if (!tiledWindows) { + // the last workspace, on that monitor, is empty + // select the same layout for last workspace as well + selected[lastWs.index()][monitorIndex] = layoutToSelectId; + } + } + Settings.save_selected_layouts(selected); this.menu.toggle(); } diff --git a/src/indicator/utils.ts b/src/indicator/utils.ts index 85e50ca..c01d0e6 100644 --- a/src/indicator/utils.ts +++ b/src/indicator/utils.ts @@ -5,7 +5,8 @@ export const createButton = ( text: string, path?: string, ): St.Button => { - const btn = createIconButton(iconName, path); + const btn = createIconButton(iconName, path, 8); + btn.set_style('padding-left: 5px !important;'); // bring back the right padding btn.child.add_child( new St.Label({ marginBottom: 4, @@ -20,11 +21,13 @@ export const createButton = ( export const createIconButton = ( iconName: string, path?: string, + spacing = 0, ): St.Button => { const btn = new St.Button({ styleClass: 'message-list-clear-button button', canFocus: true, xExpand: true, + style: 'padding-left: 5px !important; padding-right: 5px !important;', child: new St.BoxLayout({ vertical: false, // horizontal box layout clipToAllocation: true, @@ -32,7 +35,7 @@ export const createIconButton = ( yAlign: Clutter.ActorAlign.CENTER, reactive: true, xExpand: true, - style: 'spacing: 8px', + style: spacing > 0 ? `spacing: ${spacing}px` : '', }), }); diff --git a/src/keybindings.ts b/src/keybindings.ts index 7b8b3ff..656deb5 100644 --- a/src/keybindings.ts +++ b/src/keybindings.ts @@ -16,16 +16,21 @@ export enum KeyBindingsDirection { RIGHT, } +export enum FocusSwitchDirection { + NEXT = 1, + PREV, +} + @registerGObjectClass export default class KeyBindings extends GObject.Object { static metaInfo: GObject.MetaInfo = { GTypeName: 'KeyBindings', Signals: { 'move-window': { - param_types: [Meta.Display.$gtype, GObject.TYPE_INT], // Meta.Display, Meta.Direction + param_types: [Meta.Display.$gtype, GObject.TYPE_INT], // Meta.Display, KeyBindingsDirection }, 'span-window': { - param_types: [Meta.Display.$gtype, GObject.TYPE_INT], // Meta.Display, Meta.Direction + param_types: [Meta.Display.$gtype, GObject.TYPE_INT], // Meta.Display, KeyBindingsDirection }, 'span-window-all-tiles': { param_types: [Meta.Display.$gtype], // Meta.Display @@ -36,8 +41,11 @@ export default class KeyBindings extends GObject.Object { 'move-window-center': { param_types: [Meta.Display.$gtype], // Meta.Display }, + 'focus-window-direction': { + param_types: [Meta.Display.$gtype, GObject.TYPE_INT], // Meta.Display, KeyBindingsDirection + }, 'focus-window': { - param_types: [Meta.Display.$gtype, GObject.TYPE_INT], // Meta.Display, Meta.Direction + param_types: [Meta.Display.$gtype, GObject.TYPE_INT], // Meta.Display, FocusSwitchDirection }, }, }; @@ -144,7 +152,11 @@ export default class KeyBindings extends GObject.Object { Meta.KeyBindingFlags.NONE, Shell.ActionMode.NORMAL, (display: Meta.Display) => { - this.emit('focus-window', display, KeyBindingsDirection.RIGHT); + this.emit( + 'focus-window-direction', + display, + KeyBindingsDirection.RIGHT, + ); }, ); @@ -154,7 +166,11 @@ export default class KeyBindings extends GObject.Object { Meta.KeyBindingFlags.NONE, Shell.ActionMode.NORMAL, (display: Meta.Display) => { - this.emit('focus-window', display, KeyBindingsDirection.LEFT); + this.emit( + 'focus-window-direction', + display, + KeyBindingsDirection.LEFT, + ); }, ); @@ -164,7 +180,11 @@ export default class KeyBindings extends GObject.Object { Meta.KeyBindingFlags.NONE, Shell.ActionMode.NORMAL, (display: Meta.Display) => { - this.emit('focus-window', display, KeyBindingsDirection.UP); + this.emit( + 'focus-window-direction', + display, + KeyBindingsDirection.UP, + ); }, ); @@ -174,7 +194,31 @@ export default class KeyBindings extends GObject.Object { Meta.KeyBindingFlags.NONE, Shell.ActionMode.NORMAL, (display: Meta.Display) => { - this.emit('focus-window', display, KeyBindingsDirection.DOWN); + this.emit( + 'focus-window-direction', + display, + KeyBindingsDirection.DOWN, + ); + }, + ); + + Main.wm.addKeybinding( + Settings.SETTING_FOCUS_WINDOW_NEXT, + extensionSettings, + Meta.KeyBindingFlags.NONE, + Shell.ActionMode.NORMAL, + (display: Meta.Display) => { + this.emit('focus-window', display, FocusSwitchDirection.NEXT); + }, + ); + + Main.wm.addKeybinding( + Settings.SETTING_FOCUS_WINDOW_PREV, + extensionSettings, + Meta.KeyBindingFlags.NONE, + Shell.ActionMode.NORMAL, + (display: Meta.Display) => { + this.emit('focus-window', display, FocusSwitchDirection.PREV); }, ); } @@ -266,6 +310,12 @@ export default class KeyBindings extends GObject.Object { Main.wm.removeKeybinding(Settings.SETTING_SPAN_WINDOW_ALL_TILES); Main.wm.removeKeybinding(Settings.SETTING_UNTILE_WINDOW); Main.wm.removeKeybinding(Settings.SETTING_MOVE_WINDOW_CENTER); + Main.wm.removeKeybinding(Settings.SETTING_FOCUS_WINDOW_UP); + Main.wm.removeKeybinding(Settings.SETTING_FOCUS_WINDOW_DOWN); + Main.wm.removeKeybinding(Settings.SETTING_FOCUS_WINDOW_LEFT); + Main.wm.removeKeybinding(Settings.SETTING_FOCUS_WINDOW_RIGHT); + Main.wm.removeKeybinding(Settings.SETTING_FOCUS_WINDOW_NEXT); + Main.wm.removeKeybinding(Settings.SETTING_FOCUS_WINDOW_PREV); } private _restoreNatives() { diff --git a/src/polyfill.ts b/src/polyfill.ts new file mode 100644 index 0000000..96dc42e --- /dev/null +++ b/src/polyfill.ts @@ -0,0 +1,18 @@ +import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js'; + +function openPrefs() { + // @ts-expect-error "This will be ok in GNOME <= 44 because + // the build system will provide such function" + if (Extension.openPrefs) { + // GNOME <= 44 + // @ts-expect-error "This will be ok in GNOME" + Extension.openPrefs(); + } else { + // GNOME 45+ + Extension.lookupByUUID( + 'tilingshell@ferrarodomenico.com', + )?.openPreferences(); + } +} + +export { Extension, openPrefs }; diff --git a/src/prefs.ts b/src/prefs.ts index 25c5767..ea63fbe 100644 --- a/src/prefs.ts +++ b/src/prefs.ts @@ -113,6 +113,13 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference subtitle: _('Show a border around focused window'), }); appearenceGroup.add(windowBorderRow); + windowBorderRow.add_row( + this._buildSwitchRow( + Settings.KEY_ENABLE_SMART_WINDOW_BORDER_RADIUS, + _('Smart border radius'), + _('Dynamically adapt to the window’s actual border radius'), + ), + ); windowBorderRow.add_row( this._buildSwitchRow( Settings.KEY_ENABLE_WINDOW_BORDER, @@ -581,6 +588,20 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference false, false, ], + [ + Settings.SETTING_FOCUS_WINDOW_NEXT, + _('Focus next window'), + _('Focus the window next to the current focused window'), + false, + false, + ], + [ + Settings.SETTING_FOCUS_WINDOW_PREV, + _('Focus previous window'), + _('Focus the window prior to the current focused window'), + false, + false, + ], ]; // set if the keybinding was set or not by the user @@ -663,6 +684,15 @@ export default class TilingShellExtensionPreferences extends ExtensionPreference keybindingsDialogGroup.add(row); }); + const wrapAroundRow = this._buildSwitchRow( + Settings.KEY_WRAPAROUND_FOCUS, + _('Enable next/previous window focus to wrap around'), + _( + 'When focusing next or previous window, wrap around at the window edge', + ), + ); + keybindingsGroup.add(wrapAroundRow); + // Import/export/reset section const importExportGroup = new Adw.PreferencesGroup({ title: _('Import, export and reset'), diff --git a/src/settings/settings.ts b/src/settings/settings.ts index 31a23ed..bd7e036 100644 --- a/src/settings/settings.ts +++ b/src/settings/settings.ts @@ -89,6 +89,7 @@ export default class Settings { 'span-multiple-tiles-activation-key'; static KEY_SPAN_MULTIPLE_TILES = 'enable-span-multiple-tiles'; static KEY_RESTORE_WINDOW_ORIGINAL_SIZE = 'restore-window-original-size'; + static KEY_WRAPAROUND_FOCUS = 'enable-wraparound-focus'; static KEY_RESIZE_COMPLEMENTING_WINDOWS = 'resize-complementing-windows'; static KEY_ENABLE_BLUR_SNAP_ASSISTANT = 'enable-blur-snap-assistant'; static KEY_ENABLE_BLUR_SELECTED_TILEPREVIEW = @@ -107,6 +108,8 @@ export default class Settings { static KEY_SETTING_LAYOUTS_JSON = 'layouts-json'; static KEY_SETTING_SELECTED_LAYOUTS = 'selected-layouts'; static KEY_WINDOW_BORDER_WIDTH = 'window-border-width'; + static KEY_ENABLE_SMART_WINDOW_BORDER_RADIUS = + 'enable-smart-window-border-radius'; static KEY_QUARTER_TILING_THRESHOLD = 'quarter-tiling-threshold'; static SETTING_MOVE_WINDOW_RIGHT = 'move-window-right'; @@ -124,6 +127,8 @@ export default class Settings { static SETTING_FOCUS_WINDOW_LEFT = 'focus-window-left'; static SETTING_FOCUS_WINDOW_UP = 'focus-window-up'; static SETTING_FOCUS_WINDOW_DOWN = 'focus-window-down'; + static SETTING_FOCUS_WINDOW_NEXT = 'focus-window-next'; + static SETTING_FOCUS_WINDOW_PREV = 'focus-window-prev'; static initialize(settings: Gio.Settings) { if (this._is_initialized) return; @@ -258,6 +263,14 @@ export default class Settings { set_boolean(Settings.KEY_RESTORE_WINDOW_ORIGINAL_SIZE, val); } + static get WRAPAROUND_FOCUS(): boolean { + return get_boolean(Settings.KEY_WRAPAROUND_FOCUS); + } + + static set WRAPAROUND_FOCUS(val: boolean) { + set_boolean(Settings.KEY_WRAPAROUND_FOCUS, val); + } + static get RESIZE_COMPLEMENTING_WINDOWS(): boolean { return get_boolean(Settings.KEY_RESIZE_COMPLEMENTING_WINDOWS); } @@ -354,6 +367,14 @@ export default class Settings { set_unsigned_number(Settings.KEY_WINDOW_BORDER_WIDTH, val); } + static get ENABLE_SMART_WINDOW_BORDER_RADIUS(): boolean { + return get_boolean(Settings.KEY_ENABLE_SMART_WINDOW_BORDER_RADIUS); + } + + static set ENABLE_SMART_WINDOW_BORDER_RADIUS(val: boolean) { + set_boolean(Settings.KEY_ENABLE_SMART_WINDOW_BORDER_RADIUS, val); + } + static get ENABLE_WINDOW_BORDER(): boolean { return get_boolean(Settings.KEY_ENABLE_WINDOW_BORDER); } diff --git a/src/styles/indicator.scss b/src/styles/indicator.scss index e4cc772..ef894e2 100644 --- a/src/styles/indicator.scss +++ b/src/styles/indicator.scss @@ -2,7 +2,7 @@ background-color: transparent !important; .buttons-box-layout { - spacing: 16px; + spacing: 8px; } // workaround to hide the empty space created by the popup ornament diff --git a/src/styles/window_border.scss b/src/styles/window_border.scss index 932074d..df4ebdc 100644 --- a/src/styles/window_border.scss +++ b/src/styles/window_border.scss @@ -2,10 +2,4 @@ transition: 200ms ease all; border-style: solid; border-color: none; - // top left and top right border radius only - border-radius: 11px 11px 0 0; // 14px for external border -} - -.window-border.full-radius { - border-radius: 11px; // 14px for external border } diff --git a/src/utils/globalState.ts b/src/utils/globalState.ts index 4668849..fa47be4 100644 --- a/src/utils/globalState.ts +++ b/src/utils/globalState.ts @@ -5,6 +5,8 @@ import SignalHandling from './signalHandling'; import { GObject, Meta, Gio } from '@gi.ext'; import * as Main from 'resource:///org/gnome/shell/ui/main.js'; import { logger } from './logger'; +import { getWindows } from './ui'; +import ExtendedWindow from '@components/tilingsystem/extendedWindow'; const debug = logger('GlobalState'); @@ -89,6 +91,7 @@ export default class GlobalState extends GObject.Object { return; } + const defaultLayout: Layout = this._layouts[0]; const n_monitors = Main.layoutManager.monitors.length; const n_workspaces = global.workspaceManager.get_n_workspaces(); for (let i = 0; i < n_workspaces; i++) { @@ -99,9 +102,9 @@ export default class GlobalState extends GObject.Object { const monitors_layouts = i < selected_layouts.length ? selected_layouts[i] - : [GlobalState.get().layouts[0].id]; + : [defaultLayout.id]; while (monitors_layouts.length < n_monitors) - monitors_layouts.push(this._layouts[0].id); + monitors_layouts.push(defaultLayout.id); while (monitors_layouts.length > n_monitors) monitors_layouts.pop(); @@ -114,19 +117,39 @@ export default class GlobalState extends GObject.Object { global.workspaceManager, 'workspace-added', (_, index: number) => { + const n_workspaces = global.workspaceManager.get_n_workspaces(); const newWs = global.workspaceManager.get_workspace_by_index(index); if (!newWs) return; - const layout: Layout = this._layouts[0]; debug(`added workspace ${index}`); + + const secondLastWs = + global.workspaceManager.get_workspace_by_index( + n_workspaces - 2, + ); + + const secondLastWsLayoutsId = secondLastWs + ? this._selected_layouts.get(secondLastWs) ?? [] + : []; + debug( + `second-last workspace length ${secondLastWsLayoutsId.length}`, + ); + + // the new workspace must start with the same layout of the last workspace + // use the layout at index 0 if for some reason we cannot find the layout + // of the last workspace + const layout: Layout = + this._layouts.find((lay) => + secondLastWsLayoutsId.find((id) => id === lay.id), + ) ?? this._layouts[0]; + this._selected_layouts.set( newWs, Main.layoutManager.monitors.map(() => layout.id), ); const to_be_saved: string[][] = []; - const n_workspaces = global.workspaceManager.get_n_workspaces(); for (let i = 0; i < n_workspaces; i++) { const ws = global.workspaceManager.get_workspace_by_index(i); diff --git a/src/utils/ui.ts b/src/utils/ui.ts index 4c52a11..9b980e8 100644 --- a/src/utils/ui.ts +++ b/src/utils/ui.ts @@ -89,9 +89,11 @@ export const enableScalingFactorSupport = ( monitorScalingFactor?: number, ) => { if (!monitorScalingFactor) return; - widget.set_style( - `scaling-reference: 1px; monitor-scaling-factor: ${monitorScalingFactor}px;`, - ); + widget.set_style(`${getScalingFactorSupportString(monitorScalingFactor)};`); +}; + +export const getScalingFactorSupportString = (monitorScalingFactor: number) => { + return `scaling-reference: 1px; monitor-scaling-factor: ${monitorScalingFactor}px`; }; export function getWindowsOfMonitor(monitor: Monitor): Meta.Window[] { @@ -161,23 +163,39 @@ export function buildBlurEffect(sigma: number): Shell.BlurEffect { return effect; } +function getTransientOrParent(window: Meta.Window): Meta.Window { + const transient = window.get_transient_for(); + return window.is_attached_dialog() && transient !== null + ? transient + : window; +} + +export function filterUnfocusableWindows( + windows: Meta.Window[], +): Meta.Window[] { + // we want to filter out + // - top-level windows which are precluded by dialogs + // - anything tagged skip-taskbar + // - duplicates + return windows + .map(getTransientOrParent) + .filter((win: Meta.Window, idx: number, arr: Meta.Window[]) => { + // typings indicate win will not be null, but this check is found + // in the source, so... + return win !== null && !win.skipTaskbar && arr.indexOf(win) === idx; + }); +} + /** From Gnome Shell: https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/altTab.js#L53 */ -export function getWindows(): Meta.Window[] { - const workspace = global.workspaceManager.get_active_workspace(); +export function getWindows(workspace?: Meta.Workspace): Meta.Window[] { + if (!workspace) workspace = global.workspaceManager.get_active_workspace(); // We ignore skip-taskbar windows in switchers, but if they are attached // to their parent, their position in the MRU list may be more appropriate // than the parent; so start with the complete list ... // ... map windows to their parent where appropriate ... - return global.display - .get_tab_list(Meta.TabList.NORMAL_ALL, workspace) - .map((w) => { - const transient = w.get_transient_for(); - return w.is_attached_dialog() && transient !== null ? transient : w; - // ... and filter out skip-taskbar windows and duplicates - }) - .filter( - (w, i, a) => w !== null && !w.skipTaskbar && a.indexOf(w) === i, - ); + return filterUnfocusableWindows( + global.display.get_tab_list(Meta.TabList.NORMAL_ALL, workspace), + ); } export function squaredEuclideanDistance( diff --git a/translations/it.po b/translations/it.po index a550fdc..42a3cf4 100644 --- a/translations/it.po +++ b/translations/it.po @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: Tiling Shell\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-31 15:31+0100\n" +"POT-Creation-Date: 2024-12-06 22:01+0100\n" "PO-Revision-Date: 2024-10-22 12:39+0200\n" "Last-Translator: Domenico Ferraro \n" "Language-Team: Italian <>\n" @@ -11,189 +11,189 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: dist/prefs.js:664 +#: dist/prefs.js:664 dist/prefs.js:751 msgid "General" msgstr "Generale" -#: dist/prefs.js:669 +#: dist/prefs.js:669 dist/prefs.js:756 msgid "Appearance" msgstr "Aspetto" -#: dist/prefs.js:670 +#: dist/prefs.js:670 dist/prefs.js:757 msgid "Configure the appearance of Tiling Shell" msgstr "Configura l'aspetto di Tiling Shell" -#: dist/prefs.js:675 +#: dist/prefs.js:675 dist/prefs.js:762 msgid "Show Indicator" msgstr "Mostra icona" -#: dist/prefs.js:676 +#: dist/prefs.js:676 dist/prefs.js:763 msgid "Whether to show the panel indicator" msgstr "Se mostrare l'indicatore del pannello oppure no" -#: dist/prefs.js:681 +#: dist/prefs.js:681 dist/prefs.js:768 msgid "Inner gaps" msgstr "Spazi interni" -#: dist/prefs.js:682 +#: dist/prefs.js:682 dist/prefs.js:769 msgid "Gaps between windows" msgstr "Spazi tra le finestre" -#: dist/prefs.js:687 +#: dist/prefs.js:687 dist/prefs.js:774 msgid "Outer gaps" msgstr "Spazi esterni" -#: dist/prefs.js:688 +#: dist/prefs.js:688 dist/prefs.js:775 msgid "Gaps between a window and the monitor borders" msgstr "Spazi tra una finestra e i bordi del monitor" -#: dist/prefs.js:692 +#: dist/prefs.js:692 dist/prefs.js:779 msgid "Blur (experimental feature)" msgstr "Sfocatura (funzione sperimentale)" -#: dist/prefs.js:694 +#: dist/prefs.js:694 dist/prefs.js:781 msgid "Apply blur effect to Snap Assistant and tile previews" msgstr "" "Applica l'effetto sfocatura allo Snap Assistant e alle anteprime dei riquadri" -#: dist/prefs.js:700 +#: dist/prefs.js:700 dist/prefs.js:787 msgid "Snap Assistant threshold" msgstr "Soglia di attivazione dello Snap Assistant" -#: dist/prefs.js:702 +#: dist/prefs.js:702 dist/prefs.js:789 msgid "Minimum distance from the Snap Assistant to the pointer to open it" msgstr "Distanza minima dallo Snap Assistant al puntatore per aprirlo" -#: dist/prefs.js:711 +#: dist/prefs.js:711 dist/prefs.js:798 msgid "Snap Assistant" msgstr "Snap Assistant" -#: dist/prefs.js:712 +#: dist/prefs.js:712 dist/prefs.js:799 msgid "Apply blur effect to Snap Assistant" msgstr "Applica l'effetto sfocatura allo Snap Assistant" -#: dist/prefs.js:718 +#: dist/prefs.js:718 dist/prefs.js:805 msgid "Selected tile preview" msgstr "Anteprima del riquadro selezionato" -#: dist/prefs.js:719 +#: dist/prefs.js:719 dist/prefs.js:806 msgid "Apply blur effect to selected tile preview" msgstr "Applica l'effetto sfocato all'anteprima del riquadro selezionato" -#: dist/prefs.js:723 +#: dist/prefs.js:723 dist/prefs.js:810 msgid "Window border" msgstr "Bordo della finestra" -#: dist/prefs.js:724 dist/prefs.js:731 +#: dist/prefs.js:724 dist/prefs.js:731 dist/prefs.js:811 dist/prefs.js:825 msgid "Show a border around focused window" msgstr "Mostra un bordo attorno alla finestra selezionata" -#: dist/prefs.js:730 +#: dist/prefs.js:730 dist/prefs.js:824 msgid "Enable" msgstr "Abilita" -#: dist/prefs.js:737 +#: dist/prefs.js:737 dist/prefs.js:831 msgid "Width" msgstr "Larghezza" -#: dist/prefs.js:738 +#: dist/prefs.js:738 dist/prefs.js:832 msgid "The size of the border" msgstr "La dimensione del bordo" -#: dist/prefs.js:744 +#: dist/prefs.js:744 dist/prefs.js:838 msgid "Border color" msgstr "Colore bordo" -#: dist/prefs.js:745 +#: dist/prefs.js:745 dist/prefs.js:839 msgid "Choose the color of the border" msgstr "Scegli il colore del bordo" -#: dist/prefs.js:751 +#: dist/prefs.js:751 dist/prefs.js:845 msgid "Animations" msgstr "Animazioni" -#: dist/prefs.js:752 +#: dist/prefs.js:752 dist/prefs.js:846 msgid "Customize animations" msgstr "Personalizza animazioni" -#: dist/prefs.js:758 +#: dist/prefs.js:758 dist/prefs.js:852 msgid "Snap assistant animation time" msgstr "Tempo di animazione dello Snap Assistant" -#: dist/prefs.js:759 +#: dist/prefs.js:759 dist/prefs.js:853 msgid "The snap assistant animation time in milliseconds" msgstr "Tempo di animazione delo Snap Assistant in millisecondi" -#: dist/prefs.js:767 +#: dist/prefs.js:767 dist/prefs.js:861 msgid "Tiles animation time" msgstr "Tempo di animazione dei riquadri" -#: dist/prefs.js:768 +#: dist/prefs.js:768 dist/prefs.js:862 msgid "The tiles animation time in milliseconds" msgstr "Il tempo di animazione dei riquadri in millisecondi" -#: dist/prefs.js:774 +#: dist/prefs.js:774 dist/prefs.js:868 msgid "Behaviour" msgstr "Comportamento" -#: dist/prefs.js:775 +#: dist/prefs.js:775 dist/prefs.js:869 msgid "Configure the behaviour of Tiling Shell" msgstr "Configura il comportamento di Tiling Shell" -#: dist/prefs.js:780 +#: dist/prefs.js:780 dist/prefs.js:874 msgid "Enable Snap Assistant" msgstr "Abilita Snap Assistant" -#: dist/prefs.js:781 +#: dist/prefs.js:781 dist/prefs.js:875 msgid "Move the window on top of the screen to snap assist it" msgstr "" "Sposta la finestra nella parte superiore dello schermo per usare lo Snap " "Assistant" -#: dist/prefs.js:786 +#: dist/prefs.js:786 dist/prefs.js:880 msgid "Enable Tiling System" msgstr "Abilita sistema di tiling" -#: dist/prefs.js:787 +#: dist/prefs.js:787 dist/prefs.js:881 msgid "Hold the activation key while moving a window to tile it" msgstr "" "Tieni premuto il tasto di attivazione mentre sposti una finestra per " "affiancarla" -#: dist/prefs.js:805 +#: dist/prefs.js:805 dist/prefs.js:899 msgid "Span multiple tiles" msgstr "Unisci più riquadri" -#: dist/prefs.js:806 +#: dist/prefs.js:806 dist/prefs.js:900 msgid "Hold the activation key to span multiple tiles" msgstr "Tieni premuto il tasto di attivazione per unire più riquadri" -#: dist/prefs.js:821 +#: dist/prefs.js:821 dist/prefs.js:915 msgid "Enable auto-resize of the complementing tiled windows" msgstr "Abilita il ridimensionamento automatico delle finestre affiancate" -#: dist/prefs.js:823 +#: dist/prefs.js:823 dist/prefs.js:917 msgid "" "When a tiled window is resized, auto-resize the other tiled windows near it" msgstr "" "Quando una finestra viene ridimensionata, ridimensiona automaticamente le " "altre finestre affiancate ad essa" -#: dist/prefs.js:829 +#: dist/prefs.js:829 dist/prefs.js:923 msgid "Restore window size" msgstr "Ripristina le dimensioni della finestra" -#: dist/prefs.js:831 +#: dist/prefs.js:831 dist/prefs.js:925 msgid "Whether to restore the windows to their original size when untiled" msgstr "Se ripristinare le finestre alle dimensioni originali oppure no" -#: dist/prefs.js:837 +#: dist/prefs.js:837 dist/prefs.js:931 msgid "Add snap assistant and auto-tile buttons to window menu" msgstr "" "Aggiungi lo Snap Assistant e i pulsanti di affiancamento automatico al menu " "della finestra" -#: dist/prefs.js:839 +#: dist/prefs.js:839 dist/prefs.js:933 msgid "" "Add snap assistant and auto-tile buttons in the menu that shows up when you " "right click on a window title" @@ -202,325 +202,359 @@ msgstr "" "visualizzato quando si fa clic con il pulsante destro del mouse sul titolo " "di una finestra" -#: dist/prefs.js:844 +#: dist/prefs.js:844 dist/prefs.js:938 msgid "Screen Edges" msgstr "Bordi dello schermo" -#: dist/prefs.js:846 +#: dist/prefs.js:846 dist/prefs.js:940 msgid "" "Drag windows against the top, left and right screen edges to resize them" msgstr "" "Trascina le finestre contro i bordi superiore, sinistro e destro dello " "schermo per ridimensionarle" -#: dist/prefs.js:860 +#: dist/prefs.js:860 dist/prefs.js:954 msgid "Drag against top edge to maximize window" msgstr "Trascina contro il bordo superiore per ingrandire la finestra" -#: dist/prefs.js:861 +#: dist/prefs.js:861 dist/prefs.js:955 msgid "Drag windows against the top edge to maximize them" msgstr "Trascina le finestre contro il bordo superiore per massimizzarle" -#: dist/prefs.js:870 +#: dist/prefs.js:870 dist/prefs.js:964 msgid "Quarter tiling activation area" msgstr "Area di attivazione della divisione in quarti" -#: dist/prefs.js:871 +#: dist/prefs.js:871 dist/prefs.js:965 #, javascript-format msgid "Activation area to trigger quarter tiling (% of the screen)" msgstr "" "Area di attivazione per attivare la divisione in quarti (% dello schermo)" -#: dist/prefs.js:888 +#: dist/prefs.js:888 dist/prefs.js:982 msgid "Layouts" msgstr "Layouts" -#: dist/prefs.js:889 +#: dist/prefs.js:889 dist/prefs.js:983 msgid "Configure the layouts of Tiling Shell" msgstr "Configura i layout di Tiling Shell" -#: dist/prefs.js:893 dist/prefs.js:894 +#: dist/prefs.js:893 dist/prefs.js:894 dist/prefs.js:987 dist/prefs.js:988 msgid "Edit layouts" msgstr "Modifica layouts" -#: dist/prefs.js:895 +#: dist/prefs.js:895 dist/prefs.js:989 msgid "Open the layouts editor" msgstr "Apre l'editor dei layouts" -#: dist/prefs.js:900 dist/prefs.js:901 dist/prefs.js:905 +#: dist/prefs.js:900 dist/prefs.js:901 dist/prefs.js:905 dist/prefs.js:994 +#: dist/prefs.js:995 dist/prefs.js:999 msgid "Export layouts" msgstr "Esporta layouts" -#: dist/prefs.js:902 +#: dist/prefs.js:902 dist/prefs.js:996 msgid "Export layouts to a file" msgstr "Esporta layouts in un file" #: dist/prefs.js:909 dist/prefs.js:966 dist/prefs.js:1253 dist/prefs.js:1311 +#: dist/prefs.js:1003 dist/prefs.js:1060 dist/prefs.js:1369 dist/prefs.js:1427 msgid "Cancel" msgstr "Annulla" -#: dist/prefs.js:910 dist/prefs.js:1254 +#: dist/prefs.js:910 dist/prefs.js:1254 dist/prefs.js:1004 dist/prefs.js:1370 msgid "Save" msgstr "Salva" -#: dist/prefs.js:957 dist/prefs.js:958 +#: dist/prefs.js:957 dist/prefs.js:958 dist/prefs.js:1051 dist/prefs.js:1052 msgid "Import layouts" msgstr "Importa layouts" -#: dist/prefs.js:959 +#: dist/prefs.js:959 dist/prefs.js:1053 msgid "Import layouts from a file" msgstr "Importa layouts da un file" -#: dist/prefs.js:962 +#: dist/prefs.js:962 dist/prefs.js:1056 msgid "Select layouts file" msgstr "Seleziona file di layouts" -#: dist/prefs.js:967 dist/prefs.js:1312 +#: dist/prefs.js:967 dist/prefs.js:1312 dist/prefs.js:1061 dist/prefs.js:1428 msgid "Open" msgstr "Apri" -#: dist/prefs.js:1015 dist/prefs.js:1016 +#: dist/prefs.js:1015 dist/prefs.js:1016 dist/prefs.js:1109 dist/prefs.js:1110 msgid "Reset layouts" msgstr "Ripristina layouts" -#: dist/prefs.js:1017 +#: dist/prefs.js:1017 dist/prefs.js:1111 msgid "Bring back the default layouts" msgstr "Ripristina i layouts predefiniti" -#: dist/prefs.js:1030 +#: dist/prefs.js:1030 dist/prefs.js:1124 msgid "Keybindings" msgstr "Scorciatoie da tastiera" -#: dist/prefs.js:1032 +#: dist/prefs.js:1032 dist/prefs.js:1126 msgid "Use hotkeys to perform actions on the focused window" msgstr "" "Usa i tasti di scelta rapida per eseguire azioni sulla finestra selezionata" -#: dist/prefs.js:1050 +#: dist/prefs.js:1050 dist/prefs.js:1152 msgid "Move window to right tile" msgstr "Sposta la finestra nel riquadro destro" -#: dist/prefs.js:1052 +#: dist/prefs.js:1052 dist/prefs.js:1154 msgid "Move the focused window to the tile on its right" msgstr "Sposta la finestra selezionata sul riquadro alla sua destra" -#: dist/prefs.js:1061 +#: dist/prefs.js:1061 dist/prefs.js:1163 msgid "Move window to left tile" msgstr "Sposta la finestra nel riquadro sinistro" -#: dist/prefs.js:1062 +#: dist/prefs.js:1062 dist/prefs.js:1164 msgid "Move the focused window to the tile on its left" msgstr "Sposta la finestra selezionata sul riquadro alla sua sinistra" -#: dist/prefs.js:1068 +#: dist/prefs.js:1068 dist/prefs.js:1170 msgid "Move window to tile above" msgstr "Sposta la finestra nel riquadro sopra" -#: dist/prefs.js:1069 +#: dist/prefs.js:1069 dist/prefs.js:1171 msgid "Move the focused window to the tile above" msgstr "Sposta la finestra selezionata sul riquadro sopra" -#: dist/prefs.js:1075 +#: dist/prefs.js:1075 dist/prefs.js:1177 msgid "Move window to tile below" msgstr "Sposta la finestra sul riquadro sottostante" -#: dist/prefs.js:1076 +#: dist/prefs.js:1076 dist/prefs.js:1178 msgid "Move the focused window to the tile below" msgstr "Sposta la finestra selezionata sul riquadro sottostante" -#: dist/prefs.js:1082 +#: dist/prefs.js:1082 dist/prefs.js:1184 msgid "Span window to right tile" msgstr "Estendi la finestra al riquadro destro" -#: dist/prefs.js:1083 +#: dist/prefs.js:1083 dist/prefs.js:1185 msgid "Span the focused window to the tile on its right" msgstr "Extendi la finestra selezionata sul riquadro alla sua destra" -#: dist/prefs.js:1089 +#: dist/prefs.js:1089 dist/prefs.js:1191 msgid "Span window to left tile" msgstr "Estendi la finestra al riquadro sinistro" -#: dist/prefs.js:1090 +#: dist/prefs.js:1090 dist/prefs.js:1192 msgid "Span the focused window to the tile on its left" msgstr "Estendi la finestra selezionata al riquadro alla sua sinistra" -#: dist/prefs.js:1096 +#: dist/prefs.js:1096 dist/prefs.js:1198 msgid "Span window above" msgstr "Estendi finestra verso l'alto" -#: dist/prefs.js:1097 +#: dist/prefs.js:1097 dist/prefs.js:1199 msgid "Span the focused window to the tile above" msgstr "Estendi la finestra selezionata al riquadro in alto" -#: dist/prefs.js:1103 +#: dist/prefs.js:1103 dist/prefs.js:1205 msgid "Span window down" msgstr "Estendi finestra verso il basso" -#: dist/prefs.js:1104 +#: dist/prefs.js:1104 dist/prefs.js:1206 msgid "Span the focused window to the tile below" msgstr "Estendi la finestra selezionata al riquadro sottostante" -#: dist/prefs.js:1110 +#: dist/prefs.js:1110 dist/prefs.js:1212 msgid "Span window to all tiles" msgstr "Estendi la finestra a tutti i riquadri" -#: dist/prefs.js:1111 +#: dist/prefs.js:1111 dist/prefs.js:1213 msgid "Span the focused window to all the tiles" msgstr "Estendi la finestra selezionata a tutti i riquadri" -#: dist/prefs.js:1117 +#: dist/prefs.js:1117 dist/prefs.js:1219 msgid "Untile focused window" msgstr "Sgancia la finestra selezionata" -#: dist/prefs.js:1125 +#: dist/prefs.js:1125 dist/prefs.js:1227 msgid "Move window to the center" msgstr "Sposta la finestra al centro" -#: dist/prefs.js:1127 +#: dist/prefs.js:1127 dist/prefs.js:1229 msgid "Move the focused window to the center of the screen" msgstr "Sposta la finestra selezionata al centro dello schermo" -#: dist/prefs.js:1136 +#: dist/prefs.js:1136 dist/prefs.js:1238 msgid "Focus window to the right" msgstr "Seleziona finestra a destra" -#: dist/prefs.js:1138 +#: dist/prefs.js:1138 dist/prefs.js:1240 msgid "Focus the window to the right of the current focused window" msgstr "Seleziona la finestra a destra della finestra attualmente selezionata" -#: dist/prefs.js:1145 +#: dist/prefs.js:1145 dist/prefs.js:1247 msgid "Focus window to the left" msgstr "Seleziona finestra a sinistra" -#: dist/prefs.js:1146 +#: dist/prefs.js:1146 dist/prefs.js:1248 msgid "Focus the window to the left of the current focused window" msgstr "" "Seleziona la finestra a sinistra della finestra attualmente selezionata" -#: dist/prefs.js:1152 +#: dist/prefs.js:1152 dist/prefs.js:1254 msgid "Focus window above" msgstr "Seleziona finestra in alto" -#: dist/prefs.js:1153 +#: dist/prefs.js:1153 dist/prefs.js:1255 msgid "Focus the window above the current focused window" msgstr "Seleziona la finestra in alto alla finestra attualmente selezionata" -#: dist/prefs.js:1159 +#: dist/prefs.js:1159 dist/prefs.js:1261 msgid "Focus window below" msgstr "Seleziona la finestra in basso" -#: dist/prefs.js:1160 +#: dist/prefs.js:1160 dist/prefs.js:1262 msgid "Focus the window below the current focused window" msgstr "Seleziona la finestra in basso alla finestra attualmente selezionata" -#: dist/prefs.js:1187 +#: dist/prefs.js:1187 dist/prefs.js:1303 msgid "View and Customize all the Shortcuts" msgstr "Visualizza e personalizza tutte le scorciatoie" -#: dist/prefs.js:1215 dist/prefs.js:1216 +#: dist/prefs.js:1215 dist/prefs.js:1216 dist/prefs.js:1331 dist/prefs.js:1332 msgid "View and Customize Shortcuts" msgstr "Visualizza e personalizza le scorciatoie" -#: dist/prefs.js:1237 +#: dist/prefs.js:1237 dist/prefs.js:1353 msgid "Import, export and reset" msgstr "Importa, esporta e resetta" -#: dist/prefs.js:1239 +#: dist/prefs.js:1239 dist/prefs.js:1355 msgid "Import, export and reset the settings of Tiling Shell" msgstr "Importa, esporta e resetta le impostazioni di Tiling Shell" -#: dist/prefs.js:1244 dist/prefs.js:1245 +#: dist/prefs.js:1244 dist/prefs.js:1245 dist/prefs.js:1360 dist/prefs.js:1361 msgid "Export settings" msgstr "Esporta le impostazioni" -#: dist/prefs.js:1246 +#: dist/prefs.js:1246 dist/prefs.js:1362 msgid "Export settings to a file" msgstr "Esporta le impostazioni in un file" -#: dist/prefs.js:1249 +#: dist/prefs.js:1249 dist/prefs.js:1365 msgid "Export settings to a text file" msgstr "Esporta le impostazioni in un file di testo" -#: dist/prefs.js:1302 dist/prefs.js:1303 +#: dist/prefs.js:1302 dist/prefs.js:1303 dist/prefs.js:1418 dist/prefs.js:1419 msgid "Import settings" msgstr "Importa le impostazioni" -#: dist/prefs.js:1304 +#: dist/prefs.js:1304 dist/prefs.js:1420 msgid "Import settings from a file" msgstr "Importa le impostazioni da un file" -#: dist/prefs.js:1307 +#: dist/prefs.js:1307 dist/prefs.js:1423 msgid "Select a text file to import from" msgstr "Seleziona un file di testo dal quale importare le impostazioni" -#: dist/prefs.js:1351 dist/prefs.js:1352 +#: dist/prefs.js:1351 dist/prefs.js:1352 dist/prefs.js:1467 dist/prefs.js:1468 msgid "Reset settings" msgstr "Resetta le impostazioni" -#: dist/prefs.js:1353 +#: dist/prefs.js:1353 dist/prefs.js:1469 msgid "Bring back the default settings" msgstr "Ripristina le impostazioni predefinite" -#: dist/prefs.js:1368 +#: dist/prefs.js:1368 dist/prefs.js:1484 msgid "Donate on ko-fi" msgstr "Fai una donazione" -#: dist/prefs.js:1374 +#: dist/prefs.js:1374 dist/prefs.js:1490 msgid "Report a bug" msgstr "Segnala un bug" -#: dist/prefs.js:1380 +#: dist/prefs.js:1380 dist/prefs.js:1496 msgid "Request a feature" msgstr "Richiedi una funzionalità" -#: dist/prefs.js:1388 +#: dist/prefs.js:1388 dist/prefs.js:1504 msgid "Have issues, you want to suggest a new feature or contribute?" msgstr "Hai problemi, vuoi suggerire una nuova funzionalità o contribuire?" -#: dist/prefs.js:1395 +#: dist/prefs.js:1395 dist/prefs.js:1511 msgid "Open a new issue on" msgstr "Apri una nuova issue su" -#: dist/extension.js:4500 +#: dist/extension.js:4500 dist/extension.js:5361 msgid "Edit Layouts" msgstr "Modifica layouts" -#: dist/extension.js:4510 +#: dist/extension.js:4510 dist/extension.js:5371 msgid "New Layout" msgstr "Nuovo layout" -#: dist/extension.js:4691 +#: dist/extension.js:4691 dist/extension.js:5558 msgid "to split a tile" msgstr "per dividere un riquadro" -#: dist/extension.js:4730 +#: dist/extension.js:4730 dist/extension.js:5597 msgid "to split a tile vertically" msgstr "per dividere un riquadro verticalmente" -#: dist/extension.js:4750 +#: dist/extension.js:4750 dist/extension.js:5617 msgid "to delete a tile" msgstr "per cancellare un riquadro" -#: dist/extension.js:4775 +#: dist/extension.js:4775 dist/extension.js:5642 msgid "use the indicator button to save or cancel" msgstr "usa l'icona sul pannello superiore per salvare o annullare" -#: dist/prefs.js:815 +#: dist/prefs.js:815 dist/prefs.js:909 msgid "Enable Auto Tiling" msgstr "Abilita sistema di tiling automatico" -#: dist/prefs.js:816 +#: dist/prefs.js:816 dist/prefs.js:910 msgid "Automatically tile new windows to the best tile" msgstr "Posiziona automaticamente le nuove finestre nel migliore riquadro" -#: dist/prefs.js:795 +#: dist/prefs.js:795 dist/prefs.js:889 msgid "Tiling System deactivation key" msgstr "Tasto di disattivazione del sistema di tiling" -#: dist/prefs.js:797 +#: dist/prefs.js:797 dist/prefs.js:891 msgid "" "Hold the deactivation key while moving a window to deactivate the tiling " "system" msgstr "" "Tieni premuto il tasto di disattivazione mentre sposti una finestra per " "chiudere il sistema di tiling" + +#: dist/prefs.js:817 +msgid "Smart border radius" +msgstr "Border radius intelligente" + +#: dist/prefs.js:818 +msgid "Dynamically adapt to the window’s actual border radius" +msgstr "Adatta dinamicamente al border radius della finestra" + +#: dist/prefs.js:1141 +msgid "Enable next/previous window focus to wrap around" +msgstr "Permetti al focus del successore/precedente di ricominciare da capo" + +#: dist/prefs.js:1143 +msgid "When focusing next or previous window, wrap around at the window edge" +msgstr "Quando passi il focus alla finestra successiva o precedente, ricomincia da capo se non ci sono altre finestre" + +#: dist/prefs.js:1268 +msgid "Focus next window" +msgstr "Seleziona finestra successiva" + +#: dist/prefs.js:1269 +msgid "Focus the window next to the current focused window" +msgstr "Seleziona la finestra successiva alla finestra attualmente selezionata" + +#: dist/prefs.js:1275 +msgid "Focus previous window" +msgstr "Seleziona finestra precedente" + +#: dist/prefs.js:1276 +msgid "Focus the window prior to the current focused window" +msgstr "Seleziona la finestra precedente alla finestra attualmente selezionata" diff --git a/translations/tilingshell@ferrarodomenico.com.pot b/translations/tilingshell@ferrarodomenico.com.pot index 336c170..30cc714 100644 --- a/translations/tilingshell@ferrarodomenico.com.pot +++ b/translations/tilingshell@ferrarodomenico.com.pot @@ -8,7 +8,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2024-10-31 15:31+0100\n" +"POT-Creation-Date: 2024-12-06 22:04+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -17,497 +17,531 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" -#: dist/prefs.js:664 +#: dist/prefs.js:664 dist/prefs.js:751 msgid "General" msgstr "" -#: dist/prefs.js:669 +#: dist/prefs.js:669 dist/prefs.js:756 msgid "Appearance" msgstr "" -#: dist/prefs.js:670 +#: dist/prefs.js:670 dist/prefs.js:757 msgid "Configure the appearance of Tiling Shell" msgstr "" -#: dist/prefs.js:675 +#: dist/prefs.js:675 dist/prefs.js:762 msgid "Show Indicator" msgstr "" -#: dist/prefs.js:676 +#: dist/prefs.js:676 dist/prefs.js:763 msgid "Whether to show the panel indicator" msgstr "" -#: dist/prefs.js:681 +#: dist/prefs.js:681 dist/prefs.js:768 msgid "Inner gaps" msgstr "" -#: dist/prefs.js:682 +#: dist/prefs.js:682 dist/prefs.js:769 msgid "Gaps between windows" msgstr "" -#: dist/prefs.js:687 +#: dist/prefs.js:687 dist/prefs.js:774 msgid "Outer gaps" msgstr "" -#: dist/prefs.js:688 +#: dist/prefs.js:688 dist/prefs.js:775 msgid "Gaps between a window and the monitor borders" msgstr "" -#: dist/prefs.js:692 +#: dist/prefs.js:692 dist/prefs.js:779 msgid "Blur (experimental feature)" msgstr "" -#: dist/prefs.js:694 +#: dist/prefs.js:694 dist/prefs.js:781 msgid "Apply blur effect to Snap Assistant and tile previews" msgstr "" -#: dist/prefs.js:700 +#: dist/prefs.js:700 dist/prefs.js:787 msgid "Snap Assistant threshold" msgstr "" -#: dist/prefs.js:702 +#: dist/prefs.js:702 dist/prefs.js:789 msgid "Minimum distance from the Snap Assistant to the pointer to open it" msgstr "" -#: dist/prefs.js:711 +#: dist/prefs.js:711 dist/prefs.js:798 msgid "Snap Assistant" msgstr "" -#: dist/prefs.js:712 +#: dist/prefs.js:712 dist/prefs.js:799 msgid "Apply blur effect to Snap Assistant" msgstr "" -#: dist/prefs.js:718 +#: dist/prefs.js:718 dist/prefs.js:805 msgid "Selected tile preview" msgstr "" -#: dist/prefs.js:719 +#: dist/prefs.js:719 dist/prefs.js:806 msgid "Apply blur effect to selected tile preview" msgstr "" -#: dist/prefs.js:723 +#: dist/prefs.js:723 dist/prefs.js:810 msgid "Window border" msgstr "" -#: dist/prefs.js:724 dist/prefs.js:731 +#: dist/prefs.js:724 dist/prefs.js:731 dist/prefs.js:811 dist/prefs.js:825 msgid "Show a border around focused window" msgstr "" -#: dist/prefs.js:730 +#: dist/prefs.js:730 dist/prefs.js:824 msgid "Enable" msgstr "" -#: dist/prefs.js:737 +#: dist/prefs.js:737 dist/prefs.js:831 msgid "Width" msgstr "" -#: dist/prefs.js:738 +#: dist/prefs.js:738 dist/prefs.js:832 msgid "The size of the border" msgstr "" -#: dist/prefs.js:744 +#: dist/prefs.js:744 dist/prefs.js:838 msgid "Border color" msgstr "" -#: dist/prefs.js:745 +#: dist/prefs.js:745 dist/prefs.js:839 msgid "Choose the color of the border" msgstr "" -#: dist/prefs.js:751 +#: dist/prefs.js:751 dist/prefs.js:845 msgid "Animations" msgstr "" -#: dist/prefs.js:752 +#: dist/prefs.js:752 dist/prefs.js:846 msgid "Customize animations" msgstr "" -#: dist/prefs.js:758 +#: dist/prefs.js:758 dist/prefs.js:852 msgid "Snap assistant animation time" msgstr "" -#: dist/prefs.js:759 +#: dist/prefs.js:759 dist/prefs.js:853 msgid "The snap assistant animation time in milliseconds" msgstr "" -#: dist/prefs.js:767 +#: dist/prefs.js:767 dist/prefs.js:861 msgid "Tiles animation time" msgstr "" -#: dist/prefs.js:768 +#: dist/prefs.js:768 dist/prefs.js:862 msgid "The tiles animation time in milliseconds" msgstr "" -#: dist/prefs.js:774 +#: dist/prefs.js:774 dist/prefs.js:868 msgid "Behaviour" msgstr "" -#: dist/prefs.js:775 +#: dist/prefs.js:775 dist/prefs.js:869 msgid "Configure the behaviour of Tiling Shell" msgstr "" -#: dist/prefs.js:780 +#: dist/prefs.js:780 dist/prefs.js:874 msgid "Enable Snap Assistant" msgstr "" -#: dist/prefs.js:781 +#: dist/prefs.js:781 dist/prefs.js:875 msgid "Move the window on top of the screen to snap assist it" msgstr "" -#: dist/prefs.js:786 +#: dist/prefs.js:786 dist/prefs.js:880 msgid "Enable Tiling System" msgstr "" -#: dist/prefs.js:787 +#: dist/prefs.js:787 dist/prefs.js:881 msgid "Hold the activation key while moving a window to tile it" msgstr "" -#: dist/prefs.js:805 +#: dist/prefs.js:805 dist/prefs.js:899 msgid "Span multiple tiles" msgstr "" -#: dist/prefs.js:806 +#: dist/prefs.js:806 dist/prefs.js:900 msgid "Hold the activation key to span multiple tiles" msgstr "" -#: dist/prefs.js:821 +#: dist/prefs.js:821 dist/prefs.js:915 msgid "Enable auto-resize of the complementing tiled windows" msgstr "" -#: dist/prefs.js:823 +#: dist/prefs.js:823 dist/prefs.js:917 msgid "" "When a tiled window is resized, auto-resize the other tiled windows near it" msgstr "" -#: dist/prefs.js:829 +#: dist/prefs.js:829 dist/prefs.js:923 msgid "Restore window size" msgstr "" -#: dist/prefs.js:831 +#: dist/prefs.js:831 dist/prefs.js:925 msgid "Whether to restore the windows to their original size when untiled" msgstr "" -#: dist/prefs.js:837 +#: dist/prefs.js:837 dist/prefs.js:931 msgid "Add snap assistant and auto-tile buttons to window menu" msgstr "" -#: dist/prefs.js:839 +#: dist/prefs.js:839 dist/prefs.js:933 msgid "" "Add snap assistant and auto-tile buttons in the menu that shows up when you " "right click on a window title" msgstr "" -#: dist/prefs.js:844 +#: dist/prefs.js:844 dist/prefs.js:938 msgid "Screen Edges" msgstr "" -#: dist/prefs.js:846 +#: dist/prefs.js:846 dist/prefs.js:940 msgid "" "Drag windows against the top, left and right screen edges to resize them" msgstr "" -#: dist/prefs.js:860 +#: dist/prefs.js:860 dist/prefs.js:954 msgid "Drag against top edge to maximize window" msgstr "" -#: dist/prefs.js:861 +#: dist/prefs.js:861 dist/prefs.js:955 msgid "Drag windows against the top edge to maximize them" msgstr "" -#: dist/prefs.js:870 +#: dist/prefs.js:870 dist/prefs.js:964 msgid "Quarter tiling activation area" msgstr "" -#: dist/prefs.js:871 +#: dist/prefs.js:871 dist/prefs.js:965 #, javascript-format msgid "Activation area to trigger quarter tiling (% of the screen)" msgstr "" -#: dist/prefs.js:888 +#: dist/prefs.js:888 dist/prefs.js:982 msgid "Layouts" msgstr "" -#: dist/prefs.js:889 +#: dist/prefs.js:889 dist/prefs.js:983 msgid "Configure the layouts of Tiling Shell" msgstr "" -#: dist/prefs.js:893 dist/prefs.js:894 +#: dist/prefs.js:893 dist/prefs.js:894 dist/prefs.js:987 dist/prefs.js:988 msgid "Edit layouts" msgstr "" -#: dist/prefs.js:895 +#: dist/prefs.js:895 dist/prefs.js:989 msgid "Open the layouts editor" msgstr "" -#: dist/prefs.js:900 dist/prefs.js:901 dist/prefs.js:905 +#: dist/prefs.js:900 dist/prefs.js:901 dist/prefs.js:905 dist/prefs.js:994 +#: dist/prefs.js:995 dist/prefs.js:999 msgid "Export layouts" msgstr "" -#: dist/prefs.js:902 +#: dist/prefs.js:902 dist/prefs.js:996 msgid "Export layouts to a file" msgstr "" #: dist/prefs.js:909 dist/prefs.js:966 dist/prefs.js:1253 dist/prefs.js:1311 +#: dist/prefs.js:1003 dist/prefs.js:1060 dist/prefs.js:1369 dist/prefs.js:1427 msgid "Cancel" msgstr "" -#: dist/prefs.js:910 dist/prefs.js:1254 +#: dist/prefs.js:910 dist/prefs.js:1254 dist/prefs.js:1004 dist/prefs.js:1370 msgid "Save" msgstr "" -#: dist/prefs.js:957 dist/prefs.js:958 +#: dist/prefs.js:957 dist/prefs.js:958 dist/prefs.js:1051 dist/prefs.js:1052 msgid "Import layouts" msgstr "" -#: dist/prefs.js:959 +#: dist/prefs.js:959 dist/prefs.js:1053 msgid "Import layouts from a file" msgstr "" -#: dist/prefs.js:962 +#: dist/prefs.js:962 dist/prefs.js:1056 msgid "Select layouts file" msgstr "" -#: dist/prefs.js:967 dist/prefs.js:1312 +#: dist/prefs.js:967 dist/prefs.js:1312 dist/prefs.js:1061 dist/prefs.js:1428 msgid "Open" msgstr "" -#: dist/prefs.js:1015 dist/prefs.js:1016 +#: dist/prefs.js:1015 dist/prefs.js:1016 dist/prefs.js:1109 dist/prefs.js:1110 msgid "Reset layouts" msgstr "" -#: dist/prefs.js:1017 +#: dist/prefs.js:1017 dist/prefs.js:1111 msgid "Bring back the default layouts" msgstr "" -#: dist/prefs.js:1030 +#: dist/prefs.js:1030 dist/prefs.js:1124 msgid "Keybindings" msgstr "" -#: dist/prefs.js:1032 +#: dist/prefs.js:1032 dist/prefs.js:1126 msgid "Use hotkeys to perform actions on the focused window" msgstr "" -#: dist/prefs.js:1050 +#: dist/prefs.js:1050 dist/prefs.js:1152 msgid "Move window to right tile" msgstr "" -#: dist/prefs.js:1052 +#: dist/prefs.js:1052 dist/prefs.js:1154 msgid "Move the focused window to the tile on its right" msgstr "" -#: dist/prefs.js:1061 +#: dist/prefs.js:1061 dist/prefs.js:1163 msgid "Move window to left tile" msgstr "" -#: dist/prefs.js:1062 +#: dist/prefs.js:1062 dist/prefs.js:1164 msgid "Move the focused window to the tile on its left" msgstr "" -#: dist/prefs.js:1068 +#: dist/prefs.js:1068 dist/prefs.js:1170 msgid "Move window to tile above" msgstr "" -#: dist/prefs.js:1069 +#: dist/prefs.js:1069 dist/prefs.js:1171 msgid "Move the focused window to the tile above" msgstr "" -#: dist/prefs.js:1075 +#: dist/prefs.js:1075 dist/prefs.js:1177 msgid "Move window to tile below" msgstr "" -#: dist/prefs.js:1076 +#: dist/prefs.js:1076 dist/prefs.js:1178 msgid "Move the focused window to the tile below" msgstr "" -#: dist/prefs.js:1082 +#: dist/prefs.js:1082 dist/prefs.js:1184 msgid "Span window to right tile" msgstr "" -#: dist/prefs.js:1083 +#: dist/prefs.js:1083 dist/prefs.js:1185 msgid "Span the focused window to the tile on its right" msgstr "" -#: dist/prefs.js:1089 +#: dist/prefs.js:1089 dist/prefs.js:1191 msgid "Span window to left tile" msgstr "" -#: dist/prefs.js:1090 +#: dist/prefs.js:1090 dist/prefs.js:1192 msgid "Span the focused window to the tile on its left" msgstr "" -#: dist/prefs.js:1096 +#: dist/prefs.js:1096 dist/prefs.js:1198 msgid "Span window above" msgstr "" -#: dist/prefs.js:1097 +#: dist/prefs.js:1097 dist/prefs.js:1199 msgid "Span the focused window to the tile above" msgstr "" -#: dist/prefs.js:1103 +#: dist/prefs.js:1103 dist/prefs.js:1205 msgid "Span window down" msgstr "" -#: dist/prefs.js:1104 +#: dist/prefs.js:1104 dist/prefs.js:1206 msgid "Span the focused window to the tile below" msgstr "" -#: dist/prefs.js:1110 +#: dist/prefs.js:1110 dist/prefs.js:1212 msgid "Span window to all tiles" msgstr "" -#: dist/prefs.js:1111 +#: dist/prefs.js:1111 dist/prefs.js:1213 msgid "Span the focused window to all the tiles" msgstr "" -#: dist/prefs.js:1117 +#: dist/prefs.js:1117 dist/prefs.js:1219 msgid "Untile focused window" msgstr "" -#: dist/prefs.js:1125 +#: dist/prefs.js:1125 dist/prefs.js:1227 msgid "Move window to the center" msgstr "" -#: dist/prefs.js:1127 +#: dist/prefs.js:1127 dist/prefs.js:1229 msgid "Move the focused window to the center of the screen" msgstr "" -#: dist/prefs.js:1136 +#: dist/prefs.js:1136 dist/prefs.js:1238 msgid "Focus window to the right" msgstr "" -#: dist/prefs.js:1138 +#: dist/prefs.js:1138 dist/prefs.js:1240 msgid "Focus the window to the right of the current focused window" msgstr "" -#: dist/prefs.js:1145 +#: dist/prefs.js:1145 dist/prefs.js:1247 msgid "Focus window to the left" msgstr "" -#: dist/prefs.js:1146 +#: dist/prefs.js:1146 dist/prefs.js:1248 msgid "Focus the window to the left of the current focused window" msgstr "" -#: dist/prefs.js:1152 +#: dist/prefs.js:1152 dist/prefs.js:1254 msgid "Focus window above" msgstr "" -#: dist/prefs.js:1153 +#: dist/prefs.js:1153 dist/prefs.js:1255 msgid "Focus the window above the current focused window" msgstr "" -#: dist/prefs.js:1159 +#: dist/prefs.js:1159 dist/prefs.js:1261 msgid "Focus window below" msgstr "" -#: dist/prefs.js:1160 +#: dist/prefs.js:1160 dist/prefs.js:1262 msgid "Focus the window below the current focused window" msgstr "" -#: dist/prefs.js:1187 +#: dist/prefs.js:1187 dist/prefs.js:1303 msgid "View and Customize all the Shortcuts" msgstr "" -#: dist/prefs.js:1215 dist/prefs.js:1216 +#: dist/prefs.js:1215 dist/prefs.js:1216 dist/prefs.js:1331 dist/prefs.js:1332 msgid "View and Customize Shortcuts" msgstr "" -#: dist/prefs.js:1237 +#: dist/prefs.js:1237 dist/prefs.js:1353 msgid "Import, export and reset" msgstr "" -#: dist/prefs.js:1239 +#: dist/prefs.js:1239 dist/prefs.js:1355 msgid "Import, export and reset the settings of Tiling Shell" msgstr "" -#: dist/prefs.js:1244 dist/prefs.js:1245 +#: dist/prefs.js:1244 dist/prefs.js:1245 dist/prefs.js:1360 dist/prefs.js:1361 msgid "Export settings" msgstr "" -#: dist/prefs.js:1246 +#: dist/prefs.js:1246 dist/prefs.js:1362 msgid "Export settings to a file" msgstr "" -#: dist/prefs.js:1249 +#: dist/prefs.js:1249 dist/prefs.js:1365 msgid "Export settings to a text file" msgstr "" -#: dist/prefs.js:1302 dist/prefs.js:1303 +#: dist/prefs.js:1302 dist/prefs.js:1303 dist/prefs.js:1418 dist/prefs.js:1419 msgid "Import settings" msgstr "" -#: dist/prefs.js:1304 +#: dist/prefs.js:1304 dist/prefs.js:1420 msgid "Import settings from a file" msgstr "" -#: dist/prefs.js:1307 +#: dist/prefs.js:1307 dist/prefs.js:1423 msgid "Select a text file to import from" msgstr "" -#: dist/prefs.js:1351 dist/prefs.js:1352 +#: dist/prefs.js:1351 dist/prefs.js:1352 dist/prefs.js:1467 dist/prefs.js:1468 msgid "Reset settings" msgstr "" -#: dist/prefs.js:1353 +#: dist/prefs.js:1353 dist/prefs.js:1469 msgid "Bring back the default settings" msgstr "" -#: dist/prefs.js:1368 +#: dist/prefs.js:1368 dist/prefs.js:1484 msgid "Donate on ko-fi" msgstr "" -#: dist/prefs.js:1374 +#: dist/prefs.js:1374 dist/prefs.js:1490 msgid "Report a bug" msgstr "" -#: dist/prefs.js:1380 +#: dist/prefs.js:1380 dist/prefs.js:1496 msgid "Request a feature" msgstr "" -#: dist/prefs.js:1388 +#: dist/prefs.js:1388 dist/prefs.js:1504 msgid "Have issues, you want to suggest a new feature or contribute?" msgstr "" -#: dist/prefs.js:1395 +#: dist/prefs.js:1395 dist/prefs.js:1511 msgid "Open a new issue on" msgstr "" -#: dist/extension.js:4500 +#: dist/extension.js:4500 dist/extension.js:5361 msgid "Edit Layouts" msgstr "" -#: dist/extension.js:4510 +#: dist/extension.js:4510 dist/extension.js:5371 msgid "New Layout" msgstr "" -#: dist/extension.js:4691 +#: dist/extension.js:4691 dist/extension.js:5558 msgid "to split a tile" msgstr "" -#: dist/extension.js:4730 +#: dist/extension.js:4730 dist/extension.js:5597 msgid "to split a tile vertically" msgstr "" -#: dist/extension.js:4750 +#: dist/extension.js:4750 dist/extension.js:5617 msgid "to delete a tile" msgstr "" -#: dist/extension.js:4775 +#: dist/extension.js:4775 dist/extension.js:5642 msgid "use the indicator button to save or cancel" msgstr "" -#: dist/prefs.js:815 +#: dist/prefs.js:815 dist/prefs.js:909 msgid "Enable Auto Tiling" msgstr "" -#: dist/prefs.js:816 +#: dist/prefs.js:816 dist/prefs.js:910 msgid "Automatically tile new windows to the best tile" msgstr "" -#: dist/prefs.js:795 +#: dist/prefs.js:795 dist/prefs.js:889 msgid "Tiling System deactivation key" msgstr "" -#: dist/prefs.js:797 +#: dist/prefs.js:797 dist/prefs.js:891 msgid "" "Hold the deactivation key while moving a window to deactivate the tiling " "system" msgstr "" + +#: dist/prefs.js:817 +msgid "Smart border radius" +msgstr "" + +#: dist/prefs.js:818 +msgid "Dynamically adapt to the window’s actual border radius" +msgstr "" + +#: dist/prefs.js:1141 +msgid "Enable next/previous window focus to wrap around" +msgstr "" + +#: dist/prefs.js:1143 +msgid "When focusing next or previous window, wrap around at the window edge" +msgstr "" + +#: dist/prefs.js:1268 +msgid "Focus next window" +msgstr "" + +#: dist/prefs.js:1269 +msgid "Focus the window next to the current focused window" +msgstr "" + +#: dist/prefs.js:1275 +msgid "Focus previous window" +msgstr "" + +#: dist/prefs.js:1276 +msgid "Focus the window prior to the current focused window" +msgstr ""