v0.06: replace QMenu with custom BookmarkPanel widget

QMenu has z-order and event-grab issues under Openbox/X11 — the menu was
being created successfully but either appeared behind Chrome or was
dismissed instantly by the mouse-release event.

Replace with BookmarkPanel: a QWidget using Qt.Tool | WindowStaysOnTopHint
| FramelessWindowHint (same flags as the toolbar itself). Since the toolbar
already stays above Chrome, the panel does too. Features:
- Scrollable list of all bookmarks with folder headers
- Closes when clicking anywhere outside
- Manage Bookmarks entry at the bottom
- Toggle open/close on hamburger button
This commit is contained in:
2026-05-21 20:39:59 -05:00
parent 2ca1da4f8e
commit e574403853
+117 -40
View File
@@ -353,6 +353,117 @@ class ManagerDialog(QDialog):
self.tree.setCurrentItem(sel); self._commit()
# ── Bookmark dropdown panel ───────────────────────────────────────
class BookmarkPanel(QWidget):
"""
Custom dropdown that replaces QMenu for bookmarks.
Uses the same Qt.Tool | WindowStaysOnTopHint flags as the toolbar so it
always appears above Chrome, unlike QMenu which has z-order issues.
"""
def __init__(self, cdp, overlay):
super().__init__()
self.cdp = cdp
self.overlay = overlay
self.setWindowFlags(
Qt.Tool | Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
self.setAttribute(Qt.WA_ShowWithoutActivating)
self.setStyleSheet(f"""
QWidget {{ background: #1e3a5c; }}
QPushButton {{ background: transparent; color: white; border: none;
text-align: left; padding: 8px 20px;
font-size: 17px; border-radius: 6px; }}
QPushButton:hover {{ background: #3a6da4; }}
QPushButton:pressed {{ background: #222; }}
QLabel {{ color: #aac8e8; padding: 6px 20px 2px 20px;
font-size: 14px; font-weight: bold; }}
""")
QApplication.instance().installEventFilter(self)
def rebuild(self, items):
# Remove old layout
old = self.layout()
if old:
while old.count():
w = old.takeAt(0).widget()
if w:
w.deleteLater()
QWidget().setLayout(old)
scroll_content = QWidget()
scroll_content.setStyleSheet("background: #1e3a5c;")
vbox = QVBoxLayout(scroll_content)
vbox.setContentsMargins(6, 6, 6, 6)
vbox.setSpacing(1)
self._add_items(vbox, items, depth=0)
sep = QFrame()
sep.setFrameShape(QFrame.HLine)
sep.setStyleSheet("background: #3a6da4; margin: 4px 8px;")
sep.setFixedHeight(1)
vbox.addWidget(sep)
mgr = QPushButton("⚙ Manage Bookmarks…")
mgr.setFont(QFont("Arial", 16))
mgr.setMinimumHeight(48)
mgr.setCursor(Qt.PointingHandCursor)
mgr.clicked.connect(self._on_manage)
vbox.addWidget(mgr)
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
scroll.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
scroll.setStyleSheet("background: transparent; border: none;")
scroll.setWidget(scroll_content)
outer = QVBoxLayout(self)
outer.setContentsMargins(2, 2, 2, 2)
outer.setSpacing(0)
outer.addWidget(scroll)
# Size: cap height at 80 % of screen below toolbar
scroll_content.adjustSize()
ph = scroll_content.sizeHint().height() + 16
pw = max(scroll_content.sizeHint().width() + 16, 340)
max_h = SCREEN_H - TOOLBAR_H - 20
self.resize(pw, min(ph, max_h))
self.move(SCREEN_W - pw - 4, TOOLBAR_H)
def _add_items(self, vbox, items, depth):
indent = " " * depth
for item in items:
if is_folder(item):
lbl = QLabel(f"{indent}{item['name']}")
lbl.setFont(QFont("Arial", 14, QFont.Bold))
vbox.addWidget(lbl)
self._add_items(vbox, item["children"], depth + 1)
else:
btn = QPushButton(f"{indent} {item['name']}")
btn.setFont(QFont("Arial", 17))
btn.setMinimumHeight(46)
btn.setCursor(Qt.PointingHandCursor)
url = item["url"]
btn.clicked.connect(lambda _, u=url: self._nav(u))
vbox.addWidget(btn)
def _nav(self, url):
self.hide()
self.cdp.navigate(url)
def _on_manage(self):
self.hide()
self.overlay.open_manager()
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):
self.hide()
return False
# ── Floating toolbar overlay ──────────────────────────────────────
class KioskOverlay(QWidget):
def __init__(self, cdp):
@@ -371,6 +482,7 @@ class KioskOverlay(QWidget):
vbox = QVBoxLayout(self)
vbox.setContentsMargins(0, 0, 0, 0); vbox.setSpacing(0)
vbox.addWidget(self._build_toolbar())
self.bm_panel = BookmarkPanel(cdp, self)
self.show()
def _build_toolbar(self):
@@ -413,47 +525,12 @@ class KioskOverlay(QWidget):
return bar
def _show_bookmarks_menu(self):
try:
menu = self._build_menu(load_bm())
menu.addSeparator()
mgr_act = QAction(" ⚙ Manage Bookmarks…", menu)
mgr_act.triggered.connect(self.open_manager)
menu.addAction(mgr_act)
# The toolbar has WA_ShowWithoutActivating, so it never steals
# focus from Chrome. QMenu.exec_() needs the window to be active
# to capture mouse/keyboard events; without this the menu flashes
# and vanishes immediately.
self.activateWindow()
self.raise_()
QApplication.instance().processEvents()
# Position right-aligned just below the toolbar.
mw = menu.sizeHint().width()
if mw < 100:
mw = 360
x = max(0, SCREEN_W - mw - 6)
menu.exec_(QPoint(x, TOOLBAR_H))
except Exception as e:
print(f"Bookmarks menu error: {e}", flush=True)
def _build_menu(self, items):
menu = QMenu()
menu.setFont(QFont("Arial", 17))
menu.setStyleSheet(MENU_STYLE)
for item in items:
if is_folder(item):
sub = self._build_menu(item["children"])
sub.setTitle(f"{item['name']}")
sub.setFont(QFont("Arial", 17))
sub.setStyleSheet(MENU_STYLE)
menu.addMenu(sub)
if self.bm_panel.isVisible():
self.bm_panel.hide()
else:
act = QAction(f" {item['name']}", menu)
url = item["url"]
act.triggered.connect(lambda _, u=url: self.cdp.navigate(u))
menu.addAction(act)
return menu
self.bm_panel.rebuild(load_bm())
self.bm_panel.show()
self.bm_panel.raise_()
def open_manager(self):
d = ManagerDialog(self)