diff --git a/tiling-assistant@leleat-on-github/media/tile-base-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-base-symbolic.svg
new file mode 100644
index 00000000..126bc597
--- /dev/null
+++ b/tiling-assistant@leleat-on-github/media/tile-base-symbolic.svg
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/tiling-assistant@leleat-on-github/media/tile-bottom-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-bottom-symbolic.svg
new file mode 100644
index 00000000..67c176a7
--- /dev/null
+++ b/tiling-assistant@leleat-on-github/media/tile-bottom-symbolic.svg
@@ -0,0 +1,22 @@
+
+
+
+
diff --git a/tiling-assistant@leleat-on-github/media/tile-horizontal-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-horizontal-symbolic.svg
new file mode 100644
index 00000000..a5bead91
--- /dev/null
+++ b/tiling-assistant@leleat-on-github/media/tile-horizontal-symbolic.svg
@@ -0,0 +1,19 @@
+
+
+
+
diff --git a/tiling-assistant@leleat-on-github/media/tile-left-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-left-symbolic.svg
new file mode 100644
index 00000000..b05a1efa
--- /dev/null
+++ b/tiling-assistant@leleat-on-github/media/tile-left-symbolic.svg
@@ -0,0 +1,22 @@
+
+
+
+
diff --git a/tiling-assistant@leleat-on-github/media/tile-maximize-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-maximize-symbolic.svg
new file mode 100644
index 00000000..0fdeecbf
--- /dev/null
+++ b/tiling-assistant@leleat-on-github/media/tile-maximize-symbolic.svg
@@ -0,0 +1,19 @@
+
+
+
+
diff --git a/tiling-assistant@leleat-on-github/media/tile-q1-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-q1-symbolic.svg
new file mode 100644
index 00000000..62bb30f8
--- /dev/null
+++ b/tiling-assistant@leleat-on-github/media/tile-q1-symbolic.svg
@@ -0,0 +1,28 @@
+
+
+
+
diff --git a/tiling-assistant@leleat-on-github/media/tile-q2-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-q2-symbolic.svg
new file mode 100644
index 00000000..9e951c38
--- /dev/null
+++ b/tiling-assistant@leleat-on-github/media/tile-q2-symbolic.svg
@@ -0,0 +1,28 @@
+
+
+
+
diff --git a/tiling-assistant@leleat-on-github/media/tile-q3-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-q3-symbolic.svg
new file mode 100644
index 00000000..492a17a3
--- /dev/null
+++ b/tiling-assistant@leleat-on-github/media/tile-q3-symbolic.svg
@@ -0,0 +1,28 @@
+
+
+
+
diff --git a/tiling-assistant@leleat-on-github/media/tile-q4-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-q4-symbolic.svg
new file mode 100644
index 00000000..c38b541b
--- /dev/null
+++ b/tiling-assistant@leleat-on-github/media/tile-q4-symbolic.svg
@@ -0,0 +1,28 @@
+
+
+
+
diff --git a/tiling-assistant@leleat-on-github/media/tile-quarter-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-quarter-symbolic.svg
new file mode 100644
index 00000000..c2e1e192
--- /dev/null
+++ b/tiling-assistant@leleat-on-github/media/tile-quarter-symbolic.svg
@@ -0,0 +1,25 @@
+
+
+
+
diff --git a/tiling-assistant@leleat-on-github/media/tile-right-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-right-symbolic.svg
new file mode 100644
index 00000000..a333b058
--- /dev/null
+++ b/tiling-assistant@leleat-on-github/media/tile-right-symbolic.svg
@@ -0,0 +1,22 @@
+
+
+
+
diff --git a/tiling-assistant@leleat-on-github/media/tile-top-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-top-symbolic.svg
new file mode 100644
index 00000000..f6346e72
--- /dev/null
+++ b/tiling-assistant@leleat-on-github/media/tile-top-symbolic.svg
@@ -0,0 +1,22 @@
+
+
+
+
diff --git a/tiling-assistant@leleat-on-github/media/tile-vertical-symbolic.svg b/tiling-assistant@leleat-on-github/media/tile-vertical-symbolic.svg
new file mode 100644
index 00000000..3ee05ea9
--- /dev/null
+++ b/tiling-assistant@leleat-on-github/media/tile-vertical-symbolic.svg
@@ -0,0 +1,19 @@
+
+
+
+
diff --git a/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js
new file mode 100644
index 00000000..60bf85ea
--- /dev/null
+++ b/tiling-assistant@leleat-on-github/src/extension/layoutPicker.js
@@ -0,0 +1,382 @@
+import { Clutter, GObject, St, Gio } from '../dependencies/gi.js';
+import { Main, Extension } from '../dependencies/shell.js';
+
+const LayoutPickerVisibility = {
+ HIDDEN: 0,
+ PEAK: 1,
+ SHOWN: 2
+};
+
+function iconPath(name) {
+ const path = Extension.lookupByURL(import.meta.url)
+ .dir
+ .get_child(`media/${name}-symbolic.svg`)
+ .get_path();
+
+ return path;
+}
+
+export const LayoutPickerTileType = {
+ NONE: 0,
+ LEFT: 1,
+ RIGHT: 2,
+ TOP: 3,
+ BOTTOM: 4,
+ Q1: 5,
+ Q2: 6,
+ Q3: 7,
+ Q4: 8,
+ MAXIMIZE: 9
+};
+
+// ommit '-symbolic.svg' as it is managed by iconPath function already
+const ICONS = {
+ vertical: {
+ none: 'tile-vertical',
+
+ states: {
+ [LayoutPickerTileType.TOP]: 'tile-top',
+ [LayoutPickerTileType.BOTTOM]: 'tile-bottom'
+ }
+ },
+
+ horizontal: {
+ none: 'tile-horizontal',
+
+ states: {
+ [LayoutPickerTileType.LEFT]: 'tile-left',
+ [LayoutPickerTileType.RIGHT]: 'tile-right'
+ }
+ },
+
+ quarter: {
+ none: 'tile-quarter',
+
+ states: {
+ [LayoutPickerTileType.Q1]: 'tile-q1',
+ [LayoutPickerTileType.Q2]: 'tile-q2',
+ [LayoutPickerTileType.Q3]: 'tile-q3',
+ [LayoutPickerTileType.Q4]: 'tile-q4'
+ }
+ },
+
+ maximize: {
+ none: 'tile-base',
+
+ states: {
+ [LayoutPickerTileType.MAXIMIZE]: 'tile-maximize'
+ }
+ }
+};
+
+export const LayoutPicker = GObject.registerClass(
+class LayoutPicker extends St.Bin {
+ _init() {
+ super._init({
+ style_class: 'tiling-menu-container'
+ });
+
+ this._container = new St.BoxLayout({
+ x_expand: true,
+ orientation: Clutter.Orientation.HORIZONTAL,
+ style_class: 'popup-menu-content'
+ });
+
+ this._visibility = LayoutPickerVisibility.HIDDEN;
+ this._dragging = false;
+
+ this.set_child(this._container);
+
+ this._addChrome();
+
+ this._icons = {};
+
+ for (const [group, config] of Object.entries(ICONS)) {
+ // start with icons that appear to be not hovered
+ const icon = this._createIcon(config.none);
+
+ config.icon = icon;
+ this._icons[group] = icon;
+
+ this._container.add_child(icon);
+ }
+
+ this._tileType = LayoutPickerTileType.NONE;
+
+ // e.g ubuntu dock is enabled and or disabled
+ global.display.connectObject('workareas-changed', () => {
+ this._updateAllocation(global.display.get_current_monitor());
+ }, this);
+
+ Main.layoutManager.connectObject('monitors-changed', () => {
+ this._updateAllocation(global.display.get_current_monitor());
+ }, this);
+
+ // just in case extension is enabled and disable
+ this._updateAllocation(global.display.get_current_monitor());
+
+ this.connectObject('notify::translation-y', () => {
+ this.set_clip(
+ 0,
+ this.height - this.translation_y,
+ this.width,
+ this.height
+ );
+ }, this);
+ }
+
+ get tileType() {
+ return this._tileType;
+ }
+
+ get picking() {
+ return this._tileType !== LayoutPickerTileType.NONE;
+ }
+
+ _setVisibility(visibility) {
+ if (this._visibility === visibility)
+ return;
+
+ this._visibility = visibility;
+
+ this.opacity = 255;
+ this.reactive = true;
+
+
+ let positions = [
+ 0,
+ this._container.get_theme_node().get_padding(St.Side.Bottom) +
+ this.get_theme_node().get_padding(St.Side.BOTTOM),
+ this.height
+ ];
+
+ this.remove_all_transitions();
+
+ this.ease({
+ translation_y: positions[visibility],
+ duration: 250,
+ mode: Clutter.AnimationMode.EASE_OUT_QUAD,
+ onComplete: () => {
+ this.opacity = this._visibility !== LayoutPickerVisibility.HIDDEN ? 255 : 0;
+ this.reactive = this._visibility !== LayoutPickerVisibility.HIDDEN;
+
+ // once picker is fully shown onMoving might not be able to update tile type
+ // for situations like no cursor update after picker is fully shown.
+ // this could causes incorrect tile type shown.
+ if (this._visibility === LayoutPickerVisibility.SHOWN) {
+ let [curX, curY] = global.get_pointer();
+ this._updateLayoutPickerTileType(curX, curY);
+ }
+ }
+
+ });
+ }
+
+ onMoving(curX, curY) {
+ if (this._dragging === false)
+ return;
+
+ this._updateAllocation(global.display.get_current_monitor());
+
+ let [w, h] = this.get_size();
+ let [mx, my_] = this.get_transformed_position();
+
+ const themeNode = this.get_theme_node();
+ let paddingLeft = themeNode.get_padding(St.Side.LEFT);
+ let paddingRight = themeNode.get_padding(St.Side.RIGHT);
+ let paddingBottom = this.get_theme_node().get_padding(St.Side.BOTTOM);
+
+ const monitorIndex = global.display.get_current_monitor();
+ const monitorArea = Main.layoutManager.monitors[monitorIndex];
+ const activeWs = global.workspace_manager.get_active_workspace();
+ const workArea = activeWs.get_work_area_for_monitor(monitorIndex);
+
+ // using monitorArea.y instead of workArea.y as upper bound to compensate with chromes such us the top bar height
+ // placing cursor above workArea.y causes visibility glitch. workArea.y and monitorArea.y will be same for other monitor anyways.
+ if (
+ curY >= monitorArea.y &&
+ curY <= workArea.y + h - paddingBottom &&
+ curX >= mx + paddingLeft &&
+ curX <= mx + w - paddingRight
+ )
+ this._setVisibility(LayoutPickerVisibility.SHOWN);
+ else
+ this._setVisibility(LayoutPickerVisibility.PEAK);
+
+ this._updateLayoutPickerTileType(curX, curY);
+ }
+
+ onMoveStarted() {
+ this._dragging = true;
+ this._setVisibility(LayoutPickerVisibility.PEAK);
+ }
+
+ onMoveFinished() {
+ this._dragging = false;
+ this._setVisibility(LayoutPickerVisibility.HIDDEN);
+ }
+
+ _setLayoutPickerIcon(tileType) {
+ this._clearIcons();
+
+ for (const config of Object.values(ICONS)) {
+ const iconName = config.states[tileType];
+
+ if (!iconName)
+ continue;
+
+ this._updateIcon(config.icon, iconName);
+ break;
+ }
+ }
+
+ _createIcon(name) {
+ let fallback_gicon = Gio.FileIcon.new(
+ Gio.File.new_for_path(iconPath(name))
+ );
+
+ let gicon = new Gio.ThemedIcon({
+ name: `${name}-symbolic`
+ });
+
+ return new St.Icon({
+ gicon,
+ fallback_gicon
+ });
+ }
+
+ _updateIcon(icon, name) {
+ let fallback_gicon = Gio.FileIcon.new(
+ Gio.File.new_for_path(iconPath(name))
+ );
+
+ let gicon = new Gio.ThemedIcon({
+ name: `${name}-symbolic`
+ });
+ icon.set({
+ gicon,
+ fallback_gicon
+ });
+ }
+
+ _clearIcons() {
+ for (const config of Object.values(ICONS))
+ this._updateIcon(config.icon, config.none);
+ }
+
+ _updateLayoutPickerTileType(curX, curY) {
+ let rect = icon => {
+ let [x, y] = icon.get_transformed_position();
+ let [w, h] = icon.get_size();
+ return { x, y, w, h };
+ };
+
+ let contains = ({ x, y, w, h }) => (
+ curX >= x &&
+ curX <= x + w &&
+ curY >= y &&
+ curY <= y + h
+ );
+
+ const horizontal = rect(this._icons.horizontal);
+
+ if (contains(horizontal)) {
+ const leftPortion = curX < horizontal.x + horizontal.w / 2;
+
+ this._tileType = leftPortion
+ ? LayoutPickerTileType.LEFT
+ : LayoutPickerTileType.RIGHT;
+
+ this._setLayoutPickerIcon(this._tileType);
+ return;
+ }
+
+ const vertical = rect(this._icons.vertical);
+
+ if (contains(vertical)) {
+ const topPortion = curY <= horizontal.y + horizontal.h / 2;
+
+ this._tileType = topPortion
+ ? LayoutPickerTileType.TOP
+ : LayoutPickerTileType.BOTTOM;
+
+ this._setLayoutPickerIcon(this._tileType);
+ return;
+ }
+
+ const quarter = rect(this._icons.quarter);
+
+ if (contains(quarter)) {
+ const leftPortion = curX < quarter.x + quarter.w / 2;
+ const topPortion = curY <= quarter.y + quarter.h / 2;
+
+ if (topPortion)
+ this._tileType = leftPortion
+ ? LayoutPickerTileType.Q2
+ : LayoutPickerTileType.Q1;
+ else
+ this._tileType = leftPortion
+ ? LayoutPickerTileType.Q3
+ : LayoutPickerTileType.Q4;
+
+ this._setLayoutPickerIcon(this._tileType);
+ return;
+ }
+
+ this._tileType = contains(rect(this._icons.maximize))
+ ? LayoutPickerTileType.MAXIMIZE
+ : LayoutPickerTileType.NONE;
+
+ this._setLayoutPickerIcon(this._tileType);
+ }
+
+ _updateAllocation(monitorIndex) {
+ const activeWs = global.workspace_manager.get_active_workspace();
+ const workArea = activeWs.get_work_area_for_monitor(monitorIndex);
+
+ if (workArea === null)
+ return;
+
+ const [, natWidth] = this.get_preferred_width(-1);
+ const [, natHeight] = this.get_preferred_height(-1);
+
+ const targetX = Math.round(workArea.x + (workArea.width - natWidth) / 2);
+ const targetY = Math.round(workArea.y - natHeight);
+
+ if (targetX === this.x && targetY === this.y)
+ return;
+
+ this.set_position(targetX, targetY);
+
+ this.remove_all_transitions();
+
+ this._visibility = LayoutPickerVisibility.HIDDEN;
+ this.translation_y = 0;
+ this.opacity = 0;
+ this.reactive = false;
+ }
+
+ _addChrome() {
+ Main.layoutManager.addChrome(this, {
+ affectsStruts: false,
+ trackFullscreen: false
+ });
+ }
+
+ _untrackChrome() {
+ Main.layoutManager.untrackChrome(this);
+ }
+
+ destroy() {
+ this._untrackChrome();
+
+ global.display.disconnectObject(this);
+ Main.layoutManager.disconnectObject(this);
+
+ this._container?.destroy();
+ this._container = null;
+
+ super.destroy();
+ }
+});
+
diff --git a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js
index 53034a4b..64a171b4 100644
--- a/tiling-assistant@leleat-on-github/src/extension/moveHandler.js
+++ b/tiling-assistant@leleat-on-github/src/extension/moveHandler.js
@@ -5,6 +5,7 @@ import { WINDOW_ANIMATION_TIME } from '../dependencies/unexported/windowManager.
import { MoveModes, Orientation, Settings } from '../common.js';
import { Rect, Util } from './utility.js';
import { TilingWindowManager as Twm } from './tilingWindowManager.js';
+import { LayoutPicker, LayoutPickerTileType } from './layoutPicker.js';
/**
* This class gets to handle the move events (grab & monitor change) of windows.
@@ -93,6 +94,8 @@ export default class TilingMoveHandler {
this
);
handleWindowActionKeyConflict();
+
+ this._layoutPicker = new LayoutPicker();
}
destroy() {
@@ -104,6 +107,8 @@ export default class TilingMoveHandler {
this._tilePreview.destroy();
+ this._layoutPicker.destroy();
+
if (this._latestMonitorLockTimerId) {
GLib.Source.remove(this._latestMonitorLockTimerId);
this._latestMonitorLockTimerId = null;
@@ -142,6 +147,8 @@ export default class TilingMoveHandler {
_onMoveStarted(window, grabOp) {
if (window.is_skip_taskbar())
return;
+
+ this._layoutPicker.onMoveStarted();
// Also work with a window, which was maximized by GNOME natively
// because it may have been tiled with this extension before being
@@ -153,6 +160,8 @@ export default class TilingMoveHandler {
// Try to restore the window size
if (window.tiledRect || this._wasMaximizedOnStart) {
+ this._layoutPicker.onMoveFinished();
+
let counter = 0;
this._restoreSizeTimerId && GLib.Source.remove(this._restoreSizeTimerId);
this._restoreSizeTimerId = GLib.timeout_add(GLib.PRIORITY_HIGH_IDLE, 10, () => {
@@ -237,6 +246,8 @@ export default class TilingMoveHandler {
}
_onMoveFinished(window) {
+ this._layoutPicker.onMoveFinished();
+
try {
window.assertExistence();
@@ -304,6 +315,8 @@ export default class TilingMoveHandler {
const [x, y] = this.getDragCoords();
const currPointerPos = { x, y };
+ this._layoutPicker.onMoving(x, y);
+
if (lowPerfMode) {
if (!this._isGrabOp) {
this._movingTimerId = null;
@@ -496,23 +509,39 @@ export default class TilingMoveHandler {
const wRect = window.get_frame_rect();
const workArea = new Rect(window.get_work_area_for_monitor(this._monitorNr));
+ const layoutPickerTileType = this._layoutPicker.tileType;
+
const vDetectionSize = Settings.getInt('vertical-preview-area');
- const pointerAtTopEdge = this._lastPointerPos.y <= workArea.y + vDetectionSize;
- const pointerAtBottomEdge = this._lastPointerPos.y >= workArea.y2 - vDetectionSize;
+ let pointerAtTopEdge = this._lastPointerPos.y <= workArea.y + vDetectionSize ||
+ layoutPickerTileType === LayoutPickerTileType.TOP ||
+ layoutPickerTileType === LayoutPickerTileType.MAXIMIZE;
+ let pointerAtBottomEdge = this._lastPointerPos.y >= workArea.y2 - vDetectionSize ||
+ layoutPickerTileType === LayoutPickerTileType.BOTTOM;
const hDetectionSize = Settings.getInt('horizontal-preview-area');
- const pointerAtLeftEdge = this._lastPointerPos.x <= workArea.x + hDetectionSize;
- const pointerAtRightEdge = this._lastPointerPos.x >= workArea.x2 - hDetectionSize;
+ let pointerAtLeftEdge = this._lastPointerPos.x <= workArea.x + hDetectionSize ||
+ layoutPickerTileType === LayoutPickerTileType.LEFT;
+ let pointerAtRightEdge = this._lastPointerPos.x >= workArea.x2 - hDetectionSize ||
+ layoutPickerTileType === LayoutPickerTileType.RIGHT;
// Also use window's pos for top and bottom area detection for quarters
// because global.get_pointer's y isn't accurate (no idea why...) when
// grabbing the titlebar & slowly going from the left/right sides to
// the top/bottom corners.
const titleBarGrabbed = this._lastPointerPos.y - wRect.y < 50;
- const windowAtTopEdge = titleBarGrabbed && wRect.y === workArea.y;
- const windowAtBottomEdge = wRect.y >= workArea.y2 - 75;
- const tileTopLeftQuarter = pointerAtLeftEdge && (pointerAtTopEdge || windowAtTopEdge);
- const tileTopRightQuarter = pointerAtRightEdge && (pointerAtTopEdge || windowAtTopEdge);
- const tileBottomLeftQuarter = pointerAtLeftEdge && (pointerAtBottomEdge || windowAtBottomEdge);
- const tileBottomRightQuarter = pointerAtRightEdge && (pointerAtBottomEdge || windowAtBottomEdge);
+ const windowAtTopEdge = titleBarGrabbed && wRect.y === workArea.y && !this._layoutPicker.picking;
+ const windowAtBottomEdge = wRect.y >= workArea.y2 - 75 && !this._layoutPicker.picking;
+ const tileTopLeftQuarter = pointerAtLeftEdge && (pointerAtTopEdge || windowAtTopEdge) ||
+ layoutPickerTileType === LayoutPickerTileType.Q2;
+ const tileTopRightQuarter = pointerAtRightEdge && (pointerAtTopEdge || windowAtTopEdge) ||
+ layoutPickerTileType === LayoutPickerTileType.Q1;
+ const tileBottomLeftQuarter = pointerAtLeftEdge && (pointerAtBottomEdge || windowAtBottomEdge)||
+ layoutPickerTileType === LayoutPickerTileType.Q3;
+ const tileBottomRightQuarter = pointerAtRightEdge && (pointerAtBottomEdge || windowAtBottomEdge) ||
+ layoutPickerTileType === LayoutPickerTileType.Q4;
+
+ // we cannot just for example do this:
+ // const tileTopLeftQuarter = pointerAtLeftEdge && (pointerAtTopEdge || windowAtTopEdge) || layoutPickerTileType == LayoutPickerTileType.Q2;
+ // this can be buggy when both are true like triggering top right preview even when in LayoutPickerTileType.RIGHT
+ // so reassigning the value is a must
if (tileTopLeftQuarter) {
this._tileRect = Twm.getTileFor('tile-topleft-quarter', workArea, this._monitorNr);
@@ -533,10 +562,10 @@ export default class TilingMoveHandler {
const shouldMaximize =
isLandscape && !Settings.getBoolean('enable-hold-maximize-inverse-landscape') ||
!isLandscape && !Settings.getBoolean('enable-hold-maximize-inverse-portrait');
- const tileRect = shouldMaximize
+ const tileRect = shouldMaximize && !this._layoutPicker.picking || layoutPickerTileType === LayoutPickerTileType.MAXIMIZE
? workArea
: Twm.getTileFor('tile-top-half', workArea, this._monitorNr);
- const holdTileRect = shouldMaximize
+ const holdTileRect = shouldMaximize && !this._layoutPicker.picking || layoutPickerTileType === LayoutPickerTileType.TOP
? Twm.getTileFor('tile-top-half', workArea, this._monitorNr)
: workArea;
// Dont open preview / start new timer if preview was already one for the top
diff --git a/tiling-assistant@leleat-on-github/stylesheet.css b/tiling-assistant@leleat-on-github/stylesheet.css
index 27878117..eb7f28b0 100644
--- a/tiling-assistant@leleat-on-github/stylesheet.css
+++ b/tiling-assistant@leleat-on-github/stylesheet.css
@@ -6,3 +6,17 @@
.layout-shortcut .boxed-list {
box-shadow: none;
}
+
+.tiling-menu-container {
+ padding: 20px;
+}
+
+.tiling-menu-container > StBoxLayout {
+ spacing: 15px;
+ padding: 15px;
+ border-radius: 20px;
+}
+
+.tiling-menu-container > StBoxLayout > StIcon {
+ icon-size: 64px;
+}