From ff276b140679ae530930f717bcb7630a6c11f60b Mon Sep 17 00:00:00 2001 From: Infernio Date: Tue, 12 Dec 2023 14:15:24 +0100 Subject: [PATCH] Properly support high DPI images Turns out the high DPI image support introduced in #557 had some significant issues - most notably, it was rendering crisp high-res images, but wx then decided to upscale them *again* if your DPI scaling was set to 200%. To fix this, we need to move to wx.BitmapBundle. This is intended to be used by bundling multiple versions of the asset and sticking them all into a bundle, then having wx choose the most appropriate resolution. Or use an SVG, statically render a bunch of sizes and stick them all in a bundle. We don't do that and instead dynamically create them, sticking only a base resolution asset and the actual version we need in. That way wx will use the higher-resolution version without applying its own scaling. This does of course look ugly with checkboxes and such, but those are temporary anyway - #511 will replace them with SVGs. Note that we now pass in iconSize for (nearly?) everything, so maybe we should just make it a required parameter. Under #511, #557 --- Mopy/bash/balt.py | 13 +++++---- Mopy/bash/basher/links_init.py | 2 +- Mopy/bash/gui/_gui_globals.py | 4 +-- Mopy/bash/gui/images.py | 46 +++++++++++++++++++++++++------- Mopy/bash/gui/misc_components.py | 12 ++++++--- 5 files changed, 56 insertions(+), 21 deletions(-) diff --git a/Mopy/bash/balt.py b/Mopy/bash/balt.py index 0c0f910abf..bf88b2ee65 100644 --- a/Mopy/bash/balt.py +++ b/Mopy/bash/balt.py @@ -2132,9 +2132,10 @@ def __init__(self, parent): on_drag_end_forced=self._on_drag_end_forced): self.buttons[link.uid] = link self._set_fields_size() - ##: Why - 12? I just tried values until it looked good, why does + ##: Why - 10? I just tried values until it looked good, why does # this one work best? - self._native_widget.SetMinHeight(self.icon_size - 12) + self._native_widget.SetMinHeight(self._native_widget.FromDIP( + self.icon_size - 10)) self._draw_buttons() #--Setup Drag-n-Drop reordering self._reset_drag(False) @@ -2218,10 +2219,11 @@ def _sort_buttons(self, uid_order): def _draw_buttons(self): rect = self._native_widget.GetFieldRect(0) - xPos, yPos = rect.x + 4, rect.y + xPos, yPos = rect.x + self._native_widget.FromDIP(4), rect.y + button_spacing = self._native_widget.FromDIP(self.icon_size) for button_link in self.buttons.values(): button_link.component_position = (xPos, yPos) - xPos += self.icon_size + xPos += button_spacing def toggle_buttons_visible(self, hide_ids=(), unhide_ids=()): """Toggle the visibility of the specified buttons.""" @@ -2264,7 +2266,8 @@ def _set_fields_size(self): if wx.Platform != '__WXMSW__': text_length_px += 10 self._native_widget.SetStatusWidths( - [self.icon_size * len(self.buttons), -1, text_length_px]) + [self._native_widget.FromDIP(self.icon_size) * len(self.buttons), + -1, text_length_px]) @classmethod def set_tooltips(cls): diff --git a/Mopy/bash/basher/links_init.py b/Mopy/bash/basher/links_init.py index 16b810bbae..3d0876a1d3 100644 --- a/Mopy/bash/basher/links_init.py +++ b/Mopy/bash/basher/links_init.py @@ -62,7 +62,7 @@ def InitStatusBar(): badIcons = [get_image('error_cross.16')] * 3 ##: 16, 24, 32? __fp = GuiImage.from_path def _png_list(template): - return [__fp(template % i) for i in (16, 24, 32)] + return [__fp(template % i, iconSize=i) for i in (16, 24, 32)] def _svg_list(svg_fname): return [__fp(svg_fname, iconSize=i) for i in (16, 24, 32)] #--Bash Status/LinkBar diff --git a/Mopy/bash/gui/_gui_globals.py b/Mopy/bash/gui/_gui_globals.py index 85407c0873..c4e9e4c5e8 100644 --- a/Mopy/bash/gui/_gui_globals.py +++ b/Mopy/bash/gui/_gui_globals.py @@ -78,11 +78,11 @@ def _icc(fname, bm_px_size=16): f'{inst_key}.{col}') or _icc(f'checkbox_{col}_{st}.png') _gui_images.update(_color_checks) # PNGs -------------------------------------------------------------------- - # Checkboxes ##: some of it loaded with iconSize 16 above - why we use _png?? + # Checkboxes pixs = (16, 24, 32) for st, col, pix in product(['off', 'on'], ('blue', 'green', 'red'), pixs): fname = f'checkbox_{col}_{st}%s.png' % ('' if pix == 16 else f'_{pix}') - _gui_images[f'checkbox.{col}.{st}.{pix}'] = GuiImage.from_path(fname) + _gui_images[f'checkbox.{col}.{st}.{pix}'] = _icc(fname, pix) # SVGs -------------------------------------------------------------------- # Modification time button _gui_images['calendar.16'] = _icc('calendar.svg') diff --git a/Mopy/bash/gui/images.py b/Mopy/bash/gui/images.py index df75fa0b08..e19a1bc972 100644 --- a/Mopy/bash/gui/images.py +++ b/Mopy/bash/gui/images.py @@ -92,7 +92,7 @@ def from_path(cls, img_path: str| Path, imageType=None, iconSize=-1, class _SvgFromPath(GuiImage): """Wrap an svg.""" - _native_widget: _svg.SVGimage.ConvertToScaledBitmap + _native_widget: _wx.BitmapBundle.FromBitmaps @property def _native_widget(self): @@ -103,8 +103,11 @@ def _native_widget(self): svg_data = svg_data.replace(b'var(--invert)', b'#FFF' if self._should_invert_svg() else b'#000') svg_img = _svg.SVGimage.CreateFromBytes(svg_data) - svg_size = scaled(self.iconSize) - self._cached_args = svg_img, (svg_size, svg_size) + # Use a bitmap bundle so we get an actual high-res asset at high + # DPIs, rather than wx deciding to scale up the low-res asset + wanted_svgs = [svg_img.ConvertToScaledBitmap((s, s)) + for s in (self.iconSize, scaled(self.iconSize))] + self._cached_args = (wanted_svgs,) return super()._native_widget @staticmethod @@ -124,7 +127,10 @@ def __init__(self, gui_image): def _native_widget(self): if self._is_created(): return self._cached_widget native = super()._native_widget # create a plain wx.Icon - native.CopyFromBitmap(self._resolve(self._gui_image)) + native_bmp = self._resolve(self._gui_image) + if isinstance(native_bmp, _wx.BitmapBundle): + native_bmp = native_bmp.GetBitmap(native_bmp.GetDefaultSize()) + native.CopyFromBitmap(native_bmp) return native class _IcoFromPath(GuiImage): @@ -177,7 +183,9 @@ def _native_widget(self): self._cached_args = self._img_path, self._img_type native = super()._native_widget if self.iconSize != -1: - wanted_size = scaled(self.iconSize) + # Don't use the scaled icon size here - _BmpFromPath performs its + # own scaling and Screen_ConvertTo wouldn't want to scale anyways + wanted_size = self.iconSize if self.get_img_size() != (wanted_size, wanted_size): native.Rescale(wanted_size, wanted_size, _wx.IMAGE_QUALITY_HIGH) @@ -189,12 +197,23 @@ def save_bmp(self, imagePath, exten='.jpg'): return self._native_widget.SaveFile(imagePath, self.img_types[exten]) class _BmpFromPath(GuiImage): - _native_widget: _wx.Bitmap + _native_widget: _wx.BitmapBundle.FromBitmaps @property def _native_widget(self): - self._cached_args = ImgFromPath(self._img_path, self.iconSize, - self._img_type)._native_widget, # pass wx.Image to wx.Bitmap + # Pass wx.Image to wx.Bitmap + base_img: _wx.Image = self._resolve(ImgFromPath(self._img_path, + imageType=self._img_type)) + scaled_imgs = [base_img] + if self.iconSize != -1: + # If we can, also add a scaled-up version so wx stops trying to + # scale this by itself - using a higher-res image here if we have + # one would be better, but that would be very difficult to + # implement, something for the (far) future + wanted_size = scaled(self.iconSize) + scaled_imgs.append(base_img.Scale(wanted_size, wanted_size, + quality=_wx.IMAGE_QUALITY_HIGH)) + self._cached_args = (list(map(_wx.Bitmap, scaled_imgs)),) return super()._native_widget class BmpFromStream(GuiImage): @@ -257,9 +276,16 @@ def _native_widget(self): def native_init(self, *args, **kwargs): kwargs.setdefault('recreate', False) freshly_created = super().native_init(*args, **kwargs) + ##: Accessing these like this feels wrong - maybe store the scaled size + # somewhere and retrieve it here? + scaled_sb_size = self._cached_args[0:2] if freshly_created: # ONCE! we don't support adding more images - self._indices = {k: self._native_widget.Add(self._resolve(im)) for - k, im in self._images} + self._indices = {} + for k, im in self._images: + nat_img = self._resolve(im) + if isinstance(nat_img, _wx.BitmapBundle): + nat_img = nat_img.GetBitmap(scaled_sb_size) + self._indices[k] = self._native_widget.Add(nat_img) def img_dex(self, *args) -> int | None: """Return the index of the specified image in the native control.""" diff --git a/Mopy/bash/gui/misc_components.py b/Mopy/bash/gui/misc_components.py index 06a024df4e..6e9c779dd5 100644 --- a/Mopy/bash/gui/misc_components.py +++ b/Mopy/bash/gui/misc_components.py @@ -92,6 +92,13 @@ def set_bitmap(self, bmp): caching""" if isinstance(bmp, Path): bmp = (bmp.is_file() and GuiImage.from_path(bmp)) or None + if bmp is not None: + bmp = self._resolve(bmp) + # If bmp comes from a BmpFromStream, this will be a bitmap; if it + # comes from a _BmpFromPath, it will be a bitmap bundle (with only + # one bitmap in it) + if isinstance(bmp, _wx.BitmapBundle): + bmp = bmp.GetBitmap(bmp.GetDefaultSize()) self._gui_bitmap = bmp self._handle_resize() return self._gui_bitmap @@ -106,14 +113,13 @@ def _handle_resize(self): ##: is all these wx.Bitmap calls needed? One right way dc.SetBackground(self.background) dc.Clear() if self._gui_bitmap is not None: - ##: wrap _native_widget calls below - a better way? - old_x,old_y = self._gui_bitmap._native_widget.GetSize() + old_x,old_y = self._gui_bitmap.GetSize() scale = min(float(x)/old_x, float(y)/old_y) new_x = old_x * scale new_y = old_y * scale pos_x = max(0,x-new_x)/2 pos_y = max(0,y-new_y)/2 - image = self._gui_bitmap._native_widget.ConvertToImage() + image = self._gui_bitmap.ConvertToImage() image.Rescale(int(new_x), int(new_y), _wx.IMAGE_QUALITY_HIGH) dc.DrawBitmap(_wx.Bitmap(image), int(pos_x), int(pos_y)) del dc