mirror of
https://github.com/gosticks/PaperWM.git
synced 2026-08-16 14:10:28 +00:00
510 lines
18 KiB
JavaScript
510 lines
18 KiB
JavaScript
Clutter = imports.gi.Clutter;
|
|
Tweener = imports.ui.tweener;
|
|
Lang = imports.lang;
|
|
St = imports.gi.St;
|
|
Workspace = imports.ui.workspace;
|
|
Background = imports.ui.background;
|
|
Meta = imports.gi.Meta;
|
|
Pango = imports.gi.Pango;
|
|
|
|
|
|
MINIMAP_SCALE = 0.15;
|
|
|
|
calcOffset = function(metaWindow) {
|
|
let buffer = metaWindow.get_buffer_rect();
|
|
let frame = metaWindow.get_frame_rect();
|
|
let x_offset = frame.x - buffer.x;
|
|
let y_offset = frame.y - buffer.y;
|
|
return [x_offset, y_offset];
|
|
}
|
|
|
|
WindowCloneLayout = new Lang.Class({
|
|
Name: 'PaperWindowCloneLayout',
|
|
Extends: Clutter.LayoutManager,
|
|
|
|
_init: function() {
|
|
this.parent();
|
|
},
|
|
|
|
_makeBoxForWindow: function(window) {
|
|
// We need to adjust the position of the actor because of the
|
|
// consequences of invisible borders -- in reality, the texture
|
|
// has an extra set of "padding" around it that we need to trim
|
|
// down.
|
|
|
|
// The outer rect (from which we compute the bounding box)
|
|
// paradoxically is the smaller rectangle, containing the positions
|
|
// of the visible frame. The input rect contains everything,
|
|
// including the invisible border padding.
|
|
let inputRect = window.get_buffer_rect();
|
|
let frame = window.get_frame_rect();
|
|
|
|
let box = new Clutter.ActorBox();
|
|
|
|
box.set_origin((inputRect.x - frame.x),
|
|
(inputRect.y - frame.y));
|
|
box.set_size(inputRect.width, inputRect.height);
|
|
|
|
return box;
|
|
},
|
|
|
|
vfunc_get_preferred_height: function(container, forWidth) {
|
|
let frame = container.get_first_child().meta_window.get_frame_rect();
|
|
return [frame.height, frame.height];
|
|
},
|
|
|
|
vfunc_get_preferred_width: function(container, forHeight) {
|
|
let frame = container.get_first_child().meta_window.get_frame_rect();
|
|
return [frame.width, frame.width];
|
|
},
|
|
|
|
vfunc_allocate: function(container, box, flags) {
|
|
container.get_children().forEach(Lang.bind(this, function (child) {
|
|
let realWindow;
|
|
// if (child == container._delegate._windowClone)
|
|
// realWindow = container._delegate.realWindow;
|
|
// else
|
|
realWindow = child.source;
|
|
|
|
child.allocate(this._makeBoxForWindow(realWindow.meta_window),
|
|
flags);
|
|
}));
|
|
}
|
|
});
|
|
|
|
Minimap = new Lang.Class({
|
|
Name: 'Minimap',
|
|
|
|
_init: function(space) {
|
|
this.actor = new Clutter.Actor({ name: "minimap-this" });
|
|
this.space = space;
|
|
this.minimapActor = new Clutter.Actor({ name: "minimap-container"} );
|
|
|
|
this.clones = [];
|
|
this.actor.set_scale(MINIMAP_SCALE, MINIMAP_SCALE);
|
|
this.actor.height = primary.height;
|
|
this.actor.width = primary.width;
|
|
this.actor.add_actor(this.minimapActor);
|
|
|
|
},
|
|
|
|
createClones: function(windows) {
|
|
return windows.map((mw) => {
|
|
let windowActor = mw.get_compositor_private()
|
|
let clone = new Clutter.Clone({ source: windowActor });
|
|
let container = new Clutter.Actor({ layout_manager: new WindowCloneLayout(),
|
|
name: "window-clone-container"
|
|
});
|
|
clone.meta_window = mw;
|
|
|
|
container.add_actor(clone);
|
|
return container;
|
|
});
|
|
|
|
},
|
|
|
|
restack: function (around) {
|
|
this.minimapActor.remove_all_children();
|
|
let clones = this.clones;
|
|
|
|
for (let i=0; i < around; i++) {
|
|
this.minimapActor.add_actor(clones[i]);
|
|
}
|
|
for (let i=clones.length-1; i>around; i--) {
|
|
this.minimapActor.add_actor(clones[i]);
|
|
}
|
|
this.minimapActor.add_actor(clones[around]);
|
|
},
|
|
|
|
fold: function (around, animate = true) {
|
|
around = around || this.space.indexOf(this.space.selectedWindow);
|
|
if (around < 0) {
|
|
return;
|
|
}
|
|
|
|
let time = 0;
|
|
if (animate) {
|
|
time = 0.25;
|
|
}
|
|
|
|
this.restack(around);
|
|
let clones = this.clones;
|
|
|
|
let maxProtrusion = 500;
|
|
let leftStackGap = maxProtrusion/(around);
|
|
let rightStackGap = maxProtrusion/(clones.length - 1 - around);
|
|
for (let i=0; i < around; i++) {
|
|
let clone = clones[i];
|
|
clone.set_pivot_point(0, 0.5);
|
|
if (clone.x + this.minimapActor.x <= -maxProtrusion) {
|
|
let scale = 1 - 0.05*(around - i);
|
|
Tweener.addTween(clone, {x: -this.minimapActor.x
|
|
- (maxProtrusion - i*leftStackGap)
|
|
, scale_x: scale
|
|
, scale_y: scale
|
|
, transition: "easeInOutQuad"
|
|
, time: time});
|
|
} else {
|
|
this.minimapActor
|
|
.set_child_above_sibling(clone,
|
|
this.minimapActor.last_child);
|
|
}
|
|
}
|
|
for (let i=clones.length-1; i>around; i--) {
|
|
let clone = clones[i];
|
|
clone.set_pivot_point(1, 0.5);
|
|
if (clone.x + clone.width + this.minimapActor.x >= primary.width + maxProtrusion) {
|
|
let scale = 1 - 0.05*(i - around);
|
|
Tweener.addTween(clone, {x: -this.minimapActor.x
|
|
+ primary.width
|
|
+ (maxProtrusion - (clones.length-1 - i)*rightStackGap) - clone.width
|
|
, scale_x: scale
|
|
, scale_y: scale
|
|
, transition: "easeInOutQuad"
|
|
, time: time});
|
|
}
|
|
}
|
|
},
|
|
|
|
unfold: function (animate = true) {
|
|
this.layout(animate);
|
|
},
|
|
|
|
toggle: function() {
|
|
if (!this.visible) {
|
|
this.refresh();
|
|
}
|
|
this.actor.visible = !this.actor.visible;
|
|
},
|
|
|
|
refresh: function() {
|
|
this.clones = this.createClones(this.space);
|
|
let selectedIndex = this.space.selectedIndex();
|
|
if(selectedIndex > -1) {
|
|
this.restack(selectedIndex);
|
|
this.layout(false);
|
|
let frame = this.space.selectedWindow.get_frame_rect();
|
|
this.sync(frame.x, false);
|
|
}
|
|
},
|
|
|
|
layout: function(animate = true) {
|
|
let actors = this.clones;
|
|
function tweenTo(actor, x) {
|
|
let time = 0;
|
|
if (animate) {
|
|
time = 0.25;
|
|
}
|
|
actor.destinationX = x;
|
|
Tweener.addTween(actor, { x: actor.destinationX
|
|
, y: 0
|
|
, scale_x: 1
|
|
, scale_y: 1
|
|
, time: time
|
|
, transition: "easeInOutQuad"});
|
|
}
|
|
|
|
function propagate_forward(i, leftEdge, gap) {
|
|
if(i < 0 || i >= actors.length)
|
|
return;
|
|
let actor = actors[i];
|
|
let w = actor.width;
|
|
let x = leftEdge;
|
|
|
|
tweenTo(actor, x);
|
|
|
|
propagate_forward(i+1, x + w + gap, gap);
|
|
}
|
|
|
|
propagate_forward(0, 0, 5*window_gap);
|
|
},
|
|
|
|
reorder: function(index, targetIndex, targetX) {
|
|
// targetX is the destination of the moving window in viewport
|
|
// coordinates
|
|
|
|
let movingClone = this.clones[index];
|
|
|
|
// Make sure the moving window is on top
|
|
this.minimapActor.set_child_above_sibling(movingClone, this.minimapActor.last_child);
|
|
|
|
let temp = this.clones[index];
|
|
this.clones[index] = this.clones[targetIndex];
|
|
this.clones[targetIndex] = temp;
|
|
|
|
this.layout();
|
|
|
|
// this.layout sets destinationX
|
|
this.minimapActor.destinationX = -(movingClone.destinationX - targetX);
|
|
Tweener.addTween(this.minimapActor
|
|
, { x: this.minimapActor.destinationX
|
|
, time: 0.25, transition: 'easeInOutQuad'
|
|
});
|
|
|
|
},
|
|
|
|
sync: function(selectedWindowX, animate=true) {
|
|
let time = 0;
|
|
if (animate)
|
|
time = 0.25
|
|
let selectedIndex = this.space.selectedIndex();
|
|
let clone = this.clones[selectedIndex];
|
|
this.minimapActor.destinationX = -(clone.destinationX - selectedWindowX);
|
|
Tweener.addTween(this.minimapActor
|
|
, { x: this.minimapActor.destinationX
|
|
, time: time, transition: 'easeInOutQuad' });
|
|
},
|
|
})
|
|
|
|
|
|
MultiMap = new Lang.Class({
|
|
Name: 'MultiMap',
|
|
|
|
_init: function(mru) {
|
|
this.actor = new St.Widget({ name: "multimap-viewport" });
|
|
this.container = new St.BoxLayout({ name: "multimap-container" });
|
|
this.actor.add_actor(this.container);
|
|
this.container.set_vertical(true)
|
|
this.container.remove_all_children()
|
|
this.minimaps = [];
|
|
|
|
if (mru) {
|
|
this.selectedIndex = 0;
|
|
let seen = {};
|
|
this.addSpace(spaces[global.screen.get_active_workspace_index()], 0)
|
|
seen[global.screen.get_active_workspace()] = true;
|
|
let i = 1;
|
|
global.display.get_tab_list(Meta.TabList.NORMAL_ALL, null)
|
|
.forEach(metaWindow => {
|
|
let workspace = metaWindow.get_workspace();
|
|
if (!seen[workspace]) {
|
|
debug('add workspace');
|
|
this.addSpace(spaces[workspace.workspace_index], i)
|
|
seen[workspace] = true;
|
|
i++;
|
|
}
|
|
});
|
|
|
|
let workspaces = global.screen.get_n_workspaces();
|
|
for (let j=0; j < workspaces; j++) {
|
|
let workspace = global.screen.get_workspace_by_index(j);
|
|
if (!seen[workspace]) {
|
|
debug('add workspace');
|
|
this.addSpace(spaces[workspace.workspace_index], i)
|
|
i++;
|
|
seen[workspace] = true;
|
|
}
|
|
}
|
|
} else {
|
|
this.selectedIndex = global.screen.get_active_workspace_index();
|
|
spaces.forEach((s, i) => {
|
|
this.addSpace(s, i);
|
|
})
|
|
}
|
|
this.rowHeight = this.container.first_child.height;
|
|
this.actor.height = this.rowHeight;
|
|
this.actor.width = this.container.first_child.width;
|
|
|
|
let minimap = this.setSelected(this.selectedIndex, false);
|
|
|
|
this.selectionChrome = new St.Widget({ style_class: 'window-clone-border'});
|
|
let label = new St.Label({ style_class: "window-caption"});
|
|
this.selectionChrome.add_child(label);
|
|
label.y = Math.round(this.rowHeight / 2 - 30);
|
|
// label.x = 4 + 2;
|
|
|
|
label.clutter_text.ellipsize = Pango.EllipsizeMode.END;
|
|
this.selectionChrome.label = label;
|
|
this.actor.add_child(this.selectionChrome);
|
|
|
|
|
|
// let chrome = new St.Widget();
|
|
// this.actor.add_child(chrome);
|
|
// chrome.set_size(this.actor.width + 2*4, this.actor.height + 2*4);
|
|
// chrome.set_position(-4, -4);
|
|
// chrome.set_style('border: 4px #454f52; border-radius: 8px;');
|
|
},
|
|
|
|
addSpace: function(space, i) {
|
|
let wrapper = new St.Widget({ name: "minimap-wrapper-"+i });
|
|
let minimap = new Minimap(space);
|
|
|
|
// Create the background, see overview.js
|
|
let background = new Meta.BackgroundGroup();
|
|
let bgManager = new Background.BackgroundManager({ container: background,
|
|
monitorIndex: 0,
|
|
vignette: false });
|
|
|
|
|
|
let chrome = new St.Widget();
|
|
|
|
wrapper.add_child(background);
|
|
wrapper.add_child(chrome);
|
|
|
|
minimap.actor.reparent(wrapper);
|
|
this.container.add_child(wrapper);
|
|
minimap.actor.visible = true;
|
|
minimap.refresh();
|
|
minimap.fold(undefined, false);
|
|
wrapper.width =
|
|
Math.ceil(minimap.actor.width * minimap.actor.scale_x) + 20;
|
|
wrapper.height =
|
|
Math.ceil(minimap.actor.height * minimap.actor.scale_y) + 38;
|
|
|
|
let workspaceLabel = new St.Label({ style_class: "window-caption" });
|
|
workspaceLabel.text = Meta.prefs_get_workspace_name(space.workspace.index());
|
|
if (i == this.selectedIndex) {
|
|
workspaceLabel.opacity = 0;
|
|
}
|
|
wrapper.add_child(workspaceLabel)
|
|
wrapper.workspaceLabel = workspaceLabel;
|
|
|
|
chrome.set_size(wrapper.width + 2*4, wrapper.height + 4);
|
|
chrome.set_position(-4, -4);
|
|
chrome.set_style('border: 4px #454f52; border-radius: 6px;');
|
|
let backgroundScaleX = wrapper.width/background.width;
|
|
let backgroundScaleY = wrapper.height/background.height;
|
|
background.set_scale(backgroundScaleX, backgroundScaleY);
|
|
minimap.actor.set_position(10, 8);
|
|
this.minimaps.push(minimap);
|
|
},
|
|
|
|
setSelected: function(i, animate = true) {
|
|
if (i >= this.container.get_children().length ||
|
|
i < 0) {
|
|
return;
|
|
}
|
|
if (i !== this.selectedIndex) {
|
|
this.minimaps[this.selectedIndex].fold(undefined, animate);
|
|
Tweener.addTween(this.container.get_children()[this.selectedIndex].workspaceLabel,
|
|
{ opacity: 255, time: 0.25, transition: 'linear' });
|
|
}
|
|
|
|
this.selectedIndex = i;
|
|
let time = 0;
|
|
if (animate)
|
|
time = 0.25;
|
|
Tweener.addTween(this.container, { y: -i*this.rowHeight, time: time, transition: 'easeInOutQuad' });
|
|
this.minimaps[this.selectedIndex].unfold(animate);
|
|
Tweener.addTween(this.container.get_children()[this.selectedIndex].workspaceLabel,
|
|
{ opacity: 0, time: 0.50, transition: 'linear' });
|
|
|
|
return this.minimaps[this.selectedIndex];
|
|
},
|
|
|
|
getSelected: function() {
|
|
return this.minimaps[this.selectedIndex];
|
|
},
|
|
|
|
onlyShowSelected: function() {
|
|
this.container.get_children().forEach((wrapper, i) => {
|
|
if (i !== this.selectedIndex) {
|
|
wrapper.hide();
|
|
}
|
|
});
|
|
this.container.y = 0;
|
|
},
|
|
|
|
showAll: function() {
|
|
this.container.get_children().forEach((wrapper, i) => {
|
|
wrapper.show();
|
|
let workspaceLabel = wrapper.workspaceLabel;
|
|
workspaceLabel.x = Math.round((wrapper.width - workspaceLabel.width) / 2);
|
|
workspaceLabel.y = Math.round((wrapper.height - 38 - workspaceLabel.height) / 2)
|
|
+ this.minimaps[i].actor.y;
|
|
|
|
});
|
|
this.setSelected(this.selectedIndex, false);
|
|
},
|
|
|
|
highlight: function(index) {
|
|
// Note that both the minimapActor and the clone could be moving at this
|
|
// point.
|
|
//
|
|
// For some reason Clutter.Actor.apply_relative_transform_to_point
|
|
// fail to work when changing workspace.. (ie. when going from only one
|
|
// minimap visible to all visible)
|
|
|
|
/**
|
|
* Transformation a point in `actor`-relative coordinates to `ancestor`
|
|
* relative coordinates. (NB: ignores rotations for now)
|
|
* Uses actor's `destinationX/Y` if set.
|
|
*/
|
|
function transform(actor, ancestor, x, y) {
|
|
if (actor === ancestor || !actor)
|
|
return [x, y];
|
|
|
|
let actorX = actor.x;
|
|
if (actor.destinationX !== undefined)
|
|
actorX = actor.destinationX;
|
|
|
|
let actorY = actor.y;
|
|
if (actor.destinationY !== undefined)
|
|
actorX = actor.destinationY;
|
|
|
|
return transform(actor.get_parent(), ancestor,
|
|
x*actor.scale_x + actorX,
|
|
y*actor.scale_y + actorY);
|
|
}
|
|
|
|
let minimap = this.getSelected();
|
|
let clone = minimap.clones[index];
|
|
let label = this.selectionChrome.label;
|
|
|
|
let size, x, y, newWidth;
|
|
if (clone) {
|
|
// Calculate destinationX of selected clone in viewport coordinates and
|
|
// tween the chrome there
|
|
[x, yIgnored] = transform(minimap.minimapActor, this.actor, clone.destinationX, 0);
|
|
x = Math.round(x);
|
|
y = 4;
|
|
size = clone.get_transformed_size();
|
|
|
|
// When width have been set on the label we can't get the
|
|
// required width out anymore. Easiest workaround is making
|
|
// a new label. This enables animating the width.
|
|
let newLabel = new St.Label({
|
|
text: clone.first_child.meta_window.title,
|
|
style_class: "window-caption"});
|
|
|
|
newLabel.set_position(label.x, label.y);
|
|
label.destroy();
|
|
|
|
this.selectionChrome.add_child(newLabel);
|
|
this.selectionChrome.label = newLabel;
|
|
|
|
newWidth = newLabel.width;
|
|
newLabel.width = label.width;
|
|
label = newLabel;
|
|
} else {
|
|
// We're in an empty workspace
|
|
size = this.actor.get_size();
|
|
// Apply correction due to overlapping borders
|
|
size[1] -= 4;
|
|
x = 0;
|
|
y = -4;
|
|
newWidth = 0;
|
|
label.hide();
|
|
}
|
|
|
|
Tweener.addTween(label, {x: Math.round((size[0] + 8 - newWidth)/2),
|
|
y: Math.round(size[1] + 4),
|
|
width: newWidth,
|
|
time: 0.25,
|
|
transition: 'easeInOutQuad'
|
|
});
|
|
|
|
Tweener.addTween(this.selectionChrome,
|
|
{x: x - 4,
|
|
y: y,
|
|
width: Math.round(size[0] + 8),
|
|
height: Math.round(size[1] + 8),
|
|
time: 0.25,
|
|
transition: 'easeInOutQuad'
|
|
})
|
|
}
|
|
})
|
|
|
|
const Signals = imports.signals;
|
|
Signals.addSignalMethods(MultiMap.prototype);
|