Redesign toolbar: single row with icon buttons and hamburger bookmark menu

This commit is contained in:
2026-05-22 00:50:47 +00:00
parent 02b44e3972
commit a1a4b2e10c
+79 -130
View File
@@ -1,6 +1,7 @@
#!/usr/bin/env python3 #!/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. Chrome is controlled via Chrome DevTools Protocol (CDP) over WebSocket.
""" """
import sys, json, os, subprocess, time, threading, urllib.request 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" HOME_URL = "file:///home/jgitta/kiosk-home/index.html"
BOOKMARKS_FILE = "/home/jgitta/kiosk-browser/bookmarks.json" BOOKMARKS_FILE = "/home/jgitta/kiosk-browser/bookmarks.json"
CDP_PORT = 9222 CDP_PORT = 9222
TOOLBAR_H = 82 TOOLBAR_H = 72 # single row — was 142 (82 + 60)
BM_BAR_H = 60 TOP_H = TOOLBAR_H
TOP_H = TOOLBAR_H + BM_BAR_H # 142 px overlay height
SCREEN_W = 1920 SCREEN_W = 1920
SCREEN_H = 1080 SCREEN_H = 1080
# ── Colour palette ──────────────────────────────────────────────── # ── Colour palette ────────────────────────────────────────────────
TOOLBAR_COLOR = "#1a3a5c" TOOLBAR_COLOR = "#1a3a5c"
BAR_COLOR = "#162f4a"
BTN_COLOR = "#2e6da4"; BTN_HOVER = "#3a87cc" BTN_COLOR = "#2e6da4"; BTN_HOVER = "#3a87cc"
GOLD = "#e8a020"; GOLD_HOVER = "#f0b030" GOLD = "#e8a020"; GOLD_HOVER = "#f0b030"
GREEN = "#2e7d32"; GREEN_HOVER = "#388e3c" GREEN = "#2e7d32"; GREEN_HOVER = "#388e3c"
RED = "#c62828"; RED_HOVER = "#d32f2f" RED = "#c62828"; RED_HOVER = "#d32f2f"
AMBER = "#7a5a10"; AMBER_HOVER = "#9a7a20" AMBER = "#7a5a10"; AMBER_HOVER = "#9a7a20"
PURPLE = "#5a5a8a"; PURPLE_HOVER = "#7a7aaa" PURPLE = "#5a5a8a"; PURPLE_HOVER = "#7a7aaa"
FOLDER_BG = "#3a4a6a"; FOLDER_HOVER = "#4a5a7a"
FONT_SIZE = 18 FONT_SIZE = 18
MENU_STYLE = """ MENU_STYLE = """
@@ -39,7 +37,7 @@ MENU_STYLE = """
background: #1e3a5c; color: white; background: #1e3a5c; color: white;
border: 2px solid #3a6da4; border-radius: 8px; padding: 4px; 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::item:selected { background: #3a6da4; color: white; }
QMenu::separator { height: 1px; background: #3a6da4; margin: 4px 8px; } 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 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(): def load_bm():
try: try:
with open(BOOKMARKS_FILE) as f: return json.load(f) with open(BOOKMARKS_FILE) as f: return json.load(f)
@@ -105,7 +117,7 @@ class CDP:
try: try:
ws = websocket.create_connection(self._ws_url(), timeout=3) ws = websocket.create_connection(self._ws_url(), timeout=3)
ws.send(json.dumps({"id": 1, "method": method, ws.send(json.dumps({"id": 1, "method": method,
**({"params": params} if params else {})})) **( {"params": params} if params else {})}))
ws.recv() ws.recv()
ws.close() ws.close()
except Exception as e: except Exception as e:
@@ -341,104 +353,6 @@ class ManagerDialog(QDialog):
self.tree.setCurrentItem(sel); self._commit() 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 ────────────────────────────────────── # ── Floating toolbar overlay ──────────────────────────────────────
class KioskOverlay(QWidget): class KioskOverlay(QWidget):
def __init__(self, cdp): def __init__(self, cdp):
@@ -457,42 +371,82 @@ class KioskOverlay(QWidget):
vbox = QVBoxLayout(self) vbox = QVBoxLayout(self)
vbox.setContentsMargins(0, 0, 0, 0); vbox.setSpacing(0) vbox.setContentsMargins(0, 0, 0, 0); vbox.setSpacing(0)
vbox.addWidget(self._build_toolbar()) vbox.addWidget(self._build_toolbar())
self.bm_bar = BookmarksBar(cdp.navigate)
vbox.addWidget(self.bm_bar)
self.show() self.show()
def _build_toolbar(self): def _build_toolbar(self):
bar = QWidget(); bar.setFixedHeight(TOOLBAR_H) bar = QWidget(); bar.setFixedHeight(TOOLBAR_H)
bar.setStyleSheet(f"background:{TOOLBAR_COLOR};") 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 = QLabel("SeniorNet")
brand.setFont(QFont("Arial", 24, QFont.Bold)) brand.setFont(QFont("Arial", 22, QFont.Bold))
brand.setStyleSheet("color:white;") brand.setStyleSheet("color:white;")
tl.addWidget(brand); tl.addStretch() 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)
# ◀ Back
back_btn = mk_icon_btn("", BTN_COLOR, BTN_HOVER)
back_btn.setToolTip("Go Back")
back_btn.clicked.connect(self.cdp.back) back_btn.clicked.connect(self.cdp.back)
fwd_btn.clicked.connect(self.cdp.forward) tl.addWidget(back_btn)
home_btn.clicked.connect(lambda: self.cdp.navigate(HOME_URL))
mgr_btn.clicked.connect(self.open_manager) # ▶ 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 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): def open_manager(self):
d = ManagerDialog(self); d.exec_() d = ManagerDialog(self)
self.bm_bar.reload() d.exec_()
def keyPressEvent(self, e): def keyPressEvent(self, e):
# Home key -> home screen
if e.key() == Qt.Key_Home: if e.key() == Qt.Key_Home:
self.cdp.navigate(HOME_URL) self.cdp.navigate(HOME_URL)
# Block all other dangerous keys silently
e.accept() e.accept()
def eventFilter(self, obj, event): def eventFilter(self, obj, event):
@@ -500,15 +454,13 @@ class KioskOverlay(QWidget):
if event.type() == QEvent.KeyPress: if event.type() == QEvent.KeyPress:
key = event.key() key = event.key()
mods = event.modifiers() 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, 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_Tab, Qt.Key_F11, Qt.Key_Escape,
Qt.Key_F1, Qt.Key_F2, Qt.Key_F3, Qt.Key_F1, Qt.Key_F2, Qt.Key_F3,
Qt.Key_F5, Qt.Key_F6, Qt.Key_F10} Qt.Key_F5, Qt.Key_F6, Qt.Key_F10}
if mods & (Qt.AltModifier | Qt.ControlModifier): if mods & (Qt.AltModifier | Qt.ControlModifier):
if key in blocked_keys: if key in blocked_keys:
return True # swallow the event return True
# Also block bare Escape and F-keys
if key in {Qt.Key_Escape, Qt.Key_F11}: if key in {Qt.Key_Escape, Qt.Key_F11}:
return True return True
return super().eventFilter(obj, event) return super().eventFilter(obj, event)
@@ -520,12 +472,10 @@ def launch_chrome():
profile_dir = pathlib.Path("/home/jgitta/.config/google-chrome-kiosk") profile_dir = pathlib.Path("/home/jgitta/.config/google-chrome-kiosk")
profile_dir.mkdir(parents=True, exist_ok=True) profile_dir.mkdir(parents=True, exist_ok=True)
# Remove stale singleton locks
for name in ("SingletonLock", "SingletonSocket", "SingletonCookie"): for name in ("SingletonLock", "SingletonSocket", "SingletonCookie"):
try: (profile_dir / name).unlink() try: (profile_dir / name).unlink()
except FileNotFoundError: pass except FileNotFoundError: pass
# Wipe GPU/ML caches that corrupt across restarts
for rel in ("GraphiteDawnCache", "GrShaderCache", "ShaderCache", for rel in ("GraphiteDawnCache", "GrShaderCache", "ShaderCache",
"OptGuideOnDeviceClassifierModel", "OptGuideOnDeviceModel", "OptGuideOnDeviceClassifierModel", "OptGuideOnDeviceModel",
"optimization_guide_model_store", "optimization_guide_model_store",
@@ -535,7 +485,6 @@ def launch_chrome():
if p.exists(): if p.exists():
shutil.rmtree(p, ignore_errors=True) shutil.rmtree(p, ignore_errors=True)
# Patch Local State
ls_path = profile_dir / "Local State" ls_path = profile_dir / "Local State"
if ls_path.exists(): if ls_path.exists():
try: try: