1 """Themes generators"""
12 from .widgets
import defs
16 from . import resources
23 CUSTOM
= 3 # Files located in ~/.config/git-cola/themes/*.qss
32 style_sheet
=EStylesheet
.DEFAULT
,
34 macos_appearance
=None,
38 self
.is_dark
= is_dark
39 self
.is_palette_dark
= None
40 self
.style_sheet
= style_sheet
41 self
.main_color
= main_color
42 self
.macos_appearance
= macos_appearance
43 self
.disabled_text_color
= None
44 self
.text_color
= None
45 self
.highlight_color
= None
46 self
.background_color
= None
49 def build_style_sheet(self
, app_palette
):
50 if self
.style_sheet
== EStylesheet
.CUSTOM
:
51 return self
.style_sheet_custom(app_palette
)
52 if self
.style_sheet
== EStylesheet
.FLAT
:
53 return self
.style_sheet_flat()
54 window
= app_palette
.color(QtGui
.QPalette
.Window
)
55 self
.is_palette_dark
= window
.lightnessF() < 0.5
56 return style_sheet_default(app_palette
)
58 def build_palette(self
, app_palette
):
59 QPalette
= QtGui
.QPalette
60 palette_dark
= app_palette
.color(QPalette
.Base
).lightnessF() < 0.5
61 if self
.is_palette_dark
is None:
62 self
.is_palette_dark
= palette_dark
64 if palette_dark
and self
.is_dark
:
65 self
.palette
= app_palette
67 if not palette_dark
and not self
.is_dark
:
68 self
.palette
= app_palette
71 background
= '#202025'
73 background
= '#edeef3'
75 bg_color
= qtutils
.css_color(background
)
76 txt_color
= qtutils
.css_color('#777777')
77 palette
= QPalette(bg_color
)
78 palette
.setColor(QPalette
.Base
, bg_color
)
79 palette
.setColor(QPalette
.Disabled
, QPalette
.Text
, txt_color
)
80 self
.background_color
= background
81 self
.palette
= palette
84 def style_sheet_flat(self
):
85 main_color
= self
.main_color
86 color
= qtutils
.css_color(main_color
)
87 color_rgb
= qtutils
.rgb_css(color
)
88 self
.is_palette_dark
= self
.is_dark
91 background
= '#2e2f30'
94 button_text
= '#000000'
95 field_text
= '#d0d0d0'
96 darker
= qtutils
.hsl_css(
97 color
.hslHueF(), color
.hslSaturationF() * 0.3, color
.lightnessF() * 1.3
99 lighter
= qtutils
.hsl_css(
100 color
.hslHueF(), color
.hslSaturationF() * 0.7, color
.lightnessF() * 0.6
102 focus
= qtutils
.hsl_css(
103 color
.hslHueF(), color
.hslSaturationF() * 0.7, color
.lightnessF() * 0.7
106 background
= '#edeef3'
109 button_text
= '#ffffff'
110 field_text
= '#000000'
111 darker
= qtutils
.hsl_css(
112 color
.hslHueF(), color
.hslSaturationF(), color
.lightnessF() * 0.4
114 lighter
= qtutils
.hsl_css(color
.hslHueF(), color
.hslSaturationF(), 0.92)
117 self
.disabled_text_color
= grayed
118 self
.text_color
= field_text
119 self
.highlight_color
= lighter
120 self
.background_color
= background
123 /* regular widgets */
125 background-color: {background};
127 selection-background-color: {lighter};
128 alternate-background-color: {field};
129 selection-color: {field_text};
130 show-decoration-selected: 1;
134 /* Focused widths get a thin border */
135 QTreeView:focus, QListView:focus,
136 QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus {{
139 border-color: {focus};
143 border-color: {grayed};
146 QDockWidget > QFrame {{
147 margin: 0px 0px 0px 0px;
149 QPlainTextEdit, QLineEdit, QTextEdit, QAbstractItemView,
151 background-color: {field};
152 border-color: {grayed};
156 QAbstractItemView::item:selected {{
157 background-color: {lighter};
159 QAbstractItemView::item:hover {{
160 background-color: {lighter};
164 background-color: transparent;
166 DockTitleBarWidget {{
171 QPushButton[flat="false"] {{
172 background-color: {button};
173 color: {button_text};
180 QPushButton[flat="true"], QToolButton {{
181 background-color: transparent;
184 QPushButton[flat="true"] {{
187 QPushButton:hover, QToolButton:hover {{
188 background-color: {darker};
190 QPushButton[flat="false"]:pressed, QToolButton:pressed {{
191 background-color: {darker};
192 margin: 1px 1px 2px 1px;
194 QPushButton:disabled {{
195 background-color: {grayed};
200 QPushButton[flat="true"]:disabled {{
201 background-color: transparent;
206 background-color: {background};
212 background: transparent;
214 QMenuBar::item:selected {{
215 background: {button};
217 QMenuBar::item:pressed {{
218 background: {button};
221 background-color: {field};
224 background: {background};
230 background-color: {field};
231 border-color: {grayed};
239 QComboBox::drop-down {{
240 border-color: {field_text} {field} {field} {field};
242 subcontrol-position: right;
243 border-width: 4px 3px 0 3px;
248 QComboBox::drop-down:hover {{
249 border-color: {button} {field} {field} {field};
252 background-color: {button};
253 color: {button_text};
257 QComboBox:item:selected {{
258 background-color: {darker};
259 color: {button_text};
261 QComboBox:item:checked {{
262 background-color: {darker};
263 color: {button_text};
266 /* MainWindow separator */
267 QMainWindow::separator {{
268 width: {separator}px;
269 height: {separator}px;
271 QMainWindow::separator:hover {{
277 background-color: {field};
280 QScrollBar::handle {{
281 background: {background}
283 QScrollBar::handle:hover {{
286 QScrollBar:horizontal {{
287 margin: 0 11px 0 11px;
290 QScrollBar:vertical {{
291 margin: 11px 0 11px 0;
294 QScrollBar::add-line, QScrollBar::sub-line {{
295 background: {background};
296 subcontrol-origin: margin;
298 QScrollBar::sub-line:horizontal {{ /*required by a buggy Qt version*/
299 subcontrol-position: left;
301 QScrollBar::add-line:hover, QScrollBar::sub-line:hover {{
302 background: {button};
304 QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{
307 QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{
310 QScrollBar:left-arrow, QScrollBar::right-arrow,
311 QScrollBar::up-arrow, QScrollBar::down-arrow {{
316 QScrollBar:right-arrow {{
317 border-color: {background} {background}
318 {background} {darker};
319 border-width: 3px 0 3px 4px;
321 QScrollBar:left-arrow {{
322 border-color: {background} {darker}
323 {background} {background};
324 border-width: 3px 4px 3px 0;
326 QScrollBar:up-arrow {{
327 border-color: {background} {background}
328 {darker} {background};
329 border-width: 0 3px 4px 3px;
331 QScrollBar:down-arrow {{
332 border-color: {darker} {background}
333 {background} {background};
334 border-width: 4px 3px 0 3px;
336 QScrollBar:right-arrow:hover {{
337 border-color: {button} {button}
340 QScrollBar:left-arrow:hover {{
341 border-color: {button} {darker}
344 QScrollBar:up-arrow:hover {{
345 border-color: {button} {button}
348 QScrollBar:down-arrow:hover {{
349 border-color: {darker} {button}
353 /* tab bar (stacked & docked widgets) */
355 background: transparent;
356 border-color: {darker};
361 QTabBar::tab:selected {{
362 background: {grayed};
369 background-color: transparent;
371 QCheckBox::indicator {{
372 background-color: {field};
373 border-color: {darker};
375 subcontrol-position: left;
380 QCheckBox::indicator:unchecked:hover {{
381 background-color: {button};
383 QCheckBox::indicator:unchecked:pressed {{
384 background-color: {darker};
386 QCheckBox::indicator:checked {{
387 background-color: {darker};
389 QCheckBox::indicator:checked:hover {{
390 background-color: {button};
392 QCheckBox::indicator:checked:pressed {{
393 background-color: {field};
401 QRadioButton::indicator {{
408 background-color: {field};
409 border: 1px solid {darker};
411 QProgressBar::chunk {{
412 background-color: {button};
417 QAbstractSpinBox::up-button, QAbstractSpinBox::down-button {{
418 background-color: transparent;
420 QAbstractSpinBox::up-arrow, QAbstractSpinBox::down-arrow {{
425 QAbstractSpinBox::up-arrow {{
426 border-color: {field} {field} {darker} {field};
427 border-width: 0 3px 4px 3px;
429 QAbstractSpinBox::up-arrow:hover {{
430 border-color: {field} {field} {button} {field};
431 border-width: 0 3px 4px 3px;
433 QAbstractSpinBox::down-arrow {{
434 border-color: {darker} {field} {field} {field};
435 border-width: 4px 3px 0 3px;
437 QAbstractSpinBox::down-arrow:hover {{
438 border-color: {button} {field} {field} {field};
439 border-width: 4px 3px 0 3px;
444 margin: 2px 2px 2px 2px;
451 border-width: 0 0 1px 0;
452 border-color: {grayed};
454 QHeaderView::section {{
456 border-right: 1px solid {grayed};
457 background-color: {background};
466 border-width: 0 0 1px 0;
467 border-color: {grayed};
469 QHeaderView::section {{
471 border-right: 1px solid {grayed};
472 background-color: {background};
478 background
=background
,
484 button_text
=button_text
,
485 field_text
=field_text
,
486 separator
=defs
.separator
,
490 def style_sheet_custom(self
, app_palette
):
491 """Get custom style sheet.
492 File name is saved in variable self.name.
493 If user has deleted file, use default style"""
495 # check if path exists
496 filename
= resources
.config_home('themes', self
.name
+ '.qss')
497 if not core
.exists(filename
):
498 return style_sheet_default(app_palette
)
500 return core
.read(filename
)
501 except OSError as err
:
502 core
.print_stderr(f
'warning: unable to read custom theme {filename}: {err}')
503 return style_sheet_default(app_palette
)
505 def get_palette(self
):
506 """Get a QPalette for the current theme"""
507 if self
.palette
is None:
508 palette
= qtutils
.current_palette()
510 palette
= self
.palette
513 def highlight_color_rgb(self
):
514 """Return an rgb(r,g,b) CSS color value for the selection highlight"""
515 if self
.highlight_color
:
516 highlight_rgb
= self
.highlight_color
517 elif self
.main_color
:
518 highlight_rgb
= qtutils
.rgb_css(
519 qtutils
.css_color(self
.main_color
).lighter()
522 palette
= self
.get_palette()
523 color
= palette
.color(QtGui
.QPalette
.Highlight
)
524 highlight_rgb
= qtutils
.rgb_css(color
)
527 def selection_color(self
):
528 """Return a color suitable for selections"""
529 highlight
= qtutils
.css_color(self
.highlight_color_rgb())
530 if highlight
.lightnessF() > 0.7: # Avoid clamping light colors to white.
533 color
= highlight
.lighter()
536 def text_colors_rgb(self
):
537 """Return a pair of rgb(r,g,b) CSS color values for text and selected text"""
539 text_rgb
= self
.text_color
540 highlight_text_rgb
= self
.text_color
542 palette
= self
.get_palette()
543 color
= palette
.text().color()
544 text_rgb
= qtutils
.rgb_css(color
)
546 color
= palette
.highlightedText().color()
547 highlight_text_rgb
= qtutils
.rgb_css(color
)
548 return text_rgb
, highlight_text_rgb
550 def disabled_text_color_rgb(self
):
551 """Return an rgb(r,g,b) CSS color value for the disabled text"""
552 if self
.disabled_text_color
:
553 disabled_text_rgb
= self
.disabled_text_color
555 palette
= self
.get_palette()
556 color
= palette
.color(QtGui
.QPalette
.Disabled
, QtGui
.QPalette
.Text
)
557 disabled_text_rgb
= qtutils
.rgb_css(color
)
558 return disabled_text_rgb
560 def background_color_rgb(self
):
561 """Return an rgb(r,g,b) CSS color value for the window background"""
562 if self
.background_color
:
563 background_color
= self
.background_color
565 palette
= self
.get_palette()
566 window
= palette
.color(QtGui
.QPalette
.Base
)
567 background_color
= qtutils
.rgb_css(window
)
568 return background_color
571 def style_sheet_default(palette
):
572 highlight
= palette
.color(QtGui
.QPalette
.Highlight
)
573 shadow
= palette
.color(QtGui
.QPalette
.Shadow
)
574 base
= palette
.color(QtGui
.QPalette
.Base
)
576 highlight_rgb
= qtutils
.rgb_css(highlight
)
577 shadow_rgb
= qtutils
.rgb_css(shadow
)
578 base_rgb
= qtutils
.rgb_css(base
)
581 QCheckBox::indicator {{
582 width: {checkbox_size}px;
583 height: {checkbox_size}px;
585 QCheckBox::indicator::unchecked {{
586 border: {checkbox_border}px solid {shadow_rgb};
587 background: {base_rgb};
589 QCheckBox::indicator::checked {{
590 image: url({checkbox_icon});
591 border: {checkbox_border}px solid {shadow_rgb};
592 background: {base_rgb};
595 QRadioButton::indicator {{
596 width: {radio_size}px;
597 height: {radio_size}px;
599 QRadioButton::indicator::unchecked {{
600 border: {radio_border}px solid {shadow_rgb};
601 border-radius: {radio_radius}px;
602 background: {base_rgb};
604 QRadioButton::indicator::checked {{
605 image: url({radio_icon});
606 border: {radio_border}px solid {shadow_rgb};
607 border-radius: {radio_radius}px;
608 background: {base_rgb};
611 QSplitter::handle:hover {{
612 background: {highlight_rgb};
615 QMainWindow::separator {{
617 width: {separator}px;
618 height: {separator}px;
620 QMainWindow::separator:hover {{
621 background: {highlight_rgb};
625 separator
=defs
.separator
,
626 highlight_rgb
=highlight_rgb
,
627 shadow_rgb
=shadow_rgb
,
629 checkbox_border
=defs
.border
,
630 checkbox_icon
=icons
.check_name(),
631 checkbox_size
=defs
.checkbox
,
632 radio_border
=defs
.radio_border
,
633 radio_icon
=icons
.dot_name(),
634 radio_radius
=defs
.radio
// 2,
635 radio_size
=defs
.radio
,
639 def get_all_themes():
645 style_sheet
=EStylesheet
.DEFAULT
,
650 if utils
.is_darwin():
651 themes
.extend(get_macos_themes().values())
656 N_('Flat light blue'),
658 style_sheet
=EStylesheet
.FLAT
,
659 main_color
='#5271cc',
663 N_('Flat light red'),
665 style_sheet
=EStylesheet
.FLAT
,
666 main_color
='#cc5452',
670 N_('Flat light grey'),
672 style_sheet
=EStylesheet
.FLAT
,
673 main_color
='#707478',
677 N_('Flat light green'),
679 style_sheet
=EStylesheet
.FLAT
,
680 main_color
='#42a65c',
684 N_('Flat dark blue'),
686 style_sheet
=EStylesheet
.FLAT
,
687 main_color
='#5271cc',
693 style_sheet
=EStylesheet
.FLAT
,
694 main_color
='#cc5452',
698 N_('Flat dark grey'),
700 style_sheet
=EStylesheet
.FLAT
,
701 main_color
='#aaaaaa',
705 N_('Flat dark green'),
707 style_sheet
=EStylesheet
.FLAT
,
708 main_color
='#42a65c',
712 # check if themes path exists in user folder
713 path
= resources
.config_home('themes')
714 if not os
.path
.isdir(path
):
717 # Gather Qt .qss stylesheet themes
719 filenames
= core
.listdir(path
)
723 for filename
in filenames
:
724 name
, ext
= os
.path
.splitext(filename
)
726 themes
.append(Theme(name
, N_(name
), False, EStylesheet
.CUSTOM
, None))
731 def apply_platform_theme(theme
):
732 """Apply platform-specific themes (e.g. dark mode on macOS)"""
733 # https://developer.apple.com/documentation/appkit/nsappearancecustomization/choosing_a_specific_appearance_for_your_macos_app
734 # https://github.com/git-cola/git-cola/issues/905#issuecomment-461118465
735 if utils
.is_darwin():
738 app
= AppKit
.NSApplication
.sharedApplication()
739 macos_themes
= get_macos_themes()
741 macos_appearance
= macos_themes
[theme
].macos_appearance
744 if macos_appearance
is None:
746 appearance
= AppKit
.NSAppearance
.appearanceNamed_(macos_appearance
)
747 app
.setAppearance_(appearance
)
750 def get_macos_themes():
751 """Get a mapping from theme names to macOS NSAppearanceName values"""
756 def add_macos_theme(name
, description
, is_dark
, attr
):
757 """Add an AppKit theme if it exists"""
758 if hasattr(AppKit
, attr
):
759 themes
[name
] = Theme(
760 name
, description
, is_dark
, macos_appearance
=getattr(AppKit
, attr
)
764 'macos-aqua-light', N_('MacOS Aqua light'), False, 'NSAppearanceNameAqua'
768 N_('MacOS Aqua dark'),
770 'NSAppearanceNameDarkAqua',
773 'macos-vibrant-light',
774 N_('MacOS Vibrant light'),
776 'NSAppearanceNameVibrantLight',
779 'macos-vibrant-dark',
780 N_('MacOS Vibrant dark'),
782 'NSAppearanceNameVibrantDark',
787 def options(themes
=None):
788 """Return a dictionary mapping display names to theme names"""
790 themes
= get_all_themes()
791 return [(theme
.title
, theme
.name
) for theme
in themes
]
794 def find_theme(name
):
795 themes
= get_all_themes()
797 if item
.name
== name
: