mirror of
https://github.com/gosticks/PaperWM.git
synced 2026-10-04 11:36:54 +00:00
wip 3 finger touchpad scrolling
This commit is contained in:
@@ -14,7 +14,7 @@ It will link the repo to `~/.local/share/gnome-shell-extensions/` where gnome-sh
|
||||
|
||||
## Usage ##
|
||||
|
||||
Most functionality is available using a mouse, eg. by clicking on a window at the edge of a monitor. But the primary focus is making an environment which works well with a keyboard.
|
||||
Most functionality is available using a mouse, eg. activating a window at the edge of the monitor by clicking on it. In wayland its possible to navigate with 3-finger swipes on the trackpad. But the primary focus is making an environment which works well with a keyboard.
|
||||
|
||||
All keybindings start with the <kbd>Super</kbd> modifier. On most keyboards it's the Windows key, on mac keyboards it's the Command key. It's possible and recommended to modify the keyboard layout so that <kbd>Super</kbd> is switched with <kbd>Alt</kbd> making all the keybindings easier to reach. This can be done through Gnome Tweaks under `Keybard & Mouse` ⟶ `Additional Layout Options` ⟶ `Alt/Win key behavior` ⟶ `Left Alt is swapped with Left Win`.
|
||||
|
||||
@@ -34,6 +34,8 @@ Activating a window will ensure it's fully visible, scrolling the tiling if nece
|
||||
|
||||
Pressing <kbd>Super</kbd><kbd>I</kbd> will move the window to the right below the active window, tiling them vertically in a column. <kbd>Super</kbd><kbd>O</kbd> will do the opposite, pushing the bottom window out of the current column.
|
||||
|
||||
Swiping the trackpad horizontally with three fingers will scroll the tiling (only available in Wayland).
|
||||
|
||||
<kbd>Alt</kbd><kbd>Tab</kbd> is of course also available.
|
||||
|
||||
| Keybindings | |
|
||||
@@ -67,6 +69,8 @@ The workspace name is shown in the top left corner replacing the `Activities` bu
|
||||
|
||||

|
||||
|
||||
Swiping the trackpad vertically with three fingers lets you navigate the workspace stack (only available in Wayland).
|
||||
|
||||
There's a single scrollable tiling per workspace. Adding another monitor simply makes it possible to have another workspace visible. The workspace stack is shared among all the monitors, windows being resized vertically as necessary when workspace is displayed on another monitor.
|
||||
|
||||
| Keybindings | |
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ var modules = [
|
||||
Extension.imports.stackoverlay, Extension.imports.app,
|
||||
Extension.imports.kludges, Extension.imports.topbar,
|
||||
Extension.imports.navigator, Extension.imports.settings,
|
||||
Extension.imports.keybindings,
|
||||
Extension.imports.keybindings, Extension.imports.gestures
|
||||
];
|
||||
var [ Tiling, Scratch, LiveAltTab,
|
||||
utils, StackOverlay,
|
||||
|
||||
+314
@@ -0,0 +1,314 @@
|
||||
var Extension = imports.misc.extensionUtils.extensions['paperwm@hedning:matrix.org'];
|
||||
var Meta = imports.gi.Meta;
|
||||
var St = imports.gi.St;
|
||||
var Gio = imports.gi.Gio;
|
||||
var PanelMenu = imports.ui.panelMenu;
|
||||
var PopupMenu = imports.ui.popupMenu;
|
||||
var Clutter = imports.gi.Clutter;
|
||||
var Main = imports.ui.main;
|
||||
var Tweener = imports.ui.tweener;
|
||||
var Gdk = imports.gi.Gdk;
|
||||
|
||||
var Utils = Extension.imports.utils;
|
||||
var Tiling = Extension.imports.tiling;
|
||||
var Navigator = Extension.imports.navigator;
|
||||
var prefs = Extension.imports.settings.prefs;
|
||||
|
||||
const stage = global.stage;
|
||||
|
||||
var signals;
|
||||
function init() {
|
||||
signals = new Utils.Signals();
|
||||
}
|
||||
|
||||
const DIRECTIONS = {
|
||||
Horizontal: true,
|
||||
Vertical: false,
|
||||
}
|
||||
|
||||
var vy;
|
||||
var time;
|
||||
var hState, vState;
|
||||
var navigator;
|
||||
var direction = undefined;
|
||||
function enable() {
|
||||
// Touchpad swipes only works in Wayland
|
||||
if (!Meta.is_wayland_compositor())
|
||||
return;
|
||||
|
||||
signals.destroy();
|
||||
/**
|
||||
In order for the space.background actors to get any input we need to hide
|
||||
all the window actors from the stage.
|
||||
|
||||
The stage takes care of scrolling vertically through the workspace mru.
|
||||
Delegating the horizontal scrolling to each space. This way vertical
|
||||
scrolling works anywhere, while horizontal scrolling is done on the space
|
||||
under the mouse cursor.
|
||||
*/
|
||||
signals.connect(stage, 'captured-event', (actor, event) => {
|
||||
if (event.type() !== Clutter.EventType.TOUCHPAD_SWIPE ||
|
||||
event.get_touchpad_gesture_finger_count() > 3) {
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
const phase = event.get_gesture_phase();
|
||||
switch (phase) {
|
||||
case Clutter.TouchpadGesturePhase.UPDATE:
|
||||
if (direction === DIRECTIONS.Horizontal) {
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
let [dx, dy] = event.get_gesture_motion_delta();
|
||||
if (direction === undefined) {
|
||||
log(`set direction ${dx} ${dy}`);
|
||||
if (Math.abs(dx) < Math.abs(dy)) {
|
||||
vy = 0;
|
||||
vState = phase;
|
||||
direction = DIRECTIONS.Vertical;
|
||||
}
|
||||
}
|
||||
if (direction === DIRECTIONS.Vertical) {
|
||||
updateVertical(-dy*2, event.get_time());
|
||||
return Clutter.EVENT_STOP;
|
||||
}
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
case Clutter.TouchpadGesturePhase.BEGIN:
|
||||
time = event.get_time();
|
||||
direction = undefined;
|
||||
navigator = Navigator.getNavigator();
|
||||
navigator.connect('destroy', () => {
|
||||
vState = -1;
|
||||
});
|
||||
return Clutter.EVENT_STOP;
|
||||
case Clutter.TouchpadGesturePhase.CANCEL:
|
||||
case Clutter.TouchpadGesturePhase.END:
|
||||
if (direction === DIRECTIONS.Vertical) {
|
||||
vState = phase;
|
||||
endVertical();
|
||||
return Clutter.EVENT_STOP;
|
||||
}
|
||||
};
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
});
|
||||
}
|
||||
|
||||
function disable() {
|
||||
signals.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
Handle scrolling horizontally in a space. The handler is meant to be
|
||||
connected from each space.background. See Tiling.space.constructor.
|
||||
*/
|
||||
function horizontalScroll(actor, event) {
|
||||
if (event.type() !== Clutter.EventType.TOUCHPAD_SWIPE ||
|
||||
event.get_touchpad_gesture_finger_count() > 3) {
|
||||
return Clutter.EVENT_PROPAGATE;
|
||||
}
|
||||
const phase = event.get_gesture_phase();
|
||||
switch (phase) {
|
||||
case Clutter.TouchpadGesturePhase.UPDATE:
|
||||
let [dx, dy] = event.get_gesture_motion_delta();
|
||||
if (direction === undefined) {
|
||||
actor.vx = 0;
|
||||
hState = phase;
|
||||
direction = DIRECTIONS.Horizontal;
|
||||
}
|
||||
return update(actor, -dx, event.get_time());
|
||||
case Clutter.TouchpadGesturePhase.CANCEL:
|
||||
case Clutter.TouchpadGesturePhase.END:
|
||||
hState = phase;
|
||||
return done(actor);
|
||||
}
|
||||
}
|
||||
|
||||
function update(actor, dx, t) {
|
||||
let space = actor.space;
|
||||
|
||||
let v = dx/(t - time);
|
||||
if (Number.isFinite(v)) {
|
||||
actor.vx = (actor.vx + v)/2;
|
||||
}
|
||||
time = t;
|
||||
space.targetX -= dx;
|
||||
space.cloneContainer.x = space.targetX;
|
||||
return Clutter.EVENT_STOP;
|
||||
}
|
||||
|
||||
function done(actor) {
|
||||
if (!Number.isFinite(actor.vx)) {
|
||||
log(`${actor.vx} is not finite`)
|
||||
navigator.finish();
|
||||
hState = -1;
|
||||
return Clutter.EVENT_STOP;
|
||||
}
|
||||
let test = actor.vx > 0 ?
|
||||
() => actor.vx < 0 :
|
||||
() => actor.vx > 0;
|
||||
|
||||
let space = actor.space;
|
||||
log(`space ${space.actor}`);
|
||||
|
||||
let glide = () => {
|
||||
if (hState < Clutter.TouchpadGesturePhase.END)
|
||||
return false;
|
||||
|
||||
if (test() ||
|
||||
space.targetX > 0 ||
|
||||
space.targetX + space.cloneContainer.width < space.width) {
|
||||
log(`end: ${space.targetX + space.cloneContainer.width} ${space.targetX} ${space.cloneContainer.width}`)
|
||||
focusWindowAtPointer(actor);
|
||||
space.cloneContainer.set_scale(1, 1);
|
||||
return false;
|
||||
}
|
||||
let dx = actor.vx*16;
|
||||
space.targetX -= dx;
|
||||
space.cloneContainer.x = space.targetX;
|
||||
actor.vx = actor.vx + (actor.vx > 0 ? -0.2 : 0.2);
|
||||
return true;
|
||||
};
|
||||
|
||||
imports.mainloop.timeout_add(16, glide, 0);
|
||||
return Clutter.EVENT_STOP;
|
||||
}
|
||||
|
||||
function focusWindowAtPointer(actor) {
|
||||
log(`focus at pointer`)
|
||||
let display = Gdk.Display.get_default();
|
||||
let deviceManager = display.get_device_manager();
|
||||
let pointer = deviceManager.get_client_pointer();
|
||||
let [$, x, y] = pointer.get_position();
|
||||
let space = actor.space;
|
||||
x -= space.monitor.x;
|
||||
y -= space.monitor.y;
|
||||
|
||||
space.targetX = Math.round(space.targetX);
|
||||
space.cloneContainer.x = space.targetX;
|
||||
|
||||
let gap = prefs.window_gap/2;
|
||||
let target;
|
||||
let selected = space.selectedWindow.clone;
|
||||
if (selected.x + space.targetX >= 0 &&
|
||||
selected.x + selected.width + space.targetX <= space.width) {
|
||||
log(`selected fully visible`);
|
||||
target = space.selectedWindow;
|
||||
}
|
||||
|
||||
if (!target) {
|
||||
for (let w of space.getWindows()) {
|
||||
let clone = w.clone;
|
||||
if (clone.x + space.targetX - gap <= x
|
||||
&& x <= clone.x + space.targetX + clone.width + gap) {
|
||||
target = w;
|
||||
log(`cursor over ${w.title}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target && Tiling.ensureViewport(target, space);
|
||||
if (space.cloneContainer.width < space.width) {
|
||||
target = target || space.selectedWindow;
|
||||
space.targetX = Math.round((space.width - (space.cloneContainer.width - gap))/2);
|
||||
Tweener.addTween(space.cloneContainer,
|
||||
{ x: space.targetX,
|
||||
time: 0.25,
|
||||
transition: 'easeInOutQuad',
|
||||
onComplete: space.moveDone.bind(space)
|
||||
});
|
||||
} else if (0 <= space.cloneContainer.x) {
|
||||
log(`last`)
|
||||
let first = space[0][0];
|
||||
target = target || first;
|
||||
// Tiling.ensureViewport(target, space);
|
||||
Tiling.move_to(space, first, {x: 0});
|
||||
} else if (space.targetX + space.cloneContainer.width <= space.width) {
|
||||
log(`last`)
|
||||
let last = space[space.length-1][0];
|
||||
target = target || last;
|
||||
Tiling.move_to(space, last, {x: space.width - last.clone.width});
|
||||
}
|
||||
|
||||
if (!Tiling.inPreview)
|
||||
Navigator.getNavigator().finish();
|
||||
}
|
||||
|
||||
var transition = 'easeOutQuad';
|
||||
function updateVertical(dy, t) {
|
||||
if (!Tiling.inPreview) {
|
||||
Tiling.spaces._initWorkspaceStack();
|
||||
}
|
||||
let selected = Tiling.spaces.selectedSpace;
|
||||
let monitor = navigator.monitor;
|
||||
let v = dy/(t - time);
|
||||
if (dy > 0
|
||||
&& selected !== navigator.from
|
||||
&& (selected.actor.y - dy < 0.035*monitor.height)
|
||||
) {
|
||||
dy = 0;
|
||||
vy = 1;
|
||||
selected.actor.y = 0.035*selected.height;
|
||||
Tiling.spaces.selectSpace(Meta.MotionDirection.UP, false, transition);
|
||||
selected = Tiling.spaces.selectedSpace;
|
||||
Tweener.removeTweens(selected.actor);
|
||||
Tweener.addTween(selected.actor,
|
||||
{scale_x: 0.9, scale_y: 0.9, time: 0.25, transition});
|
||||
} else if (dy < 0
|
||||
&& (selected.actor.y - dy > 0.95*monitor.height)) {
|
||||
dy = 0;
|
||||
vy = -1;
|
||||
selected.actor.y = 0.95*selected.height;
|
||||
Tiling.spaces.selectSpace(Meta.MotionDirection.DOWN, false, transition);
|
||||
selected = Tiling.spaces.selectedSpace;
|
||||
Tweener.removeTweens(selected.actor);
|
||||
Tweener.addTween(selected.actor,
|
||||
{scale_x: 0.9, scale_y: 0.9, time: 0.25, transition});
|
||||
} else if (Number.isFinite(v)) {
|
||||
vy = (v + vy)/2;
|
||||
}
|
||||
|
||||
selected.actor.y -= dy;
|
||||
if (selected === navigator.from) {
|
||||
let scale = 0.90;
|
||||
let s = 1 - (1 - scale)*(selected.actor.y/(0.1*monitor.height));
|
||||
s = Math.max(s, scale);
|
||||
Tweener.removeTweens(selected.actor);
|
||||
selected.actor.set_scale(s, s);
|
||||
}
|
||||
}
|
||||
|
||||
function endVertical() {
|
||||
let test = vy > 0 ?
|
||||
() => vy < 0 :
|
||||
() => vy > 0;
|
||||
|
||||
let glide = () => {
|
||||
if (vState < Clutter.TouchpadGesturePhase.END)
|
||||
return false;
|
||||
|
||||
if (!Number.isFinite(vy)) {
|
||||
log(`vertical velocity isn't finite`);
|
||||
return false;
|
||||
}
|
||||
|
||||
let selected = Tiling.spaces.selectedSpace;
|
||||
let y = selected.actor.y;
|
||||
if (selected === navigator.from && y <= 0.1*selected.height) {
|
||||
log(`finish ${y}`);
|
||||
navigator.finish();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (test()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let friction = 0.05;
|
||||
let dy = vy*16;
|
||||
let v = vy;
|
||||
updateVertical(dy, time + 16);
|
||||
vy = vy + (v > 0 ? -0.1 : 0.1);
|
||||
return true; // repeat
|
||||
};
|
||||
|
||||
imports.mainloop.timeout_add(16, glide, 0);
|
||||
}
|
||||
@@ -153,6 +153,7 @@ class Space extends Array {
|
||||
reactive: true // Disable the background menu
|
||||
}, meta_display)
|
||||
);
|
||||
background.space = this;
|
||||
|
||||
// This kills all input on X11, but works on wayland...
|
||||
if (Meta.is_wayland_compositor()) {
|
||||
@@ -164,6 +165,11 @@ class Space extends Array {
|
||||
spaces.selectedSpace = this;
|
||||
nav.finish();
|
||||
});
|
||||
|
||||
this.signals.connect(background, 'captured-event',
|
||||
(actor, event) => {
|
||||
Extension.imports.gestures.horizontalScroll(actor, event);
|
||||
});
|
||||
}
|
||||
this.background = background;
|
||||
this.shadow = new St.Widget();;
|
||||
@@ -280,6 +286,7 @@ class Space extends Array {
|
||||
}
|
||||
}
|
||||
this._inLayout = false;
|
||||
this.cloneContainer.width = x - gap;
|
||||
|
||||
let width = x - gap;
|
||||
this.cloneContainer.width = width;
|
||||
@@ -1052,6 +1059,8 @@ class Spaces extends Map {
|
||||
}
|
||||
|
||||
_initWorkspaceStack() {
|
||||
if (inPreview)
|
||||
return;
|
||||
const scale = 0.9;
|
||||
let space = this.spaceOf(workspaceManager.get_active_workspace());
|
||||
let mru = [space, ...this.stack];
|
||||
|
||||
Reference in New Issue
Block a user