Skip to content

Commit f43c1de

Browse files
committed
✨ feat: feat(viewer): resize lock, fullscreen support, RAM optimization + cleanup on delete
- Lock window size via min/max constraints; only unlock during user- initiated grip drag, fullscreen, or maximize. Prevents web content from resizing the window. - Add custom _ResizeGrip subclass that manages lock/unlock lifecycle. - Inject JS to neutralize window.resizeTo/resizeBy/moveTo/moveBy. - Enable FullScreenSupportEnabled and handle JS fullscreen requests (e.g. video players). - Handle newWindowRequested — open popups in same window. - Center window on primary screen at startup. - Reduce default height from 768 to 720. - Disable WebGL, AutoLoadIcons, TouchIcons, and PluginsEnabled to reduce memory footprint. - Add Chromium flags: --disable-sync, --disable-translate, --disable-background-networking, --renderer-process-limit=1. - Clean up viewer config and persistent data when deleting an app-mode webapp (command_executor.py).
1 parent 316bf97 commit f43c1de

2 files changed

Lines changed: 132 additions & 6 deletions

File tree

biglinux-webapps/usr/bin/big-webapps-viewer

Lines changed: 110 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
CSD headerbar replicating GTK4/Adwaita style. Fullscreen auto-hide overlay.
44
"""
55

6-
APP_VERSION = "3.3.1"
6+
APP_VERSION = "3.4.0"
77

88
import argparse
99
import json
@@ -28,7 +28,11 @@ from PySide6.QtGui import (
2828
QRegion,
2929
)
3030
from PySide6.QtSvg import QSvgRenderer
31-
from PySide6.QtWebEngineCore import QWebEngineProfile, QWebEngineSettings
31+
from PySide6.QtWebEngineCore import (
32+
QWebEngineProfile,
33+
QWebEngineScript,
34+
QWebEngineSettings,
35+
)
3236
from PySide6.QtWebEngineWidgets import QWebEngineView
3337
from PySide6.QtWidgets import (
3438
QApplication,
@@ -347,6 +351,18 @@ class NavOverlay(QWidget):
347351
return btn
348352

349353

354+
class _ResizeGrip(QSizeGrip):
355+
"""SizeGrip that unlocks window resize during user drag."""
356+
357+
def mousePressEvent(self, e):
358+
self.parentWidget()._unlock_size()
359+
super().mousePressEvent(e)
360+
361+
def mouseReleaseEvent(self, e):
362+
super().mouseReleaseEvent(e)
363+
self.parentWidget()._lock_size()
364+
365+
350366
class WebAppWindow(QMainWindow):
351367
"""CSD window + Chromium WebEngine. Adwaita-style headerbar."""
352368

@@ -357,16 +373,35 @@ class WebAppWindow(QMainWindow):
357373
self.setWindowFlags(Qt.FramelessWindowHint | Qt.Window)
358374
self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True)
359375

376+
# resize lock: only user-initiated resize (grip drag) is allowed
377+
self._resize_locked = False
378+
360379
self._setup_profile(app_id)
361380
self._setup_ui(url, title, icon)
362381
self._setup_shortcuts()
363382
self._load_geometry()
364383

384+
# lock window size hard via min/max constraints
385+
self._lock_size()
386+
365387
# fullscreen hover detection
366388
self._hover_timer = QTimer(self)
367389
self._hover_timer.timeout.connect(self._check_hover)
368390
self._hover_timer.start(150)
369391

392+
def _lock_size(self) -> None:
393+
"""Lock current size by setting min=max=current."""
394+
self._resize_locked = True
395+
s = self.size()
396+
self.setMinimumSize(s)
397+
self.setMaximumSize(s)
398+
399+
def _unlock_size(self) -> None:
400+
"""Unlock resize constraints."""
401+
self._resize_locked = False
402+
self.setMinimumSize(0, 0)
403+
self.setMaximumSize(16777215, 16777215)
404+
370405
def _setup_profile(self, app_id: str) -> None:
371406
storage = str(DATA_BASE / app_id)
372407
Path(storage).mkdir(parents=True, exist_ok=True)
@@ -405,13 +440,38 @@ class WebAppWindow(QMainWindow):
405440
QWebEngineSettings.WebAttribute.JavascriptEnabled,
406441
QWebEngineSettings.WebAttribute.LocalStorageEnabled,
407442
QWebEngineSettings.WebAttribute.ScrollAnimatorEnabled,
408-
QWebEngineSettings.WebAttribute.PluginsEnabled,
409443
QWebEngineSettings.WebAttribute.JavascriptCanAccessClipboard,
410444
):
411445
s.setAttribute(attr, True)
412446
s.setAttribute(
413447
QWebEngineSettings.WebAttribute.PlaybackRequiresUserGesture, False
414448
)
449+
s.setAttribute(
450+
QWebEngineSettings.WebAttribute.FullScreenSupportEnabled, True
451+
)
452+
# disable non-essential features → reduce RAM
453+
for off_attr in (
454+
QWebEngineSettings.WebAttribute.WebGLEnabled,
455+
QWebEngineSettings.WebAttribute.AutoLoadIconsForPage,
456+
QWebEngineSettings.WebAttribute.TouchIconsEnabled,
457+
):
458+
s.setAttribute(off_attr, False)
459+
460+
# inject JS to neutralize window resize/move from web content
461+
_no_resize_js = QWebEngineScript()
462+
_no_resize_js.setName("no-resize")
463+
_no_resize_js.setSourceCode(
464+
"window.resizeTo=function(){};"
465+
"window.resizeBy=function(){};"
466+
"window.moveTo=function(){};"
467+
"window.moveBy=function(){};"
468+
)
469+
_no_resize_js.setInjectionPoint(
470+
QWebEngineScript.InjectionPoint.DocumentCreation
471+
)
472+
_no_resize_js.setWorldId(QWebEngineScript.ScriptWorldId.MainWorld)
473+
_no_resize_js.setRunsOnSubFrames(True)
474+
self.webview.page().scripts().insert(_no_resize_js)
415475

416476
self.webview.setUrl(QUrl(url))
417477
vbox.addWidget(self.webview)
@@ -441,9 +501,11 @@ class WebAppWindow(QMainWindow):
441501
# webview signals
442502
self.webview.titleChanged.connect(self._on_title)
443503
self.webview.urlChanged.connect(self._on_nav)
504+
self.webview.page().fullScreenRequested.connect(self._on_fullscreen_req)
505+
self.webview.page().newWindowRequested.connect(self._on_new_window)
444506

445507
# resize grip — bottom-right
446-
self._grip = QSizeGrip(self)
508+
self._grip = _ResizeGrip(self)
447509
self._grip.setFixedSize(16, 16)
448510

449511
def _setup_shortcuts(self) -> None:
@@ -476,6 +538,7 @@ class WebAppWindow(QMainWindow):
476538
self.nav.raise_()
477539

478540
def _toggle_fullscreen(self) -> None:
541+
self._unlock_size()
479542
if self.isFullScreen():
480543
self._exit_fullscreen()
481544
else:
@@ -487,12 +550,15 @@ class WebAppWindow(QMainWindow):
487550
def _exit_fullscreen(self) -> None:
488551
if not self.isFullScreen():
489552
return
553+
self._unlock_size()
490554
self.header.setVisible(True)
491555
self._grip.setVisible(True)
492556
self.showNormal()
493557
self._apply_mask()
558+
self._lock_size()
494559

495560
def _toggle_maximize(self) -> None:
561+
self._unlock_size()
496562
fg = _get_theme_colors()["fg"]
497563
if self.isMaximized():
498564
self.showNormal()
@@ -501,6 +567,9 @@ class WebAppWindow(QMainWindow):
501567
self.showMaximized()
502568
self.header.max_btn.setIcon(_make_adw_icon("restore", size=20, fg_hex=fg))
503569
self._apply_mask()
570+
# re-lock at new size (but not when maximized — WM controls it)
571+
if not self.isMaximized():
572+
self._lock_size()
504573

505574
def _on_title(self, title: str) -> None:
506575
if title:
@@ -515,6 +584,18 @@ class WebAppWindow(QMainWindow):
515584
self.nav.back_btn.setEnabled(can_back)
516585
self.nav.fwd_btn.setEnabled(can_fwd)
517586

587+
def _on_fullscreen_req(self, request) -> None:
588+
"""Handle JS fullscreen requests (e.g. video players)."""
589+
request.accept()
590+
if request.toggleOn():
591+
self._toggle_fullscreen()
592+
else:
593+
self._exit_fullscreen()
594+
595+
def _on_new_window(self, request) -> None:
596+
"""Handle JS popups — open in same window instead of spawning new ones."""
597+
self.webview.setUrl(request.requestedUrl())
598+
518599
# --- geometry ---
519600

520601
def resizeEvent(self, event) -> None:
@@ -537,13 +618,28 @@ class WebAppWindow(QMainWindow):
537618
self.setMask(QRegion(path.toFillPolygon().toPolygon()))
538619

539620
def _load_geometry(self) -> None:
621+
screen = QApplication.primaryScreen()
622+
sg = screen.availableGeometry() if screen else None
623+
540624
try:
541625
d = json.loads(self.config_path.read_text())
542-
self.resize(d.get("width", 1024), d.get("height", 768))
626+
w = d.get("width", 1024)
627+
h = d.get("height", 720)
628+
self.resize(w, h)
543629
if d.get("maximized"):
630+
self._resize_locked = False
544631
self.showMaximized()
632+
self._resize_locked = True
633+
elif sg:
634+
x = sg.x() + (sg.width() - w) // 2
635+
y = sg.y() + (sg.height() - h) // 2
636+
self.move(x, y)
545637
except (FileNotFoundError, json.JSONDecodeError, OSError):
546-
self.resize(1024, 768)
638+
self.resize(1024, 720)
639+
if sg:
640+
x = sg.x() + (sg.width() - 1024) // 2
641+
y = sg.y() + (sg.height() - 720) // 2
642+
self.move(x, y)
547643

548644
def _save_geometry(self) -> None:
549645
if self.isFullScreen():
@@ -579,6 +675,14 @@ def main() -> int:
579675
if not url.startswith(("http://", "https://", "file://")):
580676
url = "https://" + url
581677

678+
# strip non-essential Chromium features → reduce RAM w/o breaking sites
679+
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = " ".join([
680+
"--disable-sync",
681+
"--disable-translate",
682+
"--disable-background-networking",
683+
"--renderer-process-limit=1",
684+
])
685+
582686
app = QApplication(sys.argv[:1])
583687
# restore default SIGINT behavior → Ctrl+C terminates cleanly
584688
signal.signal(signal.SIGINT, signal.SIG_DFL)

biglinux-webapps/usr/share/biglinux/webapps/webapps/utils/command_executor.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
import logging
66
import json
7+
import re
8+
import shutil
79
import subprocess
810
from pathlib import Path
911

@@ -174,8 +176,28 @@ def remove_webapp(self, webapp, delete_folder: bool = False) -> bool:
174176
webapp.app_profile,
175177
]
176178
output = self.execute_command(argv)
179+
# cleanup viewer config/data if this was an app-mode webapp
180+
if webapp.app_mode == "app":
181+
self._cleanup_viewer_data(webapp.app_url)
177182
return output != ""
178183

184+
def _cleanup_viewer_data(self, url: str) -> None:
185+
"""Remove viewer config and persistent data for a given URL."""
186+
app_id = re.sub(r"https?://", "", url)
187+
app_id = app_id.replace("/", "_")
188+
app_id = re.sub(r"[^a-zA-Z0-9_-]", "", app_id)
189+
if not app_id:
190+
return
191+
home = Path.home()
192+
config_json = home / ".config" / "biglinux-webapps" / f"{app_id}.json"
193+
data_dir = home / ".local" / "share" / "biglinux-webapps" / app_id
194+
if config_json.exists():
195+
config_json.unlink()
196+
logger.debug("Removed viewer config: %s", config_json)
197+
if data_dir.exists():
198+
shutil.rmtree(data_dir, ignore_errors=True)
199+
logger.debug("Removed viewer data: %s", data_dir)
200+
179201
def select_icon(self) -> str:
180202
"""
181203
Open the icon selector dialog.

0 commit comments

Comments
 (0)