%PDF- %PDF-
| Direktori : /usr/share/gnome-shell/extensions/tiling-assistant@ubuntu.com/src/extension/ |
| Current File : //usr/share/gnome-shell/extensions/tiling-assistant@ubuntu.com/src/extension/utility.js |
import { Clutter, Gio, GLib, Mtk, St } from '../dependencies/gi.js';
import { Main } from '../dependencies/shell.js';
import { Direction, Orientation, Settings } from '../common.js';
/**
* Library of commonly used functions for the extension.js' files
* (and *not* the prefs files)
*/
export class Util {
/**
* Performs an approximate equality check. There will be times when
* there will be inaccuracies. For example, the user may enable window
* gaps and resize 2 tiled windows and try to line them up manually.
* But since the gaps are implemented with this extension, there will
* be no window snapping. So the windows won't be aligned pixel
* perfectly... in that case we first check approximately and correct
* the inaccuracies afterwards.
*
* @param {number} value
* @param {number} value2
* @param {number} [margin=4]
* @returns {boolean} whether the values are approximately equal.
*/
static equal(value, value2, margin = 4) {
return Math.abs(value - value2) <= margin;
}
/**
* @param {{x, y}} pointA
* @param {{x, y}} pointB
* @returns {number} the distance between `pointA` and `pointB`,
*/
static getDistance(pointA, pointB) {
const diffX = pointA.x - pointB.x;
const diffY = pointA.y - pointB.y;
return Math.sqrt(diffX * diffX + diffY * diffY);
}
/**
* @param {number} keyVal
* @param {Direction} direction
* @returns {boolean} whether the `keyVal` is considered to be in the
* direction of `direction`.
*/
static isDirection(keyVal, direction) {
switch (direction) {
case Direction.N:
return keyVal === Clutter.KEY_Up ||
keyVal === Clutter.KEY_w || keyVal === Clutter.KEY_W ||
keyVal === Clutter.KEY_k || keyVal === Clutter.KEY_K;
case Direction.S:
return keyVal === Clutter.KEY_Down ||
keyVal === Clutter.KEY_s || keyVal === Clutter.KEY_S ||
keyVal === Clutter.KEY_j || keyVal === Clutter.KEY_J;
case Direction.W:
return keyVal === Clutter.KEY_Left ||
keyVal === Clutter.KEY_a || keyVal === Clutter.KEY_A ||
keyVal === Clutter.KEY_h || keyVal === Clutter.KEY_H;
case Direction.E:
return keyVal === Clutter.KEY_Right ||
keyVal === Clutter.KEY_d || keyVal === Clutter.KEY_D ||
keyVal === Clutter.KEY_l || keyVal === Clutter.KEY_L;
}
return false;
}
/**
* @param {number} keyVal
* @returns {Direction}
*/
static getDirection(keyVal) {
if (this.isDirection(keyVal, Direction.N))
return Direction.N;
else if (this.isDirection(keyVal, Direction.S))
return Direction.S;
else if (this.isDirection(keyVal, Direction.W))
return Direction.W;
else if (this.isDirection(keyVal, Direction.E))
return Direction.E;
else
return null;
}
/**
* Get the window or screen gaps scaled to the monitor scale.
*
* @param {String} settingsKey the key for the gap
* @param {number} monitor the number of the monitor to scale the gap to
* @returns {number} the scaled gap as a even number since the window gap
* will be divided by 2.
*/
static getScaledGap(settingsKey, monitor) {
const gap = Settings.getInt(settingsKey);
const scaledGap = gap * global.display.get_monitor_scale(monitor);
return scaledGap % 2 === 0 ? scaledGap : scaledGap + 1;
}
static useIndividualGaps(monitor) {
// Prefer individual gaps over the single one
const screenTopGap = this.getScaledGap(Settings.SCREEN_TOP_GAP, monitor);
const screenLeftGap = this.getScaledGap(Settings.SCREEN_LEFT_GAP, monitor);
const screenRightGap = this.getScaledGap(Settings.SCREEN_RIGHT_GAP, monitor);
const screenBottomGap = this.getScaledGap(Settings.SCREEN_BOTTOM_GAP, monitor);
return screenTopGap || screenLeftGap || screenRightGap || screenBottomGap;
}
/**
* @param {number} modMask a Clutter.ModifierType.
* @returns whether the current event the modifier at `modMask`.
*/
static isModPressed(modMask) {
return global.get_pointer()[2] & modMask;
}
/**
* @returns {Layout[]} the layouts
*/
static getLayouts() {
const userDir = GLib.get_user_config_dir();
const pathArr = [userDir, '/tiling-assistant/layouts.json'];
const path = GLib.build_filenamev(pathArr);
const file = Gio.File.new_for_path(path);
if (!file.query_exists(null))
return [];
const [success, contents] = file.load_contents(null);
if (!success || !contents.length)
return [];
return JSON.parse(new TextDecoder().decode(contents));
}
/**
* @param {number|null} monitorNr determines which monitor the layout scales
* to. Sometimes we want the monitor of the pointer (when using dnd) and
* sometimes not (when using layouts with the keyboard shortcuts).
* @returns {Rect[]}
*/
static getFavoriteLayout(monitorNr = null) {
// I don't know when the layout may have changed on the disk(?),
// so always get it anew.
const monitor = monitorNr ?? global.display.get_current_monitor();
const favoriteLayout = [];
const layouts = this.getLayouts();
const layout = layouts?.[Settings.getStrv(Settings.FAVORITE_LAYOUTS)[monitor]];
if (!layout)
return [];
const activeWs = global.workspace_manager.get_active_workspace();
const workArea = new Rect(activeWs.get_work_area_for_monitor(monitor));
// Scale the rect's ratios to the workArea. Try to align the rects to
// each other and the workArea to workaround possible rounding errors
// due to the scaling.
layout._items.forEach(({ rect: rectRatios }, idx) => {
const rect = new Rect(
workArea.x + Math.floor(rectRatios.x * workArea.width),
workArea.y + Math.floor(rectRatios.y * workArea.height),
Math.ceil(rectRatios.width * workArea.width),
Math.ceil(rectRatios.height * workArea.height)
);
favoriteLayout.push(rect);
for (let i = 0; i < idx; i++)
rect.tryAlignWith(favoriteLayout[i]);
});
favoriteLayout.forEach(rect => rect.tryAlignWith(workArea));
return favoriteLayout;
}
/**
* Shows the tiled rects of the top tile group.
*
* @returns {St.Widget[]} an array of St.Widgets to indicate the tiled rects.
*/
static async ___debugShowTiledRects() {
const twm = (await import('./tilingWindowManager.js')).TilingWindowManager;
const topTileGroup = twm.getTopTileGroup();
if (!topTileGroup.length) {
Main.notify('Tiling Assistant', 'No tiled windows / tiled rects.');
return null;
}
const indicators = [];
topTileGroup.forEach(w => {
const indicator = new St.Widget({
style_class: 'tile-preview',
opacity: 160,
x: w.tiledRect.x,
y: w.tiledRect.y,
width: w.tiledRect.width,
height: w.tiledRect.height
});
Main.uiGroup.add_child(indicator);
indicators.push(indicator);
});
return indicators;
}
/**
* Shows the free screen rects based on the top tile group.
*
* @returns {St.Widget[]} an array of St.Widgets to indicate the free
* screen rects.
*/
static async ___debugShowFreeScreenRects() {
const activeWs = global.workspace_manager.get_active_workspace();
const monitor = global.display.get_current_monitor();
const workArea = new Rect(activeWs.get_work_area_for_monitor(monitor));
const twm = (await import('./tilingWindowManager.js')).TilingWindowManager;
const topTileGroup = twm.getTopTileGroup();
const tRects = topTileGroup.map(w => w.tiledRect);
const freeScreenSpace = twm.getFreeScreen(tRects);
const rects = freeScreenSpace ? [freeScreenSpace] : workArea.minus(tRects);
if (!rects.length) {
Main.notify('Tiling Assistant', 'No free screen rects to show.');
return null;
}
const indicators = [];
rects.forEach(rect => {
const indicator = new St.Widget({
style_class: 'tile-preview',
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height
});
Main.uiGroup.add_child(indicator);
indicators.push(indicator);
});
return indicators.length ? indicators : null;
}
/**
* Print the tile groups to the logs.
*/
static async __debugPrintTileGroups() {
log('--- Tiling Assistant: Start ---');
const twm = await import('./tilingWindowManager.js');
const openWindows = twm.getWindows();
openWindows.forEach(w => {
if (!w.isTiled)
return;
log(`Tile group for: ${w.get_wm_class()}`);
const tileGroup = twm.getTileGroupFor(w);
tileGroup.forEach(tw => log(tw.get_wm_class()));
log('---');
});
log('--- Tiling Assistant: End ---');
}
}
/**
* Wrapper for Mtk.Rectangle to add some more functions.
*/
export class Rect {
/**
* @param {...any} params No parameters, 1 Mtk.Rectangle or the x, y,
* width and height values should be passed to the constructor.
*/
constructor(...params) {
this._rect = new Mtk.Rectangle();
switch (params.length) {
case 0:
break;
case 1:
this._rect.x = params[0].x;
this._rect.y = params[0].y;
this._rect.width = params[0].width;
this._rect.height = params[0].height;
break;
case 4:
this._rect.x = params[0];
this._rect.y = params[1];
this._rect.width = params[2];
this._rect.height = params[3];
break;
default:
log('Tiling Assistant: Invalid param count for Rect constructor!');
}
}
/**
* Gets a new rectangle where the screen and window gaps were
* added/subbed to/from `this`.
*
* @param {Rect} rect a tiled Rect
* @param {number} monitor the number of the monitor to scale the gap to
* @returns {Rect} the rectangle after the gaps were taken into account
*/
addGaps(workArea, monitor) {
const screenTopGap = Util.getScaledGap(Settings.SCREEN_TOP_GAP, monitor);
const screenLeftGap = Util.getScaledGap(Settings.SCREEN_LEFT_GAP, monitor);
const screenRightGap = Util.getScaledGap(Settings.SCREEN_RIGHT_GAP, monitor);
const screenBottomGap = Util.getScaledGap(Settings.SCREEN_BOTTOM_GAP, monitor);
const singleScreenGap = Util.getScaledGap(Settings.SINGLE_SCREEN_GAP, monitor);
const windowGap = Util.getScaledGap(Settings.WINDOW_GAP, monitor);
const r = this.copy();
// Prefer individual gaps
if (Util.useIndividualGaps(monitor)) {
[['x', 'width', screenLeftGap, screenRightGap],
['y', 'height', screenTopGap, screenBottomGap]]
.forEach(([pos, dim, posGap, dimGap]) => {
if (this[pos] === workArea[pos]) {
r[pos] = this[pos] + posGap;
r[dim] -= posGap;
} else {
r[pos] = this[pos] + windowGap / 2;
r[dim] -= windowGap / 2;
}
if (this[pos] + this[dim] === workArea[pos] + workArea[dim])
r[dim] -= dimGap;
else
r[dim] -= windowGap / 2;
});
// Use the single screen gap
} else {
[['x', 'width'], ['y', 'height']].forEach(([pos, dim]) => {
if (this[pos] === workArea[pos]) {
r[pos] = this[pos] + singleScreenGap;
r[dim] -= singleScreenGap;
} else {
r[pos] = this[pos] + windowGap / 2;
r[dim] -= windowGap / 2;
}
if (this[pos] + this[dim] === workArea[pos] + workArea[dim])
r[dim] -= singleScreenGap;
else
r[dim] -= windowGap / 2;
});
}
return r;
}
/**
* Checks whether `this` borders another rectangle on this' east edge.
*
* @param {Rect} rect
* @returns {boolean}
*/
bordersOnE(rect) {
return this.vertOverlap && this.x2 === rect.x;
}
/**
* Checks whether `this` borders another rectangle on this' south edge.
*
* @param {Rect} rect
* @returns {boolean}
*/
bordersOnS(rect) {
return this.horizOverlap && this.y2 === rect.y;
}
/**
* @param {{x: number, y: number}} point
* @returns {boolean}
*/
containsPoint(point) {
return point.x >= this.x && point.x <= this.x2 &&
point.y >= this.y && point.y <= this.y2;
}
/**
* @param {Rect} rect
* @returns {boolean}
*/
containsRect(rect) {
rect = rect instanceof Mtk.Rectangle ? rect : rect.meta;
return this._rect.contains_rect(rect);
}
/**
* @returns {Rect}
*/
copy() {
return new Rect(this._rect);
}
/**
* @param {Rect} rect
* @returns {boolean}
*/
couldFitRect(rect) {
rect = rect instanceof Mtk.Rectangle ? rect : rect.meta;
return this._rect.could_fit_rect(rect);
}
/**
* @param {Rect} rect
* @returns {boolean}
*/
equal(rect) {
rect = rect instanceof Mtk.Rectangle ? rect : rect.meta;
return this._rect.equal(rect);
}
/**
* Gets the neighbor in the direction `dir` within the list of Rects
* `rects`.
*
* @param {Direction} dir the direction that is looked into.
* @param {Rect[]} rects an array of the available Rects. It may contain
* `this` itself. The rects shouldn't overlap each other.
* @param {boolean} [wrap=true] whether wrap is enabled,
* if there is no Rect in the direction of `dir`.
* @returns {Rect|null} the nearest Rect.
*/
getNeighbor(dir, rects, wrap = true) {
// Since we can only move into 1 direction at a time, we just need
// to check 1 axis / property of the rects per movement (...almost).
// An example probably makes this clearer. If we want to get the
// neighbor in the N direction, we just look at the y's of the rects.
// More specifically, we look for the y2's ('cmprProp') of the other
// rects which are bigger than the y1 ('startProp') of `this`. The
// nearest neighbor has y2 == this.y1. i. e. the neighbor and `this`
// share a border. There may be multiple windows with the same distance.
// In our example it might happen, if 2 windows are tiled side by side
// bordering `this`. In that case we choose the window, which is the
// nearest on the non-compared axis ('nonCmprProp'). The x property
// in the this example.
let startProp, cmprProp, nonCmprProp;
if (dir === Direction.N)
[startProp, cmprProp, nonCmprProp] = ['y', 'y2', 'x'];
else if (dir === Direction.S)
[startProp, cmprProp, nonCmprProp] = ['y2', 'y', 'x'];
else if (dir === Direction.W)
[startProp, cmprProp, nonCmprProp] = ['x', 'x2', 'y'];
else if (dir === Direction.E)
[startProp, cmprProp, nonCmprProp] = ['x2', 'x', 'y'];
// Put rects into a Map with their relevenat pos'es as the keys and
// filter out `this`.
const posMap = rects.reduce((map, rect) => {
if (rect.equal(this))
return map;
const pos = rect[cmprProp];
if (!map.has(pos))
map.set(pos, []);
map.get(pos).push(rect);
return map;
}, new Map());
// Sort the pos'es in an ascending / descending order.
const goForward = [Direction.S, Direction.E].includes(dir);
const sortedPoses = [...posMap.keys()].sort((a, b) =>
goForward ? a - b : b - a);
const neighborPos = goForward
? sortedPoses.find(pos => pos >= this[startProp])
: sortedPoses.find(pos => pos <= this[startProp]);
if (!neighborPos && !wrap)
return null;
// Since the sortedPoses array is in descending order when 'going
// backwards', we always wrap by getting the 0-th item, if there
// is no actual neighbor.
const neighbors = posMap.get(neighborPos ?? sortedPoses[0]);
return neighbors.reduce((currNearest, rect) => {
return Math.abs(currNearest[nonCmprProp] - this[nonCmprProp]) <=
Math.abs(rect[nonCmprProp] - this[nonCmprProp])
? currNearest
: rect;
});
}
/**
* Gets the rectangle at `index`, if `this` is split into equally
* sized rects. This function is meant to prevent rounding errors.
* Rounding errors may lead to rects not aligning properly and thus
* messing up other calculations etc... This solution may lead to the
* last rect's size being off by a few pixels compared to the other
* rects, if we split `this` multiple times.
*
* @param {number} index the position of the rectangle we want after
* splitting this rectangle.
* @param {number} unitSize the size of 1 partial unit of the rectangle.
* @param {Orientation} orientation determines the split orientation
* (horizontally or vertically).
* @returns {Rect} the rectangle at `index` after the split.
*/
getUnitAt(index, unitSize, orientation) {
unitSize = Math.floor(unitSize);
const isVertical = orientation === Orientation.V;
const lastIndex = Math.round(this[isVertical ? 'width' : 'height'] / unitSize) - 1;
const getLastRect = () => {
const margin = unitSize * index;
return new Rect(
isVertical ? this.x + margin : this.x,
isVertical ? this.y : this.y + margin,
isVertical ? this.width - margin : this.width,
isVertical ? this.height : this.height - margin
);
};
const getNonLastRect = (remainingRect, idx) => {
const firstUnitRect = new Rect(
remainingRect.x,
remainingRect.y,
isVertical ? unitSize : remainingRect.width,
isVertical ? remainingRect.height : unitSize
);
if (idx <= 0) {
return firstUnitRect;
} else {
const remaining = remainingRect.minus(firstUnitRect)[0];
return getNonLastRect(remaining, idx - 1);
}
};
if (index === lastIndex)
return getLastRect();
else
return getNonLastRect(this, index);
}
/**
* @param {Rect} rect
* @returns {boolean}
*/
horizOverlap(rect) {
rect = rect instanceof Mtk.Rectangle ? rect : rect.meta;
return this._rect.horiz_overlap(rect);
}
/**
* @param {Rect} rect
* @returns {[boolean, Rect]}
*/
intersect(rect) {
rect = rect instanceof Mtk.Rectangle ? rect : rect.meta;
const [ok, intersection] = this._rect.intersect(rect);
return [ok, new Rect(intersection)];
}
/**
* Get the Rects that remain from `this`, if `r` is cut off from it.
*
* @param {Rect|Rect[]} r either a single Rect or an array of Rects.
* @returns {Rect[]} an array of Rects.
*/
minus(r) {
return Array.isArray(r) ? this._minusRectArray(r) : this._minusRect(r);
}
/**
* Gets the Rects, which remain from `this` after `rect` was cut off
* / subtracted from it.
*
* Original idea from: \
* https://en.wikibooks.org/wiki/Algorithm_Implementation/Geometry/Rectangle_difference \
* No license is given except the general CC-BY-AS (for text) mentioned
* in the footer. Since the algorithm seems fairly generic (just a few
* additions / substractions), I think I should be good regardless...
* I've modified the algorithm to make the left / right result rects bigger
* instead of the top / bottom rects since screens usually have horizontal
* orientations; so having the vertical rects take priority makes more sense.
*
* @param {Rect} rect the Rect to cut off from `this`.
* @returns {Rect[]} an array of Rects. It contains 0 - 4 rects.
*/
_minusRect(rect) {
rect = rect instanceof Mtk.Rectangle ? new Rect(rect) : rect;
if (rect.containsRect(this))
return [];
const [intersect] = this.intersect(rect);
if (!intersect)
return [this.copy()];
const resultRects = [];
// Left rect
const leftRectWidth = rect.x - this.x;
if (leftRectWidth > 0 && this.height > 0)
resultRects.push(new Rect(this.x, this.y, leftRectWidth, this.height));
// Right rect
const rightRectWidth = this.x2 - rect.x2;
if (rightRectWidth > 0 && this.height > 0)
resultRects.push(new Rect(rect.x2, this.y, rightRectWidth, this.height));
const vertRectsX1 = rect.x > this.x ? rect.x : this.x;
const vertRectsX2 = rect.x2 < this.x2 ? rect.x2 : this.x2;
const vertRectsWidth = vertRectsX2 - vertRectsX1;
// Top rect
const topRectHeight = rect.y - this.y;
if (topRectHeight > 0 && vertRectsWidth > 0)
resultRects.push(new Rect(vertRectsX1, this.y, vertRectsWidth, topRectHeight));
// Bottom rect
const bottomRectHeight = this.y2 - rect.y2;
if (bottomRectHeight > 0 && vertRectsWidth > 0)
resultRects.push(new Rect(vertRectsX1, rect.y2, vertRectsWidth, bottomRectHeight));
return resultRects;
}
/**
* Gets the Rects that remain from `this`, if a list of rects is cut
* off from it.
*
* @param {Rect[]} rects the list of Rects to cut off from `this`.
* @returns {Rect[]} an array of the remaining Rects.
*/
_minusRectArray(rects) {
if (!rects.length)
return [this.copy()];
// First cut off all rects individually from `this`. The result is an
// array of leftover rects (which are arrays themselves) from `this`.
const individualLeftOvers = rects.map(r => this.minus(r));
// Get the final result by intersecting all leftover rects.
return individualLeftOvers.reduce((result, currLeftOvers) => {
const intersections = [];
for (const leftOver of currLeftOvers) {
for (const currFreeRect of result) {
const [ok, inters] = currFreeRect.intersect(leftOver);
ok && intersections.push(new Rect(inters));
}
}
return intersections;
});
}
/**
* @param {Rect} rect
* @returns {boolean}
*/
overlap(rect) {
rect = rect instanceof Mtk.Rectangle ? rect : rect.meta;
return this._rect.overlap(rect);
}
/**
* Makes `this` stick to `rect`, if they are close to each other. Use it
* as a last resort to prevent rounding errors, if you can't use minus()
* or getUnitAt().
*
* @param {Rect} rect the rectangle to align `this` with.
* @param {number} margin only align, if `this` and the `rect` are at most
* this far away.
* @returns {Rect} a reference to this.
*/
tryAlignWith(rect, margin = 4) {
rect = rect instanceof Mtk.Rectangle ? new Rect(rect) : rect;
const equalApprox = (value1, value2) => Math.abs(value1 - value2) <= margin;
if (equalApprox(rect.x, this.x))
this.x = rect.x;
else if (equalApprox(rect.x2, this.x))
this.x = rect.x2;
if (equalApprox(rect.y, this.y))
this.y = rect.y;
else if (equalApprox(rect.y2, this.y))
this.y = rect.y2;
if (equalApprox(rect.x, this.x2))
this.width = rect.x - this.x;
else if (equalApprox(rect.x2, this.x2))
this.width = rect.x2 - this.x;
if (equalApprox(rect.y, this.y2))
this.height = rect.y - this.y;
else if (equalApprox(rect.y2, this.y2))
this.height = rect.y2 - this.y;
return this;
}
/**
* @param {Rect} rect
* @returns {Rect}
*/
union(rect) {
rect = rect instanceof Mtk.Rectangle ? rect : rect.meta;
return new Rect(this._rect.union(rect));
}
/**
* @param {Rect} rect
* @returns {boolean}
*/
vertOverlap(rect) {
rect = rect instanceof Mtk.Rectangle ? rect : rect.meta;
return this._rect.vert_overlap(rect);
}
/**
* Getters
*/
get meta() {
return this._rect.copy();
}
get area() {
return this._rect.area();
}
get x() {
return this._rect.x;
}
get x2() {
return this._rect.x + this._rect.width;
}
get y() {
return this._rect.y;
}
get y2() {
return this._rect.y + this._rect.height;
}
get center() {
return {
x: this.x + Math.floor(this.width / 2),
y: this.y + Math.floor(this.height / 2)
};
}
get width() {
return this._rect.width;
}
get height() {
return this._rect.height;
}
/**
* Setters
*/
set x(value) {
this._rect.x = Math.floor(value);
}
set x2(value) {
this._rect.width = Math.floor(value) - this.x;
}
set y(value) {
this._rect.y = Math.floor(value);
}
set y2(value) {
this._rect.height = Math.floor(value) - this.y;
}
set width(value) {
this._rect.width = Math.floor(value);
}
set height(value) {
this._rect.height = Math.floor(value);
}
}