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
+78 -129
View File
@@ -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)
@@ -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: