Fix bookmarks menu dismiss, home Email URL, and deploy hygiene.
Close the hamburger panel on outside click/focus via X11 (xinput/xdotool) without a fullscreen overlay; point home Email to Roundcube; sync home-screen-index.html in deploy/install; remove credentials.md from git and document private LAN-only Gitea usage. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+97
-17
@@ -5,7 +5,7 @@ Navigation: ◀ ▶ ⌂ icon buttons + ☰ hamburger menu for bookmarks.
|
||||
Chrome is controlled via Chrome DevTools Protocol (CDP) over WebSocket.
|
||||
"""
|
||||
import sys, json, os, subprocess, time, threading, urllib.request
|
||||
from PyQt5.QtCore import Qt, QTimer, QPoint
|
||||
from PyQt5.QtCore import Qt, QTimer, QPoint, QRect
|
||||
from PyQt5.QtGui import QFont, QColor, QPalette
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QWidget, QHBoxLayout, QVBoxLayout,
|
||||
@@ -377,6 +377,43 @@ class ManagerDialog(QDialog):
|
||||
self.tree.setCurrentItem(sel); self._commit()
|
||||
|
||||
|
||||
# ── X11 helpers (clicks/focus on Chromium never reach Qt) ───────────
|
||||
def _x11_env():
|
||||
env = os.environ.copy()
|
||||
env.setdefault("DISPLAY", ":0")
|
||||
return env
|
||||
|
||||
|
||||
def _x11_run(cmd, timeout=0.4):
|
||||
try:
|
||||
r = subprocess.run(
|
||||
cmd, capture_output=True, text=True,
|
||||
timeout=timeout, env=_x11_env())
|
||||
return r.stdout.strip() if r.returncode == 0 else ""
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def _x11_active_window_id():
|
||||
out = _x11_run(["xdotool", "getactivewindow"])
|
||||
return int(out) if out.isdigit() else 0
|
||||
|
||||
|
||||
def _x11_left_button_down():
|
||||
"""True when the physical left mouse button is held (works across all apps)."""
|
||||
try:
|
||||
r = subprocess.run(
|
||||
["bash", "-c",
|
||||
"DISPLAY=:0 dev=$(xinput --list --short 2>/dev/null | "
|
||||
"awk '/slave.*pointer/ {print $2; exit}'); "
|
||||
"[ -n \"$dev\" ] && xinput query-state \"$dev\" 2>/dev/null | "
|
||||
"grep -q 'button\\[1\\]=down'"],
|
||||
timeout=0.3, env=_x11_env())
|
||||
return r.returncode == 0
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# ── Bookmark dropdown panel ───────────────────────────────────────
|
||||
class BookmarkPanel(QWidget):
|
||||
"""
|
||||
@@ -384,10 +421,15 @@ class BookmarkPanel(QWidget):
|
||||
Uses the same Qt.Tool | WindowStaysOnTopHint flags as the toolbar so it
|
||||
always appears above Chrome, unlike QMenu which has z-order issues.
|
||||
"""
|
||||
_OPEN_GRACE_SEC = 0.35
|
||||
|
||||
def __init__(self, cdp, overlay):
|
||||
super().__init__()
|
||||
self.cdp = cdp
|
||||
self.overlay = overlay
|
||||
self._shown_at = 0.0
|
||||
self._focus_at_open = 0
|
||||
self._our_window_ids = set()
|
||||
self.setWindowFlags(
|
||||
Qt.Tool | Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
|
||||
self.setAttribute(Qt.WA_ShowWithoutActivating)
|
||||
@@ -403,10 +445,8 @@ class BookmarkPanel(QWidget):
|
||||
""")
|
||||
QApplication.instance().installEventFilter(self)
|
||||
|
||||
# Timer: hide panel when user clicks anywhere outside it
|
||||
# (clicks on Chromium never reach Qt's eventFilter)
|
||||
self._click_guard = QTimer()
|
||||
self._click_guard.timeout.connect(self._check_outside_click)
|
||||
self._dismiss_timer = QTimer()
|
||||
self._dismiss_timer.timeout.connect(self._check_dismiss)
|
||||
|
||||
def rebuild(self, items):
|
||||
# Remove old layout
|
||||
@@ -459,6 +499,19 @@ class BookmarkPanel(QWidget):
|
||||
self.resize(pw, min(ph, max_h))
|
||||
self.move(SCREEN_W - pw - 4, TOOLBAR_H)
|
||||
|
||||
def _refresh_our_window_ids(self):
|
||||
ids = set()
|
||||
for w in (self, self.overlay):
|
||||
wid = int(w.winId())
|
||||
if wid:
|
||||
ids.add(wid)
|
||||
self._our_window_ids = ids
|
||||
|
||||
def _burger_global_rect(self):
|
||||
btn = self.overlay.burger_btn
|
||||
origin = btn.mapToGlobal(QPoint(0, 0))
|
||||
return QRect(origin, btn.size())
|
||||
|
||||
def _add_items(self, vbox, items, depth):
|
||||
indent = " " * depth
|
||||
for item in items:
|
||||
@@ -484,28 +537,53 @@ class BookmarkPanel(QWidget):
|
||||
self.hide()
|
||||
self.overlay.open_manager()
|
||||
|
||||
def hide(self):
|
||||
self._dismiss_timer.stop()
|
||||
super().hide()
|
||||
|
||||
def setVisible(self, visible):
|
||||
super().setVisible(visible)
|
||||
if visible:
|
||||
self._click_guard.start(100)
|
||||
self._shown_at = time.time()
|
||||
self._refresh_our_window_ids()
|
||||
self._focus_at_open = _x11_active_window_id()
|
||||
self.raise_()
|
||||
self.overlay.raise_()
|
||||
self._dismiss_timer.start(100)
|
||||
else:
|
||||
self._click_guard.stop()
|
||||
self._dismiss_timer.stop()
|
||||
|
||||
def _check_outside_click(self):
|
||||
"""Hide panel when a click lands outside it — even on the Chromium window."""
|
||||
def _cursor_outside_panel(self):
|
||||
from PyQt5.QtGui import QCursor
|
||||
pos = QCursor.pos()
|
||||
if self.frameGeometry().contains(pos):
|
||||
return False
|
||||
if self._burger_global_rect().contains(pos):
|
||||
return False
|
||||
return True
|
||||
|
||||
def _check_dismiss(self):
|
||||
"""Close when focus leaves or user clicks outside the panel."""
|
||||
if not self.isVisible():
|
||||
self._click_guard.stop()
|
||||
self._dismiss_timer.stop()
|
||||
return
|
||||
if QApplication.mouseButtons() & Qt.LeftButton:
|
||||
if not self.geometry().contains(QCursor.pos()):
|
||||
self.hide()
|
||||
if time.time() - self._shown_at < self._OPEN_GRACE_SEC:
|
||||
return
|
||||
|
||||
# Click outside (X11 — works even when Chromium has focus)
|
||||
if _x11_left_button_down() and self._cursor_outside_panel():
|
||||
self.hide()
|
||||
return
|
||||
|
||||
# Focus moved away from toolbar / menu (e.g. clicked the web page)
|
||||
active = _x11_active_window_id()
|
||||
if active and active != self._focus_at_open and active not in self._our_window_ids:
|
||||
self.hide()
|
||||
|
||||
def eventFilter(self, obj, event):
|
||||
from PyQt5.QtCore import QEvent
|
||||
if event.type() == QEvent.MouseButtonPress and self.isVisible():
|
||||
gp = event.globalPos()
|
||||
if not self.geometry().contains(gp):
|
||||
if not self.frameGeometry().contains(event.globalPos()):
|
||||
self.hide()
|
||||
return False
|
||||
|
||||
@@ -549,6 +627,9 @@ class KioskOverlay(QWidget):
|
||||
SCREEN_W, SCREEN_H = w, h
|
||||
self.setGeometry(0, 0, SCREEN_W, TOP_H)
|
||||
self.resize(SCREEN_W, TOP_H)
|
||||
if self.bm_panel.isVisible():
|
||||
pw = self.bm_panel.width()
|
||||
self.bm_panel.move(SCREEN_W - pw - 4, TOOLBAR_H)
|
||||
# Resize Chrome after brief delay so X has settled
|
||||
QTimer.singleShot(500, self._resize_chrome)
|
||||
|
||||
@@ -634,8 +715,7 @@ class KioskOverlay(QWidget):
|
||||
self.bm_panel.hide()
|
||||
else:
|
||||
self.bm_panel.rebuild(load_bm())
|
||||
self.bm_panel.show()
|
||||
self.bm_panel.raise_()
|
||||
self.bm_panel.setVisible(True)
|
||||
|
||||
def open_manager(self):
|
||||
d = ManagerDialog(self)
|
||||
|
||||
Reference in New Issue
Block a user