dev: format code using "garden fmt" (black)
[git-cola.git] / cola / themes.py
blob7ff015b890d3900db7a4709f4b1cf9389870c4eb
1 """Themes generators"""
2 from __future__ import absolute_import, division, print_function, unicode_literals
3 import os
5 from qtpy import QtGui
7 from .i18n import N_
8 from .widgets import defs
9 from . import core
10 from . import icons
11 from . import qtutils
12 from . import resources
15 class EStylesheet(object):
16 DEFAULT = 1
17 FLAT = 2
18 CUSTOM = 3 # Files located in ~/.config/git-cola/themes/*.qss
21 class Theme(object):
22 def __init__(
23 self, name, hr_name, is_dark, style_sheet=EStylesheet.DEFAULT, main_color=None
25 self.name = name
26 self.hr_name = hr_name
27 self.is_dark = is_dark
28 self.is_palette_dark = None
29 self.style_sheet = style_sheet
30 self.main_color = main_color
31 self.disabled_text_color = None
32 self.text_color = None
33 self.highlight_color = None
34 self.background_color = None
35 self.palette = None
37 def build_style_sheet(self, app_palette):
38 if self.style_sheet == EStylesheet.CUSTOM:
39 return self.style_sheet_custom(app_palette)
40 if self.style_sheet == EStylesheet.FLAT:
41 return self.style_sheet_flat()
42 window = app_palette.color(QtGui.QPalette.Window)
43 self.is_palette_dark = window.lightnessF() < 0.5
44 return style_sheet_default(app_palette)
46 def build_palette(self, app_palette):
47 QPalette = QtGui.QPalette
48 palette_dark = app_palette.color(QPalette.Base).lightnessF() < 0.5
49 if self.is_palette_dark is None:
50 self.is_palette_dark = palette_dark
52 if palette_dark and self.is_dark:
53 self.palette = app_palette
54 return app_palette
55 if not palette_dark and not self.is_dark:
56 self.palette = app_palette
57 return app_palette
58 if self.is_dark:
59 background = '#202025'
60 else:
61 background = '#edeef3'
63 bg_color = qtutils.css_color(background)
64 txt_color = qtutils.css_color('#777777')
65 palette = QPalette(bg_color)
66 palette.setColor(QPalette.Base, bg_color)
67 palette.setColor(QPalette.Disabled, QPalette.Text, txt_color)
68 self.background_color = background
69 self.palette = palette
70 return palette
72 def style_sheet_flat(self):
73 main_color = self.main_color
74 color = qtutils.css_color(main_color)
75 color_rgb = qtutils.rgb_css(color)
76 self.is_palette_dark = self.is_dark
78 if self.is_dark:
79 background = '#2e2f30'
80 field = '#383a3c'
81 grayed = '#06080a'
82 button_text = '#000000'
83 field_text = '#d0d0d0'
84 darker = qtutils.hsl_css(
85 color.hslHueF(), color.hslSaturationF() * 0.3, color.lightnessF() * 1.3
87 lighter = qtutils.hsl_css(
88 color.hslHueF(), color.hslSaturationF() * 0.7, color.lightnessF() * 0.6
90 focus = qtutils.hsl_css(
91 color.hslHueF(), color.hslSaturationF() * 0.7, color.lightnessF() * 0.7
93 else:
94 background = '#edeef3'
95 field = '#ffffff'
96 grayed = '#a2a2b0'
97 button_text = '#ffffff'
98 field_text = '#000000'
99 darker = qtutils.hsl_css(
100 color.hslHueF(), color.hslSaturationF(), color.lightnessF() * 0.4
102 lighter = qtutils.hsl_css(color.hslHueF(), color.hslSaturationF(), 0.92)
103 focus = color_rgb
105 self.disabled_text_color = grayed
106 self.text_color = field_text
107 self.highlight_color = lighter
108 self.background_color = background
110 return """
111 /* regular widgets */
113 background-color: %(background)s;
114 color: %(field_text)s;
115 selection-background-color: %(lighter)s;
116 alternate-background-color: %(field)s;
117 selection-color: %(field_text)s;
118 show-decoration-selected: 1;
119 spacing: 2px;
122 /* Focused widths get a thin border */
123 QTreeView:focus, QListView:focus,
124 QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus {
125 border-width: 1px;
126 border-style: solid;
127 border-color: %(focus)s;
130 QWidget:disabled {
131 border-color: %(grayed)s;
132 color: %(grayed)s;
134 QDockWidget > QFrame {
135 margin: 0px 0px 0px 0px;
137 QPlainTextEdit, QLineEdit, QTextEdit, QAbstractItemView,
138 QAbstractSpinBox {
139 background-color: %(field)s;
140 border-color: %(grayed)s;
141 border-style: solid;
142 border-width: 1px;
144 QAbstractItemView::item:selected {
145 background-color: %(lighter)s;
147 QAbstractItemView::item:hover {
148 background-color: %(lighter)s;
150 QLabel {
151 color: %(darker)s;
152 background-color: transparent;
154 DockTitleBarWidget {
155 padding-bottom: 4px;
158 /* buttons */
159 QPushButton[flat="false"] {
160 background-color: %(button)s;
161 color: %(button_text)s;
162 border-radius: 2px;
163 border-width: 0;
164 margin-bottom: 1px;
165 min-width: 55px;
166 padding: 4px 5px;
168 QPushButton[flat="true"], QToolButton {
169 background-color: transparent;
170 border-radius: 0px;
172 QPushButton[flat="true"] {
173 margin-bottom: 10px;
175 QPushButton:hover, QToolButton:hover {
176 background-color: %(darker)s;
178 QPushButton[flat="false"]:pressed, QToolButton:pressed {
179 background-color: %(darker)s;
180 margin: 1px 1px 2px 1px;
182 QPushButton:disabled {
183 background-color: %(grayed)s;
184 color: %(field)s;
185 padding-left: 5px;
186 padding-top: 5px;
188 QPushButton[flat="true"]:disabled {
189 background-color: transparent;
192 /*menus*/
193 QMenuBar {
194 background-color: %(background)s;
195 color: %(field_text)s;
196 border-width: 0;
197 padding: 1px;
199 QMenuBar::item {
200 background: transparent;
202 QMenuBar::item:selected {
203 background: %(button)s;
205 QMenuBar::item:pressed {
206 background: %(button)s;
208 QMenu {
209 background-color: %(field)s;
211 QMenu::separator {
212 background: %(background)s;
213 height: 1px;
216 /* combo box */
217 QComboBox {
218 background-color: %(field)s;
219 border-color: %(grayed)s;
220 border-style: solid;
221 color: %(field_text)s;
222 border-radius: 0px;
223 border-width: 1px;
224 margin-bottom: 1px;
225 padding: 0 5px;
227 QComboBox::drop-down {
228 border-color: %(field_text)s %(field)s %(field)s %(field)s;
229 border-style: solid;
230 subcontrol-position: right;
231 border-width: 4px 3px 0 3px;
232 height: 0;
233 margin-right: 5px;
234 width: 0;
236 QComboBox::drop-down:hover {
237 border-color: %(button)s %(field)s %(field)s %(field)s;
239 QComboBox:item {
240 background-color: %(button)s;
241 color: %(button_text)s;
242 border-width: 0;
243 height: 22px;
245 QComboBox:item:selected {
246 background-color: %(darker)s;
247 color: %(button_text)s;
249 QComboBox:item:checked {
250 background-color: %(darker)s;
251 color: %(button_text)s;
254 /* MainWindow separator */
255 QMainWindow::separator {
256 width: %(separator)spx;
257 height: %(separator)spx;
259 QMainWindow::separator:hover {
260 background: %(focus)s;
263 /* scroll bar */
264 QScrollBar {
265 background-color: %(field)s;
266 border: 0;
268 QScrollBar::handle {
269 background: %(background)s
271 QScrollBar::handle:hover {
272 background: %(button)s
274 QScrollBar:horizontal {
275 margin: 0 11px 0 11px;
276 height: 10px;
278 QScrollBar:vertical {
279 margin: 11px 0 11px 0;
280 width: 10px;
282 QScrollBar::add-line, QScrollBar::sub-line {
283 background: %(background)s;
284 subcontrol-origin: margin;
286 QScrollBar::sub-line:horizontal { /*required by a buggy Qt version*/
287 subcontrol-position: left;
289 QScrollBar::add-line:hover, QScrollBar::sub-line:hover {
290 background: %(button)s;
292 QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
293 width: 10px;
295 QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
296 height: 10px;
298 QScrollBar:left-arrow, QScrollBar::right-arrow,
299 QScrollBar::up-arrow, QScrollBar::down-arrow {
300 border-style: solid;
301 height: 0;
302 width: 0;
304 QScrollBar:right-arrow {
305 border-color: %(background)s %(background)s
306 %(background)s %(darker)s;
307 border-width: 3px 0 3px 4px;
309 QScrollBar:left-arrow {
310 border-color: %(background)s %(darker)s
311 %(background)s %(background)s;
312 border-width: 3px 4px 3px 0;
314 QScrollBar:up-arrow {
315 border-color: %(background)s %(background)s
316 %(darker)s %(background)s;
317 border-width: 0 3px 4px 3px;
319 QScrollBar:down-arrow {
320 border-color: %(darker)s %(background)s
321 %(background)s %(background)s;
322 border-width: 4px 3px 0 3px;
324 QScrollBar:right-arrow:hover {
325 border-color: %(button)s %(button)s
326 %(button)s %(darker)s;
328 QScrollBar:left-arrow:hover {
329 border-color: %(button)s %(darker)s
330 %(button)s %(button)s;
332 QScrollBar:up-arrow:hover {
333 border-color: %(button)s %(button)s
334 %(darker)s %(button)s;
336 QScrollBar:down-arrow:hover {
337 border-color: %(darker)s %(button)s
338 %(button)s %(button)s;
341 /* tab bar (stacked & docked widgets) */
342 QTabBar::tab {
343 background: transparent;
344 border-color: %(darker)s;
345 border-width: 1px;
346 margin: 1px;
347 padding: 3px 5px;
349 QTabBar::tab:selected {
350 background: %(grayed)s;
353 /* check box */
354 QCheckBox {
355 spacing: 8px;
356 margin: 4px;
357 background-color: transparent;
359 QCheckBox::indicator {
360 background-color: %(field)s;
361 border-color: %(darker)s;
362 border-style: solid;
363 subcontrol-position: left;
364 border-width: 1px;
365 height: 13px;
366 width: 13px;
368 QCheckBox::indicator:unchecked:hover {
369 background-color: %(button)s;
371 QCheckBox::indicator:unchecked:pressed {
372 background-color: %(darker)s;
374 QCheckBox::indicator:checked {
375 background-color: %(darker)s;
377 QCheckBox::indicator:checked:hover {
378 background-color: %(button)s;
380 QCheckBox::indicator:checked:pressed {
381 background-color: %(field)s;
384 /* radio checkbox */
385 QRadioButton {
386 spacing: 8px;
387 margin: 4px;
389 QRadioButton::indicator {
390 height: 0.75em;
391 width: 0.75em;
394 /* progress bar */
395 QProgressBar {
396 background-color: %(field)s;
397 border: 1px solid %(darker)s;
399 QProgressBar::chunk {
400 background-color: %(button)s;
401 width: 1px;
404 /* spin box */
405 QAbstractSpinBox::up-button, QAbstractSpinBox::down-button {
406 background-color: transparent;
408 QAbstractSpinBox::up-arrow, QAbstractSpinBox::down-arrow {
409 border-style: solid;
410 height: 0;
411 width: 0;
413 QAbstractSpinBox::up-arrow {
414 border-color: %(field)s %(field)s %(darker)s %(field)s;
415 border-width: 0 3px 4px 3px;
417 QAbstractSpinBox::up-arrow:hover {
418 border-color: %(field)s %(field)s %(button)s %(field)s;
419 border-width: 0 3px 4px 3px;
421 QAbstractSpinBox::down-arrow {
422 border-color: %(darker)s %(field)s %(field)s %(field)s;
423 border-width: 4px 3px 0 3px;
425 QAbstractSpinBox::down-arrow:hover {
426 border-color: %(button)s %(field)s %(field)s %(field)s;
427 border-width: 4px 3px 0 3px;
430 /* dialogs */
431 QDialog > QFrame {
432 margin: 2px 2px 2px 2px;
435 /* headers */
436 QHeaderView {
437 color: %(field_text)s;
438 border-style: solid;
439 border-width: 0 0 1px 0;
440 border-color: %(grayed)s;
442 QHeaderView::section {
443 border-style: solid;
444 border-right: 1px solid %(grayed)s;
445 background-color: %(background)s;
446 color: %(field_text)s;
447 padding-left: 4px;
450 /* headers */
451 QHeaderView {
452 color: %(field_text)s;
453 border-style: solid;
454 border-width: 0 0 1px 0;
455 border-color: %(grayed)s;
457 QHeaderView::section {
458 border-style: solid;
459 border-right: 1px solid %(grayed)s;
460 background-color: %(background)s;
461 color: %(field_text)s;
462 padding-left: 4px;
465 """ % {
466 'background': background,
467 'field': field,
468 'button': color_rgb,
469 'darker': darker,
470 'lighter': lighter,
471 'grayed': grayed,
472 'button_text': button_text,
473 'field_text': field_text,
474 'separator': defs.separator,
475 'focus': focus,
478 def style_sheet_custom(self, app_palette):
479 """Get custom style sheet.
480 File name is saved in variable self.name.
481 If user has deleted file, use default style"""
483 # check if path exists
484 filename = resources.config_home('themes', self.name + '.qss')
485 if not core.exists(filename):
486 return style_sheet_default(app_palette)
487 try:
488 return core.read(filename)
489 except (IOError, OSError) as err:
490 core.print_stderr(
491 'warning: unable to read custom theme %s: %s' % (filename, err)
493 return style_sheet_default(app_palette)
495 def get_palette(self):
496 """Get a QPalette for the current theme"""
497 if self.palette is None:
498 palette = qtutils.current_palette()
499 else:
500 palette = self.palette
501 return palette
503 def highlight_color_rgb(self):
504 """Return an rgb(r,g,b) css color value for the selection highlight"""
505 if self.highlight_color:
506 highlight_rgb = self.highlight_color
507 elif self.main_color:
508 highlight_rgb = qtutils.rgb_css(
509 qtutils.css_color(self.main_color).lighter()
511 else:
512 palette = self.get_palette()
513 color = palette.color(QtGui.QPalette.Highlight)
514 highlight_rgb = qtutils.rgb_css(color)
515 return highlight_rgb
517 def selection_color(self):
518 """Return a color suitable for selections"""
519 highlight = qtutils.css_color(self.highlight_color_rgb())
520 if highlight.lightnessF() > 0.7: # Avoid clamping light colors to white.
521 color = highlight
522 else:
523 color = highlight.lighter()
524 return color
526 def text_colors_rgb(self):
527 """Return a pair of rgb(r,g,b) css color values for text and selected text"""
528 if self.text_color:
529 text_rgb = self.text_color
530 highlight_text_rgb = self.text_color
531 else:
532 palette = self.get_palette()
533 color = palette.text().color()
534 text_rgb = qtutils.rgb_css(color)
536 color = palette.highlightedText().color()
537 highlight_text_rgb = qtutils.rgb_css(color)
538 return text_rgb, highlight_text_rgb
540 def disabled_text_color_rgb(self):
541 """Return an rgb(r,g,b) css color value for the disabled text"""
542 if self.disabled_text_color:
543 disabled_text_rgb = self.disabled_text_color
544 else:
545 palette = self.get_palette()
546 color = palette.color(QtGui.QPalette.Disabled, QtGui.QPalette.Text)
547 disabled_text_rgb = qtutils.rgb_css(color)
548 return disabled_text_rgb
550 def background_color_rgb(self):
551 """Return an rgb(r,g,b) css color value for the window background"""
552 if self.background_color:
553 background_color = self.background_color
554 else:
555 palette = self.get_palette()
556 window = palette.color(QtGui.QPalette.Base)
557 background_color = qtutils.rgb_css(window)
558 return background_color
561 def style_sheet_default(palette):
562 window = palette.color(QtGui.QPalette.Window)
563 highlight = palette.color(QtGui.QPalette.Highlight)
564 shadow = palette.color(QtGui.QPalette.Shadow)
565 base = palette.color(QtGui.QPalette.Base)
567 window_rgb = qtutils.rgb_css(window)
568 highlight_rgb = qtutils.rgb_css(highlight)
569 shadow_rgb = qtutils.rgb_css(shadow)
570 base_rgb = qtutils.rgb_css(base)
572 return """
573 QCheckBox::indicator {
574 width: %(checkbox_size)spx;
575 height: %(checkbox_size)spx;
577 QCheckBox::indicator::unchecked {
578 border: %(checkbox_border)spx solid %(shadow_rgb)s;
579 background: %(base_rgb)s;
581 QCheckBox::indicator::checked {
582 image: url(%(checkbox_icon)s);
583 border: %(checkbox_border)spx solid %(shadow_rgb)s;
584 background: %(base_rgb)s;
587 QRadioButton::indicator {
588 width: %(radio_size)spx;
589 height: %(radio_size)spx;
591 QRadioButton::indicator::unchecked {
592 border: %(radio_border)spx solid %(shadow_rgb)s;
593 border-radius: %(radio_radius)spx;
594 background: %(base_rgb)s;
596 QRadioButton::indicator::checked {
597 image: url(%(radio_icon)s);
598 border: %(radio_border)spx solid %(shadow_rgb)s;
599 border-radius: %(radio_radius)spx;
600 background: %(base_rgb)s;
603 QSplitter::handle:hover {
604 background: %(highlight_rgb)s;
607 QMainWindow::separator {
608 background: %(window_rgb)s;
609 width: %(separator)spx;
610 height: %(separator)spx;
612 QMainWindow::separator:hover {
613 background: %(highlight_rgb)s;
616 """ % {
617 'separator': defs.separator,
618 'window_rgb': window_rgb,
619 'highlight_rgb': highlight_rgb,
620 'shadow_rgb': shadow_rgb,
621 'base_rgb': base_rgb,
622 'checkbox_border': defs.border,
623 'checkbox_icon': icons.check_name(),
624 'checkbox_size': defs.checkbox,
625 'radio_border': defs.radio_border,
626 'radio_icon': icons.dot_name(),
627 'radio_radius': defs.radio // 2,
628 'radio_size': defs.radio,
632 def get_all_themes():
633 themes = [
634 Theme(
635 'default',
636 N_('Default'),
637 False,
638 style_sheet=EStylesheet.DEFAULT,
639 main_color=None,
641 Theme(
642 'flat-light-blue',
643 N_('Flat light blue'),
644 False,
645 style_sheet=EStylesheet.FLAT,
646 main_color='#5271cc',
648 Theme(
649 'flat-light-red',
650 N_('Flat light red'),
651 False,
652 style_sheet=EStylesheet.FLAT,
653 main_color='#cc5452',
655 Theme(
656 'flat-light-grey',
657 N_('Flat light grey'),
658 False,
659 style_sheet=EStylesheet.FLAT,
660 main_color='#707478',
662 Theme(
663 'flat-light-green',
664 N_('Flat light green'),
665 False,
666 style_sheet=EStylesheet.FLAT,
667 main_color='#42a65c',
669 Theme(
670 'flat-dark-blue',
671 N_('Flat dark blue'),
672 True,
673 style_sheet=EStylesheet.FLAT,
674 main_color='#5271cc',
676 Theme(
677 'flat-dark-red',
678 N_('Flat dark red'),
679 True,
680 style_sheet=EStylesheet.FLAT,
681 main_color='#cc5452',
683 Theme(
684 'flat-dark-grey',
685 N_('Flat dark grey'),
686 True,
687 style_sheet=EStylesheet.FLAT,
688 main_color='#aaaaaa',
690 Theme(
691 'flat-dark-green',
692 N_('Flat dark green'),
693 True,
694 style_sheet=EStylesheet.FLAT,
695 main_color='#42a65c',
699 # check if themes path exists in user folder
700 path = resources.config_home('themes')
701 if not os.path.isdir(path):
702 return themes
704 # Gather Qt .qss stylesheet themes
705 try:
706 filenames = core.listdir(path)
707 except OSError:
708 return themes
710 for filename in filenames:
711 name, ext = os.path.splitext(filename)
712 if ext == '.qss':
713 themes.append(Theme(name, N_(name), False, EStylesheet.CUSTOM, None))
715 return themes
718 def options():
719 """Return a dictionary mapping display names to theme names"""
720 items = get_all_themes()
721 return [(item.hr_name, item.name) for item in items]
724 def find_theme(name):
725 themes = get_all_themes()
726 for item in themes:
727 if item.name == name:
728 return item
729 return themes[0]