diff --git a/graphs/gui/canvasPanel.py b/graphs/gui/canvasPanel.py index 4c862f6001..1c9d7f2397 100644 --- a/graphs/gui/canvasPanel.py +++ b/graphs/gui/canvasPanel.py @@ -31,6 +31,8 @@ from graphs.style import BASE_COLORS, LIGHTNESSES, STYLES, hsl_to_hsv +from gui.utils.themes import Themes +from gui.utils.dark import isDark from gui.utils.numberFormatter import roundToPrec @@ -84,7 +86,7 @@ def __init__(self, graphFrame, parent): mainSizer = wx.BoxSizer(wx.VERTICAL) self.figure = Figure(figsize=(5, 3), tight_layout={'pad': 1.08}) - rgbtuple = wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE).Get() + rgbtuple = Themes.buttonFace().Get() clr = [c / 255. for c in rgbtuple] self.figure.set_facecolor(clr) self.figure.set_edgecolor(clr) @@ -93,6 +95,10 @@ def __init__(self, graphFrame, parent): self.canvas.mpl_connect('button_press_event', self.OnMplCanvasClick) self.subplot = self.figure.add_subplot(111) self.subplot.grid(True) + + # Style axes for dark mode + self._styleAxes() + mainSizer.Add(self.canvas, 1, wx.EXPAND | wx.ALL, 0) self.SetSizer(mainSizer) @@ -104,6 +110,7 @@ def __init__(self, graphFrame, parent): def draw(self, accurateMarks=True): self.subplot.clear() self.subplot.grid(True) + self._styleAxes() # Re-apply styling after clear allXs = set() allYs = set() plotData = {} @@ -294,6 +301,32 @@ def unmarkX(self): self.xMark = None self.draw() + def _styleAxes(self): + """Style the matplotlib axes for dark/light mode.""" + if isDark(): + textColor = '#DCDCDC' # Light gray text for dark mode + axisColor = '#888888' # Gray for axis lines + else: + textColor = '#000000' # Black text for light mode + axisColor = '#000000' # Black for axis lines + + # Set background color for the plot area + bgColor = Themes.windowBackground().Get() + bgColorNorm = [c / 255. for c in bgColor] + self.subplot.set_facecolor(bgColorNorm) + + # Style axis spines (the lines around the plot) + for spine in self.subplot.spines.values(): + spine.set_color(axisColor) + + # Style tick labels and axis labels + self.subplot.tick_params(colors=textColor, labelcolor=textColor) + self.subplot.xaxis.label.set_color(textColor) + self.subplot.yaxis.label.set_color(textColor) + + # Style grid + self.subplot.grid(True, color=axisColor, alpha=0.3) + @staticmethod def _getLimits(vals, minExtra=0, maxExtra=0): minVal = min(vals, default=0) diff --git a/graphs/gui/ctrlPanel.py b/graphs/gui/ctrlPanel.py index 418bbe468d..1827c1b388 100644 --- a/graphs/gui/ctrlPanel.py +++ b/graphs/gui/ctrlPanel.py @@ -25,6 +25,7 @@ from gui.bitmap_loader import BitmapLoader from gui.contextMenu import ContextMenu +from gui.utils.themes import ThemedPanel, Themes from gui.utils.inputs import FloatBox, FloatRangeBox from service.const import GraphCacheCleanupReason from service.fit import Fit @@ -37,7 +38,7 @@ _t = wx.GetTranslation -class GraphControlPanel(wx.Panel): +class GraphControlPanel(ThemedPanel): def __init__(self, graphFrame, parent): super().__init__(parent) @@ -56,6 +57,7 @@ def __init__(self, graphFrame, parent): yText = wx.StaticText(self, wx.ID_ANY, _t('Axis Y:')) ySubSelectionSizer.Add(yText, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) self.ySubSelection = wx.Choice(self, wx.ID_ANY) + Themes.styleInput(self.ySubSelection) self.ySubSelection.Bind(wx.EVT_CHOICE, self.OnYTypeUpdate) ySubSelectionSizer.Add(self.ySubSelection, 1, wx.EXPAND | wx.ALL, 0) commonOptsSizer.Add(ySubSelectionSizer, 0, wx.EXPAND | wx.ALL, 0) @@ -64,6 +66,7 @@ def __init__(self, graphFrame, parent): xText = wx.StaticText(self, wx.ID_ANY, _t('Axis X:')) xSubSelectionSizer.Add(xText, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, 5) self.xSubSelection = wx.Choice(self, wx.ID_ANY) + Themes.styleInput(self.xSubSelection) self.xSubSelection.Bind(wx.EVT_CHOICE, self.OnXTypeUpdate) xSubSelectionSizer.Add(self.xSubSelection, 1, wx.EXPAND | wx.ALL, 0) commonOptsSizer.Add(xSubSelectionSizer, 0, wx.EXPAND | wx.TOP, 5) diff --git a/graphs/gui/frame.py b/graphs/gui/frame.py index 4313b81d70..8922f7cc7a 100644 --- a/graphs/gui/frame.py +++ b/graphs/gui/frame.py @@ -29,6 +29,7 @@ from graphs.events import RESIST_MODE_CHANGED from gui.auxWindow import AuxiliaryFrame from gui.bitmap_loader import BitmapLoader +from gui.utils.themes import Themes from service.const import GraphCacheCleanupReason from service.settings import GraphSettings from . import canvasPanel @@ -58,6 +59,7 @@ def __init__(self, parent, includeHidden=False): # Layout - graph selector self.graphSelection = wx.Choice(self, wx.ID_ANY, style=0) + Themes.styleInput(self.graphSelection) self.graphSelection.Bind(wx.EVT_CHOICE, self.OnGraphSwitched) mainSizer.Add(self.graphSelection, 0, wx.EXPAND) diff --git a/graphs/gui/stylePickers.py b/graphs/gui/stylePickers.py index 233ff4302c..00940000b2 100644 --- a/graphs/gui/stylePickers.py +++ b/graphs/gui/stylePickers.py @@ -23,6 +23,7 @@ from graphs.style import BASE_COLORS, LIGHTNESSES, STYLES from gui.bitmap_loader import BitmapLoader +from gui.utils.themes import Themes from service.const import GraphLightness @@ -32,7 +33,7 @@ def __init__(self, parent, wrapper): super().__init__(parent, flags=wx.BORDER_SIMPLE) self.wrapper = wrapper - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + self.SetBackgroundColour(Themes.windowBackground()) sizer = wx.BoxSizer(wx.VERTICAL) grid = wx.GridSizer(self.nrows, self.ncols, 0, 0) diff --git a/graphs/gui/vector.py b/graphs/gui/vector.py index 6272779049..dabd892b93 100644 --- a/graphs/gui/vector.py +++ b/graphs/gui/vector.py @@ -24,6 +24,7 @@ import wx from eos.utils.float import floatUnerr +from gui.utils.themes import Themes class VectorPicker(wx.Window): @@ -114,13 +115,13 @@ def Draw(self, dc): width, height = self.GetScaledClientSize() if not width or not height: return - dc.SetBackground(wx.Brush(self.GetBackgroundColour(), wx.BRUSHSTYLE_SOLID)) + dc.SetBackground(wx.Brush(Themes.buttonFace(), wx.BRUSHSTYLE_SOLID)) dc.Clear() - dc.SetTextForeground(wx.Colour(0)) + dc.SetTextForeground(Themes.text()) dc.SetFont(self._font) radius = min(width, height) / 2 - 2 - dc.SetBrush(wx.WHITE_BRUSH) + dc.SetBrush(wx.Brush(Themes.windowBackground())) dc.DrawCircle(round(radius + 2), round(radius + 2), round(radius)) a = math.radians(self._angle + self._offset) x = math.cos(a) * radius diff --git a/graphs/style.py b/graphs/style.py index 678b61cd89..4164c215c3 100644 --- a/graphs/style.py +++ b/graphs/style.py @@ -23,6 +23,7 @@ # noinspection PyPackageRequirements import wx +from gui.utils.themes import Themes from service.const import GraphColor, GraphLightness, GraphLineStyle ColorData = namedtuple('ColorData', ('hsl', 'name', 'iconName')) @@ -41,7 +42,7 @@ def __init__(self, name, iconNamePrefix, mplSpec): def iconName(self): # Get lightness out of RGB color, see following link for math: # https://www.niwa.nu/2013/05/math-behind-colorspace-conversions-rgb-hsl/ - r, g, b, a = (c / 255 for c in wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + r, g, b, a = (c / 255 for c in Themes.windowBackground()) l = (max(r, g, b) + min (r, g, b)) / 2 suffix = '_black' if l > 0.3 else '_white' return '{}{}'.format(self._iconNamePrefix, suffix) diff --git a/gui/app.py b/gui/app.py index ac9ec9ed22..a1fb3b53f5 100644 --- a/gui/app.py +++ b/gui/app.py @@ -4,7 +4,8 @@ import sys from logbook import Logger pyfalog = Logger(__name__) -from service.settings import LocaleSettings +from service.settings import LocaleSettings, ThemeSettings +from gui.utils.dark import enableWindowsDarkModeSupport class PyfaApp(wx.App): @@ -16,6 +17,10 @@ def OnInit(self): # Name for my application. self.appName = "pyfa" + # Initialize theme settings early so isDark() works correctly + ThemeSettings.getInstance() + enableWindowsDarkModeSupport() + #------------ # # Simplified init method. diff --git a/gui/attribute_gauge.py b/gui/attribute_gauge.py index 08e1e1cb8d..9e7519bc57 100644 --- a/gui/attribute_gauge.py +++ b/gui/attribute_gauge.py @@ -3,6 +3,7 @@ import wx from gui.utils import anim_effects +from gui.utils.themes import Themes # todo: clean class up. Took from pyfa gauge, has a bunch of extra shit we don't need @@ -50,7 +51,7 @@ def __init__( self._old_percentage = 0 self._show_remaining = False - self.SetBackgroundColour(wx.Colour(51, 51, 51)) + self.SetBackgroundColour(Themes.gaugeBackground()) self._tooltip = wx.ToolTip("0.00/100.00") self.SetToolTip(self._tooltip) @@ -169,10 +170,10 @@ def OnPaint(self, event): dc = wx.AutoBufferedPaintDC(self) rect = self.GetClientRect() - dc.SetBackground(wx.Brush(self.GetBackgroundColour())) + dc.SetBackground(wx.Brush(Themes.gaugeBackground())) dc.Clear() - colour = self.GetBackgroundColour() + colour = Themes.gaugeBackground() dc.SetBrush(wx.Brush(colour)) dc.SetPen(wx.Pen(colour)) diff --git a/gui/auxWindow.py b/gui/auxWindow.py index 00a32923c7..0ceb8eaa99 100644 --- a/gui/auxWindow.py +++ b/gui/auxWindow.py @@ -21,6 +21,9 @@ # noinspection PyPackageRequirements import wx +from gui.utils.themes import Themes +from gui.utils.dark import setDarkTitleBar + class AuxiliaryMixin: @@ -52,7 +55,9 @@ def __init__(self, parent, id=None, title=None, pos=None, size=None, style=None, self.Bind(wx.EVT_MENU, self.OnSuppressedAction, id=wx.ID_COPY) self.Bind(wx.EVT_MENU, self.OnSuppressedAction, id=wx.ID_PASTE) if 'wxMSW' in wx.PlatformInfo: - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) + self.SetBackgroundColour(Themes.buttonFace()) + self.SetForegroundColour(Themes.text()) + setDarkTitleBar(self) @classmethod def openOne(cls, parent, *args, forceReopen=False, **kwargs): diff --git a/gui/builtinAdditionPanes/fighterView.py b/gui/builtinAdditionPanes/fighterView.py index 82a9abc18d..c9dc51241f 100644 --- a/gui/builtinAdditionPanes/fighterView.py +++ b/gui/builtinAdditionPanes/fighterView.py @@ -29,6 +29,7 @@ from gui.builtinViewColumns.state import State from gui.contextMenu import ContextMenu from gui.fitCommands.helpers import getSimilarFighters +from gui.utils.themes import Themes from gui.utils.staticHelpers import DragDropHelper from service.fit import Fit from service.market import Market @@ -115,8 +116,7 @@ def fitChanged(self, event): slot = getattr(FittingSlot, "F_{}".format(x.upper())) used = fit.getSlotsUsed(slot) total = fit.getNumSlots(slot) - color = wx.Colour(204, 51, 51) if used > total else wx.SystemSettings.GetColour( - wx.SYS_COLOUR_WINDOWTEXT) + color = wx.Colour(204, 51, 51) if used > total else Themes.text() lbl = getattr(self, "label%sUsed" % x.capitalize()) lbl.SetLabel(str(int(used))) diff --git a/gui/builtinContextMenus/droneSplitStack.py b/gui/builtinContextMenus/droneSplitStack.py index b50d3bce1e..d5761dc0ee 100644 --- a/gui/builtinContextMenus/droneSplitStack.py +++ b/gui/builtinContextMenus/droneSplitStack.py @@ -6,6 +6,7 @@ import gui.fitCommands as cmd import gui.mainFrame from gui.contextMenu import ContextMenuSingle +from gui.utils.themes import ThemedDialog, Themes from service.fit import Fit _t = wx.GetTranslation @@ -49,7 +50,7 @@ def activate(self, callingWindow, fullContext, mainItem, i): DroneSplitStack.register() -class DroneStackSplit(wx.Dialog): +class DroneStackSplit(ThemedDialog): def __init__(self, parent, value): super().__init__(parent, title="Split Drone Stack", style=wx.DEFAULT_DIALOG_STYLE) @@ -64,6 +65,7 @@ def __init__(self, parent, value): bSizer1.Add(bSizer2, 0, wx.ALL, 10) self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER) + Themes.styleInput(self.input) self.input.SetValue(str(value)) bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15) diff --git a/gui/builtinContextMenus/fitPilotSecurity.py b/gui/builtinContextMenus/fitPilotSecurity.py index 87b5d310d0..e1571d2e29 100644 --- a/gui/builtinContextMenus/fitPilotSecurity.py +++ b/gui/builtinContextMenus/fitPilotSecurity.py @@ -5,6 +5,7 @@ import gui.fitCommands as cmd import gui.mainFrame from gui.contextMenu import ContextMenuUnconditional +from gui.utils.themes import ThemedDialog, Themes from service.fit import Fit _t = wx.GetTranslation @@ -101,7 +102,7 @@ def handleModeCustom(self, event): FitPilotSecurityMenu.register() -class SecStatusChanger(wx.Dialog): +class SecStatusChanger(ThemedDialog): def __init__(self, parent, value): super().__init__(parent, title=_t('Change Security Status'), style=wx.DEFAULT_DIALOG_STYLE) @@ -116,6 +117,7 @@ def __init__(self, parent, value): bSizer1.Add(bSizer2, 0, wx.ALL, 10) self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER) + Themes.styleInput(self.input) if value is None: value = '0.0' else: diff --git a/gui/builtinContextMenus/implantSetSave.py b/gui/builtinContextMenus/implantSetSave.py index 4f96286473..88e7d80681 100644 --- a/gui/builtinContextMenus/implantSetSave.py +++ b/gui/builtinContextMenus/implantSetSave.py @@ -2,6 +2,7 @@ import gui.mainFrame from gui.contextMenu import ContextMenuUnconditional +from gui.utils.themes import ThemedDialog, Themes from service.fit import Fit _t = wx.GetTranslation @@ -39,7 +40,7 @@ def activate(self, callingWindow, fullContext, i): ImplantSetSave.register() -class NameDialog(wx.Dialog): +class NameDialog(ThemedDialog): def __init__(self, parent, value): super().__init__(parent, title=_t('New Implant Set'), style=wx.DEFAULT_DIALOG_STYLE) @@ -54,6 +55,7 @@ def __init__(self, parent, value): bSizer1.Add(bSizer2, 0, wx.ALL, 10) self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER) + Themes.styleInput(self.input) if value is None: value = '' else: diff --git a/gui/builtinContextMenus/itemAmountChange.py b/gui/builtinContextMenus/itemAmountChange.py index 71482d919b..3090b5f3bc 100644 --- a/gui/builtinContextMenus/itemAmountChange.py +++ b/gui/builtinContextMenus/itemAmountChange.py @@ -9,6 +9,7 @@ from eos.saveddata.fighter import Fighter as es_Fighter from eos.saveddata.fit import Fit as es_Fit from gui.contextMenu import ContextMenuCombined +from gui.utils.themes import ThemedDialog, Themes from service.fit import Fit # noinspection PyPackageRequirements @@ -92,7 +93,7 @@ def activate(self, callingWindow, fullContext, mainItem, selection, i): ChangeItemAmount.register() -class AmountChanger(wx.Dialog): +class AmountChanger(ThemedDialog): def __init__(self, parent, value, limits=None): super().__init__(parent, title=_t("Change Amount"), style=wx.DEFAULT_DIALOG_STYLE) @@ -107,6 +108,7 @@ def __init__(self, parent, value, limits=None): bSizer1.Add(bSizer2, 0, wx.ALL, 10) self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER) + Themes.styleInput(self.input) self.input.SetValue(str(value)) bSizer1.Add(self.input, 0, wx.LEFT | wx.RIGHT | wx.EXPAND, 15) diff --git a/gui/builtinContextMenus/itemProjectionRange.py b/gui/builtinContextMenus/itemProjectionRange.py index 0854d0cb76..975d0b2dbe 100644 --- a/gui/builtinContextMenus/itemProjectionRange.py +++ b/gui/builtinContextMenus/itemProjectionRange.py @@ -9,6 +9,7 @@ from eos.saveddata.module import Module as EosModule from gui.contextMenu import ContextMenuCombined from gui.fitCommands.helpers import getSimilarFighters, getSimilarModPositions +from gui.utils.themes import ThemedDialog, Themes from service.fit import Fit # noinspection PyPackageRequirements @@ -72,7 +73,7 @@ def activate(self, callingWindow, fullContext, mainItem, selection, i): ChangeItemProjectionRange.register() -class RangeChanger(wx.Dialog): +class RangeChanger(ThemedDialog): def __init__(self, parent, value): super().__init__(parent, title='Change Projection Range', style=wx.DEFAULT_DIALOG_STYLE) @@ -87,6 +88,7 @@ def __init__(self, parent, value): bSizer1.Add(bSizer2, 0, wx.ALL, 10) self.input = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER) + Themes.styleInput(self.input) if value is None: value = '' else: diff --git a/gui/builtinItemStatsViews/itemAffectedBy.py b/gui/builtinItemStatsViews/itemAffectedBy.py index 8d8075f067..60c20a24a7 100644 --- a/gui/builtinItemStatsViews/itemAffectedBy.py +++ b/gui/builtinItemStatsViews/itemAffectedBy.py @@ -16,6 +16,7 @@ import gui.mainFrame from gui.contextMenu import ContextMenu from gui.bitmap_loader import BitmapLoader +from gui.utils.themes import Themes _t = wx.GetTranslation @@ -41,7 +42,7 @@ class ItemAffectedBy(wx.Panel): def __init__(self, parent, stuff, item): wx.Panel.__init__(self, parent) - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) + self.SetBackgroundColour(Themes.buttonFace()) self.stuff = stuff self.item = item @@ -56,7 +57,7 @@ def __init__(self, parent, stuff, item): mainSizer = wx.BoxSizer(wx.VERTICAL) self.affectedBy = wx.TreeCtrl(self, style=wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT | wx.NO_BORDER) - self.affectedBy.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + self.affectedBy.SetBackgroundColour(Themes.windowBackground()) mainSizer.Add(self.affectedBy, 1, wx.ALL | wx.EXPAND, 0) self.m_staticline = wx.StaticLine(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LI_HORIZONTAL) diff --git a/gui/builtinItemStatsViews/itemAttributes.py b/gui/builtinItemStatsViews/itemAttributes.py index ad0e78e1df..0c5f2ca592 100644 --- a/gui/builtinItemStatsViews/itemAttributes.py +++ b/gui/builtinItemStatsViews/itemAttributes.py @@ -10,6 +10,7 @@ from gui import globalEvents as GE from gui.bitmap_loader import BitmapLoader from gui.builtinItemStatsViews.attributeGrouping import * +from gui.utils.themes import Themes from gui.utils.numberFormatter import formatAmount, roundDec from service.const import GuiAttrGroup @@ -26,7 +27,8 @@ class ItemParams(wx.Panel): def __init__(self, parent, stuff, item, context=None): # Had to manually set the size here, otherwise column widths couldn't be calculated correctly. See #1878 wx.Panel.__init__(self, parent, size=(1000, 1000)) - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) + self.SetBackgroundColour(Themes.buttonFace()) + self.SetForegroundColour(Themes.text()) self.mainFrame = gui.mainFrame.MainFrame.getInstance() @@ -34,7 +36,7 @@ def __init__(self, parent, stuff, item, context=None): self.paramList = wx.lib.agw.hypertreelist.HyperTreeList(self, wx.ID_ANY, agwStyle=wx.TR_HIDE_ROOT | wx.TR_NO_LINES | wx.TR_FULL_ROW_HIGHLIGHT | wx.TR_HAS_BUTTONS) - self.paramList.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + self.paramList.SetBackgroundColour(Themes.windowBackground()) mainSizer.Add(self.paramList, 1, wx.ALL | wx.EXPAND, 0) self.SetSizer(mainSizer) @@ -209,7 +211,7 @@ def AddAttribute(self, parent, attr): attrIcon, attrName, currentVal, baseVal = data attr_item = self.paramList.AppendItem(parent, attrName) - self.paramList.SetItemTextColour(attr_item, wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)) + self.paramList.SetItemTextColour(attr_item, Themes.text()) self.paramList.SetItemText(attr_item, currentVal, 1) if self.stuff is not None: @@ -238,7 +240,7 @@ def PopulateList(self): heading = data.get("label") header_item = self.paramList.AppendItem(root, heading) - self.paramList.SetItemTextColour(header_item, wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)) + self.paramList.SetItemTextColour(header_item, Themes.text()) for attr in data.get("attributes", []): # Attribute is a "grouped" attr (eg: damage, sensor strengths, etc). Automatically group these into a child item if attr in GroupedAttributes: @@ -249,7 +251,7 @@ def PopulateList(self): # create a child item with the groups label item = self.paramList.AppendItem(header_item, grouping[1]) - self.paramList.SetItemTextColour(item, wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)) + self.paramList.SetItemTextColour(item, Themes.text()) for attr2 in grouping[0]: # add each attribute in the group self.AddAttribute(item, attr2) @@ -274,7 +276,7 @@ def PopulateList(self): # get all attributes in group item = self.paramList.AppendItem(root, grouping[1]) - self.paramList.SetItemTextColour(item, wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)) + self.paramList.SetItemTextColour(item, Themes.text()) for attr2 in grouping[0]: self.AddAttribute(item, attr2) diff --git a/gui/builtinItemStatsViews/itemCompare.py b/gui/builtinItemStatsViews/itemCompare.py index 87346e4824..8fba21f803 100644 --- a/gui/builtinItemStatsViews/itemCompare.py +++ b/gui/builtinItemStatsViews/itemCompare.py @@ -2,6 +2,7 @@ import wx from .helpers import AutoListCtrl +from gui.utils.themes import Themes from service.price import Price as ServicePrice from service.market import Market from service.attribute import Attribute @@ -21,7 +22,8 @@ def __init__(self, parent, stuff, item, items, context=None): sPrice.getPrices(items, self.UpdateList, fetchTimeout=90) wx.Panel.__init__(self, parent) - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) + self.SetBackgroundColour(Themes.buttonFace()) + self.SetForegroundColour(Themes.text()) mainSizer = wx.BoxSizer(wx.VERTICAL) self.paramList = AutoListCtrl(self, wx.ID_ANY, diff --git a/gui/builtinItemStatsViews/itemDescription.py b/gui/builtinItemStatsViews/itemDescription.py index 50292e361a..c08e255e5f 100644 --- a/gui/builtinItemStatsViews/itemDescription.py +++ b/gui/builtinItemStatsViews/itemDescription.py @@ -4,6 +4,8 @@ import wx.html import re +from gui.utils.themes import Themes + _t = wx.GetTranslation @@ -13,8 +15,8 @@ def __init__(self, parent, stuff, item): mainSizer = wx.BoxSizer(wx.VERTICAL) self.SetSizer(mainSizer) - bgcolor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) - fgcolor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) + bgcolor = Themes.windowBackground() + fgcolor = Themes.text() self.description = wx.html.HtmlWindow(self) if not item.description: diff --git a/gui/builtinItemStatsViews/itemMutator.py b/gui/builtinItemStatsViews/itemMutator.py index a7424b0fc4..895eb9527c 100644 --- a/gui/builtinItemStatsViews/itemMutator.py +++ b/gui/builtinItemStatsViews/itemMutator.py @@ -8,6 +8,7 @@ import gui.globalEvents as GE import gui.mainFrame from gui.bitmap_loader import BitmapLoader +from gui.utils.themes import Themes from service.fit import Fit from .attributeSlider import AttributeSlider, EVT_VALUE_CHANGED from .itemAttributes import ItemParams @@ -21,7 +22,8 @@ class ItemMutatorPanel(wx.Panel): def __init__(self, parent, stuff): wx.Panel.__init__(self, parent) self.stuff = stuff - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) + self.SetBackgroundColour(Themes.buttonFace()) + self.SetForegroundColour(Themes.text()) mainSizer = wx.BoxSizer(wx.VERTICAL) @@ -72,7 +74,8 @@ def __init__(self, parent, stuff): self.SetScrollRate(0, 15) self.carryingFitID = gui.mainFrame.MainFrame.getInstance().getActiveFit() self.initialMutations = {} - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + self.SetBackgroundColour(Themes.windowBackground()) + self.SetForegroundColour(Themes.text()) self.stuff = stuff self.timer = None self.isModified = False diff --git a/gui/builtinItemStatsViews/itemTraits.py b/gui/builtinItemStatsViews/itemTraits.py index 6a09691e1d..b377e84618 100644 --- a/gui/builtinItemStatsViews/itemTraits.py +++ b/gui/builtinItemStatsViews/itemTraits.py @@ -3,6 +3,8 @@ # noinspection PyPackageRequirements import wx.html +from gui.utils.themes import Themes + _t = wx.GetTranslation @@ -14,8 +16,8 @@ def __init__(self, parent, stuff, item): self.traits = wx.html.HtmlWindow(self) - bgcolor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) - fgcolor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) + bgcolor = Themes.windowBackground() + fgcolor = Themes.text() self.traits.SetPage("{}".format( bgcolor.GetAsString(wx.C2S_HTML_SYNTAX), fgcolor.GetAsString(wx.C2S_HTML_SYNTAX), item.traits.display)) diff --git a/gui/builtinMarketBrowser/marketTree.py b/gui/builtinMarketBrowser/marketTree.py index 9c6fb73755..54ca397e69 100644 --- a/gui/builtinMarketBrowser/marketTree.py +++ b/gui/builtinMarketBrowser/marketTree.py @@ -2,6 +2,7 @@ from gui.cachingImageList import CachingImageList from gui.builtinMarketBrowser.events import RECENTLY_USED_MODULES, CHARGES_FOR_FIT +from gui.utils.themes import Themes from logbook import Logger @@ -13,6 +14,8 @@ class MarketTree(wx.TreeCtrl): def __init__(self, parent, marketBrowser): wx.TreeCtrl.__init__(self, parent, style=wx.TR_DEFAULT_STYLE | wx.TR_HIDE_ROOT) + self.SetBackgroundColour(Themes.listBackground()) + self.SetForegroundColour(Themes.text()) pyfalog.debug("Initialize marketTree") self.root = self.AddRoot("root") diff --git a/gui/builtinMarketBrowser/pfSearchBox.py b/gui/builtinMarketBrowser/pfSearchBox.py index 0cd8ea5569..1331633962 100644 --- a/gui/builtinMarketBrowser/pfSearchBox.py +++ b/gui/builtinMarketBrowser/pfSearchBox.py @@ -2,6 +2,7 @@ import wx import gui.utils.color as colorUtils import gui.utils.draw as drawUtils +from gui.utils.themes import Themes from gui.utils.helpers_wxPython import HandleCtrlBackspace SearchButton, EVT_SEARCH_BTN = wx.lib.newevent.NewEvent() @@ -44,6 +45,7 @@ def __init__(self, parent, id=wx.ID_ANY, value="", pos=wx.DefaultPosition, size= self.EditBox = wx.TextCtrl(self, wx.ID_ANY, "", wx.DefaultPosition, (-1, h - 2 if 'wxGTK' in wx.PlatformInfo else -1), wx.TE_PROCESS_ENTER | (wx.BORDER_NONE if 'wxGTK' in wx.PlatformInfo else 0)) + Themes.styleInput(self.EditBox) self.Bind(wx.EVT_PAINT, self.OnPaint) self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBk) @@ -235,7 +237,7 @@ def UpdateElementsPos(self, dc): def OnPaint(self, event): dc = wx.AutoBufferedPaintDC(self) - bkColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + bkColor = Themes.windowBackground() sepColor = colorUtils.GetSuitable(bkColor, 0.2) rect = self.GetRect() diff --git a/gui/builtinPreferenceViews/pyfaDatabasePreferences.py b/gui/builtinPreferenceViews/pyfaDatabasePreferences.py index 980b0bf655..5ecad15513 100644 --- a/gui/builtinPreferenceViews/pyfaDatabasePreferences.py +++ b/gui/builtinPreferenceViews/pyfaDatabasePreferences.py @@ -5,6 +5,7 @@ from gui.bitmap_loader import BitmapLoader from gui.preferenceView import PreferenceView from gui.utils import helpers_wxPython as wxHelpers +from gui.utils.themes import Themes _t = wx.GetTranslation @@ -40,7 +41,7 @@ def populatePanel(self, panel): mainSizer.Add(self.stSetUserPath, 0, wx.ALL, 5) self.inputUserPath = wx.TextCtrl(panel, wx.ID_ANY, config.savePath, wx.DefaultPosition, wx.DefaultSize, 0) self.inputUserPath.SetEditable(False) - self.inputUserPath.SetBackgroundColour((200, 200, 200)) + Themes.styleInput(self.inputUserPath, disabled=True) mainSizer.Add(self.inputUserPath, 0, wx.ALL | wx.EXPAND, 5) # Save DB @@ -50,7 +51,7 @@ def populatePanel(self, panel): self.inputFitDB = wx.TextCtrl(panel, wx.ID_ANY, config.saveDB, wx.DefaultPosition, wx.DefaultSize, 0) self.inputFitDB.SetEditable(False) - self.inputFitDB.SetBackgroundColour((200, 200, 200)) + Themes.styleInput(self.inputFitDB, disabled=True) mainSizer.Add(self.inputFitDB, 0, wx.ALL | wx.EXPAND, 5) # Game Data DB @@ -60,7 +61,7 @@ def populatePanel(self, panel): self.inputGameDB = wx.TextCtrl(panel, wx.ID_ANY, config.gameDB, wx.DefaultPosition, wx.DefaultSize, 0) self.inputGameDB.SetEditable(False) - self.inputGameDB.SetBackgroundColour((200, 200, 200)) + Themes.styleInput(self.inputGameDB, disabled=True) mainSizer.Add(self.inputGameDB, 0, wx.ALL | wx.EXPAND, 5) self.cbsaveInRoot.SetValue(config.saveInRoot) diff --git a/gui/builtinPreferenceViews/pyfaEsiPreferences.py b/gui/builtinPreferenceViews/pyfaEsiPreferences.py index ba327a8d6f..e10b26ec41 100644 --- a/gui/builtinPreferenceViews/pyfaEsiPreferences.py +++ b/gui/builtinPreferenceViews/pyfaEsiPreferences.py @@ -5,6 +5,7 @@ import gui.mainFrame from gui.bitmap_loader import BitmapLoader from gui.preferenceView import PreferenceView +from gui.utils.themes import Themes from service.esi import Esi from service.settings import EsiSettings @@ -67,6 +68,7 @@ def populatePanel(self, panel): self.esiServer.SetToolTip(wx.ToolTip(_t('The source you choose will be used on connection.'))) self.chESIserver = wx.Choice(panel, choices=list(self.settings.keys())) + Themes.styleInput(self.chESIserver) self.chESIserver.SetStringSelection(self.settings.get("server")) diff --git a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py index 40c17083b2..84b4f96f34 100644 --- a/gui/builtinPreferenceViews/pyfaGeneralPreferences.py +++ b/gui/builtinPreferenceViews/pyfaGeneralPreferences.py @@ -5,8 +5,9 @@ import gui.mainFrame from gui.bitmap_loader import BitmapLoader from gui.preferenceView import PreferenceView +from gui.utils.themes import Themes from service.fit import Fit -from service.settings import SettingsProvider, LocaleSettings +from service.settings import SettingsProvider, LocaleSettings, ThemeSettings import eos.config import wx.lib.agw.hyperlink as hl @@ -54,6 +55,7 @@ def langDisplay(langInfo): return langInfo.Description + progress_display self.chLang = wx.Choice(panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, [langDisplay(x) for x in self.langChoices], 0) + Themes.styleInput(self.chLang) self.chLang.Bind(wx.EVT_CHOICE, self.onLangSelection) selectedIndex = self.langChoices.index(next((x for x in self.langChoices if x.CanonicalName == self.localeSettings.get('locale')), None)) @@ -72,7 +74,6 @@ def langDisplay(langInfo): langBox.Add(langSizer) eosLangSizer = wx.BoxSizer(wx.HORIZONTAL) - self.stEosLangLabel = wx.StaticText(panel, wx.ID_ANY, _t("EVE Data:"), wx.DefaultPosition, wx.DefaultSize, 0) self.stEosLangLabel.Wrap(-1) eosLangSizer.Add(self.stEosLangLabel, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) @@ -81,6 +82,7 @@ def langDisplay(langInfo): sorted([(wx.Locale.FindLanguageInfo(x).Description, x) for x in eos.config.translation_mapping.keys()], key=lambda x: x[0]) self.chEosLang = wx.Choice(panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, [x[0] for x in self.eosLangChoices], 0) + Themes.styleInput(self.chEosLang) self.chEosLang.Bind(wx.EVT_CHOICE, self.onEosLangSelection) selectedIndex = self.eosLangChoices.index( @@ -95,6 +97,24 @@ def langDisplay(langInfo): wx.DefaultPosition, wx.DefaultSize, 0), 0, wx.LEFT, 15) + self.themeSettings = ThemeSettings.getInstance() + themeBox = wx.StaticBoxSizer(wx.VERTICAL, panel, _t("Theme (requires restart)")) + mainSizer.Add(themeBox, 0, wx.EXPAND | wx.TOP | wx.RIGHT | wx.BOTTOM, 10) + + themeSizer = wx.BoxSizer(wx.HORIZONTAL) + self.stThemeLabel = wx.StaticText(panel, wx.ID_ANY, _t("Appearance:"), wx.DefaultPosition, wx.DefaultSize, 0) + self.stThemeLabel.Wrap(-1) + themeSizer.Add(self.stThemeLabel, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + + self.themeChoices = [_t("System Default"), _t("Pyfa Dark"), _t("Pyfa Light")] + self.chTheme = wx.Choice(panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, self.themeChoices, 0) + Themes.styleInput(self.chTheme) + self.chTheme.SetSelection(self.themeSettings.get('theme_mode')) + self.chTheme.Bind(wx.EVT_CHOICE, self.onThemeSelection) + themeSizer.Add(self.chTheme, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) + + themeBox.Add(themeSizer) + self.cbGlobalChar = wx.CheckBox(panel, wx.ID_ANY, _t("Use global character"), wx.DefaultPosition, wx.DefaultSize, 0) mainSizer.Add(self.cbGlobalChar, 0, wx.ALL | wx.EXPAND, 5) @@ -210,6 +230,27 @@ def onEosLangSelection(self, event): locale = self.eosLangChoices[selection] self.localeSettings.set('eos_locale', locale[1]) + def onThemeSelection(self, event): + selection = self.chTheme.GetSelection() + self.themeSettings.set('theme_mode', selection) + dlg = wx.MessageDialog( + self.mainFrame, + _t("Theme changed. Close pyfa now to apply?"), + _t("Restart Required"), + wx.YES_NO | wx.ICON_QUESTION + ) + result = dlg.ShowModal() + dlg.Destroy() + if result == wx.ID_YES: + # Save settings explicitly before closing + SettingsProvider.getInstance().saveAll() + # Find the PreferenceDialog parent and close it first + # to avoid hanging (can't close mainFrame while modal is open) + prefDialog = self.chTheme.GetTopLevelParent() + if prefDialog and hasattr(prefDialog, 'EndModal'): + prefDialog.EndModal(wx.ID_OK) + wx.CallAfter(self.mainFrame.Close) + def onCBGlobalColorBySlot(self, event): # todo: maybe create a SettingChanged event that we can fire, and have other things hook into, instead of having the preference panel itself handle the # updating of things related to settings. diff --git a/gui/builtinPreferenceViews/pyfaLoggingPreferences.py b/gui/builtinPreferenceViews/pyfaLoggingPreferences.py index a89d788cce..e74dcd00cd 100644 --- a/gui/builtinPreferenceViews/pyfaLoggingPreferences.py +++ b/gui/builtinPreferenceViews/pyfaLoggingPreferences.py @@ -2,6 +2,7 @@ from gui.preferenceView import PreferenceView from gui.bitmap_loader import BitmapLoader +from gui.utils.themes import Themes import config from logbook import Logger @@ -40,7 +41,7 @@ def populatePanel(self, panel): mainSizer.Add(self.stLogPath, 0, wx.ALL, 5) self.inputLogPath = wx.TextCtrl(panel, wx.ID_ANY, config.logPath, wx.DefaultPosition, wx.DefaultSize, 0) self.inputLogPath.SetEditable(False) - self.inputLogPath.SetBackgroundColour((200, 200, 200)) + Themes.styleInput(self.inputLogPath, disabled=True) mainSizer.Add(self.inputLogPath, 0, wx.ALL | wx.EXPAND, 5) import requests @@ -49,7 +50,7 @@ def populatePanel(self, panel): mainSizer.Add(self.certPath, 0, wx.ALL, 5) self.certPathCtrl = wx.TextCtrl(panel, wx.ID_ANY, requests.certs.where(), wx.DefaultPosition, wx.DefaultSize, 0) self.certPathCtrl.SetEditable(False) - self.certPathCtrl.SetBackgroundColour((200, 200, 200)) + Themes.styleInput(self.certPathCtrl, disabled=True) mainSizer.Add(self.certPathCtrl, 0, wx.ALL | wx.EXPAND, 5) # Debug Logging diff --git a/gui/builtinPreferenceViews/pyfaMarketPreferences.py b/gui/builtinPreferenceViews/pyfaMarketPreferences.py index 5f407e92ba..42a7c99684 100644 --- a/gui/builtinPreferenceViews/pyfaMarketPreferences.py +++ b/gui/builtinPreferenceViews/pyfaMarketPreferences.py @@ -4,6 +4,7 @@ from gui.preferenceView import PreferenceView from gui.bitmap_loader import BitmapLoader +from gui.utils.themes import Themes import gui.mainFrame import gui.globalEvents as GE @@ -44,6 +45,7 @@ def populatePanel(self, panel): _t('The delay between a keystroke and the market search. Can help reduce lag when typing fast in the market search box.'))) delayTimer.Add(self.stMarketDelay, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) self.intDelay = IntCtrl(panel, max=1000, limited=True) + Themes.styleInput(self.intDelay) delayTimer.Add(self.intDelay, 0, wx.ALL, 5) mainSizer.Add(delayTimer, 0, wx.EXPAND | wx.TOP | wx.RIGHT, 10) self.intDelay.SetValue(self.sFit.serviceFittingOptions["marketSearchDelay"]) @@ -65,6 +67,8 @@ def populatePanel(self, panel): 'The system you choose will also be tried first, and if no data is available, global price will be used.'))) self.chPriceSource = wx.Choice(panel, choices=sorted(Price.sources.keys())) self.chPriceSystem = wx.Choice(panel, choices=list(Price.systemsList.keys())) + Themes.styleInput(self.chPriceSource) + Themes.styleInput(self.chPriceSystem) priceSizer.Add(self.chPriceSource, 1, wx.ALL | wx.EXPAND, 5) priceSizer.Add(self.chPriceSystem, 1, wx.ALL | wx.EXPAND, 5) mainSizer.Add(priceSizer, 0, wx.EXPAND | wx.TOP | wx.RIGHT, 10) diff --git a/gui/builtinPreferenceViews/pyfaNetworkPreferences.py b/gui/builtinPreferenceViews/pyfaNetworkPreferences.py index b1ce3f80ab..27ab9807a1 100644 --- a/gui/builtinPreferenceViews/pyfaNetworkPreferences.py +++ b/gui/builtinPreferenceViews/pyfaNetworkPreferences.py @@ -3,6 +3,7 @@ from gui.preferenceView import PreferenceView from gui.bitmap_loader import BitmapLoader +from gui.utils.themes import Themes import gui.mainFrame from service.settings import NetworkSettings @@ -86,6 +87,7 @@ def populatePanel(self, panel): self.chProxyTypeChoices = [_t("No proxy"), _t("Auto-detected proxy settings"), _t("Manual proxy settings")] self.chProxyType = wx.Choice(panel, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, self.chProxyTypeChoices, 0) + Themes.styleInput(self.chProxyType) self.chProxyType.SetSelection(self.nMode) @@ -103,6 +105,7 @@ def populatePanel(self, panel): fgAddrSizer.Add(self.stPSetAddr, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) self.editProxySettingsAddr = wx.TextCtrl(panel, wx.ID_ANY, self.nAddr, wx.DefaultPosition, wx.DefaultSize, 0) + Themes.styleInput(self.editProxySettingsAddr) fgAddrSizer.Add(self.editProxySettingsAddr, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5) @@ -112,6 +115,7 @@ def populatePanel(self, panel): fgAddrSizer.Add(self.stPSetPort, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) self.editProxySettingsPort = wx.TextCtrl(panel, wx.ID_ANY, self.nPort, wx.DefaultPosition, wx.DefaultSize, 0) + Themes.styleInput(self.editProxySettingsPort) fgAddrSizer.Add(self.editProxySettingsPort, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL | wx.EXPAND, 5) @@ -122,10 +126,12 @@ def populatePanel(self, panel): self.stPSetLogin.Wrap(-1) self.editProxySettingsLogin = wx.TextCtrl(panel, wx.ID_ANY, self.nAuth[0], wx.DefaultPosition, wx.DefaultSize, 0) + Themes.styleInput(self.editProxySettingsLogin) self.stPSetPassword = wx.StaticText(panel, wx.ID_ANY, _t("Password:"), wx.DefaultPosition, wx.DefaultSize, 0) self.stPSetPassword.Wrap(-1) self.editProxySettingsPassword = wx.TextCtrl(panel, wx.ID_ANY, self.nAuth[1], wx.DefaultPosition, wx.DefaultSize, wx.TE_PASSWORD) + Themes.styleInput(self.editProxySettingsPassword) pAuthSizer = wx.BoxSizer(wx.HORIZONTAL) pAuthSizer.Add(self.stPSetLogin, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) pAuthSizer.Add(self.editProxySettingsLogin, 0, wx.ALL | wx.ALIGN_CENTER_VERTICAL, 5) diff --git a/gui/builtinShipBrowser/categoryItem.py b/gui/builtinShipBrowser/categoryItem.py index 6267edb935..ce3cbb14b9 100644 --- a/gui/builtinShipBrowser/categoryItem.py +++ b/gui/builtinShipBrowser/categoryItem.py @@ -8,6 +8,7 @@ import gui.utils.draw as drawUtils import gui.utils.fonts as fonts from gui.bitmap_loader import BitmapLoader +from gui.utils.themes import Themes from .events import Stage2Selected pyfalog = Logger(__name__) @@ -100,7 +101,7 @@ def DrawItem(self, mdc): # rect = self.GetRect() self.UpdateElementsPos(mdc) - windowColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + windowColor = Themes.windowBackground() textColor = colorUtils.GetSuitable(windowColor, 1) mdc.SetTextForeground(textColor) diff --git a/gui/builtinShipBrowser/fitItem.py b/gui/builtinShipBrowser/fitItem.py index 3384fedd70..fdf61740f1 100644 --- a/gui/builtinShipBrowser/fitItem.py +++ b/gui/builtinShipBrowser/fitItem.py @@ -15,6 +15,7 @@ import gui.utils.draw as drawUtils import gui.utils.fonts as fonts from gui.bitmap_loader import BitmapLoader +from gui.utils.themes import Themes from gui.builtinShipBrowser.pfBitmapFrame import PFBitmapFrame from service.fit import Fit from .events import BoosterListUpdated, FitSelected, ImportSelected, SearchSelected, Stage3Selected @@ -421,7 +422,7 @@ def MouseMove(self, event): bmpWidth = self.toolbarx if self.toolbarx < 200 else 200 self.dragTLFBmp = wx.Bitmap(round(bmpWidth), round(self.GetRect().height)) tdc.SelectObject(self.dragTLFBmp) - tdc.SetBrush(wx.Brush(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))) + tdc.SetBrush(wx.Brush(Themes.windowBackground())) tdc.DrawRectangle(0, 0, bmpWidth, self.GetRect().height) self.DrawItem(tdc) tdc.SelectObject(wx.NullBitmap) @@ -488,7 +489,7 @@ def UpdateElementsPos(self, mdc): def DrawItem(self, mdc): rect = self.GetRect() - windowColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + windowColor = Themes.windowBackground() textColor = colorUtils.GetSuitable(windowColor, 1) mdc.SetTextForeground(textColor) @@ -572,7 +573,7 @@ def Refresh(self): def RenderBackground(self): rect = self.GetRect() - windowColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + windowColor = Themes.windowBackground() # activeFitID = self.mainFrame.getActiveFit() state = self.GetState() diff --git a/gui/builtinShipBrowser/navigationPanel.py b/gui/builtinShipBrowser/navigationPanel.py index f36311f74b..11c57c1c26 100644 --- a/gui/builtinShipBrowser/navigationPanel.py +++ b/gui/builtinShipBrowser/navigationPanel.py @@ -9,6 +9,7 @@ import gui.utils.draw as drawUtils import gui.utils.fonts as fonts from gui.bitmap_loader import BitmapLoader +from gui.utils.themes import Themes from gui.utils.helpers_wxPython import HandleCtrlBackspace from service.fit import Fit from utils.cjk import isStringCjk @@ -219,7 +220,7 @@ def UpdateElementsPos(self, mdc): def DrawItem(self, mdc): rect = self.GetRect() - windowColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + windowColor = Themes.windowBackground() textColor = colorUtils.GetSuitable(windowColor, 1) sepColor = colorUtils.GetSuitable(windowColor, 0.2) @@ -238,7 +239,7 @@ def DrawItem(self, mdc): def RenderBackground(self): rect = self.GetRect() - windowColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + windowColor = Themes.windowBackground() sFactor = 0.1 diff --git a/gui/builtinShipBrowser/pfListPane.py b/gui/builtinShipBrowser/pfListPane.py index faddcfecff..708982fe99 100644 --- a/gui/builtinShipBrowser/pfListPane.py +++ b/gui/builtinShipBrowser/pfListPane.py @@ -20,6 +20,8 @@ # noinspection PyPackageRequirements import wx +from gui.utils.themes import Themes + class PFListPane(wx.ScrolledWindow): @@ -30,7 +32,8 @@ def __init__(self, parent): self._wCount = 0 self.itemsHeight = 1 - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + self.SetBackgroundColour(Themes.windowBackground()) + self.SetForegroundColour(Themes.text()) self.SetVirtualSize((1, 1)) self.SetScrollRate(0, 1) diff --git a/gui/builtinShipBrowser/pfStaticText.py b/gui/builtinShipBrowser/pfStaticText.py index f5122ecfa4..c1beb90e9a 100644 --- a/gui/builtinShipBrowser/pfStaticText.py +++ b/gui/builtinShipBrowser/pfStaticText.py @@ -3,13 +3,16 @@ import wx from logbook import Logger +from gui.utils.themes import Themes + pyfalog = Logger(__name__) class PFStaticText(wx.Panel): def __init__(self, parent, label=wx.EmptyString): wx.Panel.__init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=parent.GetSize()) - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + self.SetBackgroundColour(Themes.windowBackground()) + self.SetForegroundColour(Themes.text()) mainSizer = wx.BoxSizer(wx.VERTICAL) text = wx.StaticText(self, wx.ID_ANY, label, wx.DefaultPosition, wx.DefaultSize, wx.ALIGN_CENTRE) diff --git a/gui/builtinShipBrowser/raceSelector.py b/gui/builtinShipBrowser/raceSelector.py index 228b02fe07..cc92434d68 100644 --- a/gui/builtinShipBrowser/raceSelector.py +++ b/gui/builtinShipBrowser/raceSelector.py @@ -6,6 +6,7 @@ import gui.utils.anim_effects as animEffects import gui.utils.color as colorUtils import gui.utils.draw as drawUtils +from gui.utils.themes import Themes from .events import Stage2Selected from gui.bitmap_loader import BitmapLoader @@ -61,7 +62,7 @@ def __init__(self, parent, id=wx.ID_ANY, label="", pos=wx.DefaultPosition, size= # Make the bitmaps have the same color as window text - sysTextColour = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) + sysTextColour = Themes.text() img = self.bmpArrow.ConvertToImage() if layout == wx.VERTICAL: @@ -168,7 +169,7 @@ def OnBackgroundErase(self, event): def OnPaint(self, event): rect = self.GetRect() - windowColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + windowColor = Themes.windowBackground() # bkColor = colorUtils.GetSuitable(windowColor, 0.1) sepColor = colorUtils.GetSuitable(windowColor, 0.2) diff --git a/gui/builtinShipBrowser/sfBrowserItem.py b/gui/builtinShipBrowser/sfBrowserItem.py index 40920eaa79..abae96539b 100644 --- a/gui/builtinShipBrowser/sfBrowserItem.py +++ b/gui/builtinShipBrowser/sfBrowserItem.py @@ -2,6 +2,7 @@ import wx import gui.utils.draw as drawUtils import gui.mainFrame +from gui.utils.themes import Themes SB_ITEM_NORMAL = 0 SB_ITEM_SELECTED = 1 @@ -405,7 +406,7 @@ def GetState(self): def RenderBackground(self): rect = self.GetRect() - windowColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + windowColor = Themes.windowBackground() state = self.GetState() diff --git a/gui/builtinShipBrowser/shipItem.py b/gui/builtinShipBrowser/shipItem.py index daff6b5d08..a211476f5e 100644 --- a/gui/builtinShipBrowser/shipItem.py +++ b/gui/builtinShipBrowser/shipItem.py @@ -9,6 +9,7 @@ import gui.utils.draw as drawUtils import gui.utils.fonts as fonts from gui.bitmap_loader import BitmapLoader +from gui.utils.themes import Themes from gui.contextMenu import ContextMenu from service.fit import Fit from service.market import Market @@ -233,7 +234,7 @@ def UpdateElementsPos(self, mdc): def DrawItem(self, mdc): # rect = self.GetRect() - windowColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + windowColor = Themes.windowBackground() textColor = colorUtils.GetSuitable(windowColor, 1) mdc.SetTextForeground(textColor) diff --git a/gui/builtinStatsViews/resourcesViewFull.py b/gui/builtinStatsViews/resourcesViewFull.py index 77d0b57553..b86ea2002e 100644 --- a/gui/builtinStatsViews/resourcesViewFull.py +++ b/gui/builtinStatsViews/resourcesViewFull.py @@ -25,6 +25,7 @@ import gui.mainFrame from gui.chrome_tabs import EVT_NOTEBOOK_PAGE_CHANGED from gui.utils import fonts +from gui.utils.themes import Themes from eos.const import FittingHardpoint @@ -304,7 +305,7 @@ def refreshPanel(self, fit): label.InvalidateBestSize() colorWarn = wx.Colour(204, 51, 51) - colorNormal = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) + colorNormal = Themes.text() if usedTurretHardpoints > totalTurretHardpoints: colorT = colorWarn diff --git a/gui/builtinViews/emptyView.py b/gui/builtinViews/emptyView.py index b6ddd94f68..1c5c0e22d9 100644 --- a/gui/builtinViews/emptyView.py +++ b/gui/builtinViews/emptyView.py @@ -3,6 +3,7 @@ import gui.globalEvents as GE from gui.chrome_tabs import EVT_NOTEBOOK_PAGE_CHANGED import gui.mainFrame +from gui.utils.themes import Themes class BlankPage(wx.Panel): @@ -13,7 +14,7 @@ def __init__(self, parent): self.parent = parent self.parent.Bind(EVT_NOTEBOOK_PAGE_CHANGED, self.pageChanged) - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + self.SetBackgroundColour(Themes.windowBackground()) wx.PostEvent(self.mainFrame, GE.FitChanged(fitIDs=())) diff --git a/gui/builtinViews/entityEditor.py b/gui/builtinViews/entityEditor.py index b0bd0d0d8d..78eab25408 100644 --- a/gui/builtinViews/entityEditor.py +++ b/gui/builtinViews/entityEditor.py @@ -1,6 +1,7 @@ # noinspection PyPackageRequirements import wx from gui.bitmap_loader import BitmapLoader +from gui.utils.themes import Themes class BaseValidator(wx.Validator): @@ -45,6 +46,7 @@ def __init__(self, parent, entityName, selected=None): self.choices = [] self.choices.sort(key=lambda p: p.name) self.entityChoices = wx.Choice(self, choices=[p.name for p in self.choices]) + Themes.styleInput(self.entityChoices) self.navSizer.Add(self.entityChoices, 1, wx.ALL, 5) buttons = (("new", wx.ART_NEW, self.OnNew), diff --git a/gui/builtinViews/fittingView.py b/gui/builtinViews/fittingView.py index 8df91591c7..ff5f839979 100644 --- a/gui/builtinViews/fittingView.py +++ b/gui/builtinViews/fittingView.py @@ -40,6 +40,7 @@ from gui.contextMenu import ContextMenu from gui.utils.staticHelpers import DragDropHelper from gui.utils.dark import isDark +from gui.utils.themes import Themes from service.fit import Fit from service.market import Market from config import slotColourMap, slotColourMapDark, errColor, errColorDark @@ -923,11 +924,11 @@ def MakeSnapshot(self, maxColumns=1337): mdc.SelectObject(mbmp) - mdc.SetBackground(wx.Brush(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))) + mdc.SetBackground(wx.Brush(Themes.windowBackground())) mdc.Clear() mdc.SetFont(self.font) - mdc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)) + mdc.SetTextForeground(Themes.text()) cx = padding for i, col in enumerate(self.activeColumns): diff --git a/gui/builtinViews/implantEditor.py b/gui/builtinViews/implantEditor.py index cf735fe8f6..9ed1bb6377 100644 --- a/gui/builtinViews/implantEditor.py +++ b/gui/builtinViews/implantEditor.py @@ -9,6 +9,7 @@ import gui.display as d from gui.bitmap_loader import BitmapLoader from gui.marketBrowser import SearchBox +from gui.utils.themes import Themes from service.market import Market @@ -32,7 +33,7 @@ def addMarketViewImage(self, iconFile): def __init__(self, parent): wx.Panel.__init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.TAB_TRAVERSAL) - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + self.SetBackgroundColour(Themes.windowBackground()) pmainSizer = wx.BoxSizer(wx.HORIZONTAL) diff --git a/gui/characterEditor.py b/gui/characterEditor.py index f9f307b134..35157a8817 100644 --- a/gui/characterEditor.py +++ b/gui/characterEditor.py @@ -40,6 +40,7 @@ from gui.builtinViews.implantEditor import BaseImplantEditorView from gui.contextMenu import ContextMenu from gui.utils.clipboard import fromClipboard, toClipboard +from gui.utils.themes import Themes, ThemedDialog from service.character import Character from service.esi import Esi from service.esiAccess import APIException @@ -297,13 +298,15 @@ def __init__(self, parent): wx.Panel.__init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.DefaultSize, style=wx.TAB_TRAVERSAL) self.charEditor = self.Parent.Parent # first parent is Notebook, second is Character Editor - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + self.SetBackgroundColour(Themes.windowBackground()) + self.SetForegroundColour(Themes.text()) pmainSizer = wx.BoxSizer(wx.VERTICAL) hSizer = wx.BoxSizer(wx.HORIZONTAL) self.clonesChoice = wx.Choice(self, wx.ID_ANY, style=0) + Themes.styleInput(self.clonesChoice) i = self.clonesChoice.Append("Omega Clone", None) self.clonesChoice.SetSelection(i) hSizer.Add(self.clonesChoice, 5, wx.ALL | wx.EXPAND, 5) @@ -780,7 +783,8 @@ def __init__(self, parent): wx.Panel.__init__(self, parent, id=wx.ID_ANY, pos=wx.DefaultPosition, size=wx.Size(500, 300), style=wx.TAB_TRAVERSAL) self.charEditor = self.Parent.Parent # first parent is Notebook, second is Character Editor - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + self.SetBackgroundColour(Themes.windowBackground()) + self.SetForegroundColour(Themes.text()) pmainSizer = wx.BoxSizer(wx.VERTICAL) @@ -806,6 +810,7 @@ def __init__(self, parent): fgSizerInput.Add(self.m_staticCharText, 0, wx.ALL | wx.ALIGN_RIGHT | wx.ALIGN_CENTER_VERTICAL, 10) self.charChoice = wx.Choice(self, wx.ID_ANY, style=0) + Themes.styleInput(self.charChoice) fgSizerInput.Add(self.charChoice, 1, wx.TOP | wx.BOTTOM | wx.EXPAND, 10) self.fetchButton = wx.Button(self, wx.ID_ANY, _t("Get Skills"), wx.DefaultPosition, wx.DefaultSize, 0) @@ -932,7 +937,7 @@ def fetchCallback(e=None): _t("Successfully fetched skills"), _t("Success"), wx.ICON_INFORMATION | wx.STAY_ON_TOP) -class SecStatusDialog(wx.Dialog): +class SecStatusDialog(ThemedDialog): def __init__(self, parent, sec): super().__init__(parent, title=_t("Set Security Status"), size=(300, 175), style=wx.DEFAULT_DIALOG_STYLE) diff --git a/gui/characterSelection.py b/gui/characterSelection.py index d4528d64d8..e5817c4506 100644 --- a/gui/characterSelection.py +++ b/gui/characterSelection.py @@ -30,6 +30,7 @@ import gui.mainFrame from gui.bitmap_loader import BitmapLoader from gui.utils.clipboard import toClipboard +from gui.utils.themes import Themes, ThemedPanel from service.character import Character from service.fit import Fit @@ -37,20 +38,23 @@ pyfalog = Logger(__name__) -class CharacterSelection(wx.Panel): +class CharacterSelection(ThemedPanel): def __init__(self, parent): self.mainFrame = gui.mainFrame.MainFrame.getInstance() - wx.Panel.__init__(self, parent) + super().__init__(parent) + mainSizer = wx.BoxSizer(wx.HORIZONTAL) self.SetSizer(mainSizer) - mainSizer.Add(wx.StaticText(self, wx.ID_ANY, _t("Character: ")), 0, wx.CENTER | wx.RIGHT | wx.LEFT, 3) + charLabel = wx.StaticText(self, wx.ID_ANY, _t("Character: ")) + mainSizer.Add(charLabel, 0, wx.CENTER | wx.RIGHT | wx.LEFT, 3) # cache current selection to fall back in case we choose to open char editor self.charCache = None self.charChoice = wx.Choice(self) + Themes.styleInput(self.charChoice) mainSizer.Add(self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT | wx.LEFT, 3) self.refreshCharacterList() diff --git a/gui/chrome_tabs.py b/gui/chrome_tabs.py index 3097f15c7f..736f132d13 100644 --- a/gui/chrome_tabs.py +++ b/gui/chrome_tabs.py @@ -22,6 +22,7 @@ from gui.bitmap_loader import BitmapLoader from gui.utils import color as color_utils, draw, fonts +from gui.utils.themes import Themes from service.fit import Fit _t = wx.GetTranslation @@ -118,7 +119,7 @@ def __init__(self, parent, can_add=True, tabWidthMode=0): else: style = wx.SIMPLE_BORDER - back_color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + back_color = Themes.windowBackground() content_sizer = wx.BoxSizer(wx.VERTICAL) self.page_container = wx.Panel(self, style=style) @@ -559,7 +560,7 @@ def InitTabRegions(self): def InitColors(self): """Determines colors used for tab, based on system settings""" - self.tab_color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DFACE) + self.tab_color = Themes.buttonFace() self.inactive_color = color_utils.GetSuitable(self.tab_color, 0.25) self.selected_color = color_utils.GetSuitable(self.tab_color, 0.10) @@ -1187,7 +1188,7 @@ def OnPaint(self, event): # from Carbon.Appearance import kThemeBrushDialogBackgroundActive # brush.MacSetTheme(kThemeBrushDialogBackgroundActive) # else: - color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_3DFACE) + color = Themes.buttonFace() brush = wx.Brush(color) if "wxGTK" not in wx.PlatformInfo: @@ -1504,7 +1505,7 @@ def OnWindowPaint(self, event): canvas = wx.Bitmap(round(rect.width), round(rect.height)) mdc = wx.BufferedPaintDC(self) mdc.SelectObject(canvas) - color = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + color = Themes.windowBackground() mdc.SetBackground(wx.Brush(color)) mdc.Clear() @@ -1513,10 +1514,10 @@ def OnWindowPaint(self, event): x, y = mdc.GetTextExtent(self.title) - mdc.SetBrush(wx.Brush(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT))) + mdc.SetBrush(wx.Brush(Themes.text())) mdc.DrawRectangle(0, 0, round(rect.width), 16) - mdc.SetTextForeground(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW)) + mdc.SetTextForeground(Themes.windowBackground()) mdc.DrawBitmap(self.bitmap, 0, 16) diff --git a/gui/copySelectDialog.py b/gui/copySelectDialog.py index 349d45b05e..867bb54549 100644 --- a/gui/copySelectDialog.py +++ b/gui/copySelectDialog.py @@ -25,6 +25,7 @@ from eos.db import getFit from gui.utils.clipboard import toClipboard +from gui.utils.themes import ThemedDialog from service.const import PortDnaOptions, PortEftOptions, PortMultiBuyOptions from service.port import EfsPort, Port from service.settings import SettingsProvider @@ -32,7 +33,7 @@ _t = wx.GetTranslation -class CopySelectDialog(wx.Dialog): +class CopySelectDialog(ThemedDialog): copyFormatEft = 0 copyFormatXml = 1 copyFormatDna = 2 diff --git a/gui/display.py b/gui/display.py index 907060f245..54587b24e1 100644 --- a/gui/display.py +++ b/gui/display.py @@ -22,6 +22,7 @@ import gui.mainFrame from gui.viewColumn import ViewColumn from gui.cachingImageList import CachingImageList +from gui.utils.themes import Themes class Display(wx.ListCtrl): @@ -32,6 +33,8 @@ def __init__(self, parent, size=wx.DefaultSize, style=0): wx.ListCtrl.__init__(self) self.EnableSystemTheme(False) self.Create(parent, size=size, style=wx.LC_REPORT | style) + self.SetBackgroundColour(Themes.listBackground()) + self.SetTextColour(Themes.text()) self.imageList = CachingImageList(16, 16) self.SetImageList(self.imageList, wx.IMAGE_LIST_SMALL) self.activeColumns = [] diff --git a/gui/esiFittings.py b/gui/esiFittings.py index 8baa8df000..d19f7ae78f 100644 --- a/gui/esiFittings.py +++ b/gui/esiFittings.py @@ -20,6 +20,7 @@ from service.port import Port from service.port.esi import ESIExportException from service.settings import EsiSettings +from gui.utils.themes import Themes _t = wx.GetTranslation @@ -39,6 +40,7 @@ def __init__(self, parent): characterSelectSizer = wx.BoxSizer(wx.HORIZONTAL) self.charChoice = wx.Choice(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, []) + Themes.styleInput(self.charChoice) characterSelectSizer.Add(self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) self.updateCharList() @@ -276,6 +278,7 @@ def __init__(self, parent): hSizer = wx.BoxSizer(wx.HORIZONTAL) self.charChoice = wx.Choice(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, []) + Themes.styleInput(self.charChoice) hSizer.Add(self.charChoice, 1, wx.ALIGN_CENTER_VERTICAL | wx.ALL, 5) self.updateCharList() self.charChoice.SetSelection(0) diff --git a/gui/fitBrowserLite.py b/gui/fitBrowserLite.py index db2f31a58b..8586e503bf 100644 --- a/gui/fitBrowserLite.py +++ b/gui/fitBrowserLite.py @@ -4,6 +4,7 @@ import wx import gui.display as d +from gui.utils.themes import ThemedDialog, Themes from service.fit import Fit _t = wx.GetTranslation @@ -12,7 +13,7 @@ def fitSorter(fit): return fit.shipName, fit.name -class FitBrowserLiteDialog(wx.Dialog): +class FitBrowserLiteDialog(ThemedDialog): def __init__(self, parent, title=_t('Add Fits'), excludedFitIDs=()): super().__init__(parent, title=title, style=wx.DEFAULT_DIALOG_STYLE) @@ -29,6 +30,7 @@ def __init__(self, parent, title=_t('Add Fits'), excludedFitIDs=()): searchSizer = wx.BoxSizer(wx.HORIZONTAL) self.searchBox = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PROCESS_ENTER) + Themes.styleInput(self.searchBox) searchSizer.Add(self.searchBox, 1, wx.EXPAND | wx.ALL, 5) mainSizer.Add(searchSizer, 0, wx.EXPAND | wx.ALL, 0) diff --git a/gui/mainFrame.py b/gui/mainFrame.py index 44bfc5c496..6820c4d6f6 100644 --- a/gui/mainFrame.py +++ b/gui/mainFrame.py @@ -58,6 +58,8 @@ from gui.setEditor import ImplantSetEditor from gui.shipBrowser import ShipBrowser from gui.statsPane import StatsPane +from gui.utils.themes import Themes +from gui.utils.dark import setDarkTitleBar from gui.targetProfileEditor import TargetProfileEditor from gui.updateDialog import UpdateDialog from gui.utils.clipboard import fromClipboard @@ -152,9 +154,12 @@ def __init__(self, title="pyfa"): self.disableOverrideEditor = disableOverrideEditor - # Fix for msw (have the frame background color match panel color - if 'wxMSW' in wx.PlatformInfo: - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) + # Set frame background color (supports dark mode) + self.SetBackgroundColour(Themes.buttonFace()) + self.SetForegroundColour(Themes.text()) + + # Enable dark title bar on Windows when in dark mode + setDarkTitleBar(self) # Load and set the icon for pyfa main window i = wx.Icon(BitmapLoader.getBitmap("pyfa", "gui")) @@ -483,8 +488,10 @@ def OnShowExportDialog(self, event): return def OnShowPreferenceDialog(self, event): - with PreferenceDialog(self) as dlg: - dlg.ShowModal() + dlg = PreferenceDialog(self) + dlg.ShowModal() + if self: # MainFrame still exists (wasn't closed from within the dialog) + dlg.Destroy() @staticmethod def goWiki(event): diff --git a/gui/marketBrowser.py b/gui/marketBrowser.py index e6956f9a16..809a2da1d3 100644 --- a/gui/marketBrowser.py +++ b/gui/marketBrowser.py @@ -24,6 +24,7 @@ from gui.builtinMarketBrowser.itemView import ItemView from gui.builtinMarketBrowser.metaButton import MetaButton from gui.builtinMarketBrowser.marketTree import MarketTree +from gui.utils.themes import ThemedPanel from service.market import Market from service.settings import MarketPriceSettings @@ -32,11 +33,11 @@ pyfalog = Logger(__name__) -class MarketBrowser(wx.Panel): +class MarketBrowser(ThemedPanel): def __init__(self, parent): - wx.Panel.__init__(self, parent) + super().__init__(parent) pyfalog.debug("Initialize marketBrowser") vbox = wx.BoxSizer(wx.VERTICAL) diff --git a/gui/preferenceDialog.py b/gui/preferenceDialog.py index 9d978427f5..1f0025ed55 100644 --- a/gui/preferenceDialog.py +++ b/gui/preferenceDialog.py @@ -21,10 +21,11 @@ import wx from gui.preferenceView import PreferenceView from gui.bitmap_loader import BitmapLoader +from gui.utils.themes import Themes, ThemedDialog _t = wx.GetTranslation -class PreferenceDialog(wx.Dialog): +class PreferenceDialog(ThemedDialog): def __init__(self, parent): super().__init__(parent, id=wx.ID_ANY, size=wx.DefaultSize, style=wx.DEFAULT_DIALOG_STYLE) @@ -36,6 +37,9 @@ def __init__(self, parent): self.listbook = wx.Listbook(self, wx.ID_ANY, wx.DefaultPosition, wx.DefaultSize, wx.LB_DEFAULT) self.listview = self.listbook.GetListView() + # Apply theme colors to the listview + self.listview.SetBackgroundColour(Themes.listBackground()) + self.listview.SetForegroundColour(Themes.text()) # self.listview.SetMinSize((500, -1)) # self.listview.SetSize((500, -1)) @@ -60,6 +64,9 @@ def __init__(self, parent): for prefView in PreferenceView.views: page = wx.ScrolledWindow(self.listbook) page.SetScrollRate(15, 15) + # Apply theme colors to the page + page.SetBackgroundColour(Themes.buttonFace()) + page.SetForegroundColour(Themes.text()) bmp = prefView.getImage() if bmp: imgID = self.imageList.Add(bmp) diff --git a/gui/pyfa_gauge.py b/gui/pyfa_gauge.py index f1d0ab114e..5a80adcb53 100644 --- a/gui/pyfa_gauge.py +++ b/gui/pyfa_gauge.py @@ -18,6 +18,7 @@ import wx from gui.utils import anim_effects, color as color_utils, draw +from gui.utils.themes import Themes _t = wx.GetTranslation @@ -68,7 +69,7 @@ def __init__(self, parent, font, max_range=100, size=(-1, 30), *args, self.font = font - self.SetBackgroundColour(wx.Colour(51, 51, 51)) + self.SetBackgroundColour(Themes.gaugeBackground()) self._tooltip = wx.ToolTip("0.00/100.00") self.SetToolTip(self._tooltip) diff --git a/gui/setEditor.py b/gui/setEditor.py index 96a6ba26ed..368c73ba87 100644 --- a/gui/setEditor.py +++ b/gui/setEditor.py @@ -25,6 +25,7 @@ from gui.builtinViews.entityEditor import BaseValidator, EntityEditor from gui.builtinViews.implantEditor import BaseImplantEditorView from gui.utils.clipboard import fromClipboard, toClipboard +from gui.utils.themes import Themes from service.implantSet import ImplantSets @@ -90,7 +91,7 @@ class ImplantSetEditorView(BaseImplantEditorView): def __init__(self, parent): BaseImplantEditorView.__init__(self, parent) if 'wxMSW' in wx.PlatformInfo: - self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE)) + self.SetBackgroundColour(Themes.buttonFace()) def bindContext(self): self.Parent.entityEditor.Bind(wx.EVT_CHOICE, self.contextChanged) diff --git a/gui/shipBrowser.py b/gui/shipBrowser.py index d5265e2020..32d29722eb 100644 --- a/gui/shipBrowser.py +++ b/gui/shipBrowser.py @@ -15,13 +15,14 @@ from gui.builtinShipBrowser.navigationPanel import NavigationPanel from gui.builtinShipBrowser.raceSelector import RaceSelector from gui.builtinShipBrowser.pfStaticText import PFStaticText +from gui.utils.themes import ThemedPanel pyfalog = Logger(__name__) -class ShipBrowser(wx.Panel): +class ShipBrowser(ThemedPanel): def __init__(self, parent): - wx.Panel.__init__(self, parent, style=0) + super().__init__(parent, style=0) self._lastWidth = 0 self._activeStage = 1 diff --git a/gui/ssoLogin.py b/gui/ssoLogin.py index 1ca41067b6..b93e108924 100644 --- a/gui/ssoLogin.py +++ b/gui/ssoLogin.py @@ -5,12 +5,13 @@ import config import time +from gui.utils.themes import ThemedDialog, Themes from service.settings import EsiSettings _t = wx.GetTranslation -class SsoLogin(wx.Dialog): +class SsoLogin(ThemedDialog): def __init__(self, server: config.ApiServer, start_local_server=True): self.mainFrame = gui.mainFrame.MainFrame.getInstance() @@ -37,6 +38,7 @@ def __init__(self, server: config.ApiServer, start_local_server=True): self.ssoInfoCtrl = wx.TextCtrl(self, wx.ID_ANY, wx.EmptyString, wx.DefaultPosition, (-1, -1), style=wx.TE_MULTILINE) self.ssoInfoCtrl.SetFont(wx.Font(8, wx.FONTFAMILY_TELETYPE, wx.NORMAL, wx.NORMAL)) + Themes.styleInput(self.ssoInfoCtrl) self.ssoInfoCtrl.Layout() self.ssoInfoCtrl.Bind(wx.EVT_TEXT, self.OnTextEnter) diff --git a/gui/statsPane.py b/gui/statsPane.py index 28d4a01554..6ae8f5b1a7 100644 --- a/gui/statsPane.py +++ b/gui/statsPane.py @@ -29,12 +29,13 @@ from gui.statsView import StatsView from gui.contextMenu import ContextMenu from gui.toggle_panel import TogglePanel +from gui.utils.themes import ThemedPanel from logbook import Logger pyfalog = Logger(__name__) -class StatsPane(wx.Panel): +class StatsPane(ThemedPanel): AVAILIBLE_VIEWS = [ "resources", "resistances", @@ -80,7 +81,7 @@ def fitChanged(self, event): view.refreshPanel(fit) def __init__(self, parent): - wx.Panel.__init__(self, parent) + super().__init__(parent) # Use 25% smaller fonts if MAC or force font size to 8 for msw/linux diff --git a/gui/toggle_panel.py b/gui/toggle_panel.py index 7c3b7449c1..6eea0e44b9 100644 --- a/gui/toggle_panel.py +++ b/gui/toggle_panel.py @@ -14,6 +14,8 @@ import wx +from gui.utils.themes import Themes + class TogglePanel(wx.Panel): def __init__(self, parent, force_layout=False, *args, **kargs): @@ -29,6 +31,8 @@ def __init__(self, parent, force_layout=False, *args, **kargs): # Create the header panel, set sizer, and add to main sizer self.header_panel = wx.Panel(self) + self.header_panel.SetBackgroundColour(Themes.buttonFace()) + self.header_panel.SetForegroundColour(Themes.text()) header_sizer = wx.BoxSizer(wx.HORIZONTAL) self.header_panel.SetSizer(header_sizer) @@ -53,6 +57,8 @@ def __init__(self, parent, force_layout=False, *args, **kargs): # Create the content panel, set sizer, and add to main sizer self.content_panel = wx.Panel(self) + self.content_panel.SetBackgroundColour(Themes.buttonFace()) + self.content_panel.SetForegroundColour(Themes.text()) self.content_sizer = wx.BoxSizer(wx.VERTICAL) self.content_panel.SetSizer(self.content_sizer) diff --git a/gui/updateDialog.py b/gui/updateDialog.py index fc465c4632..f97c8ad620 100644 --- a/gui/updateDialog.py +++ b/gui/updateDialog.py @@ -22,6 +22,7 @@ # noinspection PyPackageRequirements import dateutil.parser from service.settings import UpdateSettings as svc_UpdateSettings +from gui.utils.themes import ThemedDialog import wx.html2 import webbrowser import re @@ -47,7 +48,7 @@ """ -class UpdateDialog(wx.Dialog): +class UpdateDialog(ThemedDialog): def __init__(self, parent, release, version): super().__init__( diff --git a/gui/utils/anim.py b/gui/utils/anim.py index e2042be3a1..cf96947108 100644 --- a/gui/utils/anim.py +++ b/gui/utils/anim.py @@ -1,6 +1,7 @@ # noinspection PyPackageRequirements import wx import gui.utils.color as colorUtils +from gui.utils.themes import Themes, ThemedDialog class LoadAnimation(wx.Window): @@ -55,11 +56,11 @@ def OnEraseBackground(self, event): def OnPaint(self, event): rect = self.GetClientRect() dc = wx.AutoBufferedPaintDC(self) - windowColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + windowColor = Themes.windowBackground() dc.SetBackground(wx.Brush(windowColor)) dc.Clear() - barColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) + barColor = Themes.text() shadeColor = colorUtils.GetSuitable(barColor, 0.75) barWidth = rect.width / self.bars @@ -83,12 +84,12 @@ def OnPaint(self, event): dc.DrawRectangle(round(x), round(y), round(barWidth), round(bh)) x += barWidth - textColor = wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) + textColor = Themes.text() dc.SetTextForeground(textColor) dc.DrawLabel(self.label, rect, wx.ALIGN_CENTER) -class WaitDialog(wx.Dialog): +class WaitDialog(ThemedDialog): def __init__(self, parent, title="Processing"): super().__init__(parent, id=wx.ID_ANY, title=title, size=(300, 30), style=wx.NO_BORDER) diff --git a/gui/utils/dark.py b/gui/utils/dark.py index 0d2739f30b..81040dfb77 100644 --- a/gui/utils/dark.py +++ b/gui/utils/dark.py @@ -1,12 +1,180 @@ + +from logbook import Logger import wx +from service.settings import ThemeSettings -def isDark(): - if 'wxMSW' in wx.PlatformInfo: +pyfalog = Logger(__name__) + + +def _isSystemDark(): + """Check if the operating system is in dark mode.""" + try: + appearance = wx.SystemSettings.GetAppearance() + return appearance.IsDark() + except Exception: return False + + +def usePyfaDark(): + """ + Determine if the application should use Pyfa's custom dark colors. + + Theme behavior: + - System Default: + - macOS/Linux: Never use Pyfa Dark (use system colors) + - Windows: Use Pyfa Dark if system is in dark mode + - Light: Never use Pyfa Dark + - Pyfa Dark: Always use Pyfa Dark + + Returns True if Pyfa's custom dark colors should be applied. + """ try: - return wx.SystemSettings.GetAppearance().IsDark() - except (KeyboardInterrupt, SystemExit): - raise - except: + themeSettings = ThemeSettings.getInstance() + mode = themeSettings.get('theme_mode') + + if mode == ThemeSettings.THEME_DARK: + # "Pyfa Dark" selected - always use Pyfa dark colors + return True + elif mode == ThemeSettings.THEME_LIGHT: + # "Light" selected - never use Pyfa dark colors + return False + else: # THEME_SYSTEM - "System Default" + # On Windows: use Pyfa Dark if system is dark + # On macOS/Linux: never use Pyfa Dark (use system colors instead) + if wx.Platform == '__WXMSW__': + return _isSystemDark() + else: + return False + except Exception: return False + + +def useSystemColors(): + """ + Determine if the application should use system default colors. + + This is True on macOS/Linux when "System Default" is selected, + meaning we should not override any colors and let the OS handle theming. + + Returns True if system colors should be used without modification. + """ + try: + themeSettings = ThemeSettings.getInstance() + mode = themeSettings.get('theme_mode') + + if mode == ThemeSettings.THEME_SYSTEM: + # "System Default" on macOS/Linux means use system colors + if wx.Platform != '__WXMSW__': + return True + return False + except Exception: + return False + + +# Keep isDark as an alias for backward compatibility +def isDark(): + """ + Backward compatibility alias for usePyfaDark(). + """ + return usePyfaDark() + + +def enableWindowsDarkModeSupport(): + """ + Enable dark mode for the entire application on Windows 10/11. + This affects the menu bar, context menus, and other system UI elements. + Must be called early in app initialization, before windows are created. + """ + if not isDark(): + return + + if wx.Platform != "__WXMSW__": + return + + try: + import ctypes + + # SetPreferredAppMode constants + # 0 = Default, 1 = AllowDark, 2 = ForceDark, 3 = ForceLight + APPMODE_ALLOWDARK = 1 + APPMODE_FORCEDARK = 2 + + # Try to load uxtheme ordinal 135 (SetPreferredAppMode) - Windows 10 1903+ + uxtheme = ctypes.windll.uxtheme + + # SetPreferredAppMode is ordinal 135 + try: + SetPreferredAppMode = uxtheme[135] + SetPreferredAppMode.argtypes = [ctypes.c_int] + SetPreferredAppMode.restype = ctypes.c_int + SetPreferredAppMode(APPMODE_FORCEDARK) + pyfalog.debug("SetPreferredAppMode set to ForceDark") + except Exception: + # Try AllowDarkModeForApp (ordinal 135 on older builds, or different approach) + try: + AllowDarkModeForApp = uxtheme[135] + AllowDarkModeForApp.argtypes = [ctypes.c_bool] + AllowDarkModeForApp.restype = ctypes.c_bool + AllowDarkModeForApp(True) + pyfalog.debug("AllowDarkModeForApp enabled") + except Exception: + pass + + # FlushMenuThemes to apply immediately (ordinal 136) + try: + FlushMenuThemes = uxtheme[136] + FlushMenuThemes() + pyfalog.debug("FlushMenuThemes called") + except Exception: + pass + + except Exception as e: + pyfalog.debug("Could not set Windows dark mode: {}", str(e)) + + +def setDarkTitleBar(window): + """ + Enable dark title bar on Windows 10/11 when in dark mode. + This uses the Windows DWM API to set the immersive dark mode attribute. + """ + if not isDark(): + return + + if wx.Platform != "__WXMSW__": + return + + try: + import ctypes + + # Get the window handle + hwnd = window.GetHandle() + + # DWMWA_USE_IMMERSIVE_DARK_MODE = 20 (Windows 10 20H1+) + # For older Windows 10 builds, it was 19 + DWMWA_USE_IMMERSIVE_DARK_MODE = 20 + + dwmapi = ctypes.windll.dwmapi + + # Try with attribute 20 first (newer Windows) + value = ctypes.c_int(1) + result = dwmapi.DwmSetWindowAttribute( + hwnd, + DWMWA_USE_IMMERSIVE_DARK_MODE, + ctypes.byref(value), + ctypes.sizeof(value) + ) + + # If that failed, try with attribute 19 (older Windows 10) + if result != 0: + DWMWA_USE_IMMERSIVE_DARK_MODE = 19 + dwmapi.DwmSetWindowAttribute( + hwnd, + DWMWA_USE_IMMERSIVE_DARK_MODE, + ctypes.byref(value), + ctypes.sizeof(value) + ) + + pyfalog.debug("Dark title bar enabled for window") + except Exception as e: + pyfalog.debug("Could not set dark title bar: {}", str(e)) diff --git a/gui/utils/inputs.py b/gui/utils/inputs.py index c2beab89da..b88222909a 100644 --- a/gui/utils/inputs.py +++ b/gui/utils/inputs.py @@ -24,6 +24,7 @@ import wx from eos.utils.float import floatUnerr +from gui.utils.themes import Themes def valToStr(val): @@ -77,7 +78,7 @@ def ChangeValueFloat(self, value): def updateColor(self): if self.isValid(): - self.SetForegroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT)) + self.SetForegroundColour(Themes.text()) else: self.SetForegroundColour(wx.RED) diff --git a/gui/utils/themes.py b/gui/utils/themes.py new file mode 100644 index 0000000000..e6fe21f84a --- /dev/null +++ b/gui/utils/themes.py @@ -0,0 +1,246 @@ +""" +Centralized color definitions for Pyfa dark/light themes. +All UI components should import colors from here. + +Color Logic: +- useSystemColors() True (macOS/Linux + System Default): Return wx.SystemSettings colors +- usePyfaDark() True (Pyfa Dark theme): Return Pyfa's custom dark colors +- Otherwise (Light theme): Return wx.SystemSettings colors +""" +import wx +from gui.utils.dark import usePyfaDark, useSystemColors + + +class Themes: + """Provides theme-aware colors for the application.""" + + # ========================================================================= + # Main Window Colors (override system colors for dark mode on Windows) + # ========================================================================= + + @staticmethod + def windowBackground(): + """Main window/panel background""" + if useSystemColors(): + return wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + if usePyfaDark(): + return wx.Colour(45, 45, 45) + return wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + + @staticmethod + def buttonFace(): + """Button/toolbar background""" + if useSystemColors(): + return wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE) + if usePyfaDark(): + return wx.Colour(55, 55, 55) + return wx.SystemSettings.GetColour(wx.SYS_COLOUR_BTNFACE) + + @staticmethod + def text(): + """Standard text color""" + if useSystemColors(): + return wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) + if usePyfaDark(): + return wx.Colour(220, 220, 220) + return wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOWTEXT) + + @staticmethod + def listBackground(): + """List/tree control background""" + if useSystemColors(): + return wx.SystemSettings.GetColour(wx.SYS_COLOUR_LISTBOX) + if usePyfaDark(): + return wx.Colour(35, 35, 35) + return wx.SystemSettings.GetColour(wx.SYS_COLOUR_LISTBOX) + + # ========================================================================= + # Custom Pyfa Colors (dark/light variants) + # ========================================================================= + + @staticmethod + def gaugeBackground(): + """Background color for gauge widgets - always dark grey (original Pyfa style)""" + return wx.Colour(51, 51, 51) + + @staticmethod + def errorBackground(): + """Background color for error states""" + if useSystemColors(): + return wx.Colour(204, 51, 51) + if usePyfaDark(): + return wx.Colour(70, 20, 20) + return wx.Colour(204, 51, 51) + + @staticmethod + def warningColor(): + """Warning/alert color""" + if useSystemColors(): + return wx.Colour(204, 51, 51) + if usePyfaDark(): + return wx.Colour(180, 80, 80) + return wx.Colour(204, 51, 51) + + @staticmethod + def inputBackground(): + """Background for editable input fields (TextCtrl, ComboBox, etc.)""" + if useSystemColors(): + return wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + if usePyfaDark(): + return wx.Colour(50, 50, 50) + return wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW) + + @staticmethod + def inputDisabledBackground(): + """Background for disabled/readonly input fields""" + if useSystemColors(): + return wx.Colour(240, 240, 240) + if usePyfaDark(): + return wx.Colour(45, 45, 45) + return wx.Colour(240, 240, 240) + + @staticmethod + def separatorColor(): + """Color for separators and dividers""" + if useSystemColors(): + return wx.Colour(200, 200, 200) + if usePyfaDark(): + return wx.Colour(80, 80, 80) + return wx.Colour(200, 200, 200) + + @staticmethod + def highlightBackground(): + """Background for highlighted/selected items""" + if useSystemColors(): + return wx.Colour(220, 220, 240) + if usePyfaDark(): + return wx.Colour(70, 70, 90) + return wx.Colour(220, 220, 240) + + @staticmethod + def styleInput(ctrl: wx.Control, disabled=False): + """ + Apply theme colors to an input control (TextCtrl, Choice, ComboBox, etc.). + Call this after creating the control. + + Note: On Windows with Pyfa Dark theme, native controls may not fully respect + SetBackgroundColour due to native theming. We disable the theme for this + control to allow custom background colors to work. + + When using system colors (macOS/Linux + System Default), we don't modify + the control as the OS handles theming. + """ + # When using system colors, let the OS handle everything + if useSystemColors(): + return + + if disabled: + bgColor = Themes.inputDisabledBackground() + else: + bgColor = Themes.inputBackground() + + fgColor = Themes.text() + + # On Windows, we need to disable the native theme to allow custom colors + if wx.Platform == '__WXMSW__': + try: + import ctypes + # SetWindowTheme with empty strings disables visual styles for the control + uxtheme = ctypes.windll.uxtheme + hwnd = ctrl.GetHandle() + uxtheme.SetWindowTheme(hwnd, "", "") + except Exception: + pass + + # Also try the wx method + if hasattr(ctrl, 'SetThemeEnabled'): + ctrl.SetThemeEnabled(False) + + # Apply the colors + ctrl.SetBackgroundColour(bgColor) + ctrl.SetForegroundColour(fgColor) + + # For TextCtrl, we also need to set the default style for text + if isinstance(ctrl, wx.TextCtrl): + textAttr = wx.TextAttr(fgColor, bgColor) + ctrl.SetDefaultStyle(textAttr) + + # Force a refresh to apply the new colors + ctrl.Refresh() + + +# ============================================================================= +# Themed Base Classes +# ============================================================================= + +class ThemedPanel(wx.Panel): + """ + A wx.Panel that automatically applies theme-aware colors. + Use this as a base class for panels that need dark mode support. + + By default uses buttonFace() for background. Override _getBackgroundColor() + to use a different color (e.g., windowBackground()). + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._applyThemeColors() + + def _getBackgroundColor(self): + """Override this to use a different background color.""" + return Themes.buttonFace() + + def _applyThemeColors(self): + """Apply theme colors to this panel.""" + self.SetBackgroundColour(self._getBackgroundColor()) + self.SetForegroundColour(Themes.text()) + + +class ThemedContentPanel(ThemedPanel): + """A panel using windowBackground() - suitable for content areas.""" + + def _getBackgroundColor(self): + return Themes.windowBackground() + + +class ThemedListPanel(ThemedPanel): + """A panel using listBackground() - suitable for list containers.""" + + def _getBackgroundColor(self): + return Themes.listBackground() + + +class ThemedFrame(wx.Frame): + """ + A wx.Frame that automatically applies theme-aware colors and dark title bar. + Use this as a base class for frames that need dark mode support. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._applyThemeColors() + + def _applyThemeColors(self): + """Apply theme colors and dark title bar to this frame.""" + from gui.utils.dark import setDarkTitleBar + self.SetBackgroundColour(Themes.buttonFace()) + self.SetForegroundColour(Themes.text()) + setDarkTitleBar(self) + + +class ThemedDialog(wx.Dialog): + """ + A wx.Dialog that automatically applies theme-aware colors and dark title bar. + Use this as a base class for dialogs that need dark mode support. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._applyThemeColors() + + def _applyThemeColors(self): + """Apply theme colors and dark title bar to this dialog.""" + from gui.utils.dark import setDarkTitleBar + self.SetBackgroundColour(Themes.buttonFace()) + self.SetForegroundColour(Themes.text()) + setDarkTitleBar(self) diff --git a/locale/en_US/LC_MESSAGES/lang.po b/locale/en_US/LC_MESSAGES/lang.po index 5c9019aff4..11dabf0e17 100644 --- a/locale/en_US/LC_MESSAGES/lang.po +++ b/locale/en_US/LC_MESSAGES/lang.po @@ -92,3 +92,23 @@ msgstr "Triglavian Invasion" #: gui/builtinContextMenus/envEffectAdd.py:122 msgid "ContextMenu|ProjectedEffectManipulation|Wolf Rayet" msgstr "Wolf Rayet" + +#: gui/builtinPreferenceViews/pyfaGeneralPreferences.py:101 +msgid "Theme (requires restart)" +msgstr "Theme (requires restart)" + +#: gui/builtinPreferenceViews/pyfaGeneralPreferences.py:105 +msgid "Appearance:" +msgstr "Appearance:" + +#: gui/builtinPreferenceViews/pyfaGeneralPreferences.py:109 +msgid "System Default" +msgstr "System Default" + +#: gui/builtinPreferenceViews/pyfaGeneralPreferences.py:109 +msgid "Pyfa Dark" +msgstr "Pyfa Dark" + +#: gui/builtinPreferenceViews/pyfaGeneralPreferences.py:109 +msgid "Pyfa Light" +msgstr "Pyfa Light" diff --git a/locale/lang.pot b/locale/lang.pot index 9ce4055148..15784462ac 100644 --- a/locale/lang.pot +++ b/locale/lang.pot @@ -4450,3 +4450,23 @@ msgstr[1] "" #, no-python-format, python-brace-format msgid "{}% chance to be jammed" msgstr "" + +#: gui/builtinPreferenceViews/pyfaGeneralPreferences.py:101 +msgid "Theme (requires restart)" +msgstr "" + +#: gui/builtinPreferenceViews/pyfaGeneralPreferences.py:105 +msgid "Appearance:" +msgstr "" + +#: gui/builtinPreferenceViews/pyfaGeneralPreferences.py:109 +msgid "System Default" +msgstr "" + +#: gui/builtinPreferenceViews/pyfaGeneralPreferences.py:109 +msgid "Pyfa Dark" +msgstr "" + +#: gui/builtinPreferenceViews/pyfaGeneralPreferences.py:109 +msgid "Pyfa Light" +msgstr "" \ No newline at end of file diff --git a/service/settings.py b/service/settings.py index e80b2f6ff6..b5e725f9ad 100644 --- a/service/settings.py +++ b/service/settings.py @@ -37,10 +37,9 @@ class SettingsProvider: - if config.savePath: - BASE_PATH = os.path.join(config.savePath, 'settings') settings = {} _instance = None + _base_path = None @classmethod def getInstance(cls): @@ -49,19 +48,25 @@ def getInstance(cls): return cls._instance + @property + def BASE_PATH(self): + """Lazily compute BASE_PATH to ensure config.savePath is available.""" + if SettingsProvider._base_path is None and config.savePath: + SettingsProvider._base_path = os.path.join(config.savePath, 'settings') + return SettingsProvider._base_path + def __init__(self): - if hasattr(self, 'BASE_PATH'): - if not os.path.exists(self.BASE_PATH): - os.mkdir(self.BASE_PATH) + if self.BASE_PATH and not os.path.exists(self.BASE_PATH): + os.mkdir(self.BASE_PATH) def getSettings(self, area, defaults=None): # type: (basestring, dict) -> service.Settings # NOTE: needed to change for tests # TODO: Write to memory with mmap -> https://docs.python.org/2/library/mmap.html settings_obj = self.settings.get(area) - if settings_obj is None: # and hasattr(self, 'BASE_PATH'): - canonical_path = os.path.join(self.BASE_PATH, area) if hasattr(self, 'BASE_PATH') else "" - if not os.path.exists(canonical_path): # path string or empty string. + if settings_obj is None: + canonical_path = os.path.join(self.BASE_PATH, area) if self.BASE_PATH else "" + if not canonical_path or not os.path.exists(canonical_path): info = {} if defaults: info.update(defaults) @@ -598,3 +603,39 @@ def set(self, key, value): if key == 'locale' and value not in self.supported_languages(): self.settings[key] = self.DEFAULT self.settings[key] = value + + +class ThemeSettings: + """ + Settings for application theme/appearance (dark mode). + + Theme Mode Behavior: + - THEME_SYSTEM (0): macOS/Linux use system colors; Windows uses Pyfa Dark if system is dark + - THEME_DARK (1): Always use Pyfa's custom dark theme on all platforms + - THEME_LIGHT (2): Always use light theme (system colors) on all platforms + """ + _instance = None + + # Theme mode constants - indices match the Choice dropdown in preferences + THEME_SYSTEM = 0 # Follow system preference (platform-specific behavior) + THEME_DARK = 1 # Always use Pyfa Dark theme + THEME_LIGHT = 2 # Always use light theme + + defaults = { + 'theme_mode': 0 # THEME_SYSTEM + } + + def __init__(self): + self.settings = SettingsProvider.getInstance().getSettings('pyfaThemeSettings', self.defaults) + + @classmethod + def getInstance(cls): + if cls._instance is None: + cls._instance = ThemeSettings() + return cls._instance + + def get(self, key): + return self.settings[key] + + def set(self, key, value): + self.settings[key] = value