445 lines
16 KiB
JavaScript
445 lines
16 KiB
JavaScript
|
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
|
||
|
|
||
|
const Gio = imports.gi.Gio;
|
||
|
const Clutter = imports.gi.Clutter;
|
||
|
const GLib = imports.gi.GLib;
|
||
|
const St = imports.gi.St;
|
||
|
const Meta = imports.gi.Meta;
|
||
|
const Shell = imports.gi.Shell;
|
||
|
const Mainloop = imports.mainloop;
|
||
|
const GObject = imports.gi.GObject;
|
||
|
|
||
|
const Gettext = imports.gettext.domain('gnome-shell-extensions');
|
||
|
const _ = Gettext.gettext;
|
||
|
|
||
|
const Main = imports.ui.main;
|
||
|
const MessageTray = imports.ui.messageTray;
|
||
|
const Tweener = imports.ui.tweener;
|
||
|
const PanelMenu = imports.ui.panelMenu;
|
||
|
const PopupMenu = imports.ui.popupMenu;
|
||
|
const Slider = imports.ui.slider;
|
||
|
const Conf = imports.misc.config;
|
||
|
|
||
|
const ExtensionUtils = imports.misc.extensionUtils;
|
||
|
const Me = ExtensionUtils.getCurrentExtension();
|
||
|
const Convenience = Me.imports.convenience;
|
||
|
|
||
|
const CASCADE_WIDTH = 30;
|
||
|
const CASCADE_HEIGHT = 30;
|
||
|
const MIN_WINDOW_WIDTH = 500;
|
||
|
|
||
|
const ARRANGEWINDOWS_SCHEMA = 'org.gnome.shell.extensions.arrangeWindows';
|
||
|
const ALL_MONITOR = 'all-monitors';
|
||
|
const COLUMN_NUMBER = 'column';
|
||
|
const HOTKEY_CASCADE = 'arrangewindow-cascade';
|
||
|
const HOTKEY_TILE = 'arrangewindow-tile';
|
||
|
const HOTKEY_SIDEBYSIDE = 'arrangewindow-sidebyside';
|
||
|
const HOTKEY_STACK = 'arrangewindow-stack';
|
||
|
|
||
|
const COLUMN = ['2', '3', '4', '5', '6', '7', '8'];
|
||
|
|
||
|
let ArrangeMenu = GObject.registerClass(
|
||
|
class ArrangeMenu extends PanelMenu.Button {
|
||
|
_init() {
|
||
|
super._init(0.0, _('Arrange Windows'));
|
||
|
|
||
|
this._gsettings = Convenience.getSettings(ARRANGEWINDOWS_SCHEMA);
|
||
|
|
||
|
this._allMonitor = this._gsettings.get_boolean(ALL_MONITOR);
|
||
|
|
||
|
let icon = new St.Icon({ gicon: this._getCustIcon('arrange-windows-symbolic'),
|
||
|
style_class: 'system-status-icon' });
|
||
|
this.add_actor(icon);
|
||
|
|
||
|
this.menu.addAction(_("Cascade"),
|
||
|
() => this.cascadeWindow(),
|
||
|
this._getCustIcon('cascade-windows-symbolic'));
|
||
|
|
||
|
this.menu.addAction(_("Tile"),
|
||
|
() => this.tileWindow(),
|
||
|
this._getCustIcon('tile-windows-symbolic'));
|
||
|
|
||
|
this.menu.addAction(_("Side by side"),
|
||
|
() => this.sideBySideWindow(),
|
||
|
this._getCustIcon('sidebyside-windows-symbolic'));
|
||
|
|
||
|
this.menu.addAction(_("Stack"),
|
||
|
() => this.stackWindow(),
|
||
|
this._getCustIcon('stack-windows-symbolic'));
|
||
|
|
||
|
this.menu.addAction(_("Maximize"),
|
||
|
() => this.maximizeWindow(Meta.MaximizeFlags.BOTH),
|
||
|
this._getCustIcon('maximize-windows-symbolic'));
|
||
|
|
||
|
this.menu.addAction(_("Maximize Vertical"),
|
||
|
() => this.maximizeWindow(Meta.MaximizeFlags.VERTICAL),
|
||
|
this._getCustIcon('maximize-vertical-windows-symbolic'));
|
||
|
|
||
|
this.menu.addAction(_("Maximize Horizontal"),
|
||
|
() => this.maximizeWindow(Meta.MaximizeFlags.HORIZONTAL),
|
||
|
this._getCustIcon('maximize-horizontal-windows-symbolic'));
|
||
|
|
||
|
this.menu.addAction(_("Restoring"),
|
||
|
() => this.restoringWindow(),
|
||
|
this._getCustIcon('restoring-window-symbolic'));
|
||
|
|
||
|
this.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem());
|
||
|
|
||
|
this._allMonitorItem = new PopupMenu.PopupSwitchMenuItem(_("All monitors"), this._allMonitor)
|
||
|
this._allMonitorItem.connect('toggled', this._allMonitorToggle.bind(this));
|
||
|
this.menu.addMenuItem(this._allMonitorItem);
|
||
|
|
||
|
this._column = new Column();
|
||
|
this.menu.addMenuItem(this._column.menu);
|
||
|
|
||
|
this.show();
|
||
|
|
||
|
this.connect('destroy', this._onDestroy.bind(this));
|
||
|
}
|
||
|
|
||
|
cascadeWindow() {
|
||
|
let windows = this.getWindows();
|
||
|
if (windows.length == 0)
|
||
|
return;
|
||
|
|
||
|
let workArea = this.getWorkArea(windows[0]);
|
||
|
|
||
|
let y = workArea.y + 5;
|
||
|
let x = workArea.x + 10;
|
||
|
let width = workArea.width * 0.7;
|
||
|
let height = workArea.height * 0.7;
|
||
|
for (let i = 0; i < windows.length; i++) {
|
||
|
let win = windows[i].get_meta_window();
|
||
|
win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||
|
win.move_resize_frame(true, x, y, width, height);
|
||
|
x = x + CASCADE_WIDTH;
|
||
|
y = y + CASCADE_HEIGHT;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
sideBySideWindow() {
|
||
|
let windows = this.getWindows();
|
||
|
if (windows.length == 0)
|
||
|
return;
|
||
|
|
||
|
let workArea = this.getWorkArea(windows[0]);
|
||
|
let width = Math.round(workArea.width / windows.length)
|
||
|
|
||
|
let y = workArea.y;
|
||
|
let x = workArea.x;
|
||
|
for (let i = 0; i < windows.length; i++) {
|
||
|
let win = windows[i].get_meta_window();
|
||
|
win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||
|
win.move_resize_frame(false, x, y, width, workArea.height);
|
||
|
x = x + width;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
stackWindow() {
|
||
|
let windows = this.getWindows();
|
||
|
if (windows.length == 0)
|
||
|
return;
|
||
|
|
||
|
let workArea = this.getWorkArea(windows[0]);
|
||
|
let height = Math.round(workArea.height / windows.length)
|
||
|
|
||
|
let y = workArea.y;
|
||
|
let x = workArea.x;
|
||
|
for (let i = 0; i < windows.length; i++) {
|
||
|
let win = windows[i].get_meta_window();
|
||
|
win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||
|
win.move_resize_frame(false, x, y, workArea.width, height);
|
||
|
y += height;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
tileWindow() {
|
||
|
/* Display all windows in a grid defined by the number of columns in
|
||
|
* settings.
|
||
|
*
|
||
|
* In the last row, the rectangles may be wider so that the remaining
|
||
|
* windows equally share the total width.
|
||
|
*
|
||
|
* Try to assign the windows to the closest rectangle in the grid so that
|
||
|
* windows move by the smallest amount. This is important because they
|
||
|
* may be pressing tile from a state that is already tiled so wouldn't expect
|
||
|
* the windows to change order.
|
||
|
*
|
||
|
* A quick heuristic to approximate this is to calculate the closest grid position
|
||
|
* for each window and then assign them to the closest available in order
|
||
|
* of shortest first.
|
||
|
*/
|
||
|
|
||
|
let windows = this.getWindows();
|
||
|
if (windows.length == 0) return;
|
||
|
let workArea = this.getWorkArea(windows[0]);
|
||
|
|
||
|
// Get number of columns from settings
|
||
|
let columnNumber = parseInt(COLUMN[this._gsettings.get_int(COLUMN_NUMBER)]);
|
||
|
// Calculate number of rows based on number of windows and number of columns
|
||
|
let rowNumber = Math.ceil(windows.length / columnNumber);
|
||
|
|
||
|
// Create the grid
|
||
|
let gridCells = [];
|
||
|
for (let i = 0; i < windows.length; i ++) {
|
||
|
let row = Math.floor(i / columnNumber);
|
||
|
let col = i % columnNumber;
|
||
|
|
||
|
let gridWidth = Math.floor(workArea.width / columnNumber);
|
||
|
let gridHeight = Math.floor(workArea.height / rowNumber);
|
||
|
let numLastRow = windows.length % columnNumber;
|
||
|
|
||
|
let cell = {};
|
||
|
|
||
|
if (row + 1 === rowNumber && numLastRow !== 0) {
|
||
|
// In the last row, recalculate width so that they fill the screen
|
||
|
let gridWidthLastRow = Math.floor(workArea.width / numLastRow);
|
||
|
cell.x = workArea.x + col * gridWidthLastRow;
|
||
|
cell.w = gridWidthLastRow;
|
||
|
} else {
|
||
|
cell.x = workArea.x + col * gridWidth;
|
||
|
cell.w = gridWidth;
|
||
|
}
|
||
|
cell.y = workArea.y + row * gridHeight;
|
||
|
cell.h = gridHeight;
|
||
|
cell.centerX = cell.x + cell.w / 2;
|
||
|
cell.centerY = cell.y + cell.h / 2;
|
||
|
gridCells.push(cell);
|
||
|
}
|
||
|
|
||
|
// Calculate distances[i][j] as the distance from windows[i] to
|
||
|
// gridCells[j].
|
||
|
let distances = [];
|
||
|
for (let windowI = 0; windowI < windows.length; windowI ++) {
|
||
|
const win = windows[windowI];
|
||
|
const windowCenterX = win.x + win.width / 2;
|
||
|
const windowCenterY = win.y + win.height / 2;
|
||
|
distances[windowI] = [];
|
||
|
for (let cellJ = 0; cellJ < gridCells.length; cellJ ++) {
|
||
|
const cell = gridCells[cellJ];
|
||
|
const dist = Math.sqrt((windowCenterX - cell.centerX) ** 2 +
|
||
|
(windowCenterY - cell.centerY) ** 2);
|
||
|
distances[windowI][cellJ] = dist;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Move window into cell
|
||
|
function moveWindow(wind, cell) {
|
||
|
const win = wind.get_meta_window();
|
||
|
win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||
|
win.unminimize();
|
||
|
win.move_resize_frame(false, cell.x, cell.y, cell.w, cell.h);
|
||
|
}
|
||
|
|
||
|
// Now we can assign windows in order of closest
|
||
|
const windowIsToMove = new Set(windows.keys());
|
||
|
const cellJsToFill = new Set(gridCells.keys());
|
||
|
|
||
|
// Move windows, closest to grid position first.
|
||
|
for (let i = 0; i < windows.length; i ++) {
|
||
|
if (windowIsToMove.size !== cellJsToFill.size)
|
||
|
throw Error('Expected to assign one cell per window');
|
||
|
let minDist = Infinity;
|
||
|
let minI, minJ;
|
||
|
windowIsToMove.forEach(windowI =>
|
||
|
cellJsToFill.forEach(cellJ => {
|
||
|
if (distances[windowI][cellJ] < minDist) {
|
||
|
minDist = distances[windowI][cellJ];
|
||
|
minI = windowI;
|
||
|
minJ = cellJ;
|
||
|
}
|
||
|
}
|
||
|
)
|
||
|
);
|
||
|
moveWindow(windows[minI], gridCells[minJ]);
|
||
|
windowIsToMove.delete(minI);
|
||
|
cellJsToFill.delete(minJ);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
maximizeWindow(direction) {
|
||
|
if (this._allMonitor == true) {
|
||
|
this.maximizeWindowAllMonitor(direction);
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
let windows = this.getWindows();
|
||
|
|
||
|
for (let i = 0; i < windows.length; i++) {
|
||
|
let actor = windows[i];
|
||
|
let win = actor.get_meta_window();
|
||
|
win.maximize(direction);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
maximizeWindowAllMonitor(direction) {
|
||
|
let windows = this.getWindows();
|
||
|
if (windows.length == 0)
|
||
|
return;
|
||
|
|
||
|
let workArea = this.getWorkArea(windows[0]);
|
||
|
|
||
|
for (let i = 0; i < windows.length; i++) {
|
||
|
let win = windows[i].get_meta_window();
|
||
|
|
||
|
switch (direction) {
|
||
|
case Meta.MaximizeFlags.BOTH:
|
||
|
win.move_resize_frame(true,
|
||
|
workArea.x,
|
||
|
workArea.y,
|
||
|
workArea.width,
|
||
|
workArea.height);
|
||
|
break;
|
||
|
case Meta.MaximizeFlags.VERTICAL:
|
||
|
win.move_resize_frame(true,
|
||
|
win.get_frame_rect().x,
|
||
|
workArea.y,
|
||
|
win.get_frame_rect().width,
|
||
|
workArea.height);
|
||
|
break;
|
||
|
case Meta.MaximizeFlags.HORIZONTAL:
|
||
|
win.move_resize_frame(true,
|
||
|
workArea.x,
|
||
|
win.get_frame_rect().y,
|
||
|
workArea.width,
|
||
|
win.get_frame_rect().height);
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
restoringWindow() {
|
||
|
let windows = this.getWindows();
|
||
|
|
||
|
for (let i = 0; i < windows.length; i++) {
|
||
|
let actor = windows[i];
|
||
|
let win = actor.get_meta_window();
|
||
|
win.unmaximize(Meta.MaximizeFlags.BOTH);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
getWindows() {
|
||
|
let currentWorkspace = global.workspace_manager.get_active_workspace();
|
||
|
|
||
|
let windows = global.get_window_actors().filter(actor => {
|
||
|
if (actor.meta_window.get_window_type() == Meta.WindowType.NORMAL)
|
||
|
return actor.meta_window.located_on_workspace(currentWorkspace);
|
||
|
|
||
|
return false;
|
||
|
});
|
||
|
|
||
|
if (!(this._allMonitor)) {
|
||
|
windows = windows.filter(w => {
|
||
|
return w.meta_window.get_monitor() == this.getFocusedMonitor();
|
||
|
});
|
||
|
}
|
||
|
return windows;
|
||
|
}
|
||
|
|
||
|
getFocusedMonitor() {
|
||
|
let focusWindow = global.display.get_focus_window();
|
||
|
if (focusWindow) {
|
||
|
return focusWindow.get_monitor();
|
||
|
} else {
|
||
|
return global.display.get_current_monitor();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
getWorkArea(window) {
|
||
|
if (this._allMonitor)
|
||
|
return window.get_meta_window().get_work_area_all_monitors();
|
||
|
else
|
||
|
return window.get_meta_window().get_work_area_current_monitor();
|
||
|
}
|
||
|
|
||
|
_allMonitorToggle() {
|
||
|
this._allMonitor = this._allMonitorItem.state;
|
||
|
this._gsettings.set_boolean(ALL_MONITOR, this._allMonitorItem.state);
|
||
|
}
|
||
|
|
||
|
_getCustIcon(icon_name) {
|
||
|
let gicon = Gio.icon_new_for_string( Me.dir.get_child('icons').get_path() + "/" + icon_name + ".svg" );
|
||
|
return gicon;
|
||
|
}
|
||
|
|
||
|
_onDestroy(){
|
||
|
}
|
||
|
});
|
||
|
|
||
|
let Column = GObject.registerClass(
|
||
|
class Column extends PanelMenu.SystemIndicator {
|
||
|
_init() {
|
||
|
super._init();
|
||
|
|
||
|
this._gsettings = Convenience.getSettings(ARRANGEWINDOWS_SCHEMA);
|
||
|
|
||
|
this._item = new PopupMenu.PopupBaseMenuItem({ activate: false });
|
||
|
this.menu.addMenuItem(this._item);
|
||
|
|
||
|
this._slider = new Slider.Slider(0);
|
||
|
this._slider.connect('drag-end', this._sliderChanged.bind(this));
|
||
|
|
||
|
let number = this._gsettings.get_int(COLUMN_NUMBER);
|
||
|
this._slider.value = number / 6;
|
||
|
this._label = new St.Label({ text: 'Tile x' + COLUMN[number] });
|
||
|
|
||
|
this._item.add(this._label);
|
||
|
this._item.add(this._slider);
|
||
|
}
|
||
|
|
||
|
_sliderChanged() {
|
||
|
let number = Math.round(this._slider.value * 6);
|
||
|
this._slider.value = number / 6;
|
||
|
this._label.set_text('Tile x' + COLUMN[number]);
|
||
|
this._gsettings.set_int(COLUMN_NUMBER, number);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
function addKeybinding() {
|
||
|
let modeType = Shell.ActionMode.NORMAL;
|
||
|
|
||
|
Main.wm.addKeybinding(HOTKEY_CASCADE,
|
||
|
arrange._gsettings,
|
||
|
Meta.KeyBindingFlags.NONE,
|
||
|
modeType,
|
||
|
arrange.cascadeWindow.bind(arrange));
|
||
|
Main.wm.addKeybinding(HOTKEY_TILE,
|
||
|
arrange._gsettings,
|
||
|
Meta.KeyBindingFlags.NONE,
|
||
|
modeType,
|
||
|
arrange.tileWindow.bind(arrange));
|
||
|
Main.wm.addKeybinding(HOTKEY_SIDEBYSIDE,
|
||
|
arrange._gsettings,
|
||
|
Meta.KeyBindingFlags.NONE,
|
||
|
modeType,
|
||
|
arrange.sideBySideWindow.bind(arrange));
|
||
|
Main.wm.addKeybinding(HOTKEY_STACK,
|
||
|
arrange._gsettings,
|
||
|
Meta.KeyBindingFlags.NONE,
|
||
|
modeType,
|
||
|
arrange.stackWindow.bind(arrange));
|
||
|
}
|
||
|
|
||
|
function removeKeybinding(){
|
||
|
Main.wm.removeKeybinding(HOTKEY_CASCADE);
|
||
|
Main.wm.removeKeybinding(HOTKEY_TILE);
|
||
|
Main.wm.removeKeybinding(HOTKEY_SIDEBYSIDE);
|
||
|
Main.wm.removeKeybinding(HOTKEY_STACK);
|
||
|
}
|
||
|
|
||
|
let arrange;
|
||
|
|
||
|
function init(metadata) {
|
||
|
}
|
||
|
|
||
|
function enable() {
|
||
|
arrange = new ArrangeMenu;
|
||
|
Main.panel.addToStatusArea('arrange-menu', arrange);
|
||
|
addKeybinding();
|
||
|
}
|
||
|
|
||
|
function disable() {
|
||
|
removeKeybinding();
|
||
|
arrange.destroy();
|
||
|
}
|