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 ""