33CSD headerbar replicating GTK4/Adwaita style. Fullscreen auto-hide overlay.
44"""
55
6- APP_VERSION = "3.3.1 "
6+ APP_VERSION = "3.4.0 "
77
88import argparse
99import json
@@ -28,7 +28,11 @@ from PySide6.QtGui import (
2828 QRegion ,
2929)
3030from PySide6 .QtSvg import QSvgRenderer
31- from PySide6 .QtWebEngineCore import QWebEngineProfile , QWebEngineSettings
31+ from PySide6 .QtWebEngineCore import (
32+ QWebEngineProfile ,
33+ QWebEngineScript ,
34+ QWebEngineSettings ,
35+ )
3236from PySide6 .QtWebEngineWidgets import QWebEngineView
3337from 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+
350366class 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 )
0 commit comments