1#!/usr/local/bin/python3.8
2# -*- coding: utf-8 -*-
3
4"""QDarkStyle is a dark stylesheet for Python and Qt applications.
5
6This module provides a function to load the stylesheets transparently
7with the right resources file.
8
9First, start importing our module
10
11.. code-block:: python
12
13    import qdarkstyle
14
15Then you can get stylesheet provided by QDarkStyle for various Qt wrappers
16as shown below
17
18.. code-block:: python
19
20    # PySide
21    dark_stylesheet = qdarkstyle.load_stylesheet_pyside()
22    # PySide 2
23    dark_stylesheet = qdarkstyle.load_stylesheet_pyside2()
24    # PyQt4
25    dark_stylesheet = qdarkstyle.load_stylesheet_pyqt()
26    # PyQt5
27    dark_stylesheet = qdarkstyle.load_stylesheet_pyqt5()
28
29Alternatively, from environment variables provided by QtPy, PyQtGraph, Qt.Py
30
31.. code-block:: python
32
33    # QtPy
34    dark_stylesheet = qdarkstyle.load_stylesheet()
35    # PyQtGraph
36    dark_stylesheet = qdarkstyle.load_stylesheet(qt_api=os.environ('PYQTGRAPH_QT_LIB'))
37    # Qt.Py
38    dark_stylesheet = qdarkstyle.load_stylesheet(qt_api=Qt.__binding__)
39
40Finally, set your QApplication with it
41
42.. code-block:: python
43
44    app.setStyleSheet(dark_stylesheet)
45
46Enjoy!
47
48"""
49
50# Standard library imports
51import logging
52import os
53import platform
54import warnings
55
56# Local imports
57from qdarkstyle.palette import DarkPalette
58
59__version__ = "2.8.1"
60
61_logger = logging.getLogger(__name__)
62
63# Folder's path
64REPO_PATH = os.path.dirname(os.path.abspath(os.path.dirname(__file__)))
65
66EXAMPLE_PATH = os.path.join(REPO_PATH, 'example')
67IMAGES_PATH = os.path.join(REPO_PATH, 'images')
68PACKAGE_PATH = os.path.join(REPO_PATH, 'qdarkstyle')
69
70QSS_PATH = os.path.join(PACKAGE_PATH, 'qss')
71RC_PATH = os.path.join(PACKAGE_PATH, 'rc')
72SVG_PATH = os.path.join(PACKAGE_PATH, 'svg')
73
74# File names
75QSS_FILE = 'style.qss'
76QRC_FILE = QSS_FILE.replace('.qss', '.qrc')
77
78MAIN_SCSS_FILE = 'main.scss'
79STYLES_SCSS_FILE = '_styles.scss'
80VARIABLES_SCSS_FILE = '_variables.scss'
81
82# File paths
83QSS_FILEPATH = os.path.join(PACKAGE_PATH, QSS_FILE)
84QRC_FILEPATH = os.path.join(PACKAGE_PATH, QRC_FILE)
85
86MAIN_SCSS_FILEPATH = os.path.join(QSS_PATH, MAIN_SCSS_FILE)
87STYLES_SCSS_FILEPATH = os.path.join(QSS_PATH, STYLES_SCSS_FILE)
88VARIABLES_SCSS_FILEPATH = os.path.join(QSS_PATH, VARIABLES_SCSS_FILE)
89
90# Todo: check if we are deprecate all those functions or keep them
91DEPRECATION_MSG = '''This function will be deprecated in v3.0.
92Please, set the wanted binding by using QtPy environment variable QT_API,
93then use load_stylesheet() or use load_stylesheet()
94passing the argument qt_api='wanted_binding'.'''
95
96
97def _apply_os_patches():
98    """
99    Apply OS-only specific stylesheet pacthes.
100
101    Returns:
102        str: stylesheet string (css).
103    """
104    os_fix = ""
105
106    if platform.system().lower() == 'darwin':
107        # See issue #12
108        os_fix = '''
109        QDockWidget::title
110        {{
111            background-color: {color};
112            text-align: center;
113            height: 12px;
114        }}
115        '''.format(color=DarkPalette.COLOR_BACKGROUND_NORMAL)
116
117    # Only open the QSS file if any patch is needed
118    if os_fix:
119        _logger.info("Found OS patches to be applied.")
120
121    return os_fix
122
123
124def _apply_binding_patches():
125    """
126    Apply binding-only specific stylesheet patches for the same OS.
127
128    Returns:
129        str: stylesheet string (css).
130    """
131    binding_fix = ""
132
133    if binding_fix:
134        _logger.info("Found binding patches to be applied.")
135
136    return binding_fix
137
138
139def _apply_version_patches():
140    """
141    Apply version-only specific stylesheet patches for the same binding.
142
143    Returns:
144        str: stylesheet string (css).
145    """
146    version_fix = ""
147
148    if version_fix:
149        _logger.info("Found version patches to be applied.")
150
151    return version_fix
152
153
154def _apply_application_patches(QCoreApplication, QPalette, QColor):
155    """
156    Apply application level fixes on the QPalette.
157
158    The import names args must be passed here because the import is done
159    inside the load_stylesheet() function, as QtPy is only imported in
160    that moment for setting reasons.
161    """
162    # See issue #139
163    color = DarkPalette.COLOR_SELECTION_LIGHT
164    qcolor = QColor(color)
165
166    # Todo: check if it is qcoreapplication indeed
167    app = QCoreApplication.instance()
168
169    _logger.info("Found application patches to be applied.")
170
171    if app:
172        palette = app.palette()
173        palette.setColor(QPalette.Normal, QPalette.Link, qcolor)
174        app.setPalette(palette)
175    else:
176        _logger.warn("No QCoreApplication instance found. "
177                     "Application patches not applied. "
178                     "You have to call load_stylesheet function after "
179                     "instantiation of QApplication to take effect. ")
180
181
182def _load_stylesheet(qt_api=''):
183    """
184    Load the stylesheet based on QtPy abstraction layer environment variable.
185
186    If the argument is not passed, it uses the current QT_API environment
187    variable to make the imports of Qt bindings. If passed, it sets this
188    variable then make the imports.
189
190    Args:
191        qt_api (str): qt binding name to set QT_API environment variable.
192                      Default is ''. Possible values are pyside, pyside2
193                      pyqt4, pyqt5. Not case sensitive.
194
195    Note:
196        - Note that the variable QT_API is read when first imported. So,
197          pay attention to the import order.
198        - If you are using another abstraction layer, i.e PyQtGraph to do
199          imports on Qt things you must set both to use the same Qt
200          binding (PyQt, PySide).
201        - OS, binding and binding version number, and application specific
202          patches are applied in this order.
203
204    Returns:
205        str: stylesheet string (css).
206    """
207
208    if qt_api:
209        os.environ['QT_API'] = qt_api
210
211    # Import is made after setting QT_API
212    from qtpy.QtCore import QCoreApplication, QFile, QTextStream
213    from qtpy.QtGui import QColor, QPalette
214
215    # Then we import resources - binary qrc content
216    from qdarkstyle import style_rc
217
218    # Thus, by importing the binary we can access the resources
219    package_dir = os.path.basename(PACKAGE_PATH)
220    qss_rc_path = ":" + os.path.join(package_dir, QSS_FILE)
221
222    _logger.debug("Reading QSS file in: %s" % qss_rc_path)
223
224    # It gets the qss file from compiled style_rc that was import
225    # not from the file QSS as we are using resources
226    qss_file = QFile(qss_rc_path)
227
228    if qss_file.exists():
229        qss_file.open(QFile.ReadOnly | QFile.Text)
230        text_stream = QTextStream(qss_file)
231        stylesheet = text_stream.readAll()
232        _logger.info("QSS file sucessfuly loaded.")
233    else:
234        stylesheet = ""
235        # Todo: check this raise type and add to docs
236        raise FileNotFoundError("Unable to find QSS file '{}' "
237                                "in resources.".format(qss_rc_path))
238
239    _logger.debug("Checking patches for being applied.")
240
241    # Todo: check execution order for these functions
242    # 1. Apply OS specific patches
243    stylesheet += _apply_os_patches()
244
245    # 2. Apply binding specific patches
246    stylesheet += _apply_binding_patches()
247
248    # 3. Apply binding version specific patches
249    stylesheet += _apply_version_patches()
250
251    # 4. Apply palette fix. See issue #139
252    _apply_application_patches(QCoreApplication, QPalette, QColor)
253
254    return stylesheet
255
256
257def load_stylesheet(*args, **kwargs):
258    """
259    Load the stylesheet. Takes care of importing the rc module.
260
261    Args:
262        pyside (bool): True to load the PySide (or PySide2) rc file,
263                       False to load the PyQt4 (or PyQt5) rc file.
264                       Default is False.
265        or
266
267        qt_api (str): Qt binding name to set QT_API environment variable.
268                      Default is '', i.e PyQt5 the default QtPy binding.
269                      Possible values are pyside, pyside2 pyqt4, pyqt5.
270                      Not case sensitive.
271
272    Raises:
273        TypeError: If arguments do not match: type, keyword name nor quantity.
274
275    Returns:
276        str: the stylesheet string.
277    """
278
279    stylesheet = ""
280    arg = None
281
282    try:
283        arg = args[0]
284    except IndexError:
285        # It is already none
286        pass
287
288    # Number of arguments are wrong
289    if (kwargs and args) or len(args) > 1 or len(kwargs) > 1:
290        raise TypeError("load_stylesheet() takes zero or one argument: "
291                        "(new) string type qt_api='pyqt5' or "
292                        "(old) boolean type pyside='False'.")
293
294    # No arguments
295    if not kwargs and not args:
296        stylesheet = _load_stylesheet(qt_api='pyqt5')
297
298    # Old API arguments
299    elif 'pyside' in kwargs or isinstance(arg, bool):
300        pyside = kwargs.get('pyside', arg)
301
302        if pyside:
303            stylesheet = _load_stylesheet(qt_api='pyside2')
304            if not stylesheet:
305                stylesheet = _load_stylesheet(qt_api='pyside')
306
307        else:
308            stylesheet = _load_stylesheet(qt_api='pyqt5')
309            if not stylesheet:
310                stylesheet = _load_stylesheet(qt_api='pyqt4')
311
312        # Deprecation warning only for old API
313        warnings.warn(DEPRECATION_MSG, DeprecationWarning)
314
315    # New API arguments
316    elif 'qt_api' in kwargs or isinstance(arg, str):
317        qt_api = kwargs.get('qt_api', arg)
318        stylesheet = _load_stylesheet(qt_api=qt_api)
319
320    # Wrong API arguments name or type
321    else:
322        raise TypeError("load_stylesheet() takes only zero or one argument: "
323                        "(new) string type qt_api='pyqt5' or "
324                        "(old) boolean type pyside='False'.")
325
326    return stylesheet
327
328
329def load_stylesheet_pyside():
330    """
331    Load the stylesheet for use in a PySide application.
332
333    Returns:
334        str: the stylesheet string.
335    """
336    return _load_stylesheet(qt_api='pyside')
337
338
339def load_stylesheet_pyside2():
340    """
341    Load the stylesheet for use in a PySide2 application.
342
343    Returns:
344        str: the stylesheet string.
345    """
346    return _load_stylesheet(qt_api='pyside2')
347
348
349def load_stylesheet_pyqt():
350    """
351    Load the stylesheet for use in a PyQt4 application.
352
353    Returns:
354        str: the stylesheet string.
355    """
356    return _load_stylesheet(qt_api='pyqt4')
357
358
359def load_stylesheet_pyqt5():
360    """
361    Load the stylesheet for use in a PyQt5 application.
362
363    Returns:
364        str: the stylesheet string.
365    """
366    return _load_stylesheet(qt_api='pyqt5')
367
368
369# Deprecation Warning --------------------------------------------------------
370
371
372def load_stylesheet_from_environment(is_pyqtgraph=False):
373    """
374    Load the stylesheet from QT_API (or PYQTGRAPH_QT_LIB) environment variable.
375
376    Args:
377        is_pyqtgraph (bool): True if it is to be set using PYQTGRAPH_QT_LIB.
378
379    Raises:
380        KeyError: if PYQTGRAPH_QT_LIB does not exist.
381
382    Returns:
383        str: the stylesheet string.
384    """
385    warnings.warn(DEPRECATION_MSG, DeprecationWarning)
386
387    if is_pyqtgraph:
388        stylesheet = _load_stylesheet(qt_api=os.environ('PYQTGRAPH_QT_LIB'))
389    else:
390        stylesheet = _load_stylesheet()
391
392    return stylesheet
393
394
395# Deprecated ----------------------------------------------------------------
396