Skip to content

Commit bf38b08

Browse files
committed
Update color mode handling for dark/light theme
1 parent 34d2f46 commit bf38b08

File tree

5 files changed

+114
-48
lines changed

5 files changed

+114
-48
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
11
# Changelog #
22

3+
## Version 3.5.4 ##
4+
5+
In this release, test coverage is 74%.
6+
7+
🛠️ Bug fixes:
8+
9+
* Improved dark/light mode theme update:
10+
* The theme mode may be changed during the application lifetime
11+
* Added methods `update_color_mode` on `CodeEditor` and `ConsoleBaseWidget` widgets
12+
313
## Version 3.5.3 ##
414

515
In this release, test coverage is 74%.

guidata/__init__.py

Lines changed: 4 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
and application development tools for Qt.
99
"""
1010

11-
__version__ = "3.5.3"
11+
__version__ = "3.5.4"
1212

1313

1414
# Dear (Debian, RPM, ...) package makers, please feel free to customize the
@@ -30,7 +30,9 @@ def qapplication():
3030
if not app:
3131
app = QApplication([])
3232
install_translator(app)
33-
set_color_mode(app)
33+
from guidata import qthelpers
34+
35+
qthelpers.set_color_mode()
3436
return app
3537

3638

@@ -53,39 +55,3 @@ def install_translator(qapp):
5355
break
5456
if QT_TRANSLATOR is not None:
5557
qapp.installTranslator(QT_TRANSLATOR)
56-
57-
58-
def set_color_mode(app):
59-
"""Set color mode (dark or light), depending on OS setting"""
60-
from qtpy.QtCore import Qt
61-
from qtpy.QtGui import QColor, QPalette
62-
from qtpy.QtWidgets import QStyleFactory
63-
64-
from guidata import qthelpers
65-
66-
if qthelpers.is_dark_mode():
67-
app.setStyle(QStyleFactory.create("Fusion"))
68-
dark_palette = QPalette()
69-
dark_color = QColor(50, 50, 50)
70-
disabled_color = QColor(127, 127, 127)
71-
dpsc = dark_palette.setColor
72-
dpsc(QPalette.Window, dark_color)
73-
dpsc(QPalette.WindowText, Qt.white)
74-
dpsc(QPalette.Base, QColor(31, 31, 31))
75-
dpsc(QPalette.AlternateBase, dark_color)
76-
dpsc(QPalette.ToolTipBase, Qt.white)
77-
dpsc(QPalette.ToolTipText, Qt.white)
78-
dpsc(QPalette.Text, Qt.white)
79-
dpsc(QPalette.Disabled, QPalette.Text, disabled_color)
80-
dpsc(QPalette.Button, dark_color)
81-
dpsc(QPalette.ButtonText, Qt.white)
82-
dpsc(QPalette.Disabled, QPalette.ButtonText, disabled_color)
83-
dpsc(QPalette.BrightText, Qt.red)
84-
dpsc(QPalette.Link, QColor(42, 130, 218))
85-
dpsc(QPalette.Highlight, QColor(42, 130, 218))
86-
dpsc(QPalette.HighlightedText, Qt.black)
87-
dpsc(QPalette.Disabled, QPalette.HighlightedText, disabled_color)
88-
app.setPalette(dark_palette)
89-
app.setStyleSheet(
90-
"QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }"
91-
)

guidata/qthelpers.py

Lines changed: 75 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,23 @@
7777
from collections.abc import Callable
7878

7979

80+
def set_dark_mode(state: bool) -> None:
81+
"""Set dark mode for Qt application
82+
83+
Args:
84+
state (bool): True to enable dark mode
85+
"""
86+
if state:
87+
os.environ["QT_COLOR_MODE"] = "dark"
88+
else:
89+
os.environ["QT_COLOR_MODE"] = "light"
90+
set_color_mode()
91+
92+
# Iterate over all top-level widgets:
93+
for widget in QW.QApplication.instance().topLevelWidgets():
94+
win32_fix_title_bar_background(widget)
95+
96+
8097
def is_dark_mode() -> bool:
8198
"""Return True if current color mode is dark mode
8299
@@ -89,13 +106,60 @@ def is_dark_mode() -> bool:
89106
return darkdetect.isDark()
90107

91108

109+
DEFAULT_STYLES = None
110+
111+
112+
def set_color_mode():
113+
"""Set color mode (dark or light), depending on OS setting or on the
114+
`QT_LIGHT_COLOR_MODE` environment variable."""
115+
global DEFAULT_STYLES
116+
117+
app = QW.QApplication.instance()
118+
119+
if DEFAULT_STYLES is None:
120+
# Store default palette to be able to restore it:
121+
DEFAULT_STYLES = app.style().objectName(), app.palette(), app.styleSheet()
122+
123+
if is_dark_mode():
124+
app.setStyle(QW.QStyleFactory.create("Fusion"))
125+
dark_palette = QG.QPalette()
126+
dark_color = QG.QColor(50, 50, 50)
127+
disabled_color = QG.QColor(127, 127, 127)
128+
dpsc = dark_palette.setColor
129+
dpsc(QG.QPalette.Window, dark_color)
130+
dpsc(QG.QPalette.WindowText, QC.Qt.white)
131+
dpsc(QG.QPalette.Base, QG.QColor(31, 31, 31))
132+
dpsc(QG.QPalette.AlternateBase, dark_color)
133+
dpsc(QG.QPalette.ToolTipBase, QC.Qt.white)
134+
dpsc(QG.QPalette.ToolTipText, QC.Qt.white)
135+
dpsc(QG.QPalette.Text, QC.Qt.white)
136+
dpsc(QG.QPalette.Disabled, QG.QPalette.Text, disabled_color)
137+
dpsc(QG.QPalette.Button, dark_color)
138+
dpsc(QG.QPalette.ButtonText, QC.Qt.white)
139+
dpsc(QG.QPalette.Disabled, QG.QPalette.ButtonText, disabled_color)
140+
dpsc(QG.QPalette.BrightText, QC.Qt.red)
141+
dpsc(QG.QPalette.Link, QG.QColor(42, 130, 218))
142+
dpsc(QG.QPalette.Highlight, QG.QColor(42, 130, 218))
143+
dpsc(QG.QPalette.HighlightedText, QC.Qt.black)
144+
dpsc(QG.QPalette.Disabled, QG.QPalette.HighlightedText, disabled_color)
145+
app.setPalette(dark_palette)
146+
app.setStyleSheet(
147+
"QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }"
148+
)
149+
elif DEFAULT_STYLES is not None:
150+
style, palette, stylesheet = DEFAULT_STYLES
151+
app.setStyle(QW.QStyleFactory.create(style))
152+
app.setPalette(palette)
153+
app.setStyleSheet(stylesheet)
154+
155+
92156
def win32_fix_title_bar_background(widget: QW.QWidget) -> None:
93157
"""Fix window title bar background for Windows 10+ dark theme
94158
95159
Args:
96160
widget (QW.QWidget): Widget to fix
97161
"""
98-
if os.name != "nt" or not is_dark_mode() or sys.maxsize == 2**31 - 1:
162+
if os.name != "nt" or sys.maxsize == 2**31 - 1:
99163
return
100164

101165
import ctypes
@@ -117,10 +181,18 @@ class WINDOWCOMPOSITIONATTRIBDATA(ctypes.Structure):
117181
]
118182

119183
accent = ACCENTPOLICY()
120-
accent.AccentState = 1 # Default window Blur #ACCENT_ENABLE_BLURBEHIND
184+
if is_dark_mode():
185+
accent.AccentState = 1 # Default window Blur #ACCENT_ENABLE_BLURBEHIND
186+
else:
187+
# TODO: find a way to restore the default behavior
188+
pass
121189

122190
data = WINDOWCOMPOSITIONATTRIBDATA()
123-
data.Attribute = 26 # WCA_USEDARKMODECOLORS
191+
if is_dark_mode():
192+
data.Attribute = 26 # WCA_USEDARKMODECOLORS
193+
else:
194+
# TODO: find a way to restore the default behavior
195+
pass
124196
data.SizeOfData = ctypes.sizeof(accent)
125197
data.Data = ctypes.cast(ctypes.pointer(accent), ctypes.POINTER(ctypes.c_int))
126198

guidata/widgets/codeeditor.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -130,13 +130,11 @@ def __init__(
130130
) -> None:
131131
QPlainTextEdit.__init__(self, parent)
132132

133-
win32_fix_title_bar_background(self)
134-
135133
self.visible_blocks = []
136134
self.highlighter = None
137135
self.normal_color = None
138136
self.sideareas_color = None
139-
self.linenumbers_color = QColor(Qt.lightGray if is_dark_mode() else Qt.darkGray)
137+
self.linenumbers_color = None
140138

141139
# Line number area management
142140
self.linenumbers_margin = True
@@ -171,6 +169,20 @@ def __init__(
171169
self.setFocusPolicy(Qt.StrongFocus)
172170
self.setup(language=language, font=font, columns=columns, rows=rows)
173171

172+
self.update_color_mode()
173+
174+
def update_color_mode(self) -> None:
175+
"""Update color mode according to the current theme"""
176+
win32_fix_title_bar_background(self)
177+
suffix = "dark" if is_dark_mode() else "light"
178+
color_scheme = CONF.get("color_schemes", "default/" + suffix)
179+
self.highlighter.set_color_scheme(color_scheme)
180+
self.highlighter.rehighlight()
181+
self.linenumbers_color = QColor(Qt.lightGray if is_dark_mode() else Qt.darkGray)
182+
self.normal_color = self.highlighter.get_foreground_color()
183+
self.sideareas_color = self.highlighter.get_sideareas_color()
184+
self.linenumberarea.update()
185+
174186
def text_has_changed(self) -> None:
175187
"""Text has changed: restart the timer to emit SIG_EDIT_STOPPED after a delay"""
176188
if self.timer.isActive():

guidata/widgets/console/base.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
# pylint: disable=R0911
1212
# pylint: disable=R0201
1313

14-
1514
import os
1615
import re
1716
import sys
@@ -42,6 +41,7 @@
4241

4342
from guidata.config import CONF
4443
from guidata.configtools import get_font, get_icon
44+
from guidata.qthelpers import is_dark_mode
4545
from guidata.widgets.console.calltip import CallTipWidget
4646
from guidata.widgets.console.mixins import BaseEditMixin
4747
from guidata.widgets.console.terminal import ANSIEscapeCodeHandler
@@ -367,8 +367,9 @@ def set_palette(self, background, foreground):
367367
background color and caret (text cursor) color
368368
"""
369369
palette = QPalette()
370-
palette.setColor(QPalette.Base, background)
370+
# palette.setColor(QPalette.Base, background)
371371
palette.setColor(QPalette.Text, foreground)
372+
palette.setColor(QPalette.Background, background)
372373
self.setPalette(palette)
373374

374375
# Set the right background color when changing color schemes
@@ -1528,6 +1529,11 @@ def __init__(self, parent=None):
15281529
self.set_pythonshell_font()
15291530
self.setMouseTracking(True)
15301531

1532+
def update_color_mode(self):
1533+
"""Update color mode according to the current theme"""
1534+
light_background = not is_dark_mode()
1535+
self.set_light_background(light_background)
1536+
15311537
def set_light_background(self, state):
15321538
"""
15331539
@@ -1540,7 +1546,7 @@ def set_light_background(self, state):
15401546
)
15411547
else:
15421548
self.set_palette(
1543-
background=QColor(Qt.black), foreground=QColor(Qt.lightGray)
1549+
background=QColor(50, 50, 50), foreground=QColor(Qt.lightGray)
15441550
)
15451551
self.ansi_handler.set_light_background(state)
15461552
self.set_pythonshell_font()
@@ -1624,7 +1630,7 @@ def append_text_to_shell(self, text, error, prompt):
16241630
def set_pythonshell_font(self, font=None):
16251631
"""Python Shell only"""
16261632
if font is None:
1627-
font = QFont()
1633+
font = self.font()
16281634
for style in self.font_styles:
16291635
style.apply_style(
16301636
font=font,

0 commit comments

Comments
 (0)