1
// -*- mode: js2; indent-tabs-mode: nil; js2-basic-offset: 4 -*-
2
// import just everything from workspace.js:
3
const Clutter = imports.gi.Clutter;
4
const Gio = imports.gi.Gio;
5
const Lang = imports.lang;
6
const Mainloop = imports.mainloop;
7
const Meta = imports.gi.Meta;
8
const Pango = imports.gi.Pango;
9
const Shell = imports.gi.Shell;
10
const St = imports.gi.St;
11
const Signals = imports.signals;
13
const DND = imports.ui.dnd;
14
const Lightbox = imports.ui.lightbox;
15
const Main = imports.ui.main;
16
const Overview = imports.ui.overview;
17
const Panel = imports.ui.panel;
18
const Tweener = imports.ui.tweener;
20
const Workspace = imports.ui.workspace;
21
const WindowPositionFlags = Workspace.WindowPositionFlags;
23
const WindowPlacementStrategy = {
28
/* Begin user settings */
29
const PLACEMENT_STRATEGY = WindowPlacementStrategy.NATURAL;
30
const USE_MORE_SCREEN = true;
31
const WINDOW_CAPTIONS_ON_TOP = true;
32
/* End user settings - do not change anything below this line */
34
// testing settings for natural window placement strategy:
35
const WINDOW_PLACEMENT_NATURAL_FILLGAPS = true; // enlarge windows at the end to fill gaps // not implemented yet
36
const WINDOW_PLACEMENT_NATURAL_GRID_FALLBACK = true; // fallback to grid mode if all windows have the same size and positions. // not implemented yet
37
const WINDOW_PLACEMENT_NATURAL_ACCURACY = 20; // accuracy of window translate moves (KDE-default: 20)
38
const WINDOW_PLACEMENT_NATURAL_GAPS = 5; // half of the minimum gap between windows
39
const WINDOW_PLACEMENT_NATURAL_MAX_TRANSLATIONS = 5000; // safety limit for preventing endless loop if something is wrong in the algorithm
41
const PLACE_WINDOW_CAPTIONS_ON_TOP = true; // place window titles in overview on top of windows with overlap parameter
43
function injectToFunction(parent, name, func) {
44
let origin = parent[name];
45
parent[name] = function() {
47
ret = origin.apply(this, arguments);
48
if (ret === undefined)
49
ret = func.apply(this, arguments);
53
const WORKSPACE_BORDER_GAP = 10; // gap between the workspace area and the workspace selector
55
function Rect(x, y, width, height) {
56
[this.x, this.y, this.width, this.height] = arguments;
61
* used in _calculateWindowTransformationsNatural to replace Meta.Rectangle that is too slow.
65
return new Rect(this.x, this.y, this.width, this.height);
68
union: function(rect2) {
69
let dest = this.copy();
72
dest.width += dest.x - rect2.x;
77
dest.height += dest.y - rect2.y;
80
if (rect2.x + rect2.width > dest.x + dest.width)
81
dest.width = rect2.x + rect2.width - dest.x;
82
if (rect2.y + rect2.height > dest.y + dest.height)
83
dest.height = rect2.y + rect2.height - dest.y;
88
adjusted: function(dx, dy, dx2, dy2) {
89
let dest = this.copy();
92
dest.width += -dx + dx2;
93
dest.height += -dy + dy2;
97
overlap: function(rect2) {
98
return !((this.x + this.width <= rect2.x) ||
99
(rect2.x + rect2.width <= this.x) ||
100
(this.y + this.height <= rect2.y) ||
101
(rect2.y + rect2.height <= this.y));
105
return [this.x + this.width / 2, this.y + this.height / 2];
108
translate: function(dx, dy) {
114
let winInjections, workspaceInjections, connectedSignals;
116
function resetState() {
118
workspaceInjections = { };
119
workViewInjections = { };
120
connectedSignals = [ ];
126
let placementStrategy = PLACEMENT_STRATEGY;
127
let useMoreScreen = USE_MORE_SCREEN;
130
* _calculateWindowTransformationsNatural:
131
* @clones: Array of #MetaWindow
133
* Returns clones with matching target coordinates and scales to arrange windows in a natural way that no overlap exists and relative window size is preserved.
134
* This function is almost a 1:1 copy of the function
135
* PresentWindowsEffect::calculateWindowTransformationsNatural() from KDE, see:
136
* https://projects.kde.org/projects/kde/kdebase/kde-workspace/repository/revisions/master/entry/kwin/effects/presentwindows/presentwindows.cpp
138
Workspace.Workspace.prototype._calculateWindowTransformationsNatural = function(clones) {
139
// As we are using pseudo-random movement (See "slot") we need to make sure the list
140
// is always sorted the same way no matter which window is currently active.
141
clones = clones.sort(function (win1, win2) {
142
return win2.metaWindow.get_stable_sequence() - win1.metaWindow.get_stable_sequence();
145
// Put a gap on the right edge of the workspace to separe it from the workspace selector
146
let x_gap = WORKSPACE_BORDER_GAP;
147
let y_gap = WORKSPACE_BORDER_GAP * this._height / this._width
148
let area = new Rect(this._x, this._y, this._width - x_gap, this._height - y_gap);
150
let bounds = area.copy();
155
for (let i = 0; i < clones.length; i++) {
156
// save rectangles into 4-dimensional arrays representing two corners of the rectangular: [left_x, top_y, right_x, bottom_y]
157
let rect = clones[i].metaWindow.get_outer_rect();
158
rects[i] = new Rect(rect.x, rect.y, rect.width, rect.height);
159
bounds = bounds.union(rects[i]);
161
// This is used when the window is on the edge of the screen to try to use as much screen real estate as possible.
162
directions[i] = direction;
164
if (direction == 4) {
169
let loop_counter = 0;
173
for (let i = 0; i < rects.length; i++) {
174
for (let j = 0; j < rects.length; j++) {
175
if (i != j && rects[i].adjusted(-WINDOW_PLACEMENT_NATURAL_GAPS, -WINDOW_PLACEMENT_NATURAL_GAPS,
176
WINDOW_PLACEMENT_NATURAL_GAPS, WINDOW_PLACEMENT_NATURAL_GAPS).overlap(
177
rects[j].adjusted(-WINDOW_PLACEMENT_NATURAL_GAPS, -WINDOW_PLACEMENT_NATURAL_GAPS,
178
WINDOW_PLACEMENT_NATURAL_GAPS, WINDOW_PLACEMENT_NATURAL_GAPS))) {
182
// TODO: something like a Point2D would be nicer here:
184
// Determine pushing direction
185
let i_center = rects[i].center();
186
let j_center = rects[j].center();
187
let diff = [j_center[0] - i_center[0], j_center[1] - i_center[1]];
189
// Prevent dividing by zero and non-movement
190
if (diff[0] == 0 && diff[1] == 0)
192
// Try to keep screen/workspace aspect ratio
193
if ( bounds.height / bounds.width > area.height / area.width )
198
// Approximate a vector of between 10px and 20px in magnitude in the same direction
199
let length = Math.sqrt(diff[0] * diff[0] + diff[1] * diff[1]);
200
diff[0] = diff[0] * WINDOW_PLACEMENT_NATURAL_ACCURACY / length;
201
diff[1] = diff[1] * WINDOW_PLACEMENT_NATURAL_ACCURACY / length;
203
// Move both windows apart
204
rects[i].translate(-diff[0], -diff[1]);
205
rects[j].translate(diff[0], diff[1]);
209
// Try to keep the bounding rect the same aspect as the screen so that more
210
// screen real estate is utilised. We do this by splitting the screen into nine
211
// equal sections, if the window center is in any of the corner sections pull the
212
// window towards the outer corner. If it is in any of the other edge sections
213
// alternate between each corner on that edge. We don't want to determine it
214
// randomly as it will not produce consistant locations when using the filter.
215
// Only move one window so we don't cause large amounts of unnecessary zooming
216
// in some situations. We need to do this even when expanding later just in case
217
// all windows are the same size.
218
// (We are using an old bounding rect for this, hopefully it doesn't matter)
219
let xSection = Math.round((rects[i].x - bounds.x) / (bounds.width / 3));
220
let ySection = Math.round((rects[i].y - bounds.y) / (bounds.height / 3));
222
let i_center = rects[i].center();
225
if (xSection != 1 || ySection != 1) { // Remove this if you want the center to pull as well
227
xSection = (directions[i] / 2 ? 2 : 0);
229
ySection = (directions[i] % 2 ? 2 : 0);
231
if (xSection == 0 && ySection == 0) {
232
diff[0] = bounds.x - i_center[0];
233
diff[1] = bounds.y - i_center[1];
235
if (xSection == 2 && ySection == 0) {
236
diff[0] = bounds.x + bounds.width - i_center[0];
237
diff[1] = bounds.y - i_center[1];
239
if (xSection == 2 && ySection == 2) {
240
diff[0] = bounds.x + bounds.width - i_center[0];
241
diff[1] = bounds.y + bounds.height - i_center[1];
243
if (xSection == 0 && ySection == 2) {
244
diff[0] = bounds.x - i_center[0];
245
diff[1] = bounds.y + bounds.height - i_center[1];
247
if (diff[0] != 0 || diff[1] != 0) {
248
let length = Math.sqrt(diff[0]*diff[0] + diff[1]*diff[1]);
249
diff[0] *= WINDOW_PLACEMENT_NATURAL_ACCURACY / length / 2; // /2 to make it less influencing than the normal center-move above
250
diff[1] *= WINDOW_PLACEMENT_NATURAL_ACCURACY / length / 2;
251
rects[i].translate(diff[0], diff[1]);
255
// Update bounding rect
256
bounds = bounds.union(rects[i]);
257
bounds = bounds.union(rects[j]);
261
} while (overlap && loop_counter < WINDOW_PLACEMENT_NATURAL_MAX_TRANSLATIONS);
263
// Work out scaling by getting the most top-left and most bottom-right window coords.
265
scale = Math.min(area.width / bounds.width,
266
area.height / bounds.height,
269
// Make bounding rect fill the screen size for later steps
270
bounds.x = bounds.x - (area.width - bounds.width * scale) / 2;
271
bounds.y = bounds.y - (area.height - bounds.height * scale) / 2;
272
bounds.width = area.width / scale;
273
bounds.height = area.height / scale;
275
// Move all windows back onto the screen and set their scale
276
for (let i = 0; i < rects.length; i++) {
277
rects[i].translate(-bounds.x, -bounds.y);
280
// TODO: Implement the KDE part "Try to fill the gaps by enlarging windows if they have the space" here. (If this is wanted)
282
// rescale to workspace
285
let buttonOuterHeight, captionHeight;
286
let buttonOuterWidth = 0;
289
for (let i = 0; i < rects.length; i++) {
290
rects[i].x = rects[i].x * scale + this._x;
291
rects[i].y = rects[i].y * scale + this._y;
293
targets[i] = [rects[i].x, rects[i].y, scale];
296
return [clones, targets];
298
workspaceInjections['_calculateWindowTransformationsNatural'] = undefined;
301
* _calculateWindowTransformationsGrid:
302
* @clones: Array of #MetaWindow
304
* Returns clones with matching target coordinates and scales to arrange windows in a grid.
306
Workspace.Workspace.prototype._calculateWindowTransformationsGrid = function(clones) {
307
let slots = this._computeAllWindowSlots(clones.length);
308
clones = this._orderWindowsByMotionAndStartup(clones, slots);
311
for (let i = 0; i < clones.length; i++) {
312
targets[i] = this._computeWindowLayout(clones[i].metaWindow, slots[i]);
315
return [clones, targets];
317
workspaceInjections['_calculateWindowTransformationsGrid'] = undefined;
322
* INITIAL - this is the initial positioning of the windows.
323
* ANIMATE - Indicates that we need animate changing position.
325
workspaceInjections['positionWindows'] = Workspace.Workspace.prototype.positionWindows;
326
Workspace.Workspace.prototype.positionWindows = function(flags) {
327
if (this._repositionWindowsId > 0) {
328
Mainloop.source_remove(this._repositionWindowsId);
329
this._repositionWindowsId = 0;
332
let clones = this._windows.slice();
333
if (this._reservedSlot)
334
clones.push(this._reservedSlot);
336
let initialPositioning = flags & WindowPositionFlags.INITIAL;
337
let animate = flags & WindowPositionFlags.ANIMATE;
339
// Start the animations
343
switch (placementStrategy) {
344
case WindowPlacementStrategy.NATURAL:
345
[clones, targets] = this._calculateWindowTransformationsNatural(clones);
348
log ('Invalid window placement strategy');
349
placementStrategy = WindowPlacementStrategy.GRID;
350
case WindowPlacementStrategy.GRID:
351
[clones, targets] = this._calculateWindowTransformationsGrid(clones);
355
let currentWorkspace = global.screen.get_active_workspace();
356
let isOnCurrentWorkspace = this.metaWorkspace == null || this.metaWorkspace == currentWorkspace;
358
for (let i = 0; i < clones.length; i++) {
359
let clone = clones[i];
360
let [x, y , scale] = targets[i];
361
let metaWindow = clone.metaWindow;
362
let mainIndex = this._lookupIndex(metaWindow);
363
let overlay = this._windowOverlays[mainIndex];
365
// Positioning a window currently being dragged must be avoided;
366
// we'll just leave a blank spot in the layout for it.
372
if (animate && isOnCurrentWorkspace) {
373
if (!metaWindow.showing_on_its_workspace()) {
374
/* Hidden windows should fade in and grow
375
* therefore we need to resize them now so they
376
* can be scaled up later */
377
if (initialPositioning) {
378
clone.actor.opacity = 0;
379
clone.actor.scale_x = 0;
380
clone.actor.scale_y = 0;
385
// Make the window slightly transparent to indicate it's hidden
386
Tweener.addTween(clone.actor,
388
time: Overview.ANIMATION_TIME,
389
transition: 'easeInQuad'
393
Tweener.addTween(clone.actor,
398
time: Overview.ANIMATION_TIME,
399
transition: 'easeOutQuad',
400
onComplete: Lang.bind(this, function() {
401
this._showWindowOverlay(clone, overlay, true);
405
clone.actor.set_position(x, y);
406
clone.actor.set_scale(scale, scale);
407
this._showWindowOverlay(clone, overlay, isOnCurrentWorkspace);
412
/// position window titles on top of windows in overlay ////
413
if (WINDOW_CAPTIONS_ON_TOP) {
414
winInjections['_init'] = Workspace.WindowOverlay.prototype._init;
415
Workspace.WindowOverlay.prototype._init = function(windowClone, parentActor) {
416
let metaWindow = windowClone.metaWindow;
418
this._windowClone = windowClone;
419
this._parentActor = parentActor;
420
this._hidden = false;
422
let title = new St.Label({ style_class: 'window-caption',
423
text: metaWindow.title });
424
title.clutter_text.ellipsize = Pango.EllipsizeMode.END;
428
this._updateCaptionId = metaWindow.connect('notify::title', Lang.bind(this, function(w) {
429
this.title.text = w.title;
432
let button = new St.Button({ style_class: 'window-close' });
435
this._idleToggleCloseId = 0;
436
button.connect('clicked', Lang.bind(this, this._closeWindow));
438
windowClone.actor.connect('destroy', Lang.bind(this, this._onDestroy));
439
windowClone.actor.connect('enter-event', Lang.bind(this, this._onEnter));
440
windowClone.actor.connect('leave-event', Lang.bind(this, this._onLeave));
442
this._windowAddedId = 0;
443
windowClone.connect('zoom-start', Lang.bind(this, this.hide));
444
windowClone.connect('zoom-end', Lang.bind(this, this.show));
449
this.closeButton = button;
451
parentActor.add_actor(this.title);
452
parentActor.add_actor(this.closeButton);
453
title.connect('style-changed', Lang.bind(this, this._onStyleChanged));
454
button.connect('style-changed', Lang.bind(this, this._onStyleChanged));
456
// force a style change if we are already on a stage - otherwise
457
// the signal will be emitted normally when we are added
458
if (parentActor.get_stage())
459
this._onStyleChanged();
462
winInjections['chromeHeights'] = Workspace.WindowOverlay.prototype.chromeHeights;
463
Workspace.WindowOverlay.prototype.chromeHeights = function () {
464
return [Math.max( this.closeButton.height - this.closeButton._overlap, this.title.height - this.title._overlap),
468
winInjections['updatePositions'] = Workspace.WindowOverlay.prototype.updatePositions;
469
Workspace.WindowOverlay.prototype.updatePositions = function(cloneX, cloneY, cloneWidth, cloneHeight) {
470
let button = this.closeButton;
471
let title = this.title;
474
let buttonY = cloneY - (button.height - button._overlap);
475
if (St.Widget.get_default_direction() == St.TextDirection.RTL)
476
buttonX = cloneX - (button.width - button._overlap);
478
buttonX = cloneX + (cloneWidth - button._overlap);
480
button.set_position(Math.floor(buttonX), Math.floor(buttonY));
482
if (!title.fullWidth)
483
title.fullWidth = title.width;
484
title.width = Math.min(title.fullWidth, cloneWidth);
486
let titleX = cloneX + (cloneWidth - title.width) / 2;
487
let titleY = cloneY - title.height + title._overlap;
488
title.set_position(Math.floor(titleX), Math.floor(titleY));
491
winInjections['_onStyleChanged'] = Workspace.WindowOverlay.prototype._onStyleChanged;
492
Workspace.WindowOverlay.prototype._onStyleChanged = function() {
493
let titleNode = this.title.get_theme_node();
494
this.title._spacing = titleNode.get_length('-shell-caption-spacing');
495
this.title._overlap = titleNode.get_length('-shell-caption-overlap');
497
let closeNode = this.closeButton.get_theme_node();
498
this.closeButton._overlap = closeNode.get_length('-shell-close-overlap');
500
this._parentActor.queue_relayout();
505
function removeInjection(object, injection, name) {
506
if (injection[name] === undefined)
509
object[name] = injection[name];
513
for (i in workspaceInjections)
514
removeInjection(Workspace.Workspace.prototype, workspaceInjections, i);
515
for (i in winInjections)
516
removeInjection(Workspace.WindowOverlay.prototype, winInjections, i);
518
for each (i in connectedSignals)
519
i.obj.disconnect(i.id);
521
global.stage.queue_relayout();