diff --git a/browser.py b/browser.py index d180932..164edb0 100755 --- a/browser.py +++ b/browser.py @@ -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) - 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 + if self.bm_panel.isVisible(): + self.bm_panel.hide() + else: + self.bm_panel.rebuild(load_bm()) + self.bm_panel.show() + self.bm_panel.raise_() def open_manager(self): d = ManagerDialog(self)