1#!/usr/bin/env python
2# -*- coding: utf-8 -*-
3
4"""QDarkStyle is a dark stylesheet for Python and Qt applications.
5
6This module provides a function to transparently load the stylesheets
7with the correct rc 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 bellow
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
29Or from environment variables provided for QtPy or PyQtGraph, see
30
31.. code-block:: python
32
33    # QtPy
34    dark_stylesheet = qdarkstyle.load_stylesheet_from_environment()
35    # PyQtGraph
36    dark_stylesheet = qdarkstyle.load_stylesheet_from_environment(is_pyqtgraph)
37
38Finally, set your QApplication with it
39
40.. code-block:: python
41
42    app.setStyleSheet(dark_stylesheet)
43
44Enjoy!
45
46"""
47
48import logging
49import os
50import platform
51import sys
52import warnings
53import copy
54
55if sys.version_info >= (3, 4):
56    import importlib
57
58__version__ = "2.6.5"
59
60
61QT_BINDINGS = ['PyQt4', 'PyQt5', 'PySide', 'PySide2']
62"""list: values of all Qt bindings to import."""
63
64QT_ABSTRACTIONS = ['qtpy', 'pyqtgraph', 'Qt']
65"""list: values of all Qt abstraction layers to import."""
66
67QT4_IMPORT_API = ['QtCore', 'QtGui']
68"""list: which subpackage to import for Qt4 API."""
69
70QT5_IMPORT_API = ['QtCore', 'QtGui', 'QtWidgets']
71"""list: which subpackage to import for Qt5 API."""
72
73QT_API_VALUES = ['pyqt', 'pyqt5', 'pyside', 'pyside2']
74"""list: values for QT_API environment variable used by QtPy."""
75
76QT_LIB_VALUES = ['PyQt', 'PyQt5', 'PySide', 'PySide2']
77"""list: values for PYQTGRAPH_QT_LIB environment variable used by PyQtGraph."""
78
79QT_BINDING = 'Not set or nonexistent'
80"""str: Qt binding in use."""
81
82QT_ABSTRACTION = 'Not set or nonexistent'
83"""str: Qt abstraction layer in use."""
84
85
86def _logger():
87    return logging.getLogger('qdarkstyle')
88
89
90def _qt_wrapper_import(qt_api):
91    """
92    Check if Qt API defined can be imported.
93
94    :param qt_api: Qt API string to test import
95
96    :return load function fot given qt_api, otherwise empty string
97
98    """
99    qt_wrapper = ''
100    loader = ""
101
102    try:
103        if qt_api == 'PyQt' or qt_api == 'pyqt':
104            import PyQt4
105            qt_wrapper = 'PyQt4'
106            loader = load_stylesheet_pyqt()
107        elif qt_api == 'PyQt5' or qt_api == 'pyqt5':
108            import PyQt5
109            qt_wrapper = 'PyQt5'
110            loader = load_stylesheet_pyqt5()
111        elif qt_api == 'PySide' or qt_api == 'pyside':
112            import PySide
113            qt_wrapper = 'PySide'
114            loader = load_stylesheet_pyside()
115        elif qt_api == 'PySide2' or qt_api == 'pyside2':
116            import PySide2
117            qt_wrapper = 'PySide2'
118            loader = load_stylesheet_pyside2()
119    except ImportError as err:
120        _logger().error("Impossible import Qt wrapper.\n %s", str(err))
121    else:
122        _logger().info("Using Qt wrapper = %s ", qt_wrapper)
123        QT_BINDING = qt_wrapper
124    finally:
125        return loader
126
127
128def load_stylesheet_from_environment(is_pyqtgraph=False):
129    """
130    Load the stylesheet from QT_API (or PYQTGRAPH_QT_LIB) environment variable.
131
132    :param is_pyqtgraph: True if it is to be set using PYQTGRAPH_QT_LIB
133
134    :raise KeyError: if QT_API/PYQTGRAPH_QT_LIB does not exist
135
136    :return the stylesheet string
137    """
138    warnings.warn(
139        "load_stylesheet_from_environment() will be deprecated in version 3,"
140        "use load_stylesheet()",
141        PendingDeprecationWarning
142    )
143    qt_api = ''
144    pyqtgraph_qt_lib = ''
145
146    loader = ""
147
148    # Get values from QT_API
149    try:
150        qt_api = os.environ['QT_API']
151    except KeyError as err:
152        # Log this error just if using QT_API
153        if not is_pyqtgraph:
154            _logger().error("QT_API does not exist, do os.environ['QT_API']= "
155                            "and choose one option from %s", QT_API_VALUES)
156    else:
157        if not is_pyqtgraph:
158            if qt_api in QT_API_VALUES:
159                QT_ABSTRACTION = "qtpy"
160                _logger().info("Found QT_API='%s'", qt_api)
161                loader = _qt_wrapper_import(qt_api)
162            else:
163                # Raise this error because the function need this key/value
164                raise KeyError("QT_API=%s is unknown, please use a value "
165                               "from %s",
166                               (qt_api, QT_API_VALUES))
167
168    # Get values from PYQTGRAPH_QT_LIB
169    try:
170        pyqtgraph_qt_lib = os.environ['PYQTGRAPH_QT_LIB']
171    except KeyError as err:
172        # Log this error just if using PYQTGRAPH_QT_LIB
173        if is_pyqtgraph:
174            _logger().error("PYQTGRAP_QT_API does not exist, do "
175                            "os.environ['PYQTGRAPH_QT_LIB']= "
176                            "and choose one option from %s",
177                            QT_LIB_VALUES)
178    else:
179
180        if is_pyqtgraph:
181            if pyqtgraph_qt_lib in QT_LIB_VALUES:
182                QT_ABSTRACTION = "pyqtgraph"
183                _logger().info("Found PYQTGRAPH_QT_LIB='%s'", pyqtgraph_qt_lib)
184                loader = _qt_wrapper_import(pyqtgraph_qt_lib)
185            else:
186                # Raise this error because the function need this key/value
187                raise KeyError("PYQTGRAPH_QT_LIB=%s is unknown, please use a "
188                               "value from %s", (
189                                   pyqtgraph_qt_lib,
190                                   QT_LIB_VALUES))
191
192    # Just a warning if both are set but differs each other
193    if qt_api and pyqtgraph_qt_lib:
194        if qt_api != pyqtgraph_qt_lib.lower():
195            _logger().warning("Both QT_API=%s and PYQTGRAPH_QT_LIB=%s are set, "
196                              "but with different values, this could cause "
197                              "some issues if using them in the same project!",
198                              qt_api, pyqtgraph_qt_lib)
199
200    return loader
201
202
203def load_stylesheet(pyside=True):
204    """
205    Load the stylesheet. Takes care of importing the rc module.
206
207    :param pyside: True to load the pyside rc file, False to load the PyQt rc file
208
209    :return the stylesheet string
210    """
211    warnings.warn(
212        "load_stylesheet() will not receive pyside parameter in version 3. "
213        "Set QtPy environment variable to specify the Qt binding insteady.",
214        FutureWarning
215    )
216    # Smart import of the rc file
217
218    pyside_ver = None
219
220    if pyside:
221
222        # Detect the PySide version available
223        try:
224            import PySide
225        except ImportError: # Compatible with py27
226            import PySide2
227            pyside_ver = 2
228        else:
229            pyside_ver = 1
230
231        if pyside_ver == 1:
232            import qdarkstyle.pyside_style_rc
233        else:
234            import qdarkstyle.pyside2_style_rc
235    else:
236        import qdarkstyle.pyqt_style_rc
237
238    # Load the stylesheet content from resources
239    if not pyside:
240        from PyQt4.QtCore import QFile, QTextStream
241    else:
242        if pyside_ver == 1:
243            from PySide.QtCore import QFile, QTextStream
244        else:
245            from PySide2.QtCore import QFile, QTextStream
246
247    f = QFile(":qdarkstyle/style.qss")
248    if not f.exists():
249        _logger().error("Unable to load stylesheet, file not found in "
250                        "resources")
251        return ""
252    else:
253        f.open(QFile.ReadOnly | QFile.Text)
254        ts = QTextStream(f)
255        stylesheet = ts.readAll()
256        if platform.system().lower() == 'darwin':  # see issue #12 on github
257            mac_fix = '''
258            QDockWidget::title
259            {
260                background-color: #32414B;
261                text-align: center;
262                height: 12px;
263            }
264            '''
265            stylesheet += mac_fix
266        return stylesheet
267
268
269def load_stylesheet_pyside():
270    """
271    Load the stylesheet for use in a pyside application.
272
273    :return the stylesheet string
274    """
275    warnings.warn(
276        "load_stylesheet_pyside() will be deprecated in version 3,"
277        "set QtPy environment variable to specify the Qt binding and "
278        "use load_stylesheet()",
279        PendingDeprecationWarning
280    )
281    return load_stylesheet(pyside=True)
282
283
284def load_stylesheet_pyside2():
285    """
286    Load the stylesheet for use in a pyside2 application.
287
288    :raise NotImplementedError: Because it is not supported yet
289    """
290    warnings.warn(
291        "load_stylesheet_pyside2() will be deprecated in version 3,"
292        "set QtPy environment variable to specify the Qt binding and "
293        "use load_stylesheet()",
294        PendingDeprecationWarning
295    )
296    return load_stylesheet(pyside=True)
297
298
299def load_stylesheet_pyqt():
300    """
301    Load the stylesheet for use in a pyqt4 application.
302
303    :return the stylesheet string
304    """
305    warnings.warn(
306        "load_stylesheet_pyqt() will be deprecated in version 3,"
307        "set QtPy environment variable to specify the Qt binding and "
308        "use load_stylesheet()",
309        PendingDeprecationWarning
310    )
311    return load_stylesheet(pyside=False)
312
313
314def load_stylesheet_pyqt5():
315    """
316    Load the stylesheet for use in a pyqt5 application.
317
318    :param pyside: True to load the pyside rc file, False to load the PyQt rc file
319
320    :return the stylesheet string
321    """
322    warnings.warn(
323        "load_stylesheet_pyqt5() will be deprecated in version 3,"
324        "set QtPy environment variable to specify the Qt binding and "
325        "use load_stylesheet()",
326        PendingDeprecationWarning
327    )
328    # Smart import of the rc file
329    import qdarkstyle.pyqt5_style_rc
330
331    # Load the stylesheet content from resources
332    from PyQt5.QtCore import QFile, QTextStream
333
334    f = QFile(":qdarkstyle/style.qss")
335    if not f.exists():
336        _logger().error("Unable to load stylesheet, file not found in "
337                        "resources")
338        return ""
339    else:
340        f.open(QFile.ReadOnly | QFile.Text)
341        ts = QTextStream(f)
342        stylesheet = ts.readAll()
343        if platform.system().lower() == 'darwin':  # see issue #12 on github
344            mac_fix = '''
345            QDockWidget::title
346            {
347                background-color: #32414B;
348                text-align: center;
349                height: 12px;
350            }
351            '''
352            stylesheet += mac_fix
353        return stylesheet
354
355
356def information():
357    """Get system and runtime information."""
358    info = []
359    qt_api = ''
360    qt_lib = ''
361    qt_bin = ''
362
363    try:
364        qt_api = os.environ['QT_API']
365    except KeyError:
366        qt_api = 'Not set or nonexistent'
367
368    try:
369        from Qt import __binding__
370    except Exception:
371        # It should be (KeyError, ModuleNotFoundError, ImportError)
372        # but each python version have a different one, and not define others
373        qt_lib = 'Not set or nonexistent'
374    else:
375        qt_lib = __binding__
376
377    try:
378        qt_bin = os.environ['PYQTGRAPH_QT_LIB']
379    except KeyError:
380        qt_bin = 'Not set or nonexistent'
381
382    info.append('QDarkStyle: %s' % __version__)
383    info.append('OS: %s %s %s' % (platform.system(), platform.release(), platform.machine()))
384    info.append('Platform: %s' % sys.platform)
385    info.append('Python: %s' % '.'.join(str(e) for e in sys.version_info[:]))
386    info.append('Python API: %s' % sys.api_version)
387
388    info.append('Binding in use:     %s' % QT_BINDING)
389    info.append('Abstraction in use: %s' % QT_ABSTRACTION)
390
391    info.append('qtpy (QT_API):                %s' % qt_api)
392    info.append('pyqtgraph (PYQTGRAPH_QT_LIB): %s' % qt_lib)
393    info.append('Qt.py (__binding__):          %s' % qt_bin)
394
395    return info
396
397
398def qt_bindings():
399    """Return a list of qt bindings available."""
400    return _check_imports(import_list=QT_BINDINGS)
401
402
403def qt_abstractions():
404    """Return a list of qt abstraction layers available."""
405    return _check_imports(import_list=QT_ABSTRACTIONS)
406
407
408def _check_imports(import_list):
409    """Return a list of imports available."""
410
411    # Disable warnings here
412    warnings.filterwarnings("ignore")
413
414    import_list_return = copy.deepcopy(import_list)
415    # Using import_list_return var in for, does not work in py2.7
416    # when removing the element, it reflects on for list
417    # so it skips next element
418    for current_import in import_list:
419
420        spec = True
421        # Copy the sys path to make sure to not insert anything
422        sys_path = sys.path
423
424        # Check import
425        if sys.version_info >= (3, 4):
426            spec = importlib.util.find_spec(current_import)
427        else:
428            try:
429                __import__(current_import)
430            except RuntimeWarning:
431                spec = True
432            except Exception:
433                spec = None
434            else:
435                spec = True
436
437        if spec is None:
438            # Remove if not available
439            import_list_return.remove(current_import)
440
441        # Restore sys path
442        sys.path = sys_path
443
444    # Restore warnings
445    warnings.resetwarnings()
446
447    return import_list_return
448
449
450def _import_qt_modules_from(use_binding='pyqt5', use_abstraction='qtpy'):
451    """New approach to import modules using importlib."""
452
453    if not sys.version_info >= (3, 4):
454        print('Function not available for Python < 3.4')
455
456    spec_binding = importlib.util.find_spec(use_binding)
457    spec_abstraction = importlib.util.find_spec(use_abstraction)
458
459    if spec_binding is None:
460        print("Cannot find Qt binding: ", use_binding)
461    else:
462        module = importlib.util.module_from_spec(spec_binding)
463        spec.loader.exec_module(module)
464        # Adding the module to sys.modules is optional.
465        sys.modules[name] = module
466
467    if spec_abstraction is None:
468        print("Cannot find Qt abstraction layer: ", use_abstraction)
469    else:
470        module = importlib.util.module_from_spec(spec)
471        spec.loader.exec_module(module)
472        # Adding the module to sys.modules is optional.
473        sys.modules[name] = module
474