"""Compatibility fixes and enhancements for wxPython across platforms and versions.

It includes custom widget implementations, DPI scaling adjustments, and
additional utility functions for improved UI behavior and appearance.
"""

import contextlib
import os
import re
import shutil
import sys
import tempfile
from typing import Any

from DisplayCAL.meta import NAME as APPNAME
from DisplayCAL.meta import WX_MINVERSION

if os.getenv("GTK_CSD", "0") != "0":
    # Work-around double window decorations, see
    # https://hub.displaycal.net/issue/17331/
    os.environ["GTK_CSD"] = "0"

# wx_version will be removed in Phoenix
try:
    from DisplayCAL import wx_version
except ImportError:
    pass
else:
    if not getattr(sys, "frozen", False) and "wx" not in sys.modules:
        with contextlib.suppress(wx_version.VersionError):
            wx_version.select(["4.0", "3.0", "{}.{}.{}".format(*WX_MINVERSION[:3])])

import wx

if wx.VERSION < WX_MINVERSION:  # noqa: SIM300
    app = wx.GetApp() or wx.App(0)
    result = wx.MessageBox(
        "This application requires a version of wxPython "
        "greater than or equal to {}, but your most recent "
        "version is {}.\n\n"
        "Would you like to download a new version of wxPython?\n".format(
            ".".join(str(n) for n in WX_MINVERSION), wx.__version__
        ),
        "wxPython Upgrade Needed",
        style=wx.YES_NO,
    )
    if result == wx.YES:
        import webbrowser

        webbrowser.open("http://wxpython.org/")
    app.MainLoop()
    sys.exit()
import wx.grid
from wx import xrc
from wx.lib import platebtn
from wx.lib.buttons import GenBitmapButton as _GenBitmapButton
from wx.lib.buttons import GenBitmapTextButton as _GenBitmapTextButton
from wx.lib.buttons import ThemedGenButton as _ThemedGenButton
from wx.lib.scrolledpanel import ScrolledPanel

from DisplayCAL.colormath import convert_range

if not hasattr(wx.Window, "HasFocus"):
    # wxPython < 3.0
    wx.Window.HasFocus = lambda self: self.FindFocus() is self


if not hasattr(wx, "wxAssertionError"):
    # wxPython < 4.0
    wx.wxAssertionError = wx.PyAssertionError


if not hasattr(platebtn, "PB_STYLE_TOGGLE"):
    platebtn.PB_STYLE_TOGGLE = 32


if not hasattr(platebtn, "PB_STYLE_DROPARROW"):
    platebtn.PB_STYLE_DROPARROW = 16


if "phoenix" in wx.PlatformInfo:
    # Phoenix compatibility
    import wx.adv
    from wx.lib.agw import aui

    def BitmapFromIcon(icon):
        """Convert an icon to a bitmap."""
        bmp = wx.Bitmap()
        bmp.CopyFromIcon(icon)
        return bmp

    def IconFromBitmap(bmp):
        """Convert a bitmap to an icon."""
        icon = wx.Icon()
        icon.CopyFromBitmap(bmp)
        return bmp

    wx.BitmapFromIcon = BitmapFromIcon
    wx.CursorFromImage = wx.Cursor
    wx.EmptyBitmap = wx.Bitmap
    wx.EmptyBitmapRGBA = lambda width, height, red=0, green=0, blue=0, alpha=0: (
        wx.Bitmap.FromRGBA(width, height, red, green, blue, alpha)
    )
    wx.EmptyIcon = wx.Icon
    wx.IconFromBitmap = wx.Icon
    wx.ImageFromStream = wx.Image
    wx.ListCtrl.InsertStringItem = lambda self, index, label: self.InsertItem(
        int(index), str(label)
    )
    wx.ListCtrl.SetStringItem = lambda self, index, col, label: self.SetItem(
        int(index), int(col), str(label)
    )
    wx.Menu.RemoveItem = lambda self, item: self.Remove(item)
    wx.NamedColour = wx.Colour
    wx.PyControl = wx.Control
    wx.PyWindow = wx.Window
    wx.PyPanel = wx.Panel
    wx.StockCursor = wx.Cursor
    wx.Window.SetToolTipString = wx.Window.SetToolTip

    # Moved items

    wx.HL_DEFAULT_STYLE = wx.adv.HL_DEFAULT_STYLE
    wx.HyperlinkCtrl = wx.adv.HyperlinkCtrl
    wx.HyperlinkCtrlNameStr = wx.adv.HyperlinkCtrlNameStr

    wx.SOUND_ASYNC = wx.adv.SOUND_ASYNC
    wx.Sound = wx.adv.Sound

    wx.wxEVT_TASKBAR_BALLOON_CLICK = wx.adv.wxEVT_TASKBAR_BALLOON_CLICK
    wx.wxEVT_TASKBAR_BALLOON_TIMEOUT = wx.adv.wxEVT_TASKBAR_BALLOON_TIMEOUT
    wx.wxEVT_TASKBAR_CLICK = wx.adv.wxEVT_TASKBAR_CLICK
    wx.wxEVT_TASKBAR_LEFT_DCLICK = wx.adv.wxEVT_TASKBAR_LEFT_DCLICK
    wx.wxEVT_TASKBAR_LEFT_DOWN = wx.adv.wxEVT_TASKBAR_LEFT_DOWN
    wx.wxEVT_TASKBAR_LEFT_UP = wx.adv.wxEVT_TASKBAR_LEFT_UP
    wx.wxEVT_TASKBAR_MOVE = wx.adv.wxEVT_TASKBAR_MOVE
    wx.wxEVT_TASKBAR_RIGHT_DOWN = wx.adv.wxEVT_TASKBAR_RIGHT_DOWN
    wx.wxEVT_TASKBAR_RIGHT_UP = wx.adv.wxEVT_TASKBAR_RIGHT_UP
    wx.wxEVT_TASKBAR_RIGHT_DCLICK = wx.adv.wxEVT_TASKBAR_RIGHT_DCLICK

    wx.EVT_TASKBAR_BALLOON_CLICK = wx.adv.EVT_TASKBAR_BALLOON_CLICK
    wx.EVT_TASKBAR_BALLOON_TIMEOUT = wx.adv.EVT_TASKBAR_BALLOON_TIMEOUT
    wx.EVT_TASKBAR_CLICK = wx.adv.EVT_TASKBAR_CLICK
    wx.EVT_TASKBAR_LEFT_DCLICK = wx.adv.EVT_TASKBAR_LEFT_DCLICK
    wx.EVT_TASKBAR_LEFT_DOWN = wx.adv.EVT_TASKBAR_LEFT_DOWN
    wx.EVT_TASKBAR_LEFT_UP = wx.adv.EVT_TASKBAR_LEFT_UP
    wx.EVT_TASKBAR_MOVE = wx.adv.EVT_TASKBAR_MOVE
    wx.EVT_TASKBAR_RIGHT_DOWN = wx.adv.EVT_TASKBAR_RIGHT_DOWN
    wx.EVT_TASKBAR_RIGHT_UP = wx.adv.EVT_TASKBAR_RIGHT_UP
    wx.EVT_TASKBAR_RIGHT_DCLICK = wx.adv.EVT_TASKBAR_RIGHT_DCLICK

    # Removed items
    wx.IconBundle.AddIconFromFile = lambda file, type_=wx.BITMAP_TYPE_ANY: (
        wx.IconBundle.AddIcon(file, type_)
    )

    def ContainsRect(self, *args):
        """Check if the rectangle is contained in this rectangle."""
        rect = wx.Rect(*args) if len(args) > 1 else args[0]
        return self.Contains(rect)

    wx.Rect.ContainsRect = ContainsRect
    wx.Rect.ContainsXY = lambda self, x, y: self.Contains((x, y))
    wx.RectPS = wx.Rect
    wx.RegionFromBitmap = wx.Region

    def GetItemIndex(self, window):
        """Return the index of the item in the sizer."""
        for index, sizeritem in enumerate(self.Children):
            if sizeritem.Window == window:
                return index
        return None

    wx.Sizer.GetItemIndex = GetItemIndex
    wx.TopLevelWindow.Restore = lambda self: self.Iconize(False)

    # Renamed items

    wx.OPEN = wx.FD_OPEN
    wx.OVERWRITE_PROMPT = wx.FD_OVERWRITE_PROMPT
    wx.SAVE = wx.FD_SAVE

    wx.SystemSettings_GetColour = wx.SystemSettings.GetColour
    wx.SystemSettings_GetFont = wx.SystemSettings.GetFont
    wx.SystemSettings_GetMetric = wx.SystemSettings.GetMetric
    wx.grid.EVT_GRID_CELL_CHANGE = wx.grid.EVT_GRID_CELL_CHANGED
    if hasattr(wx.grid.Grid, "GridSelectRows"):
        # wxPython Phoenix 4.0.0a
        wx.grid.Grid.wxGridSelectRows = wx.grid.Grid.GridSelectRows
    else:
        # wxPython Phoenix 4.0.0b
        wx.grid.Grid.wxGridSelectRows = wx.grid.Grid.SelectRows
    wx.grid.PyGridCellEditor = wx.grid.GridCellEditor
    wx.grid.PyGridCellRenderer = wx.grid.GridCellRenderer
    wx.RegionFromBitmapColour = wx.Region

    # Bugfixes

    if not hasattr(wx.grid.GridEvent, "CmdDown"):
        # This may be a bug in the current development version of Phoenix
        wx.grid.GridEvent.CmdDown = lambda self: False

    # Not sure if this is a bug, but the PositionToXY signature differs from
    # current Phoenix docs. Actual return value is a 3-tuple, not a 2-tuple:
    # (bool inrange, int col, int line)
    PositionToXY = wx.TextCtrl.PositionToXY
    wx.TextCtrl.PositionToXY = lambda self, pos: PositionToXY(self, pos)[1:]

    def TabFrame__init__(self, parent):  # noqa: D417
        """Fix TabFrame.__init__ to not set the size to 0, 0.

        Args:
            parent (wx.Window): The parent window for the TabFrame.
        """
        wx.Window.__init__(self)

        self._tabs = None
        self._rect = wx.Rect(0, 0, 200, 200)
        self._tab_ctrl_height = 20
        self._tab_rect = wx.Rect()
        self._parent = parent

        # With Phoenix, the TabFrame is unintentionally visible in the top left
        # corner (horizontal and vertical lines with a length of 20px each that
        # are lighter than the border color if tabs are at the top, or a
        # 20x20px square if tabs are at the bottom). Setting its size to 0, 0
        # prevents this. Also see https://github.com/RobinD42/Phoenix/pull/91
        self.Create(parent, size=(0, 0))

    aui.TabFrame.__init__ = TabFrame__init__


wx._ListCtrl = wx.ListCtrl
wx._SpinCtrl = wx.SpinCtrl
wx._StaticText = wx.StaticText
if "gtk3" in wx.PlatformInfo:
    # GTK3 fixes

    class wx_Panel(wx.Panel):  # noqa: N801
        """Fix panel background color not working under wxGTK3."""

        def __init__(self, *args, **kwargs):
            if not args and hasattr(wx, "PreFrame"):
                pre = wx.PrePanel()
                self.PostCreate(pre)
                self.Bind(wx.EVT_WINDOW_CREATE, self.OnCreate)
            else:
                wx.Panel.__init__(self, *args, **kwargs)
                self.OnCreate(None)

        def OnCreate(self, event):
            """Handle the window creation event to bind the erase background event.

            Args:
                event (wx.Event): The window creation event.
            """
            self.Unbind(wx.EVT_WINDOW_CREATE)
            self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)

        def OnEraseBackground(self, event):
            """Handle the erase background event to prevent flickering.

            Args:
                event (wx.Event): The erase background event.
            """
            dc = event.GetDC()
            rect = self.GetClientRect()
            dc.SetClippingRegion(rect.x, rect.y, rect.width, rect.height)
            bgcolor = self.BackgroundColour
            dc.SetBrush(wx.Brush(bgcolor, wx.BRUSHSTYLE_SOLID))
            dc.SetPen(wx.Pen(bgcolor, wx.PENSTYLE_SOLID))
            dc.DrawRectangle(*rect)

    from wx import dataview

    DataViewListCtrl = dataview.DataViewListCtrl

    class ListCtrl(DataViewListCtrl):
        """Compatibility class for wx.ListCtrl.

        Implements ListCtrl as DataViewListCtrl. Works around header rendering
        ugliness with GTK3.

        Args:
            parent (wx.Window): The parent window.
            id (int): The identifier for the control.
            pos (wx.Point): The position of the control.
            size (wx.Size): The size of the control.
            style (int): The style of the control.
            validator (wx.Validator): The validator for the control.
            name (str): The name of the control.
        """

        def __init__(
            self,
            parent,
            id=wx.ID_ANY,  # noqa: A002
            pos=wx.DefaultPosition,
            size=wx.DefaultSize,
            style=wx.LC_ICON,
            validator=wx.DefaultValidator,
            name=wx.ListCtrlNameStr,
        ):
            dv_style = 0
            if style & wx.LC_SINGLE_SEL:
                dv_style |= dataview.DV_SINGLE
            DataViewListCtrl.__init__(self, parent, id, pos, size, dv_style, validator)
            self.Name = name
            self._columns = {}
            self._items = {}
            self.Bind(dataview.EVT_DATAVIEW_SELECTION_CHANGED, self.OnEvent)
            self.Bind(dataview.EVT_DATAVIEW_ITEM_ACTIVATED, self.OnEvent)

        def OnEvent(self, event):
            """Handle DataViewListCtrl events and convert them to ListCtrl events.

            Args:
                event (wx.Event): The event to handle.
            """
            typeId = event.GetEventType()
            if typeId == dataview.EVT_DATAVIEW_SELECTION_CHANGED.typeId:
                if self.GetSelectedRow() != wx.NOT_FOUND:
                    typeId = wx.EVT_LIST_ITEM_SELECTED.typeId
                else:
                    typeId = wx.EVT_LIST_ITEM_DESELECTED.typeId
                event = wx.ListEvent(typeId, self.Id)
            elif typeId == dataview.EVT_DATAVIEW_ITEM_ACTIVATED.typeId:
                event = wx.ListEvent(wx.EVT_LIST_ITEM_ACTIVATED.typeId, self.Id)
            event.SetEventObject(self)
            self.GetEventHandler().ProcessEvent(event)

        def InsertColumn(self, pos, col):
            """Insert a column at the specified position.

            Args:
                pos (int): The position to insert the column at.
                col (wx.DataViewColumn): The column to insert.
            """
            self._columns[pos] = col

        def SetColumnWidth(self, pos, width):
            """Set the width of the column at the specified position.

            Args:
                pos (int): The position of the column.
                width (int): The width to set for the column.
            """
            self.AppendTextColumn(self._columns[pos], width=int(width))

        def InsertStringItem(self, row, label):
            """Insert a string item at the specified row.

            Args:
                row (int): The row to insert the item at.
                label (str): The label of the item.

            Returns:
                int: The row index where the item was inserted.
            """
            self._items[row] = []
            return row

        def SetStringItem(self, row, col, label):
            """Set the string item at the specified row and column.

            Args:
                row (int): The row of the item.
                col (int): The column of the item.
                label (str): The label to set for the item.
            """
            self._items[row].append(label)
            if len(self._items[row]) == len(self._columns):
                # Row complete
                DataViewListCtrl.InsertItem(self, row, self._items[row])

        def GetItem(self, row, col=0):
            """Return the item at the specified row and column.

            Args:
                row (int): The row of the item.
                col (int): The column of the item (default is 0).

            Returns:
                wx.DataViewItem: The item at the specified row and column.
            """
            item = self.RowToItem(row)
            item.GetId = lambda: row
            item.GetText = lambda: self.GetTextValue(row, col)
            return item

        def SetItemState(self, row, state, stateMask):
            """Set the state of the item.

            Args:
                row (int): The row to set the state for.
                state (int): The state to set.
                stateMask (int): The state mask to apply.

            Raises:
                NotImplementedError: If the state is not wx.LIST_STATE_SELECTED.
            """
            if state == stateMask == wx.LIST_STATE_SELECTED:
                self.SelectRow(row)
            else:
                raise NotImplementedError(
                    "SetItemState is only implemented for selecting a single row"
                )

        def GetNextItem(
            self, row, geometry=wx.LIST_NEXT_ALL, state=wx.LIST_STATE_DONTCARE
        ):
            """Return the next item in the list.

            Args:
                row (int): The row to start from.
                geometry (int): The geometry of the item to return.
                state (int): The state of the item to return.

            Returns:
                int: The next item in the list, or -1 if no item is found.
            """
            if (
                row == -1
                and geometry == wx.LIST_NEXT_ALL
                and state == wx.LIST_STATE_SELECTED
            ):
                return self.GetSelectedRow()
            raise NotImplementedError(
                "GetNextItem is only implemented for returning the selected row"
            )

        def GetItemCount(self):
            """Return the number of items in the list.

            Returns:
                int: The number of items in the list.
            """
            return len(self._items)

        GetFirstSelected = DataViewListCtrl.__dict__["GetSelectedRow"]

        def GetItemState(self, row, stateMask):
            """Return the state of the item.

            Args:
                row (int): The row to check.
                stateMask (int): The state mask to check against.
            """
            if stateMask == wx.LIST_STATE_SELECTED:
                if self.GetSelectedRow() == row:
                    return wx.LIST_STATE_SELECTED
                return 0
            raise NotImplementedError(
                "GetItemState is only implemented to check if a row is selected"
            )

        def Select(self, row, on=1):
            """Select or unselect a row.

            Args:
                row (int): The row to select or unselect.
                on (bool): Whether to select (True) or unselect (False) the row.
            """
            if on:
                self.SelectRow(row)
            else:
                self.UnselectRow(row)

    wx.ListCtrl = ListCtrl

    class SpinCtrl(wx._SpinCtrl):
        """Compatibility class for wx.SpinCtrl.

        Args:
            parent (wx.Window): The parent window.
            id (int): The identifier for the control.
            value (str): The initial value of the spin control.
            pos (wx.Point): The position of the control.
            size (wx.Size): The size of the control.
            style (int): The style of the control.
            min (int): The minimum value of the spin control.
            max (int): The maximum value of the spin control.
            initial (int): The initial value to set.
            name (str): The name of the control.
        """

        _spinwidth = 0

        def __init__(
            self,
            parent,
            id=wx.ID_ANY,  # noqa: A002
            value="",
            pos=wx.DefaultPosition,
            size=wx.DefaultSize,
            style=wx.SP_ARROW_KEYS,
            min=0,  # noqa: A002
            max=100,  # noqa: A002
            initial=0,
            name="wxSpinCtrl",
        ):
            if size[0] != -1:
                # Adjust initial size for GTK3 to accommodate spin buttons
                if not SpinCtrl._spinwidth:
                    spin = wx.SpinCtrl(parent, -1)
                    text = wx.TextCtrl(parent, -1)
                    SpinCtrl._spinwidth = spin.Size[0] - text.Size[0] + 11
                    spin.Destroy()
                    text.Destroy()
                size = size[0] + SpinCtrl._spinwidth, size[1]
            wx._SpinCtrl.__init__(
                self, parent, id, value, pos, size, style, min, max, initial, name
            )

    wx.SpinCtrl = SpinCtrl

    _StaticText_SetLabel = wx._StaticText.SetLabel

    class StaticText(wx._StaticText):
        """Compatibility class for wx.StaticText."""

        def __init__(self, *args, **kwargs):
            wx._StaticText.__init__(self, *args, **kwargs)

        def SetFont(self, font):
            """Set the font of the static text.

            Args:
                font (wx.Font): The font to set.
            """
            wx.Control.SetFont(self, font)
            self.SetLabel(self.Label)

        def SetLabel(self, label):
            """Set the label of the static text.

            Args:
                label (str): The label to set.
            """
            # Fix GTK3 label auto-resize on label change not working
            if not self.WindowStyle & wx.ST_NO_AUTORESIZE:
                self.MaxSize = -1, -1
                self.MinSize = -1, -1
            _StaticText_SetLabel(self, label)
            if not self.WindowStyle & wx.ST_NO_AUTORESIZE:
                # Find widest line
                max_width = 0
                for line in label.splitlines():
                    width = self.GetTextExtent(line)[0]
                    max_width = max(max_width, width)
                self.Size = max_width, -1
                self.MaxSize = self.Size[0], -1

        def Wrap(self, width):
            """Wrap the text to the specified width.

            Args:
                width (int): The width to wrap the text to.
            """
            wx._StaticText.Wrap(self, int(width))
            self.SetLabel(self.Label)

        Label = property(lambda self: self.GetLabel(), SetLabel)

    wx.StaticText = StaticText
else:

    class wx_Panel(wx.Panel):  # noqa: N801
        """Compatibility class."""

        def __init__(self, *args, **kwargs):
            wx.Panel.__init__(self, *args, **kwargs)


wx.AnyButton._SetBitmapLabel = wx.AnyButton.SetBitmapLabel


def SetBitmapLabel(self, bitmap):
    """Override the SetBitmapLabel to avoid flickering."""
    if self.GetBitmapLabel() != bitmap:
        self._SetBitmapLabel(bitmap)


wx.AnyButton.SetBitmapLabel = SetBitmapLabel


def BitmapButtonEnable(self, enable=True):
    """Replacement for BitmapButton.Enable which circumvents repainting issues.

    (bitmap does not change on button state change)

    """
    wx.Button.Enable(self, enable)
    if not hasattr(self, "_bitmaplabel"):
        self._bitmaplabel = self.GetBitmapLabel()
    if not hasattr(self, "_bitmapdisabled"):
        self._bitmapdisabled = self.GetBitmapDisabled()
    if enable:
        if self._bitmaplabel.IsOk():
            self.SetBitmapLabel(self._bitmaplabel)
    elif self._bitmapdisabled.IsOk():
        self.SetBitmapLabel(self._bitmapdisabled)


def BitmapButtonDisable(self):
    """Replacement for BitmapButton.Disable which circumvents repainting issues.

    (bitmap does not change on button state change)
    """
    self.Enable(False)


if "phoenix" not in wx.PlatformInfo:
    wx.BitmapButton.Enable = BitmapButtonEnable
    wx.BitmapButton.Disable = BitmapButtonDisable


def FindMenuItem(self, label):
    """Replacement for wx.Menu.FindItem."""
    label = GTKMenuItemGetFixedLabel(label)
    for menuitem in self.GetMenuItems():
        try:
            fixed_label = GTKMenuItemGetFixedLabel(menuitem.Label)
        except AttributeError:
            # wxPython 4.1.0+
            fixed_label = GTKMenuItemGetFixedLabel(menuitem.GetItemLabelText())
        if fixed_label == label:
            return menuitem.GetId()
    return None


wx.Menu.FindItem = FindMenuItem


def GTKMenuItemGetFixedLabel(label):
    """Return the fixed label for GTK menu items."""
    if sys.platform not in ("darwin", "win32"):
        # The underscore is a special character under GTK, like the
        # ampersand on Mac OS X and Windows
        # Recent wxPython versions already do the right thing, but we need
        # this workaround for older releases
        if "__" in label:
            label = label.replace("__", "_")
        while label and label[0] == "_":
            label = label[1:]
    return label


if not hasattr(wx.Sizer, "GetItemIndex"):

    def GetItemIndex(self, item):
        """Return the index of the item in the sizer."""
        for i, child in enumerate(list(self.GetChildren())):
            if child.GetWindow() is item:
                return i
        return -1

    wx.Sizer.GetItemIndex = GetItemIndex


def set_maxsize(window, maxsize):
    """Set the maximum size of a window."""
    window.MaxSize = maxsize


if os.getenv("XDG_SESSION_TYPE") == "wayland":
    # Fix erroneous extra spacing around window contents under Wayland

    is_first_toplevel_window = True

    def fix_wayland_window_size(window):
        """Fix erroneous extra spacing around window contents under Wayland."""
        global is_first_toplevel_window
        if is_first_toplevel_window:
            # Not needed for first toplevel window
            is_first_toplevel_window = False
        else:
            window.MaxSize = window.Size
            wx.CallAfter(set_maxsize, window, (-1, -1))

    def TopLevelWindow_Show(self, show=True):
        """Replacement for TopLevelWindow.Show which circumvents repainting issues."""
        if show and not self.IsShown():
            fix_wayland_window_size(self)
        return _TopLevelWindow_Show(self, show)

    _TopLevelWindow_Show = wx.TopLevelWindow.Show
    wx.TopLevelWindow.Show = TopLevelWindow_Show

    # ~ def Dialog_ShowModal(self):
    # ~ fix_wayland_window_size(self)
    # ~ return _Dialog_ShowModal(self)

    # ~ _Dialog_ShowModal = wx.Dialog.ShowModal
    # ~ wx.Dialog.ShowModal = Dialog_ShowModal

    def Sizer_SetSizeHints(self, window):
        """Replacement for Sizer.SetSizeHints which circumvents repainting issues."""
        if isinstance(window, wx.Dialog):
            window.MaxSize = (-1, -1)
        _Sizer_SetSizeHints(self, window)

    _Sizer_SetSizeHints = wx.Sizer.SetSizeHints
    wx.Sizer.SetSizeHints = Sizer_SetSizeHints

    def Sizer_Layout(self):
        """Replacement for Sizer.Layout which circumvents repainting issues."""
        _Sizer_Layout(self)
        window = self.GetContainingWindow()
        if isinstance(window, wx.Dialog):
            window.MaxSize = window.Size

    _Sizer_Layout = wx.Sizer.Layout
    wx.Sizer.Layout = Sizer_Layout


if sys.platform == "darwin":
    # wxMac seem to lose the foreground color of StaticText
    # when enabled again

    @property
    def StaticTextEnabled(self):
        """Replacement for StaticText.Enabled which circumvents repainting issues."""
        return self.IsEnabled()

    @StaticTextEnabled.setter
    def StaticTextEnabled(self, enable=True):
        """Replacement for StaticText.Enabled which circumvents repainting issues."""
        self.Enable(enable)

    wx.StaticText.Enabled = StaticTextEnabled

    def StaticTextDisable(self):
        """Replacement for StaticText.Disable which circumvents repainting issues."""
        self.Enable(False)

    wx.StaticText.Disable = StaticTextDisable

    def StaticTextEnable(self, enable=True):
        """Replacement for StaticText.Enable which circumvents repainting issues."""
        enable = bool(enable)
        if self.Enabled is enable:
            return
        self._enabled = enable
        if not hasattr(self, "_fgcolor"):
            self._fgcolor = self.ForegroundColour
        color = self._fgcolor
        if not enable:
            bgcolor = self.Parent.BackgroundColour
            bgblend = 0.5
            blend = 0.5
            color = wx.Colour(
                round(bgblend * bgcolor.Red() + blend * color.Red()),
                round(bgblend * bgcolor.Green() + blend * color.Green()),
                round(bgblend * bgcolor.Blue() + blend * color.Blue()),
            )
        self.ForegroundColour = color

    wx.StaticText.Enable = StaticTextEnable

    def StaticTextIsEnabled(self) -> bool:
        """Return whether the StaticText is enabled."""
        return getattr(self, "_enabled", True)

    wx.StaticText.IsEnabled = StaticTextIsEnabled


wx.Window._SetToolTipString = wx.Window.SetToolTipString


def SetToolTipString(self, string):
    """Replacement for SetToolTipString which updates correctly."""
    wx.Window.SetToolTip(self, None)
    wx.Window._SetToolTipString(self, string)


wx.Window.SetToolTipString = SetToolTipString


def _adjust_sizer_args_scaling_for_appdpi(*args, **kwargs):
    from DisplayCAL.config import get_default_dpi, getcfg

    scale = getcfg("app.dpi") / get_default_dpi()
    if "border" in kwargs:
        kwargs["border"] = int(kwargs["border"])

    if scale > 1:
        args = list(args)
        if kwargs.get("border"):
            kwargs["border"] = round(kwargs["border"] * scale)
        elif "border" not in kwargs:
            if (
                isinstance(args[0], (tuple, wx.Size, wx.Sizer, wx.Window))
                and len(args) > 3
                and args[3]
            ):
                # item, proportion, flag, border, ...
                args[3] = round(args[3] * scale)
            elif isinstance(args[0], int) and len(args) > 4 and args[4]:
                # width, height, proportion, flag, border, ...
                args[4] = round(args[4] * scale)
        if args and isinstance(args[0], (tuple, wx.Size)):
            spacer = list(args[0])
            # print(spacer, '->')
            for i, dimension in enumerate(spacer):
                if dimension > 0:
                    spacer[i] = round(dimension * scale)
            # print(spacer)
            args = [tuple(spacer)] + args[1:]
    return args, kwargs


wx._Sizer = wx.Sizer


class Sizer(wx._Sizer):
    """wx.Sizer replacement that scales spacing for current DPI."""

    def Add(self, *args, **kwargs):
        """Add an item to the sizer.

        Returns:
            wx.SizerItem: The item added to the sizer.
        """
        args, kwargs = _adjust_sizer_args_scaling_for_appdpi(*args, **kwargs)
        return wx._Sizer.Add(self, *args, **kwargs)

    def Insert(self, index, *args, **kwargs):
        """Insert an item at the specified index in the sizer.

        Args:
            index (int): The index at which to insert the item.
            *args: Positional arguments for the item to insert.
            **kwargs: Keyword arguments for the item to insert.
        """
        args, kwargs = _adjust_sizer_args_scaling_for_appdpi(*args, **kwargs)
        return wx._Sizer.Insert(self, index, *args, **kwargs)


wx.Sizer = Sizer


wx._BoxSizer = wx.BoxSizer


class BoxSizer(wx._BoxSizer):
    """BoxSizer with DPI scaling."""

    Add = Sizer.__dict__["Add"]
    Insert = Sizer.__dict__["Insert"]


wx.BoxSizer = BoxSizer


wx._GridSizer = wx.GridSizer


class GridSizer(wx._GridSizer):
    """GridSizer with DPI scaling."""

    def __init__(self, rows=0, cols=0, vgap=0, hgap=0):
        if vgap or hgap:
            from DisplayCAL.config import get_default_dpi, getcfg

            scale = getcfg("app.dpi") / get_default_dpi()
            if scale > 1:
                # print(vgap, hgap, '->')
                vgap, hgap = [round(v * scale) for v in (vgap, hgap)]
            # print(vgap, hgap)
        super().__init__(rows, cols, vgap, hgap)

    Add = Sizer.__dict__["Add"]
    Insert = Sizer.__dict__["Insert"]


wx.GridSizer = GridSizer


wx._FlexGridSizer = wx.FlexGridSizer


class FlexGridSizer(wx._FlexGridSizer):
    """FlexGridSizer with DPI scaling."""

    def __init__(self, rows=0, cols=0, vgap=0, hgap=0):
        if vgap or hgap:
            from DisplayCAL.config import get_default_dpi, getcfg

            scale = getcfg("app.dpi") / get_default_dpi()
            if scale > 1:
                # print vgap, hgap, '->',
                vgap, hgap = [round(v * scale) for v in (vgap, hgap)]
            # print vgap, hgap
        super().__init__(rows, cols, vgap, hgap)

    Add = Sizer.__dict__["Add"]
    Insert = Sizer.__dict__["Insert"]


wx.FlexGridSizer = FlexGridSizer


def GridGetSelection(self):
    """Return selected rows, cols, block and cells."""
    sel = []
    numrows = self.GetNumberRows()
    numcols = self.GetNumberCols()
    # rows
    rows = self.GetSelectedRows()
    sel.extend((row, i) for row in rows for i in range(numcols))
    # cols
    cols = self.GetSelectedCols()
    sel.extend((i, col) for col in cols for i in range(numrows))
    # block
    tl = self.GetSelectionBlockTopLeft()
    br = self.GetSelectionBlockBottomRight()
    if tl and br:
        sel.extend(
            (i, j)
            for n in range(min(len(tl), len(br)))
            for i in range(tl[n][0], br[n][0] + 1)  # rows
            for j in range(tl[n][1], br[n][1] + 1)  # cols
        )
    # single selected cells
    sel.extend(self.GetSelectedCells())
    # TODO: I don't remember why I disabled the next line and added the for loop instead
    # sel = list(set(sel))
    new_sel = []
    for item in sel:
        if item not in new_sel:
            new_sel.append(item)
    sel = new_sel

    sel.sort()
    return sel


wx.grid.Grid.GetSelection = GridGetSelection


def adjust_font_size_for_gcdc(font):
    """Adjust font size for GCDC."""
    size = get_gcdc_font_size(font.PointSize)
    font.SetPointSize(int(size))
    return font


def get_dc_font_scale(dc):
    """Get correct font scaling factor for DC."""
    pointsize = (1.0, 1.0)
    if isinstance(dc, wx.GCDC):
        pointsize = tuple(1.0 / scale for scale in dc.GetLogicalScale())
    if sys.platform in ("darwin", "win32") or (
        not isinstance(dc, wx.GCDC) and "gtk3" not in wx.PlatformInfo
    ):
        return sum(pointsize) / 2.0
    # On Linux, we need to correct the font size by a certain factor if
    # wx.GCDC is used, to make text the same size as if wx.GCDC weren't used
    from DisplayCAL.config import get_default_dpi, getcfg, set_default_app_dpi

    set_default_app_dpi()
    scale = getcfg("app.dpi") / get_default_dpi()
    if wx.VERSION < (2, 9):
        # On Linux, we need to correct the font size by a certain factor if
        # wx.GCDC is used, to make text the same size as if wx.GCDC weren't used
        screenppi = list(map(float, wx.ScreenDC().GetPPI()))
        ppi = dc.GetPPI()
        scale *= (screenppi[0] / ppi[0] + screenppi[1] / ppi[1]) / 2.0
    return (scale * pointsize[0] + scale * pointsize[1]) / 2.0


def get_dc_font_size(size, dc):
    """Get correct font size for DC."""
    return int(size * get_dc_font_scale(dc))


def get_gcdc_font_size(size):
    """Get correct font size for GCDC."""
    dc = wx.MemoryDC(wx.EmptyBitmap(1, 1))
    with contextlib.suppress(Exception):
        dc = wx.GCDC(dc)
    return get_dc_font_size(size, dc)


def get_bitmap_disabled(bitmap):
    """Get the disabled bitmap."""
    # Use Rec. 709 luma coefficients to convert to grayscale
    image = bitmap.ConvertToImage().ConvertToGreyscale(0.2126, 0.7152, 0.0722)
    if image.HasMask() and not image.HasAlpha():
        image.InitAlpha()
    if image.HasAlpha():
        alphabuffer = image.GetAlphaBuffer()
        for i, byte in enumerate(alphabuffer):
            if byte > 0:
                alphabuffer[i] = round(byte * 0.3)
    return image.ConvertToBitmap()


def get_bitmap_hover(bitmap, ctrl=None):
    """Get the hover bitmap."""
    image = bitmap.ConvertToImage()
    if image.HasMask() and not image.HasAlpha():
        image.InitAlpha()
    if sys.platform == "darwin":
        color = [44, 93, 205]  # Use Mavericks-like color scheme
    else:
        color = list(wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT)[:3])
    R = color[0] / 255.0
    G = color[1] / 255.0
    B = color[2] / 255.0
    # Assume Rec. 709 gamma 2.2 to determine rel. luminance
    lum = 0.2126 * R**2.2 + 0.7152 * G**2.2 + 0.0722 * B**2.2
    is_bw = image.IsBW()
    if ctrl:
        bgcolor = ctrl.Parent.BackgroundColour
        if is_bw:
            # Adjust hilight color by background color if luminance difference
            # is below threshhold
            R = bgcolor[0] / 255.0
            G = bgcolor[1] / 255.0
            B = bgcolor[2] / 255.0
            # Assume Rec. 709 gamma 2.2 to determine rel. luminance
            bglum = 0.2126 * R**2.2 + 0.7152 * G**2.2 + 0.0722 * B**2.2
            lumdiff = (max(lum, bglum) - min(lum, bglum)) ** (1 / 2.2)
            if lumdiff < 1.0 / 3.0:
                if lum > bglum:
                    newmin = bglum ** (1 / 2.2) * 255
                    newmax = 255
                else:
                    newmin = 0
                    newmax = bglum ** (1 / 2.2) * 153  # 60%
                for i, v in enumerate(color):
                    color[i] = round(convert_range(v, 0, 255, newmin, newmax))
    databuffer = image.GetDataBuffer()
    alphabuffer = image.GetAlphaBuffer()
    minv = 256  # Intentionally above max possible value
    j = 0
    RGB = {}
    for i, byte in enumerate(databuffer):
        RGB[i % 3] = byte
        if i % 3 == 2:
            RGBv = list(RGB.values())
            if not is_bw:
                RGBv_max = max(RGBv)
                if minv == 256 and alphabuffer[j] > 32:
                    # If a pixel is at least 12.5% opaque, check if it's value
                    # is less than or equal the previously seen minimum pixel
                    # value as a crude means to determine if the graphic
                    # contains a black outline.
                    minv = min(RGBv_max, minv)

            for k in range(3):
                if is_bw:
                    v = color[k]
                else:
                    v = RGB[k]
                    if minv > 0 and min(RGBv) != RGBv_max:
                        # If the minimum seen at least 12.5% opaque pixel value
                        # is above zero, assume a graphic without black outline
                        # and adjust intensity by applying a power. The hilight
                        # color isn't used.
                        v = (v / 255.0) ** 0.8 * 255
                    else:
                        # If the minimum seen at least 12.5% opaque pixel value
                        # is zero, adjust value by hilight color value, so that
                        # graphics with a black outline get the hilight color
                        # at black.
                        v = convert_range(v, 0, 255, color[k], 255)
                    v = round(v)
                databuffer[i - 2 + k] = v
            j += 1
    if isinstance(ctrl, wx.BitmapButton) and sys.platform == "win32":
        # wx.BitmapButton draws the focus bitmap over the normal bitmap,
        # leading to ugly aliased edges. Yuck. Prevent this by setting the
        # background to the control's background color instead of transparent.
        bmp = wx.EmptyBitmap(image.Width, image.Height)
        dc = wx.MemoryDC()
        dc.SelectObject(bmp)
        dc.SetBrush(wx.Brush(bgcolor))
        dc.SetPen(wx.Pen(bgcolor, width=0))
        dc.DrawRectangle(0, 0, image.Width, image.Height)
        dc.DrawBitmap(image.ConvertToBitmap(), 0, 0, True)
        dc.SelectObject(wx.NullBitmap)
    else:
        bmp = image.ConvertToBitmap()
    return bmp


def get_bitmap_pressed(bitmap):
    """Get the pressed bitmap."""
    image = bitmap.ConvertToImage()
    if image.HasMask() and not image.HasAlpha():
        image.InitAlpha()
    databuffer = image.GetDataBuffer()
    for i, byte in enumerate(databuffer):
        if byte > 0:
            databuffer[i] = round(byte * 0.85)
    return image.ConvertToBitmap()


def get_bitmap_focus(bitmap):
    """Get the focus bitmap."""
    image = bitmap.ConvertToImage()
    if image.HasMask() and not image.HasAlpha():
        image.InitAlpha()
    databuffer = image.GetDataBuffer()
    for i, byte in enumerate(databuffer):
        if byte > 0:
            databuffer[i] = round((byte / 255.0) ** 0.8 * 255)
    return image.ConvertToBitmap()


def set_bitmap_labels(btn, disabled=True, focus=None, pressed=True):
    """Set the bitmap labels for a button."""
    bitmap = btn.BitmapLabel
    if not bitmap.IsOk():
        size = btn.MinSize
        if -1 in size:
            size = (16, 16)
        bitmap = wx.ArtProvider.GetBitmap(wx.ART_MISSING_IMAGE, size=size)
        # Ensure the normal bitmap is set first, fixes #507
        btn.SetBitmapLabel(bitmap)

    # Disabled
    if disabled:
        disabled_bitmap = get_bitmap_disabled(bitmap)
        with contextlib.suppress(wx._core.wxAssertionError):
            btn.SetBitmapDisabled(disabled_bitmap)

    # Focus/Hover
    if sys.platform != "darwin" and focus is not False:
        # wxMac applies hover state also to disabled buttons...
        if focus is None:
            # Use hover bitmap
            bmp = get_bitmap_hover(bitmap, btn)
        else:
            # Use focus bitmap
            bmp = get_bitmap_focus(bitmap)
        with contextlib.suppress(wx._core.wxAssertionError):
            btn.SetBitmapFocus(bmp)
        if hasattr(btn, "SetBitmapCurrent"):
            # Phoenix
            with contextlib.suppress(wx._core.wxAssertionError):
                btn.SetBitmapCurrent(bmp)
        else:
            # Classic
            btn.SetBitmapHover(bmp)

    # Selected
    if pressed:
        if sys.platform == "darwin":
            bmp = get_bitmap_hover(bitmap, btn)
        elif focus is False:
            bmp = get_bitmap_pressed(bitmap)
        if hasattr(btn, "SetBitmapPressed"):
            # Phoenix
            with contextlib.suppress(wx._core.wxAssertionError):
                btn.SetBitmapPressed(bmp)
        else:
            # Classic
            btn.SetBitmapSelected(bmp)


def get_dialogs(modal=False):
    """If there are any dialogs open, return them."""
    return [
        window
        for window in list(wx.GetTopLevelWindows())
        if window
        and isinstance(window, wx.Dialog)
        and window.IsShown()
        and (not modal or window.IsModal())
    ]


# wx.DirDialog and wx.FileDialog are normally not returned by
# wx.GetTopLevelWindows, do some trickery to work around

_DirDialog = wx.DirDialog
_FileDialog = wx.FileDialog


class PathDialogBase(wx.Dialog):
    """Base class for wx.DirDialog and wx.FileDialog.

    Args:
        parent (wx.Window): The parent window.
        name (str): The name of the dialog.
    """

    def __init__(self, parent, name):
        wx.Dialog.__init__(self, parent, -1, name=name)
        self._ismodal = False
        self._isshown = False
        self.Bind(wx.EVT_WINDOW_DESTROY, self.OnDestroy)

    def __getattr__(self, name) -> Any:
        """Get the attribute from the file dialog.

        Args:
            name (str): The name of the attribute to get.

        Returns:
            Any: The value of the attribute.s
        """
        return getattr(self.filedialog, name)

    def IsModal(self):
        """Check if the dialog is modal.

        Returns:
            bool: True if the dialog is modal, False otherwise.
        """
        return self._ismodal

    def IsShown(self):
        """Check if the dialog is shown.

        Returns:
            bool: True if the dialog is shown, False otherwise.
        """
        return self._isshown

    def OnDestroy(self, event):
        """Handle the dialog destruction event.

        Args:
            event (wx.WindowDestroyEvent): The window destroy event.
        """
        self.filedialog.Destroy()
        event.Skip()
        return 0

    def Show(self, show=True):
        """Show or hide the dialog.

        Args:
            show (bool): Whether to show or hide the dialog.
        """
        self._isshown = show
        self.filedialog.Show(show)

    def ShowModal(self):
        """Show the dialog modally.

        Returns:
            int: The return code of the dialog.
        """
        self._isshown = True
        self._ismodal = True
        returncode = self.filedialog.ShowModal()
        self._ismodal = False
        self._isshown = False
        return returncode


class DirDialog(PathDialogBase):
    """Directory dialog that works around wxGTK3 issues with file dialogs."""

    def __init__(self, *args, **kwargs):
        PathDialogBase.__init__(self, args[0], name="dirdialog")
        self.filedialog = _DirDialog(*args, **kwargs)


class FileDialog(PathDialogBase):
    """File dialog that works around wxGTK3 issues with file dialogs."""

    def __init__(self, *args, **kwargs):
        PathDialogBase.__init__(self, args[0], name="filedialog")
        self.filedialog = _FileDialog(*args, **kwargs)


wx.DirDialog = DirDialog
wx.FileDialog = FileDialog


wx._ScrolledWindow = wx.ScrolledWindow


class ScrolledWindow(wx._ScrolledWindow):
    """ScrolledWindow that scrolls child controls into view on focus.

    OnChildFocus and ScrollChildIntoView borrowed from wx.lib.scrolledpanel.
    """

    def __init__(self, *args, **kwargs):
        wx._ScrolledWindow.__init__(self, *args, **kwargs)
        self.Bind(wx.EVT_CHILD_FOCUS, self.OnChildFocus)

    def OnChildFocus(self, evt):
        """Handle child focus events.

        Args:
            evt (wx.ChildFocusEvent): The child focus event.
        """
        # If the child window that gets the focus is not visible,
        # this handler will try to scroll enough to see it.
        evt.Skip()
        child = evt.GetWindow()
        self.ScrollChildIntoView(child)

    def ScrollChildIntoView(self, child):
        """Scroll the panel such that the specified child window is in view."""
        sppu_x, sppu_y = self.GetScrollPixelsPerUnit()
        vs_x, vs_y = self.GetViewStart()
        cr = child.GetRect()
        clntsz = self.GetClientSize()
        new_vs_x, new_vs_y = -1, -1

        # is it before the left edge?
        if cr.x < 0 < sppu_x:
            new_vs_x = vs_x + (cr.x / sppu_x)

        # is it above the top?
        if cr.y < 0 < sppu_y:
            new_vs_y = vs_y + (cr.y / sppu_y)

        # For the right and bottom edges, scroll enough to show the
        # whole control if possible, but if not just scroll such that
        # the top/left edges are still visible

        # is it past the right edge ?
        if cr.right > clntsz.width and sppu_x > 0:
            diff = (cr.right - clntsz.width) / sppu_x
            if cr.x - diff * sppu_x > 0:
                new_vs_x = vs_x + diff + 1
            else:
                new_vs_x = vs_x + (cr.x / sppu_x)

        # is it below the bottom ?
        if cr.bottom > clntsz.height and sppu_y > 0:
            diff = (cr.bottom - clntsz.height) / sppu_y
            if cr.y - diff * sppu_y > 0:
                new_vs_y = vs_y + diff + 1
            else:
                new_vs_y = vs_y + (cr.y / sppu_y)

        # if we need to adjust
        if new_vs_x != -1 or new_vs_y != -1:
            # print "%s: (%s, %s)" % (self.GetName(), new_vs_x, new_vs_y)
            self.Scroll(int(new_vs_x), int(new_vs_y))


wx.ScrolledWindow = ScrolledWindow
ScrolledPanel.ScrollChildIntoView = ScrolledWindow.ScrollChildIntoView


class GenButton:
    """A generic button, based on wx.lib.buttons.GenButton.

    Fixes wx.lib.buttons.ThemedGenButton not taking into account backgroun
    color when pressed.
    """

    def __init__(self):
        self.bezelWidth = 2
        self.hasFocus = False
        self.up = True
        self.useFocusInd = True

    def OnPaint(self, event):
        """Handle the paint event for the button.

        Args:
            event (wx.PaintEvent): The paint event.
        """
        (width, height) = self.ClientSize
        x1 = y1 = 0
        x2 = width
        y2 = height

        dc = wx.PaintDC(self)
        brush = self.GetBackgroundBrush(dc)
        if brush is not None:
            brush.SetColour(self.Parent.BackgroundColour)
            dc.SetBackground(brush)
            dc.Clear()

        self.DrawBezel(dc, x1, y1, x2, y2)
        self.DrawLabel(dc, width, height)
        if self.hasFocus and self.useFocusInd:
            self.DrawFocusIndicator(dc, width, height)


if "gtk3" not in wx.PlatformInfo:
    # GTK3 doesn't respect NO_BORDER in hovered state when using wx.BitmapButton
    _GenBitmapButton = wx.BitmapButton


class GenBitmapButton(GenButton, _GenBitmapButton):
    """A generic bitmap button, based on wx.lib.buttons.GenBitmapButton."""

    def __init__(self, *args, **kwargs):
        GenButton.__init__(self)
        _GenBitmapButton.__init__(self, *args, **kwargs)
        self.hover = False
        set_bitmap_labels(self)
        if _GenBitmapButton is not wx.BitmapButton:
            self.Bind(wx.EVT_ENTER_WINDOW, self.OnMouseEnter)
            self.Bind(wx.EVT_LEAVE_WINDOW, self.OnMouseLeave)

    @property
    def BitmapFocus(self):
        """Get the bitmap used when the button has focus.

        Returns:
            wx.Bitmap: The bitmap used for the focus state.
        """
        return self.GetBitmapFocus()

    @BitmapFocus.setter
    def BitmapFocus(self, bitmap):
        """Set the bitmap to use when the button has focus.

        Args:
            bitmap (wx.Bitmap): The bitmap to set for the focus state.
        """
        self.SetBitmapFocus(bitmap)

    @property
    def BitmapDisabled(self):
        """Get the bitmap used when the button is disabled.

        Returns:
            wx.Bitmap: The bitmap used for the disabled state.
        """
        return self.GetBitmapDisabled()

    @BitmapDisabled.setter
    def BitmapDisabled(self, bitmap):
        """Set the bitmap to use when the button is disabled.

        Args:
            bitmap (wx.Bitmap): The bitmap to set for the disabled state.
        """
        self.SetBitmapDisabled(bitmap)

    @property
    def BitmapHover(self):
        """Get the bitmap used when the button is hovered.

        Returns:
            wx.Bitmap: The bitmap used for the hover state.
        """
        return self.GetBitmapHover()

    @BitmapHover.setter
    def BitmapHover(self, bitmap):
        """Set the bitmap to use when the button is hovered.

        Args:
            bitmap (wx.Bitmap): The bitmap to set for the hover state.
        """
        self.SetBitmapHover(bitmap)

    @property
    def BitmapSelected(self):
        """Get the bitmap used when the button is selected.

        Returns:
            wx.Bitmap: The bitmap used for the selected state.
        """
        return self.GetBitmapSelected()

    @BitmapSelected.setter
    def BitmapSelected(self, bitmap):
        """Set the bitmap to use when the button is selected.

        Args:
            bitmap (wx.Bitmap): The bitmap to set for the selected state.
        """
        self.SetBitmapSelected(bitmap)

    @property
    def BitmapLabel(self):
        """Get the bitmap label for the button.

        Returns:
            wx.Bitmap: The bitmap label of the button.
        """
        return self.GetBitmapLabel()

    @BitmapLabel.setter
    def BitmapLabel(self, bitmap):
        """Set the bitmap label for the button.

        Args:
            bitmap (wx.Bitmap): The bitmap to set as the label.
        """
        self.SetBitmapLabel(bitmap)

    def DrawLabel(self, dc, width, height, dx=0, dy=0):
        """Draw the bitmap label in the button.

        Args:
            dc (wx.DC): The device context to draw on.
            width (int): The width of the button.
            height (int): The height of the button.
            dx (int, optional): Horizontal offset for the bitmap. Defaults to 0.
            dy (int, optional): Vertical offset for the bitmap. Defaults to 0.
        """
        bmp = self.BitmapLabel
        if self.BitmapDisabled and not self.IsEnabled():
            bmp = self.BitmapDisabled
        elif self.BitmapSelected and not self.up:
            bmp = self.BitmapSelected
        elif self.BitmapHover and self.hover:
            bmp = self.BitmapHover
        elif self.BitmapFocus and self.hasFocus:
            bmp = self.BitmapFocus
        bw, bh = bmp.GetWidth(), bmp.GetHeight()
        hasMask = bmp.GetMask() is not None
        dc.DrawBitmap(
            bmp, int((width - bw) / 2 + dx), int((height - bh) / 2 + dy), hasMask
        )

    def GetBitmapHover(self):
        """Get the bitmap used when the button is hovered."""
        return self.bmpHover

    def OnMouseEnter(self, event):
        """Handle the mouse enter event.

        Args:
            event (wx.MouseEvent): The mouse event.
        """
        if not self.IsEnabled():
            return
        if not self.hover:
            self.hover = True
            self.Refresh()
        event.Skip()

    def OnMouseLeave(self, event):
        """Handle the mouse leave event.

        Args:
            event (wx.MouseEvent): The mouse event.
        """
        if not self.IsEnabled():
            return
        if self.hover:
            self.hover = False
            self.Refresh()
        event.Skip()

    def SetBitmapHover(self, bitmap):
        """Set the bitmap to use when the button is hovered.

        Args:
            bitmap (wx.Bitmap): The bitmap to set for the hover state.
        """
        self.bmpHover = bitmap


class ThemedGenButton(GenButton, _ThemedGenButton):
    """A themed generic button, based on wx.lib.buttons.ThemedGenButton.

    Fixes wx.lib.buttons.ThemedGenButton sometimes not reflecting enabled
    state correctly as well as not taking into account background color when
    pressed, and mimics a default button under Windows more closely by
    not drawing a focus outline and not shifting the label when pressed.

    Also implements state for SetDefault.
    """

    _really_enabled = True
    label_delta = 1

    def __init__(self, *args, **kwargs):
        GenButton.__init__(self)
        _ThemedGenButton.__init__(self, *args, **kwargs)
        self._default = False

    def Disable(self):
        """Disable the button."""
        self.Enable(False)

    def DoGetBestSize(self):
        """Determine the best size of the button based on the label and bezel size.

        Returns:
            tuple: The width and height of the button.
        """
        w, h, useMin = self._GetLabelSize()
        if self.style & wx.BU_EXACTFIT:
            width = w + 2 + 2 * self.bezelWidth + 4 * int(self.useFocusInd)
            height = h + 2 + 2 * self.bezelWidth + 4 * int(self.useFocusInd)
        else:
            defSize = wx.Button.GetDefaultSize()
            width = 12 + w
            if useMin and width < defSize.width:
                width = defSize.width
            height = 11 + h
            if useMin and height < defSize.height:
                height = defSize.height
            width = width + self.bezelWidth - 2
            height = height + self.bezelWidth - 2
        return (width, height)

    def DrawBezel(self, dc, x1, y1, x2, y2):
        """Draw the bezel around the button.

        Args:
            dc (wx.DC): The device context to draw on.
            x1 (int): The x-coordinate of the top-left corner.
            y1 (int): The y-coordinate of the top-left corner.
            x2 (int): The x-coordinate of the bottom-right corner.
            y2 (int): The y-coordinate of the bottom-right corner.
        """
        rect = wx.Rect(x1, y1, x2, y2)
        state = 0 if self.up else (wx.CONTROL_PRESSED | wx.CONTROL_SELECTED)
        if not self.IsEnabled():
            state = wx.CONTROL_DISABLED
        elif self._default:
            state |= wx.CONTROL_ISDEFAULT
        pt = self.ScreenToClient(wx.GetMousePosition())
        if self.GetClientRect().Contains(pt):
            state |= wx.CONTROL_CURRENT
        wx.RendererNative.Get().DrawPushButton(self, dc, rect, state)

    def DrawLabel(self, dc, width, height, dx=0, dy=0):
        """Draw the label text in the button.

        Args:
            dc (wx.DC): The device context to draw on.
            width (int): The width of the button.
            height (int): The height of the button.
            dx (int, optional): Horizontal offset for the label. Defaults to 0.
            dy (int, optional): Vertical offset for the label. Defaults to 0.
        """
        dc.SetFont(self.GetFont())
        if self.Enabled:
            dc.SetTextForeground(self.ForegroundColour)
        else:
            dc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT))
        label = self.Label
        tw, th = dc.GetTextExtent(label)
        if sys.platform != "win32" and not self.up:
            dx = dy = self.label_delta
        dc.DrawText(label, int((width - tw) / 2) + dx, int((height - th) / 2) + dy)

    def Enable(self, enable=True):
        """Enable or disable the button.

        Args:
            enable (bool): True to enable the button, False to disable it.
        """
        if enable == self.Enabled:
            return
        self.Enabled = enable
        wx.PyControl.Enable(self, enable)
        self.Refresh()

    @property
    def Enabled(self):
        """Get the enabled state of the button.

        Returns:
            bool: True if the button is enabled, False otherwise.
        """
        return self._really_enabled

    @Enabled.setter
    def Enabled(self, enabled):
        """Set the enabled state of the button."""
        self._really_enabled = enabled

    def IsEnabled(self):
        """Check if the button is enabled.

        Returns:
            bool: True if the button is enabled, False otherwise.
        """
        return self.Enabled

    def OnLeftDown(self, event):
        """Handle the left mouse button down event.

        Args:
            event (wx.MouseEvent): The mouse event.
        """
        if not self.Enabled:
            return
        self.up = False
        self.CaptureMouse()
        self.SetFocus()
        self.useFocusInd = False
        self.Refresh()
        event.Skip()

    def OnGainFocus(self, event):
        """Handle the focus gain event.

        Args:
            event (wx.FocusEvent): The focus event.
        """
        self.hasFocus = True
        self.useFocusInd = bool(self.bezelWidth)
        self.Refresh()
        self.Update()

    def SetDefault(self):
        """Set the button as default button."""
        self._default = True
        _ThemedGenButton.SetDefault(self)


class PlateButton(platebtn.PlateButton):
    """Fixes PlateButton enabled state and related issues."""

    _really_enabled = True

    def __init__(self, *args, **kwargs):
        from DisplayCAL.config import get_default_dpi, getcfg

        self.dpiscale = getcfg("app.dpi") / get_default_dpi()
        platebtn.PlateButton.__init__(self, *args, **kwargs)
        self._bmp["hilite"] = None
        if sys.platform == "darwin":
            # Use Sierra-like color scheme
            from DisplayCAL.wx_addons import gamma_encode

            color = wx.Colour(*gamma_encode(0, 105, 217))
        else:
            color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_HIGHLIGHT)
        self._color["hlight"] = color
        self.Unbind(wx.EVT_LEAVE_WINDOW)
        self.Bind(wx.EVT_LEAVE_WINDOW, lambda evt: wx.CallLater(80, self.__LeaveWindow))
        if "gtk2" in wx.PlatformInfo and not isinstance(self.TopLevelParent, wx.Dialog):
            self.BackgroundColour = self.Parent.BackgroundColour

    def DoGetBestSize(self):
        """Calculate the best size of the button.

        Returns:
            wx.Size: The best size of the button, taking into account.
        """
        # A little more padding left + right
        width = 6 * self.dpiscale
        height = 6 * self.dpiscale
        if self.Label:
            # NOTE: Should measure with a GraphicsContext to get right
            #       size, but due to random segfaults on linux special
            #       handling is done in the drawing instead...
            lsize = self.GetFullTextExtent(self.Label)
            width += lsize[0]
            height += lsize[1]

        if self._bmp["enable"] is not None:
            bsize = self._bmp["enable"].Size
            width += bsize[0] + 10 * self.dpiscale
            if height <= bsize[1]:
                height = bsize[1] + 6 * self.dpiscale
            else:
                # No DPI scaling needed here
                height += 3
        else:
            width += 10 * self.dpiscale

        if self._menu is not None or self._style & platebtn.PB_STYLE_DROPARROW:
            width += 12 * self.dpiscale

        best = wx.Size(int(width), int(height))
        self.CacheBestSize(best)
        return best

    def GetBitmapLabel(self):
        """Get the label bitmap.

        Returns:
            None | wx.Bitmap: wx.Bitmap or None.
        """
        return self._bmp["enable"]

    def SetBitmapHover(self, bmp):
        """Set the bitmap displayed in the button when hovered.

        Args:
            bmp (wx.Bitmap): Bitmap to set as hover bitmap.
        """
        self._bmp["hilite"] = bmp

    def __DrawBitmap(self, gc, interactable):
        """Draw the bitmap if one has been set.

        Args:
            gc (GCDC): :class:`GCDC` to draw with.
            interactable (bool): Whether the button is interactable.

        Return:
            float: x cordinate to draw text at.
        """
        if self.IsEnabled():
            if (
                self._state["cur"] in (platebtn.PLATE_HIGHLIGHT, platebtn.PLATE_PRESSED)
                and interactable
            ):
                bmp = self._bmp["hilite"]
            else:
                bmp = self._bmp["enable"]
        else:
            bmp = self._bmp["disable"]

        xpos = 4 * self.dpiscale
        if bmp is not None and bmp.IsOk():
            bw, bh = bmp.GetSize()
            ypos = (self.GetSize()[1] - bh) // 2
            gc.DrawBitmap(bmp, int(xpos), int(ypos), bmp.GetMask() is not None)
            return bw + xpos
        return xpos

    def __DrawButton(self):
        """Draw the button."""
        # TODO using a buffered paintdc on windows with the nobg style
        #      causes lots of weird drawing. So currently the use of a
        #      buffered dc is dissabled for this style.
        if platebtn.PB_STYLE_NOBG & self._style:
            dc = wx.PaintDC(self)
        else:
            dc = wx.AutoBufferedPaintDCFactory(self)
            dc.SetBackground(wx.TRANSPARENT_BRUSH)
            dc.Clear()

        gc = wx.GCDC(dc)

        # Setup
        gc.SetFont(adjust_font_size_for_gcdc(self.GetFont()))
        gc.SetBackgroundMode(wx.BRUSHSTYLE_TRANSPARENT)

        # Calc Object Positions
        width, height = self.GetSize()
        # Note: Using self.GetTextExtent instead of gc.GetTextExtent seems
        # to fix sporadic segfaults with wxPython Phoenix up to 4.0.0a2
        # under Windows (fixed in 4.0.0a3), but self.GetTextExtent is NOT
        # an equivalent replacement for gc.GetTextExtent.
        tw, th = gc.GetTextExtent(self.Label)
        txt_y = max((height - th) // 2 - 1, 1)

        # The background needs some help to look transparent on
        # on Gtk and Windows
        if wx.Platform in ["__WXGTK__", "__WXMSW__"]:
            gc.SetBrush(self.GetBackgroundBrush(gc))
            gc.SetPen(wx.TRANSPARENT_PEN)
            gc.DrawRectangle(0, 0, width, height)

        gc.SetBrush(wx.TRANSPARENT_BRUSH)

        space = 4 * self.dpiscale

        # Our IsEnabled() override only deals with explit enabled/disabled
        # state, while we also need to deal with interaction state
        # (button enabled and interactable, eg. no modal dialog shown that
        # would prevent interaction)
        dialogs = get_dialogs()
        interactable = not dialogs or self.TopLevelParent in dialogs
        if (
            self._state["cur"] == platebtn.PLATE_HIGHLIGHT
            and self.IsEnabled()
            and interactable
        ):
            gc.SetTextForeground(self._color["htxt"])
            gc.SetPen(wx.TRANSPARENT_PEN)
            self.__DrawHighlight(gc, int(width), int(height))

        elif self._state["cur"] == platebtn.PLATE_PRESSED and self.IsEnabled():
            gc.SetTextForeground(self._color["htxt"])
            pen = wx.Pen(platebtn.AdjustColour(self._color["press"], -80, 220), 1)
            gc.SetPen(pen)

            self.__DrawHighlight(gc, int(width), int(height))
            txt_x = self.__DrawBitmap(gc, interactable)
            t_x = max((width - tw - (txt_x + space)) // 2, txt_x + space)
            gc.DrawText(self.Label, int(t_x), int(txt_y))
            self.__DrawDropArrow(gc, int(width - 10), int((height // 2) - 2))

        elif self.IsEnabled():
            gc.SetTextForeground(self.GetForegroundColour())
        else:
            txt_c = wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT)
            gc.SetTextForeground(txt_c)

        # Draw bitmap and text
        if self._state["cur"] != platebtn.PLATE_PRESSED or not self.IsEnabled():
            txt_x = self.__DrawBitmap(gc, interactable)
            t_x = max((width - tw - (txt_x + space)) // 2, txt_x + space)
            gc.DrawText(self.Label, int(t_x), int(txt_y))
            self.__DrawDropArrow(gc, int(width - 10), int((height // 2) - 2))

    def __LeaveWindow(self):
        """Handle updating the buttons state when the mouse cursor leaves."""
        if (self._style & platebtn.PB_STYLE_TOGGLE) and self._pressed:
            self._SetState(platebtn.PLATE_PRESSED)
        else:
            self._SetState(platebtn.PLATE_NORMAL)
            self._pressed = False

    def __PostEvent(self):
        """Post a button event to parent of this control."""
        if self._style & platebtn.PB_STYLE_TOGGLE:
            etype = wx.wxEVT_COMMAND_TOGGLEBUTTON_CLICKED
        else:
            etype = wx.wxEVT_COMMAND_BUTTON_CLICKED
        bevt = wx.CommandEvent(etype, self.GetId())
        bevt.SetEventObject(self)
        bevt.SetString(self.GetLabel())
        self.GetEventHandler().ProcessEvent(bevt)

    _PostEvent = __PostEvent

    def Update(self):
        """Update the button's appearance."""

    Disable = ThemedGenButton.__dict__["Disable"]
    Enable = ThemedGenButton.__dict__["Enable"]
    Enabled = ThemedGenButton.__dict__["Enabled"]
    IsEnabled = ThemedGenButton.__dict__["IsEnabled"]
    Label = property(
        lambda self: self.GetLabel(), lambda self, label: self.SetLabel(label)
    )


if not hasattr(PlateButton, "_SetState"):
    # wxPython < 3.0
    PlateButton._SetState = PlateButton.SetState


class TempXmlResource:
    """Temporary XML resource class to handle scaling and cleanup.

    Args:
        xmlpath (str): Path to the XML resource file.
    """

    _temp = None

    def __init__(self, xmlpath):
        from DisplayCAL.config import get_default_dpi, getcfg

        scale = max(getcfg("app.dpi") / get_default_dpi(), 1)
        if scale > 1 or "gtk3" in wx.PlatformInfo:
            if not TempXmlResource._temp:
                with contextlib.suppress(Exception):
                    TempXmlResource._temp = tempfile.mkdtemp(prefix=f"{APPNAME}-XRC-")
            if TempXmlResource._temp and os.path.isdir(TempXmlResource._temp):
                # Read original XML
                with open(xmlpath, "rb") as xmlfile:
                    xml = xmlfile.read().decode()
                # Adjust spacing for scale
                for tag in ("border", "hgap", "vgap"):
                    xml = re.sub(
                        rf"<{tag}>(\d+)</{tag}>",
                        lambda match, tag=tag: (
                            f"<{tag}>{round(int(match.group(1)) * scale):d}</{tag}>"
                        ),
                        xml,
                    )
                for tag in ("size",):
                    xml = re.sub(
                        rf"<{tag}>(-?\d+)\s*,\s*(-?\d+)</{tag}>",
                        lambda match, tag=tag: f"<{tag}>{{:d}},{{:d}}</{tag}>".format(
                            *tuple(
                                round(int(v) * scale) if int(v) > 0 else int(v)
                                for v in match.groups()
                            )
                        ),
                        xml,
                    )
                # Set relative paths to absolute
                basedir = os.path.dirname(os.path.dirname(xmlpath))
                basedir = basedir.replace("\\", "/")
                xml = xml.replace(">../", f">{basedir}/")
                # Fix background color not working for panels under wxGTK3
                if "gtk3" in wx.PlatformInfo:
                    xml = xml.replace(
                        'class="wxPanel"',
                        'class="wxPanel" subclass="DisplayCAL.wx_fixes.wx_Panel"',
                    )
                    # 'class="wxPanel" subclass="wx_fixes.wx_Panel"')
                # Write modified XML
                xmlpath = os.path.join(TempXmlResource._temp, os.path.basename(xmlpath))
                with open(xmlpath, "wb") as xmlfile:
                    xmlfile.write(xml.encode())
                from DisplayCAL.wx_windows import BaseApp

                BaseApp.register_exitfunc(self._cleanup)
        self.xmlpath = xmlpath
        self.res = xrc.XmlResource(xmlpath)

    def _cleanup(self):
        """Clean up the temporary XML resource."""
        if (
            TempXmlResource._temp
            and self.xmlpath.startswith(TempXmlResource._temp + os.path.sep)
            and os.path.isfile(self.xmlpath)
        ):
            os.remove(self.xmlpath)
            if not os.listdir(TempXmlResource._temp):
                shutil.rmtree(TempXmlResource._temp)

    def __getattr__(self, name) -> Any:
        """Get attribute from the resource.

        Args:
            name (str): Name of the attribute to get.

        Returns:
            Any: The value of the attribute.
        """
        return getattr(self.res, name)


class GenBitmapTextButton(GenButton, _GenBitmapTextButton):
    """A generic bitmapped button with text label.

    Args:
        parent (wx.Window): The parent window.
        id (int, optional): The identifier for the button. Defaults to -1.
        bitmap (wx.Bitmap, optional): The bitmap to use. Defaults to
            wx.NullBitmap.
        label (str, optional): The label to use. Defaults to "".
        pos (wx.Point, optional): The position of the button. Defaults to
            wx.DefaultPosition.
        size (wx.Size, optional): The size of the button. Defaults to
            wx.DefaultSize.
        style (int, optional): The style of the button. Defaults to 0.
        validator (wx.Validator, optional): The validator for the button.
            Defaults to wx.DefaultValidator.
        name (str, optional): The name of the button. Defaults to "genbutton".
    """

    def __init__(
        self,
        parent,
        id=-1,  # noqa: A002
        bitmap=wx.NullBitmap,
        label="",
        pos=wx.DefaultPosition,
        size=wx.DefaultSize,
        style=0,
        validator=wx.DefaultValidator,
        name="genbutton",
    ):
        GenButton.__init__(self)
        _GenBitmapTextButton.__init__(
            self, parent, id, bitmap, label, pos, size, style, validator, name
        )
        self._default = False

    def DrawLabel(self, dc, width, height, dx=0, dy=0):
        """Draw the label of the button.

        Args:
            dc (wx.DC): The device context to draw on.
            width (int): The width of the button.
            height (int): The height of the button.
            dx (int, optional): Horizontal offset for the label. Defaults to 0.
            dy (int, optional): Vertical offset for the label. Defaults to 0.
        """
        bmp = self.bmpLabel
        if bmp is not None:  # if the bitmap is used
            if self.bmpDisabled and not self.IsEnabled():
                bmp = self.bmpDisabled
            if self.bmpFocus and self.hasFocus:
                bmp = self.bmpFocus
            if self.bmpSelected and not self.up:
                bmp = self.bmpSelected
            bw, bh = bmp.GetWidth(), bmp.GetHeight()
            if sys.platform != "win32" and not self.up:
                dx = dy = self.labelDelta
            hasMask = bmp.GetMask() is not None
        else:
            bw = bh = 0  # no bitmap -> size is zero

        dc.SetFont(self.GetFont())
        if self.IsEnabled():
            dc.SetTextForeground(self.GetForegroundColour())
        else:
            dc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_GRAYTEXT))

        label = self.GetLabel()
        tw, th = dc.GetTextExtent(label)  # size of text
        sw, sh = dc.GetTextExtent(" ")  # extra spacing from bitmap
        if sys.platform != "win32" and not self.up:
            dx = dy = self.labelDelta

        # adjust for bitmap and text to centre
        pos_x = int((width - bw - sw - tw) / 2) + dx
        if bmp is not None:
            dc.DrawBitmap(
                bmp, pos_x, int((height - bh) / 2) + dy, hasMask
            )  # draw bitmap if available
            pos_x = pos_x + sw  # extra spacing from bitmap

        # draw the text
        dc.DrawText(label, pos_x + dx + bw, int((height - th) / 2) + dy)


class ThemedGenBitmapTextButton(ThemedGenButton, GenBitmapTextButton):
    """A themed generic bitmapped button with text label."""


class BitmapWithThemedButton(wx.BoxSizer):
    """A sizer that contains a bitmap and a themed button."""

    def __init__(
        self,
        parent,
        id=-1,  # noqa: A002
        bitmap=wx.NullBitmap,
        label="",
        pos=wx.DefaultPosition,
        size=wx.DefaultSize,
        style=0,
        validator=wx.DefaultValidator,
        name="button",
    ):
        wx.BoxSizer.__init__(self, wx.HORIZONTAL)
        self._bmp = wx.StaticBitmap(parent, -1, bitmap)
        # self.Add(self._bmp, flag=wx.ALIGN_CENTER_VERTICAL)
        self.Add(self._bmp)
        btncls = ThemedGenButton if wx.Platform == "__WXMSW__" else wx.Button
        self._btn = btncls(parent, id, label, pos, size, style, validator, name)
        # self.Add(self._btn, flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT, border=8)
        self.Add(self._btn, flag=wx.LEFT, border=8)

    def __getattr__(self, name) -> Any:
        """Get attribute from button or bitmap.

        Args:
            name (str): Name of the attribute to get.

        Returns:
            Any: The value of the attribute.
        """
        return getattr(self._btn, name)

    def Bind(self, event, handler):
        """Bind an event to the button.

        Args:
            event (wx.EventType): The event type to bind.
            handler (callable): The handler function to call when the event
                occurs.
        """
        self._btn.Bind(event, handler)

    def SetBitmapLabel(self, bitmap):
        """Set the bitmap label for the static bitmap.

        Args:
            bitmap (wx.Bitmap): The bitmap to set as label.
        """
        self._bmp.SetBitmap(bitmap)
        self.Layout()
