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