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