From a1a4b2e10c243dd7b5cfc260770a5de8c17f2f43 Mon Sep 17 00:00:00 2001 From: jgitta Date: Fri, 22 May 2026 00:50:47 +0000 Subject: [PATCH] Redesign toolbar: single row with icon buttons and hamburger bookmark menu --- browser.py | 209 ++++++++++++++++++++--------------------------------- 1 file changed, 79 insertions(+), 130 deletions(-) diff --git a/browser.py b/browser.py index 95c43fb..408324f 100755 --- a/browser.py +++ b/browser.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """ -SeniorNet Kiosk — PyQt5 floating toolbar + bookmarks bar over Google Chrome. +SeniorNet Kiosk — PyQt5 single-row floating toolbar over Google Chrome. +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 @@ -16,22 +17,19 @@ from PyQt5.QtWidgets import ( HOME_URL = "file:///home/jgitta/kiosk-home/index.html" BOOKMARKS_FILE = "/home/jgitta/kiosk-browser/bookmarks.json" CDP_PORT = 9222 -TOOLBAR_H = 82 -BM_BAR_H = 60 -TOP_H = TOOLBAR_H + BM_BAR_H # 142 px overlay height +TOOLBAR_H = 72 # single row — was 142 (82 + 60) +TOP_H = TOOLBAR_H SCREEN_W = 1920 SCREEN_H = 1080 # ── Colour palette ──────────────────────────────────────────────── TOOLBAR_COLOR = "#1a3a5c" -BAR_COLOR = "#162f4a" BTN_COLOR = "#2e6da4"; BTN_HOVER = "#3a87cc" GOLD = "#e8a020"; GOLD_HOVER = "#f0b030" GREEN = "#2e7d32"; GREEN_HOVER = "#388e3c" RED = "#c62828"; RED_HOVER = "#d32f2f" AMBER = "#7a5a10"; AMBER_HOVER = "#9a7a20" PURPLE = "#5a5a8a"; PURPLE_HOVER = "#7a7aaa" -FOLDER_BG = "#3a4a6a"; FOLDER_HOVER = "#4a5a7a" FONT_SIZE = 18 MENU_STYLE = """ @@ -39,7 +37,7 @@ MENU_STYLE = """ background: #1e3a5c; color: white; border: 2px solid #3a6da4; border-radius: 8px; padding: 4px; } - QMenu::item { padding: 12px 32px 12px 14px; border-radius: 6px; font-size: 16px; } + QMenu::item { padding: 14px 36px 14px 16px; border-radius: 6px; font-size: 17px; } QMenu::item:selected { background: #3a6da4; color: white; } QMenu::separator { height: 1px; background: #3a6da4; margin: 4px 8px; } """ @@ -59,6 +57,20 @@ def mk_btn(label, color, hover, min_w=120, min_h=52, fs=FONT_SIZE): """) return b +def mk_icon_btn(icon, color, hover, size=58, fs=26): + """Square icon button for Back / Forward / Home / Hamburger.""" + b = QPushButton(icon) + b.setFont(QFont("Arial", fs)) + b.setFixedSize(size, size) + b.setCursor(Qt.PointingHandCursor) + b.setStyleSheet(f""" + QPushButton {{ background:{color}; color:white; border:none; + border-radius:12px; }} + QPushButton:hover {{ background:{hover}; }} + QPushButton:pressed {{ background:#222; }} + """) + return b + def load_bm(): try: with open(BOOKMARKS_FILE) as f: return json.load(f) @@ -105,7 +117,7 @@ class CDP: try: ws = websocket.create_connection(self._ws_url(), timeout=3) ws.send(json.dumps({"id": 1, "method": method, - **({"params": params} if params else {})})) + **( {"params": params} if params else {})})) ws.recv() ws.close() except Exception as e: @@ -341,104 +353,6 @@ class ManagerDialog(QDialog): self.tree.setCurrentItem(sel); self._commit() -# ── Bookmarks Bar ───────────────────────────────────────────────── -class BookmarksBar(QWidget): - def __init__(self, nav_cb): - super().__init__() - self.nav_cb = nav_cb - self.setFixedHeight(BM_BAR_H) - self.setStyleSheet(f"background:{BAR_COLOR};") - - outer = QHBoxLayout(self) - outer.setContentsMargins(6, 5, 6, 5); outer.setSpacing(4) - - self.left_btn = self._arrow("◀") - self.left_btn.clicked.connect(lambda: self._scroll(-240)) - outer.addWidget(self.left_btn) - - self.scroll_area = QScrollArea() - self.scroll_area.setWidgetResizable(True) - self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.scroll_area.setFrameShape(QFrame.NoFrame) - self.scroll_area.setStyleSheet(f"background:{BAR_COLOR}; border:none;") - - self.inner = QWidget() - self.inner.setStyleSheet(f"background:{BAR_COLOR};") - self.row = QHBoxLayout(self.inner) - self.row.setContentsMargins(4, 0, 4, 0); self.row.setSpacing(6) - self.scroll_area.setWidget(self.inner) - outer.addWidget(self.scroll_area) - - self.right_btn = self._arrow("▶") - self.right_btn.clicked.connect(lambda: self._scroll(240)) - outer.addWidget(self.right_btn) - - self.reload() - - def _arrow(self, label): - b = QPushButton(label) - b.setFixedSize(38, 42); b.setFont(QFont("Arial", 14, QFont.Bold)) - b.setCursor(Qt.PointingHandCursor) - b.setStyleSheet("QPushButton { background:#2a4a6c; color:white; border:none;" - " border-radius:8px; } QPushButton:hover { background:#3a6da4; }") - return b - - def reload(self): - while self.row.count(): - w = self.row.takeAt(0).widget() - if w: w.deleteLater() - for item in load_bm(): - b = self._folder_btn(item) if is_folder(item) else self._bm_btn(item["name"], item["url"]) - self.row.addWidget(b) - self.row.addStretch() - - def _bm_btn(self, name, url): - b = QPushButton(name) - b.setFont(QFont("Arial", 15)); b.setMinimumHeight(44); b.setMinimumWidth(100) - b.setCursor(Qt.PointingHandCursor); b.setToolTip(url) - b.setStyleSheet("QPushButton { background:#2e5c8a; color:white; border:none;" - " border-radius:8px; padding:4px 12px; }" - "QPushButton:hover { background:#3a7abf; }" - "QPushButton:pressed { background:#1a3a5c; }") - b.clicked.connect(lambda _, u=url: self.nav_cb(u)) - return b - - def _folder_btn(self, folder): - b = QPushButton(f"+ {folder['name']}") - b.setFont(QFont("Arial", 15)); b.setMinimumHeight(44); b.setMinimumWidth(110) - b.setCursor(Qt.PointingHandCursor) - b.setStyleSheet(f"QPushButton {{ background:{FOLDER_BG}; color:white; border:none;" - f" border-radius:8px; padding:4px 14px; }}" - f"QPushButton:hover {{ background:{FOLDER_HOVER}; }}" - f"QPushButton:pressed {{ background:#2a3a5a; }}") - b.clicked.connect(lambda _, f=folder, bb=b: self._show_menu(f, bb)) - return b - - def _show_menu(self, folder, button): - self._build_menu(folder["children"]).exec_( - button.mapToGlobal(button.rect().bottomLeft())) - - def _build_menu(self, items): - menu = QMenu(); menu.setFont(QFont("Arial", 16)); 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", 16)); sub.setStyleSheet(MENU_STYLE) - menu.addMenu(sub) - else: - act = QAction(f" {item['name']}", menu) - url = item["url"] - act.triggered.connect(lambda _, u=url: self.nav_cb(u)) - menu.addAction(act) - return menu - - def _scroll(self, dx): - sb = self.scroll_area.horizontalScrollBar() - sb.setValue(sb.value() + dx) - - # ── Floating toolbar overlay ────────────────────────────────────── class KioskOverlay(QWidget): def __init__(self, cdp): @@ -457,42 +371,82 @@ class KioskOverlay(QWidget): vbox = QVBoxLayout(self) vbox.setContentsMargins(0, 0, 0, 0); vbox.setSpacing(0) vbox.addWidget(self._build_toolbar()) - self.bm_bar = BookmarksBar(cdp.navigate) - vbox.addWidget(self.bm_bar) self.show() def _build_toolbar(self): bar = QWidget(); bar.setFixedHeight(TOOLBAR_H) bar.setStyleSheet(f"background:{TOOLBAR_COLOR};") - tl = QHBoxLayout(bar); tl.setContentsMargins(16, 8, 16, 8); tl.setSpacing(12) + tl = QHBoxLayout(bar) + tl.setContentsMargins(16, 7, 16, 7); tl.setSpacing(10) + # Brand label brand = QLabel("SeniorNet") - brand.setFont(QFont("Arial", 24, QFont.Bold)) + brand.setFont(QFont("Arial", 22, QFont.Bold)) brand.setStyleSheet("color:white;") - tl.addWidget(brand); tl.addStretch() - - back_btn = mk_btn("< Back", BTN_COLOR, BTN_HOVER, 120, 62) - fwd_btn = mk_btn("Forward >", BTN_COLOR, BTN_HOVER, 140, 62) - home_btn = mk_btn("HOME", GOLD, GOLD_HOVER, 120, 62) - mgr_btn = mk_btn("Bookmarks", PURPLE, PURPLE_HOVER, 150, 62) + tl.addWidget(brand) + tl.addStretch() + # ◀ Back + back_btn = mk_icon_btn("◀", BTN_COLOR, BTN_HOVER) + back_btn.setToolTip("Go Back") back_btn.clicked.connect(self.cdp.back) - fwd_btn.clicked.connect(self.cdp.forward) - home_btn.clicked.connect(lambda: self.cdp.navigate(HOME_URL)) - mgr_btn.clicked.connect(self.open_manager) + tl.addWidget(back_btn) + + # ▶ Forward + fwd_btn = mk_icon_btn("▶", BTN_COLOR, BTN_HOVER) + fwd_btn.setToolTip("Go Forward") + fwd_btn.clicked.connect(self.cdp.forward) + tl.addWidget(fwd_btn) + + # ⌂ Home + home_btn = mk_icon_btn("⌂", GOLD, GOLD_HOVER) + home_btn.setToolTip("Go Home") + home_btn.clicked.connect(lambda: self.cdp.navigate(HOME_URL)) + tl.addWidget(home_btn) + + # ☰ Hamburger — bookmarks menu + self.burger_btn = mk_icon_btn("☰", PURPLE, PURPLE_HOVER) + self.burger_btn.setToolTip("Bookmarks") + self.burger_btn.clicked.connect(self._show_bookmarks_menu) + tl.addWidget(self.burger_btn) - for b in [back_btn, fwd_btn, home_btn, mgr_btn]: tl.addWidget(b) return bar + def _show_bookmarks_menu(self): + 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) + # Show menu below the hamburger button + menu.exec_(self.burger_btn.mapToGlobal( + self.burger_btn.rect().bottomLeft())) + + 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 + def open_manager(self): - d = ManagerDialog(self); d.exec_() - self.bm_bar.reload() + d = ManagerDialog(self) + d.exec_() def keyPressEvent(self, e): - # Home key -> home screen if e.key() == Qt.Key_Home: self.cdp.navigate(HOME_URL) - # Block all other dangerous keys silently e.accept() def eventFilter(self, obj, event): @@ -500,15 +454,13 @@ class KioskOverlay(QWidget): if event.type() == QEvent.KeyPress: key = event.key() mods = event.modifiers() - # Block Alt+F4, Ctrl+W, Ctrl+T, Ctrl+N, Ctrl+Tab, F11, Escape blocked_keys = {Qt.Key_F4, Qt.Key_W, Qt.Key_T, Qt.Key_N, Qt.Key_Tab, Qt.Key_F11, Qt.Key_Escape, Qt.Key_F1, Qt.Key_F2, Qt.Key_F3, Qt.Key_F5, Qt.Key_F6, Qt.Key_F10} if mods & (Qt.AltModifier | Qt.ControlModifier): if key in blocked_keys: - return True # swallow the event - # Also block bare Escape and F-keys + return True if key in {Qt.Key_Escape, Qt.Key_F11}: return True return super().eventFilter(obj, event) @@ -520,12 +472,10 @@ def launch_chrome(): profile_dir = pathlib.Path("/home/jgitta/.config/google-chrome-kiosk") profile_dir.mkdir(parents=True, exist_ok=True) - # Remove stale singleton locks for name in ("SingletonLock", "SingletonSocket", "SingletonCookie"): try: (profile_dir / name).unlink() except FileNotFoundError: pass - # Wipe GPU/ML caches that corrupt across restarts for rel in ("GraphiteDawnCache", "GrShaderCache", "ShaderCache", "OptGuideOnDeviceClassifierModel", "OptGuideOnDeviceModel", "optimization_guide_model_store", @@ -535,7 +485,6 @@ def launch_chrome(): if p.exists(): shutil.rmtree(p, ignore_errors=True) - # Patch Local State ls_path = profile_dir / "Local State" if ls_path.exists(): try: