2024-10-16 21:44:45 -04:00
|
|
|
import { subprocess } from 'astal';
|
|
|
|
import { App } from 'astal/gtk3';
|
|
|
|
import GObject, { register, signal } from 'astal/gobject';
|
|
|
|
|
2024-10-22 13:09:39 -04:00
|
|
|
import AstalIO from 'gi://AstalIO';
|
2024-10-16 21:44:45 -04:00
|
|
|
|
|
|
|
import { hyprMessage } from '../lib';
|
|
|
|
|
|
|
|
const ON_RELEASE_TRIGGERS = [
|
|
|
|
'released',
|
|
|
|
'TOUCH_UP',
|
|
|
|
'HOLD_END',
|
|
|
|
];
|
|
|
|
const ON_CLICK_TRIGGERS = [
|
|
|
|
'pressed',
|
|
|
|
'TOUCH_DOWN',
|
|
|
|
];
|
|
|
|
|
|
|
|
/* Types */
|
2024-10-30 18:53:15 -04:00
|
|
|
import PopupWindow from '../widgets/misc/popup-window';
|
2024-10-16 21:44:45 -04:00
|
|
|
import { CursorPos, Layer, LayerResult } from '../lib';
|
|
|
|
|
|
|
|
|
|
|
|
@register()
|
|
|
|
export default class MonitorClicks extends GObject.Object {
|
|
|
|
@signal(Boolean)
|
|
|
|
declare procStarted: (state: boolean) => void;
|
|
|
|
|
|
|
|
@signal(Boolean)
|
|
|
|
declare procDestroyed: (state: boolean) => void;
|
|
|
|
|
|
|
|
@signal(String)
|
|
|
|
declare released: (procLine: string) => void;
|
|
|
|
|
|
|
|
@signal(String)
|
|
|
|
declare clicked: (procLine: string) => void;
|
|
|
|
|
|
|
|
private process = null as AstalIO.Process | null;
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
super();
|
|
|
|
this.#initAppConnection();
|
|
|
|
}
|
|
|
|
|
|
|
|
startProc() {
|
|
|
|
if (this.process) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.process = subprocess(
|
|
|
|
['libinput', 'debug-events'],
|
|
|
|
(output) => {
|
|
|
|
if (output.includes('cancelled')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ON_RELEASE_TRIGGERS.some((p) => output.includes(p))) {
|
|
|
|
MonitorClicks.detectClickedOutside('released');
|
|
|
|
this.emit('released', output);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (ON_CLICK_TRIGGERS.some((p) => output.includes(p))) {
|
|
|
|
MonitorClicks.detectClickedOutside('clicked');
|
|
|
|
this.emit('clicked', output);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
this.emit('proc-started', true);
|
|
|
|
}
|
|
|
|
|
|
|
|
killProc() {
|
|
|
|
if (this.process) {
|
|
|
|
this.process.kill();
|
|
|
|
this.process = null;
|
|
|
|
this.emit('proc-destroyed', true);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
#initAppConnection() {
|
|
|
|
App.connect('window-toggled', () => {
|
|
|
|
const anyVisibleAndClosable =
|
|
|
|
(App.get_windows() as PopupWindow[]).some((w) => {
|
|
|
|
const closable = w.close_on_unfocus &&
|
|
|
|
!(
|
|
|
|
w.close_on_unfocus === 'none' ||
|
|
|
|
w.close_on_unfocus === 'stay'
|
|
|
|
);
|
|
|
|
|
|
|
|
return w.visible && closable;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (anyVisibleAndClosable) {
|
|
|
|
this.startProc();
|
|
|
|
}
|
|
|
|
|
|
|
|
else {
|
|
|
|
this.killProc();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
static async detectClickedOutside(clickStage: string) {
|
|
|
|
const toClose = ((App.get_windows() as PopupWindow[])).some((w) => {
|
|
|
|
const closable = (
|
|
|
|
w.close_on_unfocus &&
|
|
|
|
w.close_on_unfocus === clickStage
|
|
|
|
);
|
|
|
|
|
|
|
|
return w.visible && closable;
|
|
|
|
});
|
|
|
|
|
|
|
|
if (!toClose) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2024-10-16 22:33:15 -04:00
|
|
|
try {
|
|
|
|
const layers = JSON.parse(await hyprMessage('j/layers')) as LayerResult;
|
|
|
|
const pos = JSON.parse(await hyprMessage('j/cursorpos')) as CursorPos;
|
|
|
|
|
|
|
|
Object.values(layers).forEach((key) => {
|
|
|
|
const overlayLayer = key.levels['3'];
|
|
|
|
|
|
|
|
if (overlayLayer) {
|
|
|
|
const noCloseWidgetsNames = [
|
|
|
|
'bar-',
|
|
|
|
'osk',
|
|
|
|
];
|
|
|
|
|
|
|
|
const getNoCloseWidgets = (names: string[]) => {
|
|
|
|
const arr = [] as Layer[];
|
|
|
|
|
|
|
|
names.forEach((name) => {
|
|
|
|
arr.push(
|
|
|
|
overlayLayer.find(
|
|
|
|
(n) => n.namespace.startsWith(name),
|
|
|
|
) ||
|
2024-10-28 14:45:10 -04:00
|
|
|
// Return an empty Layer if widget doesn't exist
|
|
|
|
{
|
|
|
|
address: '',
|
|
|
|
x: 0,
|
|
|
|
y: 0,
|
|
|
|
w: 0,
|
|
|
|
h: 0,
|
|
|
|
namespace: '',
|
|
|
|
},
|
2024-10-16 22:33:15 -04:00
|
|
|
);
|
|
|
|
});
|
2024-10-16 21:44:45 -04:00
|
|
|
|
2024-10-16 22:33:15 -04:00
|
|
|
return arr;
|
|
|
|
};
|
|
|
|
const clickIsOnWidget = (w: Layer) => {
|
2024-10-28 14:45:10 -04:00
|
|
|
return (
|
|
|
|
pos.x > w.x && pos.x < w.x + w.w &&
|
|
|
|
pos.y > w.y && pos.y < w.y + w.h
|
|
|
|
);
|
2024-10-16 22:33:15 -04:00
|
|
|
};
|
2024-10-16 21:44:45 -04:00
|
|
|
|
2024-10-16 22:33:15 -04:00
|
|
|
const noCloseWidgets = getNoCloseWidgets(noCloseWidgetsNames);
|
2024-10-16 21:44:45 -04:00
|
|
|
|
2024-10-16 22:33:15 -04:00
|
|
|
const widgets = overlayLayer.filter((n) => {
|
|
|
|
let window = null as null | PopupWindow;
|
2024-10-16 21:44:45 -04:00
|
|
|
|
2024-10-28 14:45:10 -04:00
|
|
|
if (App.get_windows().some((win) => win.name === n.namespace)) {
|
2024-10-16 22:33:15 -04:00
|
|
|
window = (App.get_window(n.namespace) as PopupWindow);
|
|
|
|
}
|
2024-10-16 21:44:45 -04:00
|
|
|
|
2024-10-16 22:33:15 -04:00
|
|
|
return window &&
|
2024-10-28 14:45:10 -04:00
|
|
|
window.close_on_unfocus &&
|
|
|
|
window.close_on_unfocus ===
|
|
|
|
clickStage;
|
2024-10-16 22:33:15 -04:00
|
|
|
});
|
2024-10-16 21:44:45 -04:00
|
|
|
|
2024-10-16 22:33:15 -04:00
|
|
|
if (noCloseWidgets.some(clickIsOnWidget)) {
|
2024-10-28 14:45:10 -04:00
|
|
|
// Don't handle clicks when on certain widgets
|
2024-10-16 22:33:15 -04:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
widgets.forEach((w) => {
|
2024-10-28 14:45:10 -04:00
|
|
|
if (!(
|
|
|
|
pos.x > w.x && pos.x < w.x + w.w &&
|
|
|
|
pos.y > w.y && pos.y < w.y + w.h
|
|
|
|
)) {
|
2024-10-16 22:33:15 -04:00
|
|
|
App.get_window(w.namespace)?.set_visible(false);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
2024-10-16 21:44:45 -04:00
|
|
|
}
|
2024-10-16 22:33:15 -04:00
|
|
|
});
|
|
|
|
}
|
|
|
|
catch (e) {
|
|
|
|
console.log(e);
|
|
|
|
}
|
2024-10-16 21:44:45 -04:00
|
|
|
}
|
|
|
|
}
|