Redesign toolbar: single row with icon buttons and hamburger bookmark menu
This commit is contained in:
+79
-130
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user