var Extension; if (imports.misc.extensionUtils.extensions) { Extension = imports.misc.extensionUtils.extensions["paperwm@hedning:matrix.org"]; } else { Extension = imports.ui.main.extensionManager.lookup("paperwm@hedning:matrix.org"); } var St = imports.gi.St; var Main = imports.ui.main; var Tiling = Extension.imports.tiling; var Scratch = Extension.imports.scratch; var prefs = Extension.imports.settings.prefs; var Utils = Extension.imports.utils; var Tweener = Utils.tweener; function isInRect(x, y, r) { return r.x <= x && x < r.x + r.width && r.y <= y && y < r.y + r.height; } function monitorAtPoint(gx, gy) { for (let monitor of Main.layoutManager.monitors) { if (isInRect(gx, gy, monitor)) return monitor; } return null; } function createDndZonesForMonitors() { let monitorToZones = new Map(); for (let [monitor, space] of Tiling.spaces.monitors) { monitorToZones.set(monitor, createDnDZones(space)); } return monitorToZones; } function createDnDZones(space) { // In pixels protuding into the window. Ie. exclusive window_gap let columnZoneMargin = 100; let tileZoneMargin = 250; function mkZoneActor(zone, color="red") { let actor = new St.Widget({style_class: "tile-preview"}) actor.visible = false; actor.x = zone.rect.x; actor.y = zone.rect.y; actor.width = zone.rect.width; actor.height = zone.rect.height; space.cloneContainer.add_actor(actor); actor.raise_top(); return actor } function mkColumnZone(x, position) { // Represent zones as center, margin instead of "regular" rects? let margin = prefs.window_gap / 2 + columnZoneMargin let detection = { x: x - prefs.window_gap - columnZoneMargin, y: 0, width: margin * 2, height: space.height }; let zone = { // Detection: rect: detection, position: position, space: space, // Visualization: center: detection.x + detection.width / 2, marginA: margin, // left marginB: margin, // right // Animation props: originProp: "x", sizeProp: "width", }; zone.actor = mkZoneActor(zone, "red"); zone.actor.y = Tiling.panelBox.height; zone.actor.height = space.height - Tiling.panelBox.height; return zone; } function mkTileZone(x, y, width, position) { let detection = { x: x + columnZoneMargin, y: y - prefs.window_gap - tileZoneMargin, width: width - columnZoneMargin * 2, height: prefs.window_gap + tileZoneMargin * 2, }; let margin = prefs.window_gap + tileZoneMargin; let cy = detection.y + detection.height / 2; let marginTop = Math.min(cy - Tiling.panelBox.height, margin); let marginBottom = margin; let zone = { rect: detection, position: position, space: space, // Visualization: center: cy, marginA: marginTop, marginB: marginBottom, // Animation props: originProp: "y", sizeProp: "height", }; zone.actor = mkZoneActor(zone, "blue"); zone.actor.x = x - prefs.window_gap / 2; zone.actor.width = width + prefs.window_gap; return zone; } let zones = []; for (let i = 0; i < space.length; i++) { let col = space[i]; zones.push(mkColumnZone(col[0].clone.targetX, [i])); for (let j = 0; j < col.length; j++) { let metaWindow = col[j]; let x = metaWindow.clone.targetX; let y = metaWindow.clone.targetY; let width = metaWindow._targetWidth || metaWindow.clone.width; zones.push(mkTileZone(x, y, width, [i, j])); } let lastRow = col[col.length-1]; let x = lastRow.clone.targetX; let y = lastRow.clone.targetY; let width = lastRow._targetWidth || lastRow.clone.width; let height = lastRow._targetHeight || lastRow.clone.height; zones.push(mkTileZone(x, y + height + prefs.window_gap, width, [i, col.length])); } zones.push(mkColumnZone(space.cloneContainer.width + prefs.window_gap, [space.length])); if (space.length === 0) { space.targetX = Math.round(space.width/2); space.cloneContainer.x = space.targetX; let width = Math.round(space.width/2); let zone = zones[0]; zone.rect.width = width; zone.rect.x = -Math.round(width/2); zone.rect.center = 0; } return zones; } var MoveGrab = class MoveGrab { constructor(metaWindow, type) { this.window = metaWindow; this.type = type; this.signals = new Utils.Signals(); this.initialSpace = Tiling.spaces.spaceOfWindow(metaWindow); } begin() { let metaWindow = this.window; let frame = metaWindow.get_frame_rect(); let space = Tiling.spaces.spaceOfWindow(metaWindow); this.initialY = frame.y; let actor = metaWindow.get_compositor_private(); let [gx, gy, $] = global.get_pointer(); let px = (gx - actor.x) / actor.width; let py = (gy - actor.y) / actor.height; actor.set_pivot_point(px, py); let clone = metaWindow.clone; let [x, y] = space.globalToScroll(gx, gy); px = (x - clone.x) / clone.width; py = (y - clone.y) / clone.height; clone.set_pivot_point(px, py); this.signals.connect( global.screen || global.display, "window-entered-monitor", this.beginDnD.bind(this) ); this.scrollAnchor = metaWindow.clone.targetX + space.monitor.x; this.signals.connect( metaWindow, 'position-changed', this.positionChanged.bind(this) ); space.startAnimate(); // Make sure the window actor is visible Tiling.showWindow(metaWindow); Tweener.removeTweens(space.cloneContainer); } beginDnD() { if (this.dnd) return; this.dnd = true; let metaWindow = this.window; let actor = metaWindow.get_compositor_private(); let space = this.initialSpace; let i = space.indexOf(metaWindow); let single = i !== -1 && space[i].length === 1; space.removeWindow(metaWindow); Tweener.addTween(actor, {time: prefs.animation_time, scale_x: 0.5, scale_y: 0.5}); this.monitorToZones = createDndZonesForMonitors(); let [gx, gy, $] = global.get_pointer(); let monitor = monitorAtPoint(gx, gy); let onSame = space === Tiling.spaces.monitors.get(monitor); let x = gx - space.monitor.x; if (onSame && single && space[i]) { Tiling.move_to(space, space[i][0], { x: x + prefs.window_gap/2 }); } else if (onSame && single && space[i-1]) { Tiling.move_to(space, space[i-1][0], { x: x - space[i-1][0].clone.width - prefs.window_gap/2 }); } else if (onSame && space.length === 0) { space.targetX = x; space.cloneContainer.x = x; } this.selectDndZone(single && onSame) } selectDndZone(initial=false) { let [gx, gy, $] = global.get_pointer(); let monitor = monitorAtPoint(gx, gy); let space = Tiling.spaces.monitors.get(monitor); let dndZones = this.monitorToZones.get(monitor); let [x, y] = space.globalToScroll(gx, gy, true); let frame = this.window.get_frame_rect(); let clone = this.window.clone; [clone.x, clone.y] = space.globalToScroll(frame.x, frame.y); let newDndTarget = null; for (let zone of dndZones) { if (isInRect(x, y, zone.rect)) { if (newDndTarget) { // Treat ambiguous zones as non-match (this way we don't have to ensure zones are non-overlapping :P) newDndTarget = null; break; } newDndTarget = zone; } } // TODO: rename dndTarget to selectedZone ? if (newDndTarget !== this.dndTarget) { this.dndTarget && this.deactivateDndTarget(this.dndTarget); if (newDndTarget) this.activateDndTarget(newDndTarget, initial); } } positionChanged(metaWindow) { Utils.assert(metaWindow === this.window); let [gx, gy, $] = global.get_pointer(); if (this.dnd) { this.selectDndZone(); } else { // Move the window and scroll the space let space = this.initialSpace; let clone = metaWindow.clone; let frame = metaWindow.get_frame_rect(); space.targetX = frame.x - this.scrollAnchor; space.cloneContainer.x = space.targetX; const threshold = 300; const dy = Math.min(threshold, Math.abs(frame.y - this.initialY)); let s = 1 - Math.pow(dy / 500, 3); let actor = metaWindow.get_compositor_private(); actor.set_scale(s, s); clone.set_scale(s, s); [clone.x, clone.y] = space.globalToScroll(frame.x, frame.y); if (dy >= threshold) { this.beginDnD(); } } } end() { this.signals.destroy(); let metaWindow = this.window; let actor = metaWindow.get_compositor_private(); let frame = metaWindow.get_frame_rect(); let clone = metaWindow.clone; let [gx, gy, $] = global.get_pointer(); if (this.dnd) { let dndTarget = this.dndTarget; for (let [monitor, zones] of this.monitorToZones) { zones.forEach(zone => zone.actor.destroy()); } if (dndTarget) { let space = dndTarget.space; space.selection.show() if (Scratch.isScratchWindow(metaWindow)) Scratch.unmakeScratch(metaWindow); space.addWindow(metaWindow, ...dndTarget.position); [clone.x, clone.y] = space.globalToScroll(frame.x, frame.y); let [x, y] = space.globalToScroll(gx, gy); let px = (x - clone.x) / clone.width; let py = (y - clone.y) / clone.height; clone.set_pivot_point(px, py); clone.set_scale(actor.scale_x, actor.scale_y); actor.set_scale(1, 1); actor.set_pivot_point(0, 0); Tiling.animateWindow(metaWindow); Tweener.addTween(clone, { time: prefs.animation_time, scale_x: 1, scale_y: 1, onComplete: () => { space.moveDone() clone.set_pivot_point(0, 0) } }); space.targetX = space.cloneContainer.x; space.selectedWindow = metaWindow if (dndTarget.position) { space.layout(true, {customAllocators: {[dndTarget.position[0]]: Tiling.allocateEqualHeight}}); } else { space.layout(); } Tiling.move_to(space, metaWindow, {x: frame.x - space.monitor.x}) Tiling.ensureViewport(metaWindow, space); clone.raise_top() } else { Scratch.makeScratch(metaWindow); this.initialSpace.moveDone(); clone.set_scale(1, 1); clone.set_pivot_point(0, 0); Tweener.addTween(actor, { time: prefs.animation_time, scale_x: 1, scale_y: 1, onComplete: () => { actor.set_pivot_point(0, 0) } }); } } else if (this.initialSpace.indexOf(metaWindow) !== -1){ let space = this.initialSpace; space.targetX = space.cloneContainer.x; clone.targetX = frame.x - space.monitor.x - space.targetX; clone.targetY = frame.y - space.monitor.y; clone.set_position(clone.targetX, clone.targetY); actor.set_scale(1, 1); actor.set_pivot_point(0, 0); Tiling.animateWindow(metaWindow); Tweener.addTween(clone, { time: prefs.animation_time, scale_x: 1, scale_y: 1, onComplete: () => { space.moveDone() clone.set_pivot_point(0, 0) } }); Tiling.ensureViewport(metaWindow, space); } // NOTE: we reset window here so `window-added` will handle the window, // and layout will work correctly etc. this.window = null; this.initialSpace.layout(); let monitor = monitorAtPoint(gx, gy); let space = Tiling.spaces.monitors.get(monitor); // Make sure the window is on the correct workspace. // If the window is transient this will take care of its parent too. metaWindow.change_workspace(space.workspace) space.workspace.activate(global.get_current_time()); } activateDndTarget(zone, first) { zone.space.selection.hide(); this.dndTarget = zone; let params = { time: prefs.animation_time, [zone.originProp]: zone.center - zone.marginA, [zone.sizeProp]: zone.marginA + zone.marginB, }; if (first) { params.height = zone.actor.height params.y = zone.actor.y let actor = this.window.get_compositor_private(); let space = zone.space; zone.actor.set_position(...space.globalToScroll(...actor.get_transformed_position())) zone.actor.set_size(...actor.get_transformed_size()) } else { zone.actor[zone.sizeProp] = 0; zone.actor[zone.originProp] = zone.center; } zone.actor.show(); zone.actor.raise_top(); Tweener.addTween(zone.actor, params); } deactivateDndTarget(zone) { zone.space.selection.show(); if (zone) { Tweener.addTween(zone.actor, { time: prefs.animation_time, [zone.originProp]: zone.center, [zone.sizeProp]: 0, onComplete: () => zone.actor.hide() }); } this.dndTarget = null; } } var ResizeGrab = class ResizeGrab { constructor(metaWindow, type) { this.window = metaWindow; this.signals = new Utils.Signals(); this.space = Tiling.spaces.spaceOfWindow(metaWindow); if (this.space.indexOf(metaWindow) === -1) return; this.scrollAnchor = metaWindow.clone.targetX + this.space.monitor.x; this.signals.connect(metaWindow, 'size-changed', () => { metaWindow._targetWidth = null; metaWindow._targetHeight = null; let frame = metaWindow.get_frame_rect(); this.space.targetX = frame.x - this.scrollAnchor; this.space.cloneContainer.x = this.space.targetX; this.space.layout(false); }) } end() { this.signals.destroy(); this.window = null; this.space.layout(); } }