widgets: guard against RuntimeError during application shutdown
[git-cola.git] / cola / themes.py
blobf544429e5da1dc81693cd65d546d8fbd392aa7b2
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.style_sheet = style_sheet
29 self.main_color = main_color
30 self.disabled_text_color = None
31 self.text_color = None
32 self.highlight_color = None
34 def build_style_sheet(self, app_palette):
35 if self.style_sheet == EStylesheet.CUSTOM:
36 return self.style_sheet_custom(app_palette)
37 elif self.style_sheet == EStylesheet.FLAT:
38 return self.style_sheet_flat()
39 else:
40 return style_sheet_default(app_palette)
42 def build_palette(self, app_palette):
43 QPalette = QtGui.QPalette
44 palette_dark = app_palette.color(QPalette.Base).lightnessF() < 0.5
46 if palette_dark and self.is_dark:
47 return app_palette
48 if not palette_dark and not self.is_dark:
49 return app_palette
50 if self.is_dark:
51 bg_color = QtGui.QColor('#202025')
52 else:
53 bg_color = QtGui.QColor('#edeef3')
55 txt_color = QtGui.QColor('#777')
56 palette = QPalette(bg_color)
57 palette.setColor(QPalette.Base, bg_color)
58 palette.setColor(QPalette.Disabled, QPalette.Text, txt_color)
59 return palette
61 def style_sheet_flat(self):
62 main_color = self.main_color
63 color = QtGui.QColor(main_color)
64 color_rgb = qtutils.rgb_css(color)
66 if self.is_dark:
67 background = '#2e2f30'
68 field = '#383a3c'
69 grayed = '#06080a'
70 button_text = '#000000'
71 field_text = '#d0d0d0'
72 darker = qtutils.hsl_css(
73 color.hslHueF(), color.hslSaturationF() * 0.3, color.lightnessF() * 1.3
75 lighter = qtutils.hsl_css(
76 color.hslHueF(), color.hslSaturationF() * 0.7, color.lightnessF() * 0.6
78 focus = qtutils.hsl_css(
79 color.hslHueF(), color.hslSaturationF() * 0.7, color.lightnessF() * 0.7
81 else:
82 background = '#edeef3'
83 field = '#ffffff'
84 grayed = '#a2a2b0'
85 button_text = '#ffffff'
86 field_text = '#000000'
87 darker = qtutils.hsl_css(
88 color.hslHueF(), color.hslSaturationF(), color.lightnessF() * 0.4
90 lighter = qtutils.hsl_css(color.hslHueF(), color.hslSaturationF() * 2, 0.92)
91 focus = color_rgb
93 self.disabled_text_color = grayed
94 self.text_color = field_text
95 self.highlight_color = lighter
97 return """
98 /* regular widgets */
99 * {
100 background-color: %(background)s;
101 color: %(field_text)s;
102 selection-background-color: %(lighter)s;
103 alternate-background-color: %(field)s;
104 selection-color: %(field_text)s;
105 show-decoration-selected: 1;
106 spacing: 2px;
109 /* Focused widths get a thin border */
110 QTreeView:focus, QListView:focus,
111 QLineEdit:focus, QTextEdit:focus, QPlainTextEdit:focus {
112 border-width: 1px;
113 border-style: solid;
114 border-color: %(focus)s;
117 QWidget:disabled {
118 border-color: %(grayed)s;
119 color: %(grayed)s;
121 QDockWidget > QFrame {
122 margin: 0 2px 2px 2px;
123 min-height: 40px;
125 QPlainTextEdit, QLineEdit, QTextEdit, QAbstractItemView,
126 QAbstractSpinBox {
127 background-color: %(field)s;
128 border-color: %(grayed)s;
129 border-style: solid;
130 border-width: 1px;
132 QAbstractItemView::item:selected {
133 background-color: %(lighter)s;
135 QAbstractItemView::item:hover {
136 background-color: %(lighter)s;
138 QLabel {
139 color: %(darker)s;
140 background-color: transparent;
142 DockTitleBarWidget {
143 padding-bottom: 4px;
146 /* buttons */
147 QPushButton[flat="false"] {
148 background-color: %(button)s;
149 color: %(button_text)s;
150 border-radius: 2px;
151 border-width: 0;
152 margin-bottom: 1px;
153 min-width: 55px;
154 padding: 4px 5px;
156 QPushButton[flat="true"], QToolButton {
157 background-color: transparent;
158 border-radius: 0px;
160 QPushButton[flat="true"] {
161 margin-bottom: 10px;
163 QPushButton:hover, QToolButton:hover {
164 background-color: %(darker)s;
166 QPushButton[flat="false"]:pressed, QToolButton:pressed {
167 background-color: %(darker)s;
168 margin: 1px 1px 2px 1px;
170 QPushButton:disabled {
171 background-color: %(grayed)s;
172 color: %(field)s;
173 padding-left: 5px;
174 padding-top: 5px;
176 QPushButton[flat="true"]:disabled {
177 background-color: transparent;
180 /*menus*/
181 QMenuBar {
182 background-color: %(background)s;
183 color: %(field_text)s;
184 border-width: 0;
185 padding: 1px;
187 QMenuBar::item {
188 background: transparent;
190 QMenuBar::item:selected {
191 background: %(button)s;
193 QMenuBar::item:pressed {
194 background: %(button)s;
196 QMenu {
197 background-color: %(field)s;
199 QMenu::separator {
200 background: %(background)s;
201 height: 1px;
204 /* combo box */
205 QComboBox {
206 background-color: %(field)s;
207 border-color: %(grayed)s;
208 border-style: solid;
209 color: %(field_text)s;
210 border-radius: 0px;
211 border-width: 1px;
212 margin-bottom: 1px;
213 padding: 0 5px;
215 QComboBox::drop-down {
216 border-color: %(field_text)s %(field)s %(field)s %(field)s;
217 border-style: solid;
218 subcontrol-position: right;
219 border-width: 4px 3px 0 3px;
220 height: 0;
221 margin-right: 5px;
222 width: 0;
224 QComboBox::drop-down:hover {
225 border-color: %(button)s %(field)s %(field)s %(field)s;
227 QComboBox:item {
228 background-color: %(button)s;
229 color: %(button_text)s;
230 border-width: 0;
231 height: 22px;
233 QComboBox:item:selected {
234 background-color: %(darker)s;
235 color: %(button_text)s;
237 QComboBox:item:checked {
238 background-color: %(darker)s;
239 color: %(button_text)s;
242 /* MainWindow separator */
243 QMainWindow::separator {
244 width: %(separator)spx;
245 height: %(separator)spx;
247 QMainWindow::separator:hover {
248 background: %(focus)s;
251 /* scroll bar */
252 QScrollBar {
253 background-color: %(field)s;
254 border: 0;
256 QScrollBar::handle {
257 background: %(background)s
259 QScrollBar::handle:hover {
260 background: %(button)s
262 QScrollBar:horizontal {
263 margin: 0 11px 0 11px;
264 height: 10px;
266 QScrollBar:vertical {
267 margin: 11px 0 11px 0;
268 width: 10px;
270 QScrollBar::add-line, QScrollBar::sub-line {
271 background: %(background)s;
272 subcontrol-origin: margin;
274 QScrollBar::sub-line:horizontal { /*required by a buggy Qt version*/
275 subcontrol-position: left;
277 QScrollBar::add-line:hover, QScrollBar::sub-line:hover {
278 background: %(button)s;
280 QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {
281 width: 10px;
283 QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {
284 height: 10px;
286 QScrollBar:left-arrow, QScrollBar::right-arrow,
287 QScrollBar::up-arrow, QScrollBar::down-arrow {
288 border-style: solid;
289 height: 0;
290 width: 0;
292 QScrollBar:right-arrow {
293 border-color: %(background)s %(background)s
294 %(background)s %(darker)s;
295 border-width: 3px 0 3px 4px;
297 QScrollBar:left-arrow {
298 border-color: %(background)s %(darker)s
299 %(background)s %(background)s;
300 border-width: 3px 4px 3px 0;
302 QScrollBar:up-arrow {
303 border-color: %(background)s %(background)s
304 %(darker)s %(background)s;
305 border-width: 0 3px 4px 3px;
307 QScrollBar:down-arrow {
308 border-color: %(darker)s %(background)s
309 %(background)s %(background)s;
310 border-width: 4px 3px 0 3px;
312 QScrollBar:right-arrow:hover {
313 border-color: %(button)s %(button)s
314 %(button)s %(darker)s;
316 QScrollBar:left-arrow:hover {
317 border-color: %(button)s %(darker)s
318 %(button)s %(button)s;
320 QScrollBar:up-arrow:hover {
321 border-color: %(button)s %(button)s
322 %(darker)s %(button)s;
324 QScrollBar:down-arrow:hover {
325 border-color: %(darker)s %(button)s
326 %(button)s %(button)s;
329 /* tab bar (stacked & docked widgets) */
330 QTabBar::tab {
331 background: transparent;
332 border-color: %(darker)s;
333 border-width: 1px;
334 margin: 1px;
335 padding: 3px 5px;
337 QTabBar::tab:selected {
338 background: %(grayed)s;
341 /* check box */
342 QCheckBox {
343 spacing: 8px;
344 margin: 4px;
345 background-color: transparent;
347 QCheckBox::indicator {
348 background-color: %(field)s;
349 border-color: %(darker)s;
350 border-style: solid;
351 subcontrol-position: left;
352 border-width: 1px;
353 height: 13px;
354 width: 13px;
356 QCheckBox::indicator:unchecked:hover {
357 background-color: %(button)s;
359 QCheckBox::indicator:unchecked:pressed {
360 background-color: %(darker)s;
362 QCheckBox::indicator:checked {
363 background-color: %(darker)s;
365 QCheckBox::indicator:checked:hover {
366 background-color: %(button)s;
368 QCheckBox::indicator:checked:pressed {
369 background-color: %(field)s;
372 /* radio checkbox */
373 QRadioButton {
374 spacing: 8px;
375 margin: 4px;
377 QRadioButton::indicator {
378 height: 0.75em;
379 width: 0.75em;
382 /* progress bar */
383 QProgressBar {
384 background-color: %(field)s;
385 border: 1px solid %(darker)s;
387 QProgressBar::chunk {
388 background-color: %(button)s;
389 width: 1px;
392 /* spin box */
393 QAbstractSpinBox::up-button, QAbstractSpinBox::down-button {
394 background-color: transparent;
396 QAbstractSpinBox::up-arrow, QAbstractSpinBox::down-arrow {
397 border-style: solid;
398 height: 0;
399 width: 0;
401 QAbstractSpinBox::up-arrow {
402 border-color: %(field)s %(field)s %(darker)s %(field)s;
403 border-width: 0 3px 4px 3px;
405 QAbstractSpinBox::up-arrow:hover {
406 border-color: %(field)s %(field)s %(button)s %(field)s;
407 border-width: 0 3px 4px 3px;
409 QAbstractSpinBox::down-arrow {
410 border-color: %(darker)s %(field)s %(field)s %(field)s;
411 border-width: 4px 3px 0 3px;
413 QAbstractSpinBox::down-arrow:hover {
414 border-color: %(button)s %(field)s %(field)s %(field)s;
415 border-width: 4px 3px 0 3px;
418 /* dialogs */
419 QDialog > QFrame {
420 margin: 2px 2px 2px 2px;
423 /* headers */
424 QHeaderView {
425 color: %(field_text)s;
426 border-style: solid;
427 border-width: 0 0 1px 0;
428 border-color: %(grayed)s;
430 QHeaderView::section {
431 border-style: solid;
432 border-right: 1px solid %(grayed)s;
433 background-color: %(background)s;
434 color: %(field_text)s;
435 padding-left: 4px;
438 /* headers */
439 QHeaderView {
440 color: %(field_text)s;
441 border-style: solid;
442 border-width: 0 0 1px 0;
443 border-color: %(grayed)s;
445 QHeaderView::section {
446 border-style: solid;
447 border-right: 1px solid %(grayed)s;
448 background-color: %(background)s;
449 color: %(field_text)s;
450 padding-left: 4px;
453 """ % dict(
454 background=background,
455 field=field,
456 button=color_rgb,
457 darker=darker,
458 lighter=lighter,
459 grayed=grayed,
460 button_text=button_text,
461 field_text=field_text,
462 separator=defs.separator,
463 focus=focus,
466 def style_sheet_custom(self, app_palette):
467 """Get custom style sheet.
468 File name is saved in variable self.name.
469 If user has deleted file, use default style"""
471 # check if path exists
472 filename = resources.config_home('themes', self.name + '.qss')
473 if not core.exists(filename):
474 return style_sheet_default(app_palette)
475 try:
476 return core.read(filename)
477 except (IOError, OSError) as err:
478 core.print_stderr(
479 'warning: unable to read custom theme %s: %s' % (filename, err)
481 return style_sheet_default(app_palette)
484 def style_sheet_default(palette):
485 window = palette.color(QtGui.QPalette.Window)
486 highlight = palette.color(QtGui.QPalette.Highlight)
487 shadow = palette.color(QtGui.QPalette.Shadow)
488 base = palette.color(QtGui.QPalette.Base)
490 window_rgb = qtutils.rgb_css(window)
491 highlight_rgb = qtutils.rgb_css(highlight)
492 shadow_rgb = qtutils.rgb_css(shadow)
493 base_rgb = qtutils.rgb_css(base)
495 return """
496 QCheckBox::indicator {
497 width: %(checkbox_size)spx;
498 height: %(checkbox_size)spx;
500 QCheckBox::indicator::unchecked {
501 border: %(checkbox_border)spx solid %(shadow_rgb)s;
502 background: %(base_rgb)s;
504 QCheckBox::indicator::checked {
505 image: url(%(checkbox_icon)s);
506 border: %(checkbox_border)spx solid %(shadow_rgb)s;
507 background: %(base_rgb)s;
510 QRadioButton::indicator {
511 width: %(radio_size)spx;
512 height: %(radio_size)spx;
514 QRadioButton::indicator::unchecked {
515 border: %(radio_border)spx solid %(shadow_rgb)s;
516 border-radius: %(radio_radius)spx;
517 background: %(base_rgb)s;
519 QRadioButton::indicator::checked {
520 image: url(%(radio_icon)s);
521 border: %(radio_border)spx solid %(shadow_rgb)s;
522 border-radius: %(radio_radius)spx;
523 background: %(base_rgb)s;
526 QSplitter::handle:hover {
527 background: %(highlight_rgb)s;
530 QMainWindow::separator {
531 background: %(window_rgb)s;
532 width: %(separator)spx;
533 height: %(separator)spx;
535 QMainWindow::separator:hover {
536 background: %(highlight_rgb)s;
539 """ % dict(
540 separator=defs.separator,
541 window_rgb=window_rgb,
542 highlight_rgb=highlight_rgb,
543 shadow_rgb=shadow_rgb,
544 base_rgb=base_rgb,
545 checkbox_border=defs.border,
546 checkbox_icon=icons.check_name(),
547 checkbox_size=defs.checkbox,
548 radio_border=defs.radio_border,
549 radio_icon=icons.dot_name(),
550 radio_radius=defs.radio // 2,
551 radio_size=defs.radio,
555 def get_all_themes():
556 themes = [
557 Theme('default', N_('Default'), False, EStylesheet.DEFAULT, None),
558 Theme(
559 'flat-light-blue', N_('Flat light blue'), False, EStylesheet.FLAT, '#5271cc'
561 Theme(
562 'flat-light-red', N_('Flat light red'), False, EStylesheet.FLAT, '#cc5452'
564 Theme(
565 'flat-light-grey', N_('Flat light grey'), False, EStylesheet.FLAT, '#707478'
567 Theme(
568 'flat-light-green',
569 N_('Flat light green'),
570 False,
571 EStylesheet.FLAT,
572 '#42a65c',
574 Theme(
575 'flat-dark-blue', N_('Flat dark blue'), True, EStylesheet.FLAT, '#5271cc'
577 Theme('flat-dark-red', N_('Flat dark red'), True, EStylesheet.FLAT, '#cc5452'),
578 Theme(
579 'flat-dark-grey', N_('Flat dark grey'), True, EStylesheet.FLAT, '#aaaaaa'
581 Theme(
582 'flat-dark-green', N_('Flat dark green'), True, EStylesheet.FLAT, '#42a65c'
586 # check if themes path exists in user folder
587 path = resources.config_home('themes')
588 if not os.path.isdir(path):
589 return themes
591 # Gather Qt .qss stylesheet themes
592 try:
593 filenames = core.listdir(path)
594 except OSError:
595 return themes
597 for filename in filenames:
598 name, ext = os.path.splitext(filename)
599 if ext == '.qss':
600 themes.append(Theme(name, N_(name), False, EStylesheet.CUSTOM, None))
602 return themes
605 def options():
606 """Return a dictionary mapping display names to theme names"""
607 items = get_all_themes()
608 return [(item.hr_name, item.name) for item in items]
611 def find_theme(name):
612 themes = get_all_themes()
613 for item in themes:
614 if item.name == name:
615 return item
616 return themes[0]