1212from logHandler import log # logging
1313from collections .abc import Callable
1414from .MathCAT import convertSSMLTextForNVDA
15+ from ._libmathcat_loader import libmathcat
1516from speech import speak
1617from zipfile import ZipFile
1718
@@ -59,10 +60,42 @@ def __init__(self, parent: wx.Window | None):
5960 # initialize parent class
6061 MathCATgui .MathCATPreferencesDialog .__init__ (self , parent )
6162
63+ # show the MathCAT rules engine version below the logo
64+ try :
65+ # Translators: shows the version of the MathCAT rules engine, e.g. "Version 0.5.14"
66+ self ._staticTextVersion .SetLabel (_ ("Version {version}" ).format (version = libmathcat .GetVersion ()))
67+ except Exception as e :
68+ log .exception (f"MathCAT: An exception occurred getting the MathCAT version: { e } " )
69+
6270 # load the logo into the dialog
6371 fullPathToLogo : str = os .path .join (os .path .dirname (os .path .abspath (__file__ )), "logo.png" )
6472 if os .path .exists (fullPathToLogo ):
65- self ._bitmapLogo .SetBitmap (wx .Bitmap (fullPathToLogo ))
73+ logoImage : wx .Image = wx .Image (fullPathToLogo , wx .BITMAP_TYPE_PNG )
74+ # 200 DIP; the source image is 300x300 so it stays sharp on hi-DPI displays
75+ targetLogoSize : int = 200
76+ # BitmapBundle pins the logical size from the smallest bitmap and paints the
77+ # full-resolution one on hi-DPI displays
78+ baselineBitmap : wx .Bitmap = wx .Bitmap (logoImage .Scale (targetLogoSize , targetLogoSize , wx .IMAGE_QUALITY_HIGH ))
79+ nativeBitmap : wx .Bitmap = wx .Bitmap (logoImage )
80+ self ._logoBitmapBundle : wx .BitmapBundle = wx .BitmapBundle .FromBitmaps ([baselineBitmap , nativeBitmap ])
81+
82+ # wx.StaticBitmap on Windows paints an opaque background through the PNG alpha.
83+ # Replace it with a panel we paint ourselves so transparency (and the dark-mode pill) work.
84+ logoPanel : wx .Panel = wx .Panel (
85+ self ._panelCategories ,
86+ wx .ID_ANY ,
87+ size = wx .Size (targetLogoSize , targetLogoSize ),
88+ )
89+ logoPanel .SetBackgroundColour (self ._panelCategories .GetBackgroundColour ())
90+ logoPanel .Bind (wx .EVT_PAINT , self .onPaintLogo )
91+ self ._bitmapLogo .GetContainingSizer ().Replace (self ._bitmapLogo , logoPanel )
92+ self ._bitmapLogo .Destroy ()
93+ self ._bitmapLogo = logoPanel
94+
95+ # The dialog was already fitted around a null placeholder; refit now that the logo size is known.
96+ self ._panelCategories .Layout ()
97+ self .Layout ()
98+ self .GetSizer ().Fit (self )
6699
67100 # load in the system values followed by the user prefs (if any)
68101 UserInterface .loadDefaultPreferences ()
@@ -89,6 +122,56 @@ def __init__(self, parent: wx.Window | None):
89122 UserInterface .getBrailleCodes (self )
90123 # set the ui items to match the preferences
91124 UserInterface .setUIValues (self )
125+ # Generated Centre(wx.BOTH) centres on NVDA's hidden mainFrame (top-left).
126+ self ._centrePreferencesDialog ()
127+
128+ def _centrePreferencesDialog (self ) -> None :
129+ """Centre the dialog on a visible parent, or on screen if there is none.
130+
131+ NVDA's mainFrame is a hidden window at the origin. Centre(wx.BOTH) would
132+ put this dialog at the top-left of the display. Other NVDA settings
133+ dialogs use CentreOnScreen() for the same reason.
134+ """
135+ parent : wx .Window | None = self .GetParent ()
136+ parentClientSize : wx .Size | None = parent .GetClientSize () if parent is not None else None
137+ if (
138+ parent is not None
139+ and parent .IsShown ()
140+ and parentClientSize is not None
141+ and parentClientSize .width > 0
142+ and parentClientSize .height > 0
143+ ):
144+ self .CentreOnParent ()
145+ else :
146+ self .CentreOnScreen ()
147+
148+ def onPaintLogo (self , event : wx .PaintEvent ) -> None :
149+ """Paint the MathCAT logo onto its panel, with a light pill behind it on dark backgrounds.
150+
151+ The logo's black text/outline is invisible against a dark dialog background, so a light pill
152+ is drawn behind it for contrast. On light backgrounds the pill is skipped.
153+ """
154+ dc : wx .PaintDC = wx .PaintDC (self ._bitmapLogo )
155+ graphicsContext : wx .GraphicsContext | None = wx .GraphicsContext .Create (dc )
156+ if graphicsContext is None :
157+ return
158+ panelSize : wx .Size = self ._bitmapLogo .GetClientSize ()
159+ # IsDark() can report dark mode even when this unthemed dialog has a light background,
160+ # so decide from the panel's actual background colour.
161+ backgroundColour : wx .Colour = self ._bitmapLogo .GetBackgroundColour ()
162+ backgroundLuminance : float = (
163+ 0.299 * backgroundColour .Red () + 0.587 * backgroundColour .Green () + 0.114 * backgroundColour .Blue ()
164+ )
165+ if backgroundLuminance < 128 :
166+ # light grey rather than white, so the pill is not glaring on a dark dialog
167+ graphicsContext .SetBrush (wx .Brush (wx .Colour (210 , 210 , 210 )))
168+ graphicsContext .SetPen (wx .TRANSPARENT_PEN )
169+ # fixed 20 DIP radius so this stays a rounded rectangle, not a circle
170+ cornerRadius : int = 20
171+ graphicsContext .DrawRoundedRectangle (0 , 0 , panelSize .width , panelSize .height , cornerRadius )
172+ # scale to the panel size so a hi-DPI bitmap is not drawn at native pixel size
173+ bitmap : wx .Bitmap = self ._logoBitmapBundle .GetBitmapFor (self ._bitmapLogo )
174+ graphicsContext .DrawBitmap (bitmap , 0 , 0 , panelSize .width , panelSize .height )
92175
93176 @staticmethod
94177 def pathToLanguagesFolder () -> str :
0 commit comments