Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package vdu_controls for openSUSE:Factory checked in at 2026-04-07 16:33:45 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/vdu_controls (Old) and /work/SRC/openSUSE:Factory/.vdu_controls.new.21863 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "vdu_controls" Tue Apr 7 16:33:45 2026 rev:18 rq:1344800 version:2.5.0 Changes: -------- --- /work/SRC/openSUSE:Factory/vdu_controls/vdu_controls.changes 2025-08-29 18:38:23.930241821 +0200 +++ /work/SRC/openSUSE:Factory/.vdu_controls.new.21863/vdu_controls.changes 2026-04-07 16:49:26.993882350 +0200 @@ -1,0 +2,22 @@ +Mon Apr 6 20:08:10 UTC 2026 - Michael Hamilton <[email protected]> + +- Version 2.5.0 + * Visual refresh of the Main-panel. + * Added option "toolbar-at-top" to configure the top/bottom placement of the toolbar + in the main-window. Top placement is more Plasma-6-like and may also be useful + when combined with top-located system-trays + * Added option "separate-status-bar" to allow the main-window's status-bar to be + separated from its toolbar. This may be useful when combined with "toolbar-at-top". + * Replaced QProgressBar with a more modern circular busy-spinner. + * Added a tooltip to the status-bar that shows the last 10 status messages. + * The context-menu now includes a Control-Panel menu-item on all desktops - previously it + was Gnome-only (for tray extensions), but Xfce's tray also needs it. + * Light-Metering window - corrected the horizontal tick mark placement on the sun-plot. + * Light-Metering window - added enlarged tick-marks to the sun-plot at 3,6,9,15,18,21 hours. + * Added option "tray-follows-theme" (default enabled) to invert the tray icon’s light/dark state + when the desktop theme changes. It can be set for trays that flip-with the desktop or + flip-opposite to the desktop, or unset for trays don’t change at all (there isn't a + common way to detect tray-themes, so this cannot be automated). + * Internal code simplifications and cleanups. + +------------------------------------------------------------------- Old: ---- vdu_controls-2.4.3.tar.gz New: ---- vdu_controls-2.5.0.tar.gz ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ vdu_controls.spec ++++++ --- /var/tmp/diff_new_pack.cYEAme/_old 2026-04-07 16:49:27.505903524 +0200 +++ /var/tmp/diff_new_pack.cYEAme/_new 2026-04-07 16:49:27.509903690 +0200 @@ -18,7 +18,7 @@ Name: vdu_controls -Version: 2.4.3 +Version: 2.5.0 Release: 0 Summary: Visual Display Unit virtual control panel License: GPL-3.0-or-later ++++++ vdu_controls-2.4.3.tar.gz -> vdu_controls-2.5.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vdu_controls-2.4.3/PKGBUILD new/vdu_controls-2.5.0/PKGBUILD --- old/vdu_controls-2.4.3/PKGBUILD 2025-08-29 04:06:52.000000000 +0200 +++ new/vdu_controls-2.5.0/PKGBUILD 2026-04-06 22:33:45.000000000 +0200 @@ -1,5 +1,5 @@ pkgname=vdu_controls -pkgver=2.4.3 +pkgver=2.5.0 pkgrel=1 pkgdesc="Visual Display Unit virtual control panel" arch=('i686' 'x86_64') diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vdu_controls-2.4.3/README.md new/vdu_controls-2.5.0/README.md --- old/vdu_controls-2.4.3/README.md 2025-08-29 04:06:52.000000000 +0200 +++ new/vdu_controls-2.5.0/README.md 2026-04-06 22:33:45.000000000 +0200 @@ -11,7 +11,8 @@ Description ----------- - + +<img src="screen-shots/ambient-slider-example.png" alt="vdu_controls v2.5" width="580"> ``vdu_controls`` is a virtual control panel for external Visual Display Units (VDUs, monitors, displays). It supports displays connected via DisplayPort, @@ -36,10 +37,10 @@ In versions >= 2.4, the _ambient-light-level_ slider has been combined with an estimate of local solar-illumination to achieve **semi-automatic brightness control** throughout the day. Adjusting the slider sets the ratio between -indoor-illumination and outdoor solar-illumination. Should circumstances change, -adjusting the slider updates the ratio. See the -[2.4 release notes](https://github.com/digitaltrails/vdu_controls/releases/tag/v2.4.0) -for a brief tutorial. +indoor-illumination and outdoor solar-illumination. Once the ratio is set, +it is used to automatically update brighness as the day proceeds. +Should the cloud-cover or weather change, adjusting the slider revises the ratio. +See the [2.4 release notes](https://github.com/digitaltrails/vdu_controls/releases/tag/v2.4.0) for a brief tutorial. (Solar-illumination is estimated for a location by using the local date-time to determine a sun-angle, and from that estimates for illumination, and air-mass.) @@ -58,6 +59,9 @@ to light/dark theme changes. (For desktops that don't integrate with Qt/KDE themeing, the `qt5ct` and `qt6ct` utilities may be used to alter the overall Qt theme.) +The main-toolbar may be dragged to either the top or bottom of the main-window. +The toolbar's location persists across restarts. + From any application window, use **F1** to access **help**, and **F10** to access the context-menu. The **Context Menu** is also available via the right-mouse button in the main-window, the hamburger-menu item on the bottom right of the main window, and the right-mouse button on the system-tray icon. The @@ -69,10 +73,15 @@ > > Within the UI, **tool-tips** are often available when hovering over UI > components. +       +> [!NOTE] +> Several language translations are provided, but with no apparent demand, they +> are currently unmaintained and will be updated on request. + #### Technical background Historically, there was little need to frequently adjust display brightness. @@ -340,7 +349,7 @@ * Denilson Sá Maia ([denilsonsa](https://github.com/denilsonsa)), for many suggestions, assistance, and contributions. * Matthew Coleman ([crashmatt](https://github.com/crashmatt)), Mark Lowne ([lowne](https://github.com/lowne)), [usr3](https://github.com/usr3), Mateo Bohorquez G. ([Milor123](https://github.com/Milor123)), Andrew Sun ([apsun](https://github.com/apsun)), - Extent ([Extent421](https://github.com/Extent421)), Niklas Hambüchen ([nh2](https://github.com/nh2)) + Extent ([Extent421](https://github.com/Extent421)), Niklas Hambüchen ([nh2](https://github.com/nh2)), Doron Behar ([doronbehar](https://github.com/doronbehar)) for contributing fixes to code and documentation. * [Jakeler](https://github.com/Jakeler), [kupiqu](https://github.com/kupiqu), Mateo Bohorquez ([Milor123](https://github.com/Milor123)), Johan Grande ([nahoj](https://github.com/nahoj)), [0xCUBE](https://github.com/0xCUB3), [RokeJulianLockhart](https://github.com/RokeJulianLockhart), [abil76](https://github.com/abil76), Andrew Sun ([apsun](https://github.com/apsun)) @@ -348,6 +357,7 @@ * Malcolm Lewis ([malcolmlewis](https://github.com/malcolmlewis)) for assistance with the OpenSUSE Open Build Service submissions. * Christopher Laws ([claws](https://github.com/claws)) for the [BH1750 library](https://github.com/claws/BH1750) and [example build](https://github.com/claws/BH1750#example) (lux-metering). +* Viktor Sharga ([ViktorSharga](https://github.com/ViktorSharga)) for assisting with UI enhancements. * Plus others who have supplied feedback and suggestions. * E. Elvegård and G. Sjöstedt, "The Calculation of Illumination from Sun and Sky," _Illuminating Engineering_, Apr. 1940. [Illuminating Engineering Society, 100 Significant Papers](https://www.ies.org/research/publications/100-significant-papers/) @@ -359,6 +369,26 @@ Version History --------------- +* 2.5.0 + * Visual refresh of the Main-panel. Inspired by [a recent fork](https://github.com/ViktorSharga/vdu_controls_vibecodedUI) + by @ViktorSharga. + * Added option "toolbar-at-top" to configure the top/bottom placement of the toolbar + in the main-window. Top placement is more Plasma-6-like and may also be useful + when combined with top-located system-trays + * Added option "separate-status-bar" to allow the main-window's status-bar to be + separated from its toolbar. This may be useful when combined with "toolbar-at-top". + * Replaced QProgressBar with a more modern circular busy-spinner. + * Added a tooltip to the status-bar that shows the last 10 status messages. + * The context-menu now includes a Control-Panel menu-item on all desktops - previously it + was Gnome-only (for tray extensions), but Xfce's tray also needs it. + * Light-Metering window - corrected the horizontal tick mark placement on the sun-plot. + * Light-Metering window - added enlarged tick-marks to the sun-plot at 3,6,9,15,18,21 hours. + * Added option "tray-follows-theme" (default enabled) to invert the tray icon’s light/dark state + when the desktop theme changes. It can be set for trays that flip-with the desktop or + flip-opposite to the desktop, or unset for trays don’t change at all (there isn't a + common way to detect tray-themes, so this cannot be automated). + * Internal code simplifications and cleanups. + * 2.4.3 * Fix a rare TypeError when light metering. * Some code cleanups for the splash screen. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vdu_controls-2.4.3/docs/_build/man/vdu_controls.1 new/vdu_controls-2.5.0/docs/_build/man/vdu_controls.1 --- old/vdu_controls-2.4.3/docs/_build/man/vdu_controls.1 2025-08-29 04:06:52.000000000 +0200 +++ new/vdu_controls-2.5.0/docs/_build/man/vdu_controls.1 2026-04-06 22:33:45.000000000 +0200 @@ -1,4 +1,5 @@ -.\" Man page generated from reStructuredText. +.\" Man page generated from reStructuredText +.\" by the Docutils 0.22.3 manpage writer. . . .nr rst2man-indent-level 0 @@ -27,9 +28,9 @@ .\" new: \\n[rst2man-indent\\n[rst2man-indent-level]] .in \\n[rst2man-indent\\n[rst2man-indent-level]]u .. -.TH "VDU_CONTROLS" "1" "Jul 10, 2025" "" "vdu_controls" +.TH "VDU_CONTROLS" "1" "Apr 02, 2026" "" "vdu_controls" .SH NAME -vdu_controls \- vdu_controls 2.4.1 +vdu_controls \- vdu_controls 2.5.0 .SH VDU_CONTROLS - A DDC CONTROL PANEL FOR MONITORS .sp A control panel for DisplayPort, DVI, HDMI, or USB\-connected VDUs (\fIVisual Display Units\fP). @@ -47,6 +48,9 @@ [\-\-hide\-on\-focus\-out|\-\-no\-hide\-on\-focus\-out] [\-\-smart\-window|\-\-no\-smart\-window] [\-smart\-uses\-xwayland|\-smart\-uses\-xwayland] [\-\-monochrome\-tray|\-\-no\-monochrome\-tray] [\-\-mono\-light\-tray|\-\-no\-mono\-light\-tray] +[\-\-tray\-follows\-theme|\-\-no\-tray\-follows\-theme] +[\-\-toolbar\-at\-top|\-no\-toolbar\-at\-top] +[\-\-separate\-status\-bar|\-\-separate\-status\-bar] [\-\-protect\-nvram|\-\-no\-protect\-nvram] [\-\-lux\-options|\-\-no\-lux\-options] [\-\-schedule|\-\-no\-schedule] [\-\-weather|\-\-no\-weather] @@ -114,6 +118,18 @@ monochrome themed system\-tray. \fB\-\-no\-mono\-light\-tray\fP is the default. .TP +.B \-\-tray\-follows\-theme|\-\-no\-tray\-follows\-theme +the tray\-theme toggles between light/dark when the desktop\-theme changes +\fB\-\-tray\-follows\-theme\fP is the default. +.TP +.B \-\-toolbar\-at\-top|\-\-no\-toolbar\-at\-top +locate the toolbar at the top or bottom of the main window +\fB\-\-no\-toolbar\-at\-top\fP is the default +.TP +.B \-\-separate\-status\-bar|\-\-no\-separate\-status\-bar +separate the status\-bar from the toolbar +\fB\-\-no\-separate\-status\-bar\fP is the default +.TP .B \-\-protect\-nvram|\-\-no\-protect\-nvram alter options and defaults to minimize VDU NVRAM writes. .TP @@ -224,7 +240,11 @@ main\-window or the system\-tray icon. The main\-menu has \fIALT\-key\fP shortcuts for all menu items (subject to sufficient letters being available to distinguish all user defined presets). .sp -For further information, including screenshots, see \X'tty: link https://github.com/digitaltrails/vdu_controls'\fI\%https://github.com/digitaltrails/vdu_controls\fP\X'tty: link' . +The main\-toolbar includes a stealthy\-drag\-handle at extreme\-left. The toolbar +can be dragged and docked at either the top or bottom top of the main\-window. +The toolbar\(aqs position persists across restarts. +.sp +For further information, including screenshots, see \%<https://\:github\:.com/\:digitaltrails/\:vdu_controls> . .sp The long\-term effects of repeatably rewriting a VDUs setting are not well understood, but some concerns have been expressed. See \fBLIMITATIONS\fP for further details. @@ -539,10 +559,10 @@ .SS Presets \- supplementary weather requirements .sp A solar elevation trigger can have a weather requirement which will be checked against the weather -reported by \X'tty: link https://wttr.in'\fI\%https://wttr.in\fP\X'tty: link'\&. +reported by \%<https://\:wttr\:.in>\&. .sp By default, there are three possible weather requirements: \fBgood\fP, \fBbad\fP, and \fBall weather\fP\&. -Each requirement is defined by a file containing a list of WWO (\X'tty: link https://www.worldweatheronline.com'\fI\%https://www.worldweatheronline.com\fP\X'tty: link') +Each requirement is defined by a file containing a list of WWO (\%<https://\:www\:.worldweatheronline\:.com>) weather codes, one per line. The three default requirements are contained in the files \fB$HOME/.config/vdu_controls/{good,bad,all}.weather\fP\&. Additional weather requirements can be created by using a text editor to create further files. The \fBall.weather\fP file exists primarily @@ -557,7 +577,7 @@ enhanced to fill out a place\-name for you. Should \fBwttr.in\fP not recognize a place\-name, the place\-name can be manually edited to something more suitable. The nearest big city or an airport\-code will do, for example: LHR, LAX, JFK. You can use a web browser to test a place\-name, -for example: \X'tty: link https://wttr.in/JFK'\fI\%https://wttr.in/JFK\fP\X'tty: link' +for example: \%<https://\:wttr\:.in/\:JFK> .sp When weather requirements are in use, \fBvdu_controls\fP will check that the coordinates in \fBSettings\fP \fBLocation\fP are a reasonable match for those returned from \fBwttr.in\fP, a warning will @@ -662,7 +682,7 @@ programming an Arduino with a GY\-30/BH1750, can be found at: .INDENT 0.0 .INDENT 3.5 -\X'tty: link https://github.com/digitaltrails/vdu_controls/blob/master/Lux-metering.md'\fI\%https://github.com/digitaltrails/vdu_controls/blob/master/Lux\-metering.md\fP\X'tty: link' +\%<https://\:github\:.com/\:digitaltrails/\:vdu_controls/\:blob/\:master/\:Lux-metering\:.md> .UNINDENT .UNINDENT .sp @@ -671,7 +691,7 @@ location: .INDENT 0.0 .INDENT 3.5 -\X'tty: link https://github.com/digitaltrails/vdu_controls/tree/master/sample-scripts'\fI\%https://github.com/digitaltrails/vdu_controls/tree/master/sample\-scripts\fP\X'tty: link'\&. +\%<https://\:github\:.com/\:digitaltrails/\:vdu_controls/\:tree/\:master/\:sample-scripts>\&. .UNINDENT .UNINDENT .sp @@ -801,8 +821,9 @@ Reducing the number of enabled controls can speed up initialization, decrease the refresh time, and reduce the time taken to restore presets. .sp -There\(aqs plenty of useful info for getting the best out of \fBddcutil\fP at \X'tty: link https://www.ddcutil.com/'\fI\%https://www.ddcutil.com/\fP\X'tty: link'\&. +There\(aqs plenty of useful info for getting the best out of \fBddcutil\fP at \%<https://\:www\:.ddcutil\:.com/>\&. .SH LIMITATIONS +.SS Possible impact on VDU lifespan .sp Repeatably altering VDU settings might affect VDU lifespan, exhausting the NVRAM write cycles, stressing the VDU power\-supply, or increasing panel burn\-in. @@ -857,6 +878,42 @@ The bottom of the About\-dialog shows the same numbers. They update dynamically. .UNINDENT .UNINDENT +.SS Cross\-platform differences +.sp +The UI attempts to step around minor differences between KDE, GNOME, and the rest, +the UI on each may not be exactly the same. +.sp +Depending on which desktop or system\-tray\-extension you are using, a +left\-mouse\-click on the app\-icon in the system\-tray may restore +the application\(aqs main\-widow or it may bring up the the application\(aqs +context\-menu. To support both kinds of desktop, the context\-menu includes a +a _Control +.nf +Panel_ +.fi + menu option that toggles visibility of the main window. +.sp +Wayland doesn\(aqt allow an application to precisely position its windows. When the +\fBsmart\-window\fP option is enabled and the desktop platform is Wayland, the +application switches its platform to XWayland (X11 xcb). +.sp +The scaling and appearance of Qt6 differs from Qt5, its more chunky and rounded. If you +have Qt5 installed and prefer it, you can uncheck prefer\-qt6 in settings. +.SS Desktop Theming +.sp +Achieving desktop neutrality comes at the price of the application not being +fully aware or compliant with the theming conventions of any particular desktop. +.sp +For some desktops, Qt can detect in\-session theme changes, such as the change from +a day\-theme to a night\-theme, and the application can respond appropriately. For +desktops where theme changes aren\(aqt detected, the application can only conform +to the theme detected at startup. +.sp +In some cases, the system\-tray or dock theming may contrast with the theming +applied to windows. There isn\(aqt a straight forward Qt mechanism to discover +whether a tray or dock is differently themed. As a result the application includes +several manual settings that can alter the tray/dock icon theming between +colored, monochrome\-dark and monochrome\-light. .SS Laptops .sp A laptop\(aqs builtin\-panel normally doesn\(aqt implement DDC and cannot be controlled @@ -866,17 +923,6 @@ option and provide \fBvdu_controls\fP with a ddcutil\-like script for getting and setting the panel brightness; then \fBvdu_controls\fP will treat the laptop panel just like any other VDU. A template script is provided in the \fBsample\-scripts\fP\&. -.SS Cross\-platform differences -.sp -Wayland doesn\(aqt allow an application to precisely position its windows. When the -\fBsmart\-window\fP option is enabled and the desktop platform is Wayland, the -application switches its platform to XWayland (X11 xcb). -.sp -The UI attempts to step around minor differences between KDE, GNOME, and the rest, -the UI on each may not be exactly the same. -.sp -The scaling and appearance of Qt6 differs from Qt5, its more chunky and rounded. If you -have Qt5 installed and prefer it, you can uncheck prefer\-qt6 in settings. .SS Other concerns .sp The power\-supplies in some older VDUs may buzz/squeel audibly when the brightness is @@ -958,14 +1004,14 @@ .UNINDENT .sp Read ddcutil documentation concerning config of i2c_dev with nvidia GPUs. Detailed ddcutil info -at \X'tty: link https://www.ddcutil.com/'\fI\%https://www.ddcutil.com/\fP\X'tty: link' +at \%<https://\:www\:.ddcutil\:.com/> .SH ENVIRONMENT .INDENT 0.0 .INDENT 3.5 .INDENT 0.0 .TP .B LC_ALL, LANG, LANGUAGE -These variables specify the locale for language translations and units of distance. +These variables specify the locale for language translations and units of distance. LC_ALL is used by python, LANGUAGE is used by Qt. Normally, they should all have the same value, for example, \fBDa_DK\fP\&. For these to have any effect on language, \fBSettings\fP \fBTranslations Enabled\fP must also be enabled. @@ -1026,7 +1072,7 @@ .UNINDENT .SH REPORTING BUGS .sp -\X'tty: link https://github.com/digitaltrails/vdu_controls/issues'\fI\%https://github.com/digitaltrails/vdu_controls/issues\fP\X'tty: link' +\%<https://\:github\:.com/\:digitaltrails/\:vdu_controls/\:issues> .SH GNU LICENSE .sp This program is free software: you can redistribute it and/or modify it @@ -1039,10 +1085,9 @@ more details. .sp You should have received a copy of the GNU General Public License along -with this program. If not, see \X'tty: link https://www.gnu.org/licenses/'\fI\%https://www.gnu.org/licenses/\fP\X'tty: link'\&. -.SH AUTHOR +with this program. If not, see \%<https://\:www\:.gnu\:.org/\:licenses/>\&. +.SH Author Michael Hamilton -.SH COPYRIGHT +.SH Copyright 2021, Michael Hamilton -.\" Generated by docutils manpage writer. -. +.\" End of generated man page. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vdu_controls-2.4.3/docs/_build/man/vdu_controls.1.html new/vdu_controls-2.5.0/docs/_build/man/vdu_controls.1.html --- old/vdu_controls-2.4.3/docs/_build/man/vdu_controls.1.html 2025-08-29 04:06:52.000000000 +0200 +++ new/vdu_controls-2.5.0/docs/_build/man/vdu_controls.1.html 2026-04-06 22:33:45.000000000 +0200 @@ -12,6 +12,9 @@ [--hide-on-focus-out|--no-hide-on-focus-out] [--smart-window|--no-smart-window] [-smart-uses-xwayland|-smart-uses-xwayland] [--monochrome-tray|--no-monochrome-tray] [--mono-light-tray|--no-mono-light-tray] + [--tray-follows-theme|--no-tray-follows-theme] + [--toolbar-at-top|-no-toolbar-at-top] + [--separate-status-bar|--separate-status-bar] [--protect-nvram|--no-protect-nvram] [--lux-options|--no-lux-options] [--schedule|--no-schedule] [--weather|--no-weather] @@ -54,6 +57,15 @@ --mono-light-tray|--no-mono-light-tray monochrome themed system-tray. ``--no-mono-light-tray`` is the default. + --tray-follows-theme|--no-tray-follows-theme + the tray-theme toggles between light/dark when the desktop-theme changes + ``--tray-follows-theme`` is the default. + --toolbar-at-top|--no-toolbar-at-top + locate the toolbar at the top or bottom of the main window + ``--no-toolbar-at-top`` is the default + --separate-status-bar|--no-separate-status-bar + separate the status-bar from the toolbar + ``--no-separate-status-bar`` is the default --protect-nvram|--no-protect-nvram alter options and defaults to minimize VDU NVRAM writes. --order-by-name|--no-order-by-name @@ -139,6 +151,9 @@ the main-window or the system-tray icon. The main-menu has <code>ALT-key</code> shortcuts for all menu items (subject to sufficient letters being available to distinguish all user defined presets).</p> +<p>The main-toolbar includes a stealthy-drag-handle at extreme-left. The +toolbar can be dragged and docked at either the top or bottom top of the +main-window. The toolbar’s position persists across restarts.</p> <p>For further information, including screenshots, see https://github.com/digitaltrails/vdu_controls .</p> <p>The long-term effects of repeatably rewriting a VDUs setting are not @@ -645,6 +660,8 @@ <p>There’s plenty of useful info for getting the best out of <code>ddcutil</code> at https://www.ddcutil.com/.</p> <h1 id="limitations">Limitations</h1> +<h2 id="possible-impact-on-vdu-lifespan">Possible impact on VDU +lifespan</h2> <p>Repeatably altering VDU settings might affect VDU lifespan, exhausting the NVRAM write cycles, stressing the VDU power-supply, or increasing panel burn-in.</p> @@ -683,6 +700,36 @@ dynamically.</li> </ul></li> </ul> +<h2 id="cross-platform-differences">Cross-platform differences</h2> +<p>The UI attempts to step around minor differences between KDE, GNOME, +and the rest, the UI on each may not be exactly the same.</p> +<p>Depending on which desktop or system-tray-extension you are using, a +left-mouse-click on the app-icon in the system-tray may restore the +application’s main-widow or it may bring up the the application’s +context-menu. To support both kinds of desktop, the context-menu +includes a a <em>Control Panel</em> menu option that toggles visibility +of the main window.</p> +<p>Wayland doesn’t allow an application to precisely position its +windows. When the <code>smart-window</code> option is enabled and the +desktop platform is Wayland, the application switches its platform to +XWayland (X11 xcb).</p> +<p>The scaling and appearance of Qt6 differs from Qt5, its more chunky +and rounded. If you have Qt5 installed and prefer it, you can uncheck +prefer-qt6 in settings.</p> +<h2 id="desktop-theming">Desktop Theming</h2> +<p>Achieving desktop neutrality comes at the price of the application +not being fully aware or compliant with the theming conventions of any +particular desktop.</p> +<p>For some desktops, Qt can detect in-session theme changes, such as +the change from a day-theme to a night-theme, and the application can +respond appropriately. For desktops where theme changes aren’t detected, +the application can only conform to the theme detected at startup.</p> +<p>In some cases, the system-tray or dock theming may contrast with the +theming applied to windows. There isn’t a straight forward Qt mechanism +to discover whether a tray or dock is differently themed. As a result +the application includes several manual settings that can alter the +tray/dock icon theming between colored, monochrome-dark and +monochrome-light.</p> <h2 id="laptops">Laptops</h2> <p>A laptop’s builtin-panel normally doesn’t implement DDC and cannot be controlled by <code>ddcutil</code> or <code>ddcutil-service</code>. @@ -693,16 +740,6 @@ and setting the panel brightness; then <code>vdu_controls</code> will treat the laptop panel just like any other VDU. A template script is provided in the <code>sample-scripts</code>.</p> -<h2 id="cross-platform-differences">Cross-platform differences</h2> -<p>Wayland doesn’t allow an application to precisely position its -windows. When the <code>smart-window</code> option is enabled and the -desktop platform is Wayland, the application switches its platform to -XWayland (X11 xcb).</p> -<p>The UI attempts to step around minor differences between KDE, GNOME, -and the rest, the UI on each may not be exactly the same.</p> -<p>The scaling and appearance of Qt6 differs from Qt5, its more chunky -and rounded. If you have Qt5 installed and prefer it, you can uncheck -prefer-qt6 in settings.</p> <h2 id="other-concerns">Other concerns</h2> <p>The power-supplies in some older VDUs may buzz/squeel audibly when the brightness is turned way down. This may not be a major issue @@ -767,7 +804,7 @@ GPUs. Detailed ddcutil info at https://www.ddcutil.com/</p> <h1 id="environment">Environment</h1> <pre><code>LC_ALL, LANG, LANGUAGE - These variables specify the locale for language translations and units of distance. + These variables specify the locale for language translations and units of distance. LC_ALL is used by python, LANGUAGE is used by Qt. Normally, they should all have the same value, for example, ``Da_DK``. For these to have any effect on language, ``Settings`` ``Translations Enabled`` must also be enabled. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vdu_controls-2.4.3/docs/conf.py new/vdu_controls-2.5.0/docs/conf.py --- old/vdu_controls-2.4.3/docs/conf.py 2025-08-29 04:06:52.000000000 +0200 +++ new/vdu_controls-2.5.0/docs/conf.py 2026-04-06 22:33:45.000000000 +0200 @@ -24,7 +24,7 @@ author = 'Michael Hamilton' # The full version, including alpha/beta/rc tags -release = '2.4.3' +release = '2.5.0' # -- General configuration --------------------------------------------------- diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vdu_controls-2.4.3/icons/vdu-power-on.svg new/vdu_controls-2.5.0/icons/vdu-power-on.svg --- old/vdu_controls-2.4.3/icons/vdu-power-on.svg 1970-01-01 01:00:00.000000000 +0100 +++ new/vdu_controls-2.5.0/icons/vdu-power-on.svg 2026-04-06 22:33:45.000000000 +0200 @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2024 Michael Hamilton License Creative Commons - Attribution CC BY --> +<svg viewBox="0 0 24 24" width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <style type="text/css" id="current-color-scheme"> .ColorScheme-Text { color:#232629; } </style> + <g class="ColorScheme-Text" stroke="currentColor" stroke-linecap="round" stroke-width="2" transform=""> + <path fill="None" d="M14 12 A 5 5 0 1 0 20 12 M 17 11 L 17 16.5 M 9 20 L 1 20 1 5 20 5 20 8"/> + </g> +</svg> \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vdu_controls-2.4.3/icons/vdu_ambient.svg new/vdu_controls-2.5.0/icons/vdu_ambient.svg --- old/vdu_controls-2.4.3/icons/vdu_ambient.svg 1970-01-01 01:00:00.000000000 +0100 +++ new/vdu_controls-2.5.0/icons/vdu_ambient.svg 2026-04-06 22:33:45.000000000 +0200 @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2024 Michael Hamilton License Creative Commons - Attribution CC BY --> +<svg viewBox="0 0 24 24" width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <style type="text/css" id="current-color-scheme"> .ColorScheme-Text { color:#232629; } </style> + <g class="ColorScheme-Text" stroke="currentColor" stroke-linecap="round" stroke-width="2"> + <path fill="none" d="M9 20 L1 20 1 5 20 5 20 7" /> + <circle cx="17" cy="16" r="5" stroke="currentColor" fill="none" /> + <rect x="11" y="21.5" width="1" height="1" rx="5" ry="5" stroke-width="1" fill="currentColor" /> + <rect x="8.5" y="15.5" width="1" height="1" rx="5" ry="5" stroke-width="1" fill="currentColor" /> + <rect x="10.5" y="10" width="1" height="1" rx="5" ry="5" stroke-width="1" fill="currentColor" /> + <rect x="15.5" y="7.5" width="1" height="1" rx="5" ry="5" stroke-width="1" fill="currentColor" /> + <rect x="21.5" y="9" width="1" height="1" rx="5" ry="5" stroke-width="1" fill="currentColor" /> + </g> +</svg> diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vdu_controls-2.4.3/icons/vdu_connected.svg new/vdu_controls-2.5.0/icons/vdu_connected.svg --- old/vdu_controls-2.4.3/icons/vdu_connected.svg 1970-01-01 01:00:00.000000000 +0100 +++ new/vdu_controls-2.5.0/icons/vdu_connected.svg 2026-04-06 22:33:45.000000000 +0200 @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright 2024 Michael Hamilton License Creative Commons - Attribution CC BY --> +<svg viewBox="0 0 24 24" width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <style type="text/css" id="current-color-scheme"> .ColorScheme-Text { color:#232629; } </style> + <g class="ColorScheme-Text" stroke="currentColor" stroke-linecap="round" stroke-width="2" transform=""> + <path fill="None" d="M 20 18 L 1 18 1 5 20 5 20 18 M 6.5 21 L 15 21"/> + </g> +</svg> \ No newline at end of file Binary files old/vdu_controls-2.4.3/screen-shots/ambient-slider-example.png and new/vdu_controls-2.5.0/screen-shots/ambient-slider-example.png differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vdu_controls-2.4.3/setup.cfg new/vdu_controls-2.5.0/setup.cfg --- old/vdu_controls-2.4.3/setup.cfg 2025-08-29 04:06:52.000000000 +0200 +++ new/vdu_controls-2.5.0/setup.cfg 2026-04-06 22:33:45.000000000 +0200 @@ -1,6 +1,6 @@ [metadata] name = vdu_controls-digitaltrails -version = 2.4.3 +version = 2.5.0 author = Michael Hamilton author_email = [email protected] description = A GUI for controlling Visual Display Units @@ -15,6 +15,9 @@ Operating System :: POSIX :: Linux [options] -scripts= vdu_controls.py, -packages= +py_modules = vdu_controls python_requires = >=3.8 + +[options.entry_points] +console_scripts = + vdu_controls = vdu_controls:main diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vdu_controls-2.4.3/vdu_controls.py new/vdu_controls-2.5.0/vdu_controls.py --- old/vdu_controls-2.4.3/vdu_controls.py 2025-08-29 04:06:52.000000000 +0200 +++ new/vdu_controls-2.5.0/vdu_controls.py 2026-04-06 22:33:45.000000000 +0200 @@ -17,6 +17,9 @@ [--hide-on-focus-out|--no-hide-on-focus-out] [--smart-window|--no-smart-window] [-smart-uses-xwayland|-smart-uses-xwayland] [--monochrome-tray|--no-monochrome-tray] [--mono-light-tray|--no-mono-light-tray] + [--tray-follows-theme|--no-tray-follows-theme] + [--toolbar-at-top|-no-toolbar-at-top] + [--separate-status-bar|--separate-status-bar] [--protect-nvram|--no-protect-nvram] [--lux-options|--no-lux-options] [--schedule|--no-schedule] [--weather|--no-weather] @@ -62,6 +65,15 @@ --mono-light-tray|--no-mono-light-tray monochrome themed system-tray. ``--no-mono-light-tray`` is the default. + --tray-follows-theme|--no-tray-follows-theme + the tray-theme toggles between light/dark when the desktop-theme changes + ``--tray-follows-theme`` is the default. + --toolbar-at-top|--no-toolbar-at-top + locate the toolbar at the top or bottom of the main window + ``--no-toolbar-at-top`` is the default + --separate-status-bar|--no-separate-status-bar + separate the status-bar from the toolbar + ``--no-separate-status-bar`` is the default --protect-nvram|--no-protect-nvram alter options and defaults to minimize VDU NVRAM writes. --order-by-name|--no-order-by-name @@ -145,6 +157,10 @@ main-window or the system-tray icon. The main-menu has `ALT-key` shortcuts for all menu items (subject to sufficient letters being available to distinguish all user defined presets). +The main-toolbar includes a stealthy-drag-handle at extreme-left. The toolbar +can be dragged and docked at either the top or bottom top of the main-window. +The toolbar's position persists across restarts. + For further information, including screenshots, see https://github.com/digitaltrails/vdu_controls . The long-term effects of repeatably rewriting a VDUs setting are not well understood, but some @@ -635,6 +651,9 @@ Limitations =========== +Possible impact on VDU lifespan +------------------------------- + Repeatably altering VDU settings might affect VDU lifespan, exhausting the NVRAM write cycles, stressing the VDU power-supply, or increasing panel burn-in. @@ -663,30 +682,52 @@ the number of VCP (NVRAM) writes. + The bottom of the About-dialog shows the same numbers. They update dynamically. -Laptops -------- - -A laptop's builtin-panel normally doesn't implement DDC and cannot be controlled -by ``ddcutil`` or ``ddcutil-service``. Laptop panel brightness is controlled -by a variety of methods that vary by vendor and hardware. If you have a laptop -where such adjustments can be scripted, you can use the ``--ddcutil-emulator`` -option and provide ``vdu_controls`` with a ddcutil-like script for getting and -setting the panel brightness; then ``vdu_controls`` will treat the laptop panel -just like any other VDU. A template script is provided in the ``sample-scripts``. - Cross-platform differences -------------------------- +The UI attempts to step around minor differences between KDE, GNOME, and the rest, +the UI on each may not be exactly the same. + +Depending on which desktop or system-tray-extension you are using, a +left-mouse-click on the app-icon in the system-tray may restore +the application's main-widow or it may bring up the the application's +context-menu. To support both kinds of desktop, the context-menu includes a +a _Control Panel_ menu option that toggles visibility of the main window. + Wayland doesn't allow an application to precisely position its windows. When the ``smart-window`` option is enabled and the desktop platform is Wayland, the application switches its platform to XWayland (X11 xcb). -The UI attempts to step around minor differences between KDE, GNOME, and the rest, -the UI on each may not be exactly the same. - The scaling and appearance of Qt6 differs from Qt5, its more chunky and rounded. If you have Qt5 installed and prefer it, you can uncheck prefer-qt6 in settings. +Desktop Theming +--------------- + +Achieving desktop neutrality comes at the price of the application not being +fully aware or compliant with the theming conventions of any particular desktop. + +For some desktops, Qt can detect in-session theme changes, such as the change from +a day-theme to a night-theme, and the application can respond appropriately. For +desktops where theme changes aren't detected, the application can only conform +to the theme detected at startup. + +In some cases, the system-tray or dock theming may contrast with the theming +applied to windows. There isn't a straight forward Qt mechanism to discover +whether a tray or dock is differently themed. As a result the application includes +several manual settings that can alter the tray/dock icon theming between +colored, monochrome-dark and monochrome-light. + +Laptops +------- + +A laptop's builtin-panel normally doesn't implement DDC and cannot be controlled +by ``ddcutil`` or ``ddcutil-service``. Laptop panel brightness is controlled +by a variety of methods that vary by vendor and hardware. If you have a laptop +where such adjustments can be scripted, you can use the ``--ddcutil-emulator`` +option and provide ``vdu_controls`` with a ddcutil-like script for getting and +setting the panel brightness; then ``vdu_controls`` will treat the laptop panel +just like any other VDU. A template script is provided in the ``sample-scripts``. Other concerns -------------- @@ -891,7 +932,8 @@ if qt_version == 6: from PyQt6 import QtCore, QtNetwork from PyQt6.QtCore import Qt, QCoreApplication, QThread, pyqtSignal, QProcess, QPoint, QObject, QEvent, \ - QSettings, QSize, QTimer, QTranslator, QLocale, QT_TR_NOOP, QVariant, pyqtSlot, QMetaType, QDir, QRegularExpression, QPointF + QSettings, QSize, QTimer, QTranslator, QLocale, QT_TR_NOOP, QVariant, pyqtSlot, QMetaType, QDir, \ + QRegularExpression, QPointF, QRect from PyQt6.QtDBus import QDBusConnection, QDBusInterface, QDBusMessage, QDBusArgument, QDBusVariant from PyQt6.QtGui import QAction, QShortcut, QPixmap, QIcon, QCursor, QImage, QPainter, QRegularExpressionValidator, \ QPalette, QGuiApplication, QColor, QValidator, QPen, QFont, QFontMetrics, QMouseEvent, QResizeEvent, QKeySequence, QPolygon, \ @@ -899,7 +941,7 @@ from PyQt6.QtSvg import QSvgRenderer from PyQt6.QtSvgWidgets import QSvgWidget from PyQt6.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QSlider, QMessageBox, QLineEdit, QLabel, \ - QSplashScreen, QPushButton, QProgressBar, QComboBox, QSystemTrayIcon, QMenu, QStyle, QTextEdit, QDialog, QTabWidget, \ + QSplashScreen, QPushButton, QComboBox, QSystemTrayIcon, QMenu, QStyle, QTextEdit, QDialog, QTabWidget, \ QCheckBox, QPlainTextEdit, QGridLayout, QSizePolicy, QMainWindow, QToolBar, QToolButton, QFileDialog, \ QWidgetItem, QScrollArea, QGroupBox, QFrame, QSplitter, QSpinBox, QDoubleSpinBox, QInputDialog, QStatusBar, \ QSpacerItem, QListWidget, QListWidgetItem @@ -908,14 +950,15 @@ elif qt_version == 5: # Covers all other values. from PyQt5 import QtCore, QtNetwork from PyQt5.QtCore import Qt, QCoreApplication, QThread, pyqtSignal, QProcess, QPoint, QObject, QEvent, \ - QSettings, QSize, QTimer, QTranslator, QLocale, QT_TR_NOOP, QVariant, pyqtSlot, QMetaType, QDir, QRegularExpression, QPointF + QSettings, QSize, QTimer, QTranslator, QLocale, QT_TR_NOOP, QVariant, pyqtSlot, QMetaType, QDir, \ + QRegularExpression, QPointF, QRect from PyQt5.QtDBus import QDBusConnection, QDBusInterface, QDBusMessage, QDBusArgument, QDBusVariant from PyQt5.QtGui import QPixmap, QIcon, QCursor, QImage, QPainter, QRegularExpressionValidator, \ QPalette, QGuiApplication, QColor, QValidator, QPen, QFont, QFontMetrics, QMouseEvent, QResizeEvent, QKeySequence, QPolygon, \ QDoubleValidator from PyQt5.QtSvg import QSvgWidget, QSvgRenderer from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QHBoxLayout, QSlider, QMessageBox, QLineEdit, QLabel, \ - QSplashScreen, QPushButton, QProgressBar, QComboBox, QSystemTrayIcon, QMenu, QStyle, QTextEdit, QDialog, QTabWidget, \ + QSplashScreen, QPushButton, QComboBox, QSystemTrayIcon, QMenu, QStyle, QTextEdit, QDialog, QTabWidget, \ QCheckBox, QPlainTextEdit, QGridLayout, QSizePolicy, QAction, QMainWindow, QToolBar, QToolButton, QFileDialog, \ QWidgetItem, QScrollArea, QGroupBox, QFrame, QSplitter, QSpinBox, QDoubleSpinBox, QInputDialog, QStatusBar, QShortcut, \ QSpacerItem, QListWidget, QListWidgetItem @@ -937,10 +980,12 @@ APPNAME = "VDU Controls" -VDU_CONTROLS_VERSION = '2.4.3' +VDU_CONTROLS_VERSION = '2.5.0' VDU_CONTROLS_VERSION_TUPLE = tuple(int(i) for i in VDU_CONTROLS_VERSION.split('.')) assert sys.version_info >= (3, 8), f'{APPNAME} utilises python version 3.8 or greater (your python is {sys.version}).' +TOOLTIP_DURATION_MSEC = 750 + WESTERN_SKY = 'western-sky' EASTERN_SKY = 'eastern-sky' @@ -969,13 +1014,14 @@ ERROR_SYMBOL = '\u274e' # NEGATIVE SQUARED CROSS MARK WARNING_SYMBOL = '\u26a0' # WARNING SIGN ALMOST_EQUAL_SYMBOL = '\u2248' # ALMOST EQUAL TO -SMOOTHING_SYMBOL = '\u21dd' # RIGHT POINTING SQUIGGLY ARROW +SMOOTHING_SYMBOL = '\u219D' # RIGHTWARDS WAVE ARROW STEPPING_SYMBOL = '\u279f' # DASHED TRIANGLE-HEADED RIGHTWARDS ARROW RAISED_HAND_SYMBOL = '\u270b' # RAISED HAND RIGHT_POINTER_WHITE = '\u25B9' # WHITE RIGHT-POINTING SMALL TRIANGLE RIGHT_POINTER_BLACK = '\u25B8' # BLACK RIGHT-POINTING SMALL TRIANGLE MENU_ACTIVE_PRESET_SYMBOL = '\u25c2' # BLACK LEFT-POINTING SMALL TRIANGLE -SET_VCP_SYMBOL = "\u25B7" # WHITE RIGHT-POINTING TRIANGLE +SET_VCP_SYMBOL = '\u25B7' # WHITE RIGHT-POINTING TRIANGLE +MESSAGE_SYMBOL = '\u23F5' # MEDIA PLAY SolarElevationKey = namedtuple('SolarElevationKey', ['direction', 'elevation']) SolarElevationData = namedtuple('SolarElevationData', ['azimuth', 'zenith', 'when']) @@ -990,14 +1036,6 @@ return type_id.value if isinstance(type_id, Enum) else type_id # awfulness of enums in pyqt6 -def is_gnome_desktop() -> bool: - return os.environ.get('XDG_CURRENT_DESKTOP', default='unknown').lower() == 'gnome' - - -def is_cosmic_desktop() -> bool: - return os.environ.get('XDG_CURRENT_DESKTOP', default='unknown').lower() == 'cosmic' - - def is_running_in_gui_thread() -> bool: return QThread.currentThread() == gui_thread @@ -1134,7 +1172,8 @@ <a href="https://github.com/digitaltrails/vdu_controls/releases/tag/v{VERSION}"> https://github.com/digitaltrails/vdu_controls/releases/tag/v{VERSION}</a> <br/>___________________________________________________________________________""" -RELEASE_INFO = QT_TR_NOOP('Bug fix release.') +RELEASE_INFO = QT_TR_NOOP('<b>Modernity</b>: Appearance Refresh. <span style="font-size: 50px;">🎉</span>"' + '<br/>Relocatable toolbar and status-bar - see Settings.') CURRENT_PRESET_NAME_FILE = CONFIG_DIR_PATH.joinpath('current_preset.txt') CUSTOM_TRAY_ICON_FILE = CONFIG_DIR_PATH.joinpath('tray_icon.svg') @@ -1166,13 +1205,29 @@ mono_light_tray = False MONOCHROME_APP_ICON = b""" <svg viewBox="0 0 22 22" version="1.1" id="svg1" xmlns="http://www.w3.org/2000/svg"> - <defs id="defs3051"><style type="text/css" id="current-color-scheme">.ColorScheme-Text {color:#ffffff;}</style></defs> + <defs id="defs3051"><style type="text/css" id="current-color-scheme">.ColorScheme-Text {color:#232629;}</style></defs> <path style="fill:currentColor;fill-opacity:1;stroke:none" class="ColorScheme-Text" d="m 3.012318,1.987629 -0.086226,13.98553 h 1 l 5.0022397,0.02464 -1e-7,2 -2.0022396,-0.02464 v 1 h 8.0000002 v -1 l -2.00224,-0.01232 -0.01232,-2 5.01456,0.01232 h 1 L 18.957944,2.0296853 17.989795,2.0050493 4.0174203,1.9774244 Z m 0.9927843,1.0339651 13.9651597,-0.01742 -0.01954,10.9465899 -14.0000001,0.02464 z" id="rect4211"/> </svg>""" + +FALLBACK_SPLASH_SVG = b""" +<svg viewBox="0 0 24 24" width="256" height="256" fill="none" xmlns="http://www.w3.org/2000/svg"> + <style type="text/css" id="current-color-scheme"> .ColorScheme-Text { color:#232629; } </style> + <linearGradient id="screenGradient" x1="0%" y1="0%" x2="100%" y2="100%"> + <stop offset="0%" stop-color="#66c0f1" /> <!-- Start color (offset 0%) --> + <stop offset="100%" stop-color="#3f7eed" /> <!-- End color (offset 100%) --> + </linearGradient> + <g class="ColorScheme-Text" stroke="currentColor" stroke-linecap="round" stroke-width="1.2" transform=""> + <rect x="2.5" y="3" width="19.25" height="15" fill="url(#screenGradient)" rx="1" ry="1"/> + <path fill="None" d="M 3 17.5 L 21.5 17.5 M 8.5 20.5 L 15.75 20.5"/> + <path stroke-width="2" stroke-linecap="square" fill="None" d="M 11 19 L 13 19"/> + </g> +</svg>""" + + # modified brightness icon from breeze5-icons: LGPL-3.0-only BRIGHTNESS_SVG = b""" <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 24 24" width="24" height="24"> @@ -1263,11 +1318,35 @@ <svg viewBox="0 0 24 24" width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"> <style type="text/css" id="current-color-scheme"> .ColorScheme-Text { color:#232629; } </style> <g class="ColorScheme-Text" stroke="currentColor" stroke-linecap="round" stroke-width="2" transform=""> + <path fill="None" d="M 20 18 L 1 18 1 5 20 5 20 18 M 6.5 21 L 15 21"/> + </g> +</svg> +""" + +VDU_POWER_ON_ICON_SOURCE = b""" +<svg viewBox="0 0 24 24" width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <style type="text/css" id="current-color-scheme"> .ColorScheme-Text { color:#232629; } </style> + <g class="ColorScheme-Text" stroke="currentColor" stroke-linecap="round" stroke-width="2" transform=""> <path fill="None" d="M14 12 A 5 5 0 1 0 20 12 M 17 11 L 17 16.5 M 9 20 L 1 20 1 5 20 5 20 8"/> </g> </svg> """ +AMBIENT_PANEL_ICON_SOURCE = b""" +<svg viewBox="0 0 24 24" width="24" height="24" fill="none" xmlns="http://www.w3.org/2000/svg"> + <style type="text/css" id="current-color-scheme"> .ColorScheme-Text { color:#232629; } </style> + <g class="ColorScheme-Text" stroke="currentColor" stroke-linecap="round" stroke-width="2"> + <path fill="none" d="M9 20 L1 20 1 5 20 5 20 7" /> + <circle cx="17" cy="16" r="5" stroke="currentColor" fill="none" /> + <rect x="11" y="21.5" width="1" height="1" rx="5" ry="5" stroke-width="1" fill="currentColor" /> + <rect x="8.5" y="15.5" width="1" height="1" rx="5" ry="5" stroke-width="1" fill="currentColor" /> + <rect x="10.5" y="10" width="1" height="1" rx="5" ry="5" stroke-width="1" fill="currentColor" /> + <rect x="15.5" y="7.5" width="1" height="1" rx="5" ry="5" stroke-width="1" fill="currentColor" /> + <rect x="21.5" y="9" width="1" height="1" rx="5" ry="5" stroke-width="1" fill="currentColor" /> + </g> +</svg> +""" + # view-refresh icon from breeze5-icons: LGPL-3.0-only REFRESH_ICON_SOURCE = b""" <svg xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 24 24" width="24" height="24"> @@ -1402,14 +1481,6 @@ return ThemeType.POLYCHROME_DARK if is_dark_theme() else ThemeType.POLYCHROME_LIGHT -def get_tray_theme_type(main_config: VduControlsConfig): - if main_config.is_set(ConfOpt.MONO_LIGHT_TRAY_ENABLED): - return ThemeType.MONOCHROME_LIGHT - if main_config.is_set(ConfOpt.MONOCHROME_TRAY_ENABLED): - return ThemeType.MONOCHROME_DARK - return ThemeType.UNTHEMED # Don't alter colors for overlay onto app icon in tray - - DEVELOPERS_NATIVE_FONT_HEIGHT = 32 # The font height in physical pixels being used on my development desktop. native_font_height_pixels: int | None = None # A metric for use in sizing components relative to DEVELOPERS_NATIVE_FONT_HEIGHT. @@ -1427,13 +1498,12 @@ def get_splash_image() -> QPixmap: - """Get the splash pixmap from the installed png, failing that, the internal splash png.""" - pixmap = QPixmap() + """Get the splash pixmap from the installed png, failing that, the internal splash svg.""" if os.path.isfile(DEFAULT_SPLASH_PNG) and os.access(DEFAULT_SPLASH_PNG, os.R_OK): + pixmap = QPixmap() pixmap.load(DEFAULT_SPLASH_PNG) - else: - pixmap.loadFromData(base64.decodebytes(FALLBACK_SPLASH_PNG_BASE64), 'PNG') - return pixmap + return pixmap + return create_pixmap_from_svg_bytes(FALLBACK_SPLASH_SVG, 256, 256) def clamp(v: int, min_v: int, max_v: int) -> int: @@ -2530,6 +2600,12 @@ tip=QT_TR_NOOP('monochrome dark themed system tray')) MONO_LIGHT_TRAY_ENABLED = _def(cname=QT_TR_NOOP('mono-light-tray-enabled'), default="no", restart=False, tip=QT_TR_NOOP('monochrome light themed system tray')) + TRAY_FOLLOWS_THEME = _def(cname=QT_TR_NOOP('tray-follows-theme'), default="yes", restart=False, + tip=QT_TR_NOOP('tray dark/light theming follows desktop-theme changes')) + TOOLBAR_AT_TOP = _def(cname=QT_TR_NOOP('toolbar-at-top'), default="no", restart=False, + tip=QT_TR_NOOP('toolbar resides at top of main window')) + SEPARATE_STATUS_BAR = _def(cname=QT_TR_NOOP('separate-status-bar'), default="no", restart=True, + tip=QT_TR_NOOP('seperate the status-bar from the tool-bar')) PROTECT_NVRAM_ENABLED = _def(cname=QT_TR_NOOP('protect-nvram'), default="yes", restart=True, tip=QT_TR_NOOP('alter options and defaults to minimize VDU NVRAM writes')) ORDER_BY_NAME = _def(cname=QT_TR_NOOP('order-by-name'), default="no", @@ -3090,7 +3166,35 @@ class SubWinDialog(QDialog): # Fix for gnome: QDialog must be a subwindow, otherwise it will always stay on top of other windows. def __init__(self, parent: QWidget | None = None) -> None: - super().__init__(parent, Qt.WindowType.SubWindow if is_gnome_desktop() else Qt.WindowType.Window) + super().__init__(parent, Qt.WindowType.SubWindow) + + + +class IconLabel(QWidget): + + def __init__(self, icon_source: bytes, main_text: str, sub_text) -> None: + super().__init__() + layout = QHBoxLayout() + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + self.setLayout(layout) + self.svg_icon: QSvgWidget | None = None + self.icon_source = icon_source + svg_icon = QSvgWidget() + svg_icon.load(handle_theme(icon_source, polychrome_light_or_dark())) + svg_icon.setFixedSize(native_font_height(scaled=1.8), native_font_height(scaled=1.8)) + self.svg_icon = svg_icon + layout.addWidget(svg_icon) + self.label = QLabel(f"<span style='font-weight:bold;'>{main_text}<br/>" + f"<span style='font-size:{native_font_height(0.5)}px; font-weight:normal;'>{sub_text}</span>") + self.label.setTextFormat(Qt.TextFormat.RichText) + self.label.setWordWrap(True) + layout.addWidget(self.label) + + def event(self, event: QEvent | None) -> bool: + if event and event.type() == QEvent.Type.PaletteChange: # PalletChange happens after the new style sheet is in use. + self.svg_icon.load(handle_theme(self.icon_source, polychrome_light_or_dark())) + return super().event(event) + class StdButton(QPushButton): # Reduce some repetitiveness in the code @@ -3723,12 +3827,13 @@ layout = QHBoxLayout() self.setLayout(layout) self.svg_icon: QSvgWidget | None = None + self.setToolTip(tr(vcp_capability.name)) + self.setToolTipDuration(TOOLTIP_DURATION_MSEC) if (vcp_capability.vcp_code in SUPPORTED_VCP_BY_CODE and SUPPORTED_VCP_BY_CODE[vcp_capability.vcp_code].icon_source is not None): svg_icon = QSvgWidget() svg_icon.load(handle_theme(SUPPORTED_VCP_BY_CODE[vcp_capability.vcp_code].icon_source, polychrome_light_or_dark())) svg_icon.setFixedSize(native_font_height(scaled=1.8), native_font_height(scaled=1.8)) - svg_icon.setToolTip(vcp_capability.translated_name()) self.svg_icon = svg_icon layout.addWidget(svg_icon) else: @@ -3800,6 +3905,8 @@ layout.addWidget(QLabel(self.translate_label(vcp_capability.name))) self.combo_box = combo_box = QComboBox() layout.addWidget(combo_box) + self.setToolTip(tr(vcp_capability.name)) + self.setToolTipDuration(TOOLTIP_DURATION_MSEC) self.keys = [] for value, desc in self.vcp_capability.values: @@ -3855,7 +3962,9 @@ def __init__(self, controller: VduController, vdu_exception_handler: Callable) -> None: super().__init__() layout = QVBoxLayout() - self.label = QLabel(tr('Monitor {}: {}').format(controller.vdu_number, controller.get_vdu_preferred_name())) + create_icon_from_svg_bytes(VDU_CONNECTED_ICON_SOURCE) + self.label = IconLabel(VDU_CONNECTED_ICON_SOURCE, + controller.get_vdu_preferred_name(), tr("Monitor {}".format(controller.vdu_number))) layout.addWidget(self.label) self.controller: VduController = controller self.vcp_controls: List[VduControlBase] = [] @@ -3881,6 +3990,11 @@ layout.addWidget(control) self.vcp_controls.append(control) + line = QFrame() + line.setFrameShape(QFrame.Shape.HLine) + line.setFrameShadow(QFrame.Shadow.Sunken) + layout.addWidget(line) + if len(self.vcp_controls) != 0: self.setLayout(layout) @@ -3919,8 +4033,9 @@ def update_stats(self): name, sid = self.controller.get_vdu_preferred_name(), self.controller.vdu_stable_id - self.label.setToolTip(tr("{}\nSet-VCP writes: {}").format(sid if id == name else f"{name}\n({sid})", - self.controller.get_write_count())) + self.label.setToolTip(tr("{}\nSet-VCP writes: {}\nMonitor {}").format(sid if id == name else f"{name}\n({sid})", + self.controller.get_write_count(), + self.controller.vdu_number)) class Preset: @@ -4247,30 +4362,66 @@ class ToolButton(QToolButton): - - def __init__(self, svg_source: bytes, tip: str | None = None, parent: QWidget | None = None) -> None: + def __init__(self, svg_source: bytes, tip: str | None = None, parent: QWidget | None = None): super().__init__(parent) if tip is not None: self.setToolTip(tip) self.svg_source = svg_source + self._original_icon = None + self._busy_timer = QTimer(self) + self._busy_timer.timeout.connect(self._update_busy_icon) + self._busy_angle = 0 + self._busy_now = False self.refresh_icon() - def refresh_icon(self, svg_source: bytes | None = None) -> None: # may refresh the theme (coloring light/dark) of the icon - if svg_source is not None: # Either a new icon or if None just a light/dark theme refresh + def refresh_icon(self, svg_source: bytes | None = None): + if svg_source is not None: self.svg_source = svg_source - self.setIcon(create_icon_from_svg_bytes(self.svg_source)) # this may alter the SVG for light/dark theme + self._original_icon = create_icon_from_svg_bytes(self.svg_source) # Store the original icon so we can restore it later + if not self._busy_now: + self.setIcon(self._original_icon) + + def setBusy(self, busy: bool): # Start or stop the busy spinner animation. + if busy == self._busy_now: + return + self._busy_now = busy + if busy: # Start spinning + self._busy_angle = 0 + self._busy_timer.start(30) # ~33 fps + else: # Stop spinning and restore original icon + self._busy_timer.stop() + self.setIcon(self._original_icon) + + def _update_busy_icon(self): + size = self.iconSize() # Use the button's icon size (or a default size if none) + if size.width() <= 0 or size.height() <= 0: + size = self.size() # fallback to button size + pixmap = QPixmap(size) + pixmap.fill(Qt.GlobalColor.transparent) + painter = QPainter(pixmap) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + if QT5_QPAINTER_HIGH_QUALITY_ANTIALIASING: + painter.setRenderHint(QT5_QPAINTER_HIGH_QUALITY_ANTIALIASING) + pen_width = max(npx(2), size.width() // 10) # Determine a good pen width relative to size + painter.setPen(QPen(self.palette().buttonText().color(), pen_width, Qt.PenStyle.SolidLine, Qt.PenCapStyle.RoundCap)) + rect = QRect(0, 0, size.width(), size.height()).adjusted(margin := pen_width // npx(2) + npx(1), margin, -margin, -margin) + painter.drawArc(rect, self._busy_angle * 16, 270 * 16) # Draw the rotating arc (270 degrees) + painter.end() + self.setIcon(QIcon(pixmap)) + self._busy_angle = (self._busy_angle + 8) % 360 # Advance the angle for the next frame -class VduPanelBottomToolBar(QToolBar): +class VduMainToolBar(QToolBar): def __init__(self, tool_buttons: List[ToolButton], app_context_menu: ContextMenu, parent: VduControlsMainPanel) -> None: super().__init__(parent=parent) + self.setObjectName('VduPanelToolBar') # Internal name for persistence - do not change or persistence will be lost. self.preset_edit_target: Preset | None = None + self.setMovable(False) self.tool_buttons = tool_buttons for button in self.tool_buttons: self.addWidget(button) self.setIconSize(QSize(native_font_height(), native_font_height())) - self.progress_bar: QProgressBar | None = None self.status_area = QStatusBar() self.addWidget(self.status_area) self.menu_button = ToolButton(MENU_ICON_SOURCE, tr("Context and Preset Menu"), self) @@ -4285,27 +4436,17 @@ self.preset_action.triggered.connect(edit_current_preset) self.addWidget(self.menu_button) - self.installEventFilter(self) - def eventFilter(self, target: QObject | None, event: QEvent | None) -> bool: - # PalletChange happens after the new style sheet is in use. - if event and event.type() == QEvent.Type.PaletteChange: - for button in self.tool_buttons: - button.refresh_icon() - self.menu_button.refresh_icon() - return super().eventFilter(target, event) + def refresh_buttons(self): + for button in self.tool_buttons: + button.refresh_icon() + self.menu_button.refresh_icon() def indicate_busy(self, is_busy: bool = True) -> None: - if is_busy and self.progress_bar is None: - self.status_area.clearMessage() - self.progress_bar = QProgressBar(self) - self.progress_bar.setTextVisible(False) # Disable text percentage label on the spinner progress-bar - self.progress_bar.setRange(0, 0) # 0,0 causes the progress bar to pulsate left/right - used as a busy spinner. - self.status_area.addWidget(self.progress_bar, 1) - self.progress_bar.show() # According to the Qt docs, this is necessary because removing it just hides it. - elif self.progress_bar is not None: - self.status_area.removeWidget(self.progress_bar) - self.progress_bar = None + if is_busy: + self.tool_buttons[0].setBusy(True) + else: + self.tool_buttons[0].setBusy(False) QApplication.sendPostedEvents(self, 0) # Flush any change events before resetting the flag QApplication.processEvents() # Force the flushed events to be processed now @@ -4331,12 +4472,13 @@ def __init__(self) -> None: super().__init__() - self.bottom_toolbar: VduPanelBottomToolBar | None = None + self.main_toolbar: VduMainToolBar | None = None self.refresh_data_task = None self.setObjectName("vdu_controls_main_panel") self.vdu_control_panels: Dict[str, VduControlPanel] = {} self.alert: QMessageBox | None = None self.main_controller: VduAppController | None = None + self.message_history = [] def initialise_control_panels(self, main_controller: VduAppController, app_context_menu: ContextMenu, main_config: VduControlsConfig, @@ -4354,9 +4496,9 @@ old_layout.removeItem(item) item.widget().deleteLater() controllers_layout = QVBoxLayout() - controllers_layout.setSpacing(0) + controllers_layout.setSpacing(npx(5)) cl_margins = controllers_layout.contentsMargins() - controllers_layout.setContentsMargins(cl_margins.left(), 0, cl_margins.right(), 0) + controllers_layout.setContentsMargins(cl_margins.left(), npx(5), cl_margins.right(), npx(5)) self.setLayout(controllers_layout) warnings_enabled = main_config.is_set(ConfOpt.WARNINGS_ENABLED) @@ -4395,8 +4537,8 @@ no_vdu_layout.addSpacing(32) controllers_layout.addWidget(no_vdu_widget) - self.bottom_toolbar = VduPanelBottomToolBar(tool_buttons=tool_buttons, app_context_menu=app_context_menu, parent=self) - controllers_layout.addWidget(self.bottom_toolbar, 0, Qt.AlignmentFlag.AlignBottom) + self.main_toolbar = VduMainToolBar(tool_buttons=tool_buttons, app_context_menu=app_context_menu, parent=self) + main_controller.replace_toolbar(self.main_toolbar) def _open_context_menu(position: QPoint) -> None: assert app_context_menu is not None @@ -4406,8 +4548,8 @@ self.customContextMenuRequested.connect(_open_context_menu) def indicate_busy(self, is_busy: bool = True, lock_controls: bool = True) -> None: - if self.bottom_toolbar is not None: - self.bottom_toolbar.indicate_busy(is_busy) + if self.main_toolbar is not None: + self.main_toolbar.indicate_busy(is_busy) if lock_controls: for control_panel in self.vdu_control_panels.values(): control_panel.setDisabled(is_busy) @@ -4420,8 +4562,8 @@ return True def show_active_preset(self, preset: Preset | None) -> None: - if self.bottom_toolbar: - self.bottom_toolbar.show_active_preset(preset) + if self.main_toolbar: + self.main_toolbar.show_active_preset(preset) def show_vdu_exception(self, exception: VduException, can_retry: bool = False) -> bool: log_error(f"{exception.vdu_description} {exception.operation} {exception.attr_id} {exception.cause}") @@ -4445,7 +4587,16 @@ return answer == MBtn.Retry def status_message(self, message: str, timeout: int): - self.bottom_toolbar.status_area.showMessage(message, timeout) if self.bottom_toolbar else None + if message.strip(): # Only non-empty messages, ignore blank messages, they're just clearing the status bar. + self.message_history.append(f"\n{datetime.now().strftime("%H:%M:%S")}{MESSAGE_SYMBOL} {message}") + self.message_history = self.message_history[-9:] + if self.main_controller.main_config.is_set(ConfOpt.SEPARATE_STATUS_BAR): + self.main_controller.main_window.statusBar().showMessage(message, timeout) + self.main_controller.main_window.statusBar().setToolTip("".join([tr('Message history:')] + self.message_history)) + elif self.main_toolbar: + self.main_toolbar.status_area.showMessage(message, timeout) + self.main_toolbar.status_area.setToolTip("".join([tr('Message history:')] + self.message_history)) + @dataclass class BulkChangeItem: @@ -5707,7 +5858,7 @@ self.vip_menu = QMenu() self.vip_menu.triggered.connect(self.vip_menu_triggered) edit_panel_layout.addWidget(self.preset_name_edit) - self.vdu_init_button = ToolButton(VDU_CONNECTED_ICON_SOURCE, tr("Create VDU specific\nInitialization-Preset"), self) + self.vdu_init_button = ToolButton(VDU_POWER_ON_ICON_SOURCE, tr("Create VDU specific\nInitialization-Preset"), self) self.vdu_init_button.setMenu(self.vip_menu) self.vdu_init_button.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) edit_panel_layout.addWidget(self.vdu_init_button) @@ -6095,14 +6246,14 @@ details=tr('Details: {}').format(''.join(traceback.format_exception(e_type, e_value, e_traceback)))).exec() -def create_pixmap_from_svg_bytes(svg_bytes: bytes) -> QPixmap: +def create_pixmap_from_svg_bytes(svg_bytes: bytes, width: int = 64, height: int = 64) -> QPixmap: """There is no QIcon option for loading SVG from a string, only from a SVG file, so roll our own.""" - return QPixmap.fromImage(create_image_from_svg_bytes(svg_bytes)) + return QPixmap.fromImage(create_image_from_svg_bytes(svg_bytes, width, height)) -def create_image_from_svg_bytes(svg_bytes) -> QImage: +def create_image_from_svg_bytes(svg_bytes, width: int = 64, height: int = 64) -> QImage: renderer = QSvgRenderer(svg_bytes) - image = QImage(64, 64, QImage.Format.Format_ARGB32) + image = QImage(width, height, QImage.Format.Format_ARGB32) image.fill(0x0) painter = QPainter(image) renderer.render(painter) @@ -6552,13 +6703,13 @@ return round(self.plot_height * percent / 100) def lux_from_x(self, x) -> int: - lux = 0 if x <= 0 else round(10.0 ** (math.log10(1) + (x / self.plot_width) * (math.log10(100_000) - math.log10(1)))) + lux = 0 if x <= 0 else round(10.0 ** ((x / self.plot_width) * math.log10(100_000))) if lux > 100_000: return 100_000 return lux def x_from_lux(self, lux: int) -> int: - return round((math.log10(lux) - math.log10(1)) / ((math.log10(100000) - math.log10(1)) / self.plot_width)) if lux > 0 else 0 + return round(math.log10(lux) / (math.log10(100000) / self.plot_width)) if lux > 0 else 0 @dataclass class LuxGaugeHistory: @@ -6690,9 +6841,10 @@ painter.drawText(QPointF(middle + npx(6), y + 4), str(i)) # Draw hour ticks along the bottom tick_len = line_width * 2 - noon_tick_len = line_width * 4 - for hx in range((hw := 60 // round(minutes_per_point)), hw * 25, hw): - painter.drawLine(x := df_plot_left + hx, plot_height, x, plot_height - (noon_tick_len if hx == (12 * hw) else tick_len)) + points_per_hour = df_plot_width / 24 + for hour in range(24): # Tick length multiplier: 3 for 0/12, 2 for multiples of 3, else 1 + x = round(df_plot_left + points_per_hour * hour) + painter.drawLine(x, plot_height, x, plot_height - tick_len * (3 if hour % 12 == 0 else (2 if hour % 3 == 0 else 1))) # Draw the sun if most_recent_df_xy and most_recent_item and most_recent_df_xy[0]: if self.sun_image is None: @@ -6743,8 +6895,9 @@ self.updates_enabled = enable def _y_from_lux(self, lux: int, required_height: int) -> int: - return required_height - ( - round((math.log10(lux) - math.log10(1)) / ((math.log10(200000) - math.log10(1)) / required_height)) if lux > 0 else 0) + if lux <= 0: + return required_height + return required_height - round(math.log10(lux) / math.log10(200000) * required_height) def lux_create_device(device_name: str) -> LuxMeterDevice: @@ -6908,7 +7061,7 @@ def __init__(self) -> None: super().__init__(requires_worker=False, manual=True, semi_auto=True) self.current_value: float = LuxMeterSemiAutoDevice.get_stored_value() - LuxMeterSemiAutoDevice.daylight_factor = None # Force initilization from file + LuxMeterSemiAutoDevice.daylight_factor = None # Force initialization from file _ = LuxMeterSemiAutoDevice.get_daylight_factor() def get_value(self) -> float | None: @@ -7228,7 +7381,7 @@ def interpolate_brightness(self, smoothed_lux: int, current_point: LuxPoint, next_point: LuxPoint) -> int: def _x_from_lux(lux: int) -> float: - return ((math.log10(lux) - math.log10(1)) / (math.log10(100000) - math.log10(1))) if lux > 0 else 0 + return (math.log10(lux) / math.log10(100000)) if lux > 0 else 0 interpolated_brightness = float(current_point.brightness) x_smoothed = _x_from_lux(smoothed_lux) @@ -7252,7 +7405,7 @@ def lux_summary(self, metered_lux: float, smoothed_lux: int) -> str: lux_int = round(metered_lux) # 256 bit char in lux_summary_text can cause issues if stdout not utf8 (force utf8 for stdout) - return f"{lux_int}{SMOOTHING_SYMBOL}{smoothed_lux} lux" if lux_int != smoothed_lux else f"{lux_int} lux" + return f"{lux_int} {SMOOTHING_SYMBOL} {smoothed_lux} lux {tr('(smoothed)')}" if lux_int != smoothed_lux else f"{lux_int} lux" def stop(self) -> None: super().stop() @@ -7399,6 +7552,7 @@ self.profile_selector_widget.setSizeAdjustPolicy(QListWidget.SizeAdjustPolicy.AdjustToContents) self.profile_selector_widget.setFlow(QListWidget.Flow.LeftToRight) self.profile_selector_widget.setSpacing(0) + self.profile_selector_widget.setMinimumWidth(npx(940)) self.profile_selector_widget.setMinimumHeight(native_font_height(scaled=1.4)) main_layout.addWidget(self.profile_selector_widget, stretch=0) @@ -7963,7 +8117,12 @@ top_layout.setSpacing(0) tl_margins = top_layout.contentsMargins() top_layout.setContentsMargins(tl_margins.left(), 0, tl_margins.right(), 0) - top_layout.addWidget(QLabel(tr("Ambient Light Level (lux)")), alignment=Qt.AlignmentFlag.AlignBottom) + + label = IconLabel(AMBIENT_PANEL_ICON_SOURCE, tr("Ambient Light Level"), tr("lux")) + label.setToolTip(tr("Set the ambient light level to adjust all monitors.")) + top_layout.addWidget(label, + alignment=Qt.AlignmentFlag.AlignBottom) + input_panel = QWidget() input_panel_layout = QHBoxLayout() @@ -7976,8 +8135,9 @@ self.slider = ClickableSlider() self.slider.setToolTip(tr("Ambient light level input (lux value)")) + self.slider.setToolTipDuration(TOOLTIP_DURATION_MSEC) self.slider.setMinimumWidth(npx(200)) - self.slider.setRange(int(math.log10(1) * 1000), int(math.log10(100000) * 1000)) + self.slider.setRange(0, int(math.log10(100000) * 1000)) self.slider.setSingleStep(1) self.slider.setPageStep(100) self.slider.setTickInterval(1000) @@ -7998,6 +8158,7 @@ self.lux_input_field = QSpinBox() self.lux_input_field.setLineEdit(LineEditAll()) self.lux_input_field.setToolTip(tr("Ambient light level input (lux value)")) + self.lux_input_field.setToolTipDuration(TOOLTIP_DURATION_MSEC) self.lux_input_field.setKeyboardTracking(False) self.lux_input_field.setRange(1, 100000) self.lux_input_field.setValue(self.current_value) @@ -8641,7 +8802,7 @@ self.status_message(tr("Error during restoration preset {}").format(preset.name), timeout=5) return log_info(f"Restored initialization-preset '{worker.context.name}'") - message = tr("Restored Preset\n{}").format(worker.context.name) + message = tr("Restored I-Preset {}").format(worker.context.name) self.status_message(message, timeout=5) self.main_window.splash_message_qtsignal.emit(message) time.sleep(1.0) # Pause to give the message time to display - TODO find non-delaying solution @@ -8940,6 +9101,15 @@ self.main_window.app_save_window_state() QCoreApplication.exit(EXIT_CODE_FOR_RESTART) + def replace_toolbar(self, main_toolbar): + target_window = self.main_window + for old_toolbar in target_window.findChildren(QToolBar): # Make sure there is only one toolbar + target_window.removeToolBar(old_toolbar) + old_toolbar.deleteLater() + at_top = self.main_config.is_set(ConfOpt.TOOLBAR_AT_TOP) + toolbar_area = Qt.ToolBarArea.TopToolBarArea if at_top else Qt.ToolBarArea.BottomToolBarArea + target_window.addToolBar(toolbar_area, main_toolbar) + class VduAppWindow(QMainWindow): splash_message_qtsignal = pyqtSignal(str) @@ -8961,6 +9131,8 @@ self.scroll_area: QScrollArea | None = None self.main_config = main_config self.hide_shortcuts = True + self.initial_theme_is_dark = is_dark_theme() + log_info(f"Started with dark theme: {self.initial_theme_is_dark}") def _run_in_gui(task: Callable): log_debug(f"Running task in gui thread {repr(task)}") if log_debug_enabled else None @@ -8968,10 +9140,6 @@ self._run_in_gui_thread_qtsignal.connect(_run_in_gui) - os_desktop = os.environ.get('XDG_CURRENT_DESKTOP', default='unknown').lower() - use_gnome_like_tray = main_config.is_set(ConfOpt.SYSTEM_TRAY_ENABLED) and (is_gnome_desktop() or is_cosmic_desktop()) - log_info(f"{os_desktop=} {use_gnome_like_tray=}") # Gnome tray doesn't provide a way to bring up the main app. - global log_debug_enabled if log_debug_enabled: for screen in app.screens(): @@ -8979,7 +9147,7 @@ self.app_context_menu = ContextMenu( app_controller=main_controller, - main_window_action=partial(self.show_main_window, True) if use_gnome_like_tray else None, + main_window_action=partial(self.show_main_window, True), # Gnome tray doesn't provide a way to bring up the main app. about_action=partial(AboutDialog.invoke, self.main_controller), help_action=HelpDialog.invoke, gray_scale_action=GreyScaleDialog, @@ -9110,17 +9278,16 @@ self.activateWindow() def show(self): - if self.main_config.is_set(ConfOpt.SMART_WINDOW): - if not self.app_restore_window_state(): # No previous state or invalid + if not self.app_restore_window_state(): # No previous state or invalid + if self.main_config.is_set(ConfOpt.SMART_WINDOW): self.adjustSize() self.app_decide_window_position() # decide initial position relative to cursor self.app_save_window_state() super().show() def hide(self): - if self.main_config.is_set(ConfOpt.SMART_WINDOW): - if self.isVisible(): # Only save position if really on screen - self.app_save_window_state() + if self.isVisible(): # Only save position if really on screen + self.app_save_window_state() super().hide() def quit_app(self) -> None: @@ -9132,7 +9299,7 @@ global mono_light_tray self.app_icon = QIcon() self.app_icon.addPixmap(get_splash_image() if splash_pixmap is None else splash_pixmap) - tray_theme_type = get_tray_theme_type(self.main_config) + tray_theme_type = self.get_tray_theme_type() if CUSTOM_TRAY_ICON_FILE.exists() and os.access(CUSTOM_TRAY_ICON_FILE.as_posix(), os.R_OK): log_info(f"Loading custom app_icon: {CUSTOM_TRAY_ICON_FILE} {tray_theme_type=}") self.tray_icon = create_icon_from_path(CUSTOM_TRAY_ICON_FILE, tray_theme_type) @@ -9179,7 +9346,6 @@ self.scroll_area.setWidgetResizable(True) self.scroll_area.setWidget(self.main_panel) self.setCentralWidget(self.scroll_area) - available_height = self.screen().availableGeometry().height() - npx(200) # Minus allowance for panel/tray hint_height = self.main_panel.sizeHint().height() # The hint is the actual required layout space hint_width = self.main_panel.sizeHint().width() @@ -9187,13 +9353,14 @@ if hint_height > available_height: log_debug(f"Main panel too high, adding scroll-area {hint_height=} {available_height=}") if log_debug_enabled else None self.setMaximumHeight(available_height) - self.setMinimumWidth(hint_width + 20) # Allow extra space for disappearing scrollbars + self.setMinimumWidth(hint_width + npx(20)) # Allow extra space for disappearing scrollbars else: # Don't mess with the size unnecessarily - let the user determine it? - self.setMinimumHeight(hint_height + 20) + number_of_vdus = len(self.main_controller.get_vdu_stable_id_list()) + self.setMinimumHeight(hint_height + npx(30) * (number_of_vdus + 1)) if hint_height != self.height(): self.setMinimumWidth(self.width()) self.adjustSize() - self.setMinimumWidth(hint_width + 20) + self.setMinimumWidth(hint_width + npx(20)) self.splash_message_qtsignal.emit(tr("Checking Presets")) @@ -9210,6 +9377,19 @@ PresetsDialog.show_status_message(message=message, timeout=timeout) self.status_message(message, timeout=timeout, destination=MsgDestination.DEFAULT) + def get_tray_theme_type(self): # Ugly because Qt has no way to access the tray theme + theme = ThemeType.UNTHEMED # Don't alter colors for overlay onto app icon in tray if unthemed + if self.main_config.is_set(ConfOpt.MONOCHROME_TRAY_ENABLED): + theme = ThemeType.MONOCHROME_DARK + if self.main_config.is_set(ConfOpt.MONO_LIGHT_TRAY_ENABLED): + theme = ThemeType.MONOCHROME_LIGHT + if theme != ThemeType.UNTHEMED: + theme_has_flipped = self.initial_theme_is_dark != is_dark_theme() + if theme_has_flipped and self.main_config.is_set(ConfOpt.TRAY_FOLLOWS_THEME): + log_info(f"Option {ConfOpt.TRAY_FOLLOWS_THEME.conf_id} is set: Desktop theme flipped - flipping tray theme") + theme = ThemeType.MONOCHROME_LIGHT if theme == ThemeType.MONOCHROME_DARK else ThemeType.MONOCHROME_DARK + return theme + def update_status_indicators(self, preset: Preset|None = None, palette_change: bool = False) -> None: assert is_running_in_gui_thread() # Boilerplate in case this is called from the wrong thread. if self.main_panel is None: # On deepin 23, events can trigger this method before initialization is complete @@ -9227,7 +9407,7 @@ self.app_context_menu.indicate_preset_active(preset) PresetsDialog.instance_indicate_active_preset(preset) title = f"{preset.get_title_name()} {PRESET_APP_SEPARATOR_SYMBOL} {title}" - tray_embedded_icon = preset.create_icon(get_tray_theme_type(self.main_config)) + tray_embedded_icon = preset.create_icon(self.get_tray_theme_type()) led1_color = PRESET_TRANSITIONING_LED_COLOR if preset.in_transition_step > 0 else None # TODO transitioning indicator if self.main_controller.lux_auto_controller is not None: if self.main_controller.lux_auto_controller.is_auto_enabled(): @@ -9237,7 +9417,7 @@ self.app_context_menu.update_lux_auto_icon(menu_lux_icon) # Won't actually update if it hasn't changed if tray_embedded_icon is None and self.main_config.is_set(ConfOpt.LUX_TRAY_ICON): if zone := self.main_controller.lux_auto_controller.get_lux_zone(): - tray_embedded_icon = create_icon_from_svg_bytes(zone.icon_svg, get_tray_theme_type(self.main_config)) + tray_embedded_icon = create_icon_from_svg_bytes(zone.icon_svg, self.get_tray_theme_type()) title = title + '\n' + tr("Lighting: {}").format(zone.name.lower()) if self.windowTitle() != title: # Don't change if not needed - prevent flickering. @@ -9276,29 +9456,28 @@ self.app_save_window_state() def app_save_window_state(self) -> None: - if self.main_config.is_set(ConfOpt.SMART_WINDOW, fallback=True) and self.isVisible(): - log_debug(f"app_save_window_state: {self.pos()=} {self.geometry()=} {QtCore.qVersion()}") if log_debug_enabled else None + if self.isVisible(): self.qt_settings.setValue(self.qt_version_key, QtCore.qVersion()) + log_debug(f"app_save_window_state: {self.pos()=} {self.geometry()=} {QtCore.qVersion()}") if log_debug_enabled else None self.qt_settings.setValue(self.qt_geometry_key, self.saveGeometry()) self.qt_settings.setValue(self.qt_state_key, self.saveState()) def app_restore_window_state(self) -> bool: log_debug(f"app_restore_window_state") - if not self.main_config.is_set(ConfOpt.SMART_WINDOW, fallback=True): - return False if len(self.qt_settings.allKeys()) == 0: # No previous state return False save_version_major = self.qt_settings.value(self.qt_version_key, '5').split('.', 1)[0] qt_version_major = QtCore.qVersion().split('.', 1)[0] if save_version_major != qt_version_major: - log_warning(f"app_restore_window_state: cannot restore: {save_version_major=} != {qt_version_major=}") - return False # Different Qt versions - cannot restore size, layout/size/scaling might be different. - if geometry := self.qt_settings.value(self.qt_geometry_key, None): - self.restoreGeometry(geometry) + log_warning(f"app_restore_window_state: restore: {save_version_major=} != {qt_version_major=}, this may cause window geometry glitches") + if smart_window := self.main_config.is_set(ConfOpt.SMART_WINDOW, fallback=True): # Restore pos and geometry + if geometry := self.qt_settings.value(self.qt_geometry_key, None): + self.restoreGeometry(geometry) + log_debug(f"app_restore_window_state: restoring {self.pos()=} {self.geometry()=}") if log_debug_enabled else None if window_state := self.qt_settings.value(self.qt_state_key, None): - self.restoreState(window_state) - log_debug(f"app_restore_window_state: {self.pos()=} {self.geometry()=}") if log_debug_enabled else None - return True + self.restoreState(window_state) # Restore component positions, such as toolbar location + log_debug(f"app_restore_window_state: restoring internal layout state") if log_debug_enabled else None + return smart_window def app_decide_window_position(self): @@ -9330,6 +9509,8 @@ log_info("PaletteChange event: New style sheet in use, update icons") self.initialise_app_icon() self.update_status_indicators(palette_change=True) + if self.main_panel: + self.main_panel.main_toolbar.refresh_buttons() return super().event(event) def refresh_preset_menu(self, palette_change: bool = False, reorder: bool = False): @@ -9431,11 +9612,11 @@ # Coding style also altered for use with vdu_controls. def calc_solar_azimuth_zenith(localised_time: datetime, latitude: float, longitude: float) -> Tuple[float, float]: """Return azimuth degrees clockwise from true north and zenith in degrees from vertical direction.""" - - utc_date_time = localised_time if localised_time.tzinfo is None else localised_time.astimezone(timezone.utc) + assert localised_time.tzinfo is not None + utc_datetime = localised_time.astimezone(timezone.utc) # UTC from now on... - hours, minutes, seconds = utc_date_time.hour, utc_date_time.minute, utc_date_time.second - year, month, day = utc_date_time.year, utc_date_time.month, utc_date_time.day + hours, minutes, seconds = utc_datetime.hour, utc_datetime.minute, utc_datetime.second + year, month, day = utc_datetime.year, utc_datetime.month, utc_datetime.day earth_mean_radius = 6371.01 astronomical_unit = 149597890 @@ -9493,22 +9674,11 @@ return azimuth, zenith_angle -def true_noon(longitude, when: datetime) -> datetime: - b = (360 / 365.25) * (when.timetuple().tm_yday - 81) # Estimate the Equation of Time (in minutes) - eot = 9.87 * math.sin(math.radians(2 * b)) - 7.53 * math.cos(math.radians(b)) - 1.5 * math.sin(math.radians(b)) - offset = 4 * longitude + eot # Calculate the time offset from UTC (in minutes) - true_noon_utc_minutes = 12 * 60 - offset # Calculate true noon in UTC (12:00 UTC +/- offset) - hours = int(true_noon_utc_minutes // 60) - minutes = int(true_noon_utc_minutes % 60) - when.replace(hour=hours, minute=minutes) - return when - - def calc_solar_lux(localised_time: datetime, location: GeoLocation, daylight_factor: float) -> int: # E. Elvegård and G. Sjöstedt, "The Calculation of Illumination from Sun and Sky," _Illuminating Engineering_, Apr. 1940. # [Illuminating Engineering Society, 100 Significant Papers](https://www.ies.org/research/publications/100-significant-papers/) latitude, longitude = location.latitude, location.longitude - azimuth, zenith = calc_solar_azimuth_zenith(true_noon(longitude, localised_time), latitude, longitude) + _, zenith = calc_solar_azimuth_zenith(localised_time, latitude, longitude) solar_altitude = 90 - zenith # After sunset use if solar_altitude < 3: # 3 degrees is a minimum, the functional limit for the algorithm return 0 @@ -9519,10 +9689,11 @@ return illumination -# Spherical distance from https://stackoverflow.com/a/21623206/609575 +# Spherical distance from https://stackoverflow.com/a/21623206/609575 (great circle distance km) def spherical_kilometers(lat1, lon1, lat2, lon2) -> float: p = math.pi / 180 a = 0.5 - math.cos((lat2 - lat1) * p) / 2 + math.cos(lat1 * p) * math.cos(lat2 * p) * (1 - math.cos((lon2 - lon1) * p)) / 2 + a = min(1.0, max(0.0, a)) # Guard against floating‑point errors return 12742 * math.asin(math.sqrt(a)) @@ -9531,7 +9702,7 @@ | None = None) -> Dict[SolarElevationKey, SolarElevationData]: # Create a minute-by-minute map of today's SolarElevations. # For a given dict[SolarElevation], record the first minute it occurs. - # Calls the callback for every 1 mimute point, not just each integer elevation. + # Calls the callback for every 1 minute point, not just each integer elevation. elevation_time_map = {} local_when = local_now.replace(hour=0, minute=0, second=0, microsecond=0) while local_when.day == local_now.day: @@ -9687,64 +9858,5 @@ buttons=MBtn.Close).exec() sys.exit(rc) - -# A fallback in case the hard coded splash screen PNG doesn't exist (which probably means KDE is not installed). -# Based on video-display.png from oxygen5-icon-theme-5: LGPL-3.0-only. -# Convert vdu_controls.png -depth 8 -colors 24 smallest.png; exiftool -all= smallest.png; base64 -w 120 smallest.png -FALLBACK_SPLASH_PNG_BASE64 = b""" -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 -7ktNAAAAAElFTkSuQmCC -""" - if __name__ == '__main__': main() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/vdu_controls-2.4.3/vdu_controls.spec new/vdu_controls-2.5.0/vdu_controls.spec --- old/vdu_controls-2.4.3/vdu_controls.spec 2025-08-29 04:06:52.000000000 +0200 +++ new/vdu_controls-2.5.0/vdu_controls.spec 2026-04-06 22:33:45.000000000 +0200 @@ -18,7 +18,7 @@ Name: vdu_controls -Version: 2.4.3 +Version: 2.5.0 Release: 0 Summary: Visual Display Unit virtual control panel License: GPL-3.0-or-later
