├── .codecov.yml ├── .coveragerc ├── .flake8 ├── .gitattributes ├── .gitignore ├── .pylintrc ├── .travis.yml ├── AUTHORS.rst ├── CONTRIBUTING.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── conda-recipe ├── build.sh └── meta.yaml ├── dev-requirements.txt ├── docs ├── Makefile ├── make.bat └── source │ ├── api.rst │ ├── conf.py │ └── index.rst ├── github_deploy_key_klauer_qtpydocking.enc ├── qtpydocking ├── __init__.py ├── _version.py ├── default.css ├── default_linux.css ├── dock_area_layout.py ├── dock_area_tab_bar.py ├── dock_area_title_bar.py ├── dock_area_widget.py ├── dock_container_widget.py ├── dock_manager.py ├── dock_overlay.py ├── dock_splitter.py ├── dock_widget.py ├── dock_widget_tab.py ├── eliding_label.py ├── enums.py ├── examples │ ├── __init__.py │ ├── demo.py │ └── simple.py ├── floating_dock_container.py ├── floating_widget_title_bar.py ├── tests │ ├── __init__.py │ ├── conftest.py │ └── test_examples.py └── util.py ├── requirements.txt ├── run_tests.py ├── setup.cfg ├── setup.py └── versioneer.py /.codecov.yml: -------------------------------------------------------------------------------- 1 | # show coverage in CI status, not as a comment. 2 | comment: off 3 | coverage: 4 | status: 5 | project: 6 | default: 7 | target: auto 8 | patch: 9 | default: 10 | target: auto 11 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = 3 | qtpydocking 4 | [report] 5 | omit = 6 | #versioning 7 | .*version.* 8 | *_version.py 9 | #tests 10 | *test* 11 | qtpydocking/tests/* 12 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = E303, E501, E225, E226, E271 3 | exclude = .git, qtpydocking/*.py 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | qtpydocking/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | venv/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *,cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/build/ 58 | docs/source/generated/ 59 | 60 | # pytest 61 | .pytest_cache/ 62 | 63 | # PyBuilder 64 | target/ 65 | 66 | # Editor files 67 | # OSX stuff 68 | .DS_Store 69 | *~ 70 | 71 | #vim 72 | *.sw[op] 73 | 74 | #pycharm 75 | .idea/* 76 | 77 | 78 | #Ipython Notebook 79 | .ipynb_checkpoints 80 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MASTER] 2 | 3 | # A comma-separated list of package or module names from where C extensions may 4 | # be loaded. Extensions are loading into the active Python interpreter and may 5 | # run arbitrary code. 6 | extension-pkg-whitelist=qtpy,PyQt5 7 | 8 | # Add files or directories to the blacklist. They should be base names, not 9 | # paths. 10 | ignore=CVS 11 | 12 | # Add files or directories matching the regex patterns to the blacklist. The 13 | # regex matches against base names, not paths. 14 | ignore-patterns= 15 | 16 | # Python code to execute, usually for sys.path manipulation such as 17 | # pygtk.require(). 18 | #init-hook= 19 | 20 | # Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the 21 | # number of processors available to use. 22 | jobs=1 23 | 24 | # Control the amount of potential inferred values when inferring a single 25 | # object. This can help the performance when dealing with large functions or 26 | # complex, nested conditions. 27 | limit-inference-results=1000 28 | 29 | # List of plugins (as comma separated values of python modules names) to load, 30 | # usually to register additional checkers. 31 | load-plugins= 32 | 33 | # Pickle collected data for later comparisons. 34 | persistent=yes 35 | 36 | # Specify a configuration file. 37 | #rcfile= 38 | 39 | # When enabled, pylint would attempt to guess common misconfiguration and emit 40 | # user-friendly hints instead of false-positive error messages. 41 | suggestion-mode=yes 42 | 43 | # Allow loading of arbitrary C extensions. Extensions are imported into the 44 | # active Python interpreter and may run arbitrary code. 45 | unsafe-load-any-extension=yes 46 | 47 | 48 | [MESSAGES CONTROL] 49 | 50 | # Only show warnings with the listed confidence levels. Leave empty to show 51 | # all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. 52 | confidence= 53 | 54 | # Disable the message, report, category or checker with the given id(s). You 55 | # can either give multiple identifiers separated by comma (,) or put this 56 | # option multiple times (only on the command line, not in the configuration 57 | # file where it should appear only once). You can also use "--disable=all" to 58 | # disable everything first and then reenable specific checks. For example, if 59 | # you want to run only the similarities checker, you can use "--disable=all 60 | # --enable=similarities". If you want to run only the classes checker, but have 61 | # no Warning level messages displayed, use "--disable=all --enable=classes 62 | # --disable=W". 63 | disable=invalid-name, 64 | too-many-public-methods, 65 | too-few-public-methods, 66 | missing-docstring, 67 | line-too-long, 68 | print-statement, 69 | parameter-unpacking, 70 | unpacking-in-except, 71 | old-raise-syntax, 72 | backtick, 73 | long-suffix, 74 | old-ne-operator, 75 | old-octal-literal, 76 | import-star-module-level, 77 | non-ascii-bytes-literal, 78 | raw-checker-failed, 79 | bad-inline-option, 80 | locally-disabled, 81 | file-ignored, 82 | suppressed-message, 83 | useless-suppression, 84 | deprecated-pragma, 85 | use-symbolic-message-instead, 86 | apply-builtin, 87 | basestring-builtin, 88 | buffer-builtin, 89 | cmp-builtin, 90 | coerce-builtin, 91 | execfile-builtin, 92 | file-builtin, 93 | long-builtin, 94 | raw_input-builtin, 95 | reduce-builtin, 96 | standarderror-builtin, 97 | unicode-builtin, 98 | xrange-builtin, 99 | coerce-method, 100 | delslice-method, 101 | getslice-method, 102 | setslice-method, 103 | no-absolute-import, 104 | old-division, 105 | dict-iter-method, 106 | dict-view-method, 107 | next-method-called, 108 | metaclass-assignment, 109 | indexing-exception, 110 | raising-string, 111 | reload-builtin, 112 | oct-method, 113 | hex-method, 114 | nonzero-method, 115 | cmp-method, 116 | input-builtin, 117 | round-builtin, 118 | intern-builtin, 119 | unichr-builtin, 120 | map-builtin-not-iterating, 121 | zip-builtin-not-iterating, 122 | range-builtin-not-iterating, 123 | filter-builtin-not-iterating, 124 | using-cmp-argument, 125 | eq-without-hash, 126 | div-method, 127 | idiv-method, 128 | rdiv-method, 129 | exception-message-attribute, 130 | invalid-str-codec, 131 | sys-max-int, 132 | bad-python3-import, 133 | deprecated-string-function, 134 | deprecated-str-translate-call, 135 | deprecated-itertools-function, 136 | deprecated-types-field, 137 | next-method-defined, 138 | dict-items-not-iterating, 139 | dict-keys-not-iterating, 140 | dict-values-not-iterating, 141 | deprecated-operator-function, 142 | deprecated-urllib-function, 143 | xreadlines-attribute, 144 | deprecated-sys-function, 145 | exception-escape, 146 | comprehension-escape 147 | 148 | # Enable the message, report, category or checker with the given id(s). You can 149 | # either give multiple identifier separated by comma (,) or put this option 150 | # multiple time (only on the command line, not in the configuration file where 151 | # it should appear only once). See also the "--disable" option for examples. 152 | enable=c-extension-no-member 153 | 154 | 155 | [REPORTS] 156 | 157 | # Python expression which should return a note less than 10 (10 is the highest 158 | # note). You have access to the variables errors warning, statement which 159 | # respectively contain the number of errors / warnings messages and the total 160 | # number of statements analyzed. This is used by the global evaluation report 161 | # (RP0004). 162 | evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) 163 | 164 | # Template used to display messages. This is a python new-style format string 165 | # used to format the message information. See doc for all details. 166 | #msg-template= 167 | 168 | # Set the output format. Available formats are text, parseable, colorized, json 169 | # and msvs (visual studio). You can also give a reporter class, e.g. 170 | # mypackage.mymodule.MyReporterClass. 171 | output-format=text 172 | 173 | # Tells whether to display a full report or only the messages. 174 | reports=no 175 | 176 | # Activate the evaluation score. 177 | score=yes 178 | 179 | 180 | [REFACTORING] 181 | 182 | # Maximum number of nested blocks for function / method body 183 | max-nested-blocks=5 184 | 185 | # Complete name of functions that never returns. When checking for 186 | # inconsistent-return-statements if a never returning function is called then 187 | # it will be considered as an explicit return statement and no message will be 188 | # printed. 189 | never-returning-functions=sys.exit 190 | 191 | 192 | [LOGGING] 193 | 194 | # Format style used to check logging format string. `old` means using % 195 | # formatting, while `new` is for `{}` formatting. 196 | logging-format-style=old 197 | 198 | # Logging modules to check that the string format arguments are in logging 199 | # function parameter format. 200 | logging-modules=logging 201 | 202 | 203 | [SPELLING] 204 | 205 | # Limits count of emitted suggestions for spelling mistakes. 206 | max-spelling-suggestions=4 207 | 208 | # Spelling dictionary name. Available dictionaries: none. To make it working 209 | # install python-enchant package.. 210 | spelling-dict= 211 | 212 | # List of comma separated words that should not be checked. 213 | spelling-ignore-words= 214 | 215 | # A path to a file that contains private dictionary; one word per line. 216 | spelling-private-dict-file= 217 | 218 | # Tells whether to store unknown words to indicated private dictionary in 219 | # --spelling-private-dict-file option instead of raising a message. 220 | spelling-store-unknown-words=no 221 | 222 | 223 | [MISCELLANEOUS] 224 | 225 | # List of note tags to take in consideration, separated by a comma. 226 | notes=FIXME, 227 | XXX, 228 | TODO 229 | 230 | 231 | [TYPECHECK] 232 | 233 | # List of decorators that produce context managers, such as 234 | # contextlib.contextmanager. Add to this list to register other decorators that 235 | # produce valid context managers. 236 | contextmanager-decorators=contextlib.contextmanager 237 | 238 | # List of members which are set dynamically and missed by pylint inference 239 | # system, and so shouldn't trigger E1101 when accessed. Python regular 240 | # expressions are accepted. 241 | generated-members= 242 | 243 | # Tells whether missing members accessed in mixin class should be ignored. A 244 | # mixin class is detected if its name ends with "mixin" (case insensitive). 245 | ignore-mixin-members=yes 246 | 247 | # Tells whether to warn about missing members when the owner of the attribute 248 | # is inferred to be None. 249 | ignore-none=yes 250 | 251 | # This flag controls whether pylint should warn about no-member and similar 252 | # checks whenever an opaque object is returned when inferring. The inference 253 | # can return multiple potential results while evaluating a Python object, but 254 | # some branches might not be evaluated, which results in partial inference. In 255 | # that case, it might be useful to still emit no-member and other checks for 256 | # the rest of the inferred objects. 257 | ignore-on-opaque-inference=yes 258 | 259 | # List of class names for which member attributes should not be checked (useful 260 | # for classes with dynamically set attributes). This supports the use of 261 | # qualified names. 262 | ignored-classes=optparse.Values,thread._local,_thread._local 263 | 264 | # List of module names for which member attributes should not be checked 265 | # (useful for modules/projects where namespaces are manipulated during runtime 266 | # and thus existing member attributes cannot be deduced by static analysis. It 267 | # supports qualified module names, as well as Unix pattern matching. 268 | ignored-modules= 269 | 270 | # Show a hint with possible names when a member name was not found. The aspect 271 | # of finding the hint is based on edit distance. 272 | missing-member-hint=yes 273 | 274 | # The minimum edit distance a name should have in order to be considered a 275 | # similar match for a missing member name. 276 | missing-member-hint-distance=1 277 | 278 | # The total number of similar names that should be taken in consideration when 279 | # showing a hint for a missing member. 280 | missing-member-max-choices=1 281 | 282 | 283 | [VARIABLES] 284 | 285 | # List of additional names supposed to be defined in builtins. Remember that 286 | # you should avoid defining new builtins when possible. 287 | additional-builtins= 288 | 289 | # Tells whether unused global variables should be treated as a violation. 290 | allow-global-unused-variables=yes 291 | 292 | # List of strings which can identify a callback function by name. A callback 293 | # name must start or end with one of those strings. 294 | callbacks=cb_, 295 | _cb 296 | 297 | # A regular expression matching the name of dummy variables (i.e. expected to 298 | # not be used). 299 | dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ 300 | 301 | # Argument names that match this expression will be ignored. Default to name 302 | # with leading underscore. 303 | ignored-argument-names=_.*|^ignored_|^unused_ 304 | 305 | # Tells whether we should check for unused import in __init__ files. 306 | init-import=no 307 | 308 | # List of qualified module names which can have objects that can redefine 309 | # builtins. 310 | redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io 311 | 312 | 313 | [FORMAT] 314 | 315 | # Expected format of line ending, e.g. empty (any line ending), LF or CRLF. 316 | expected-line-ending-format= 317 | 318 | # Regexp for a line that is allowed to be longer than the limit. 319 | ignore-long-lines=^\s*(# )??$ 320 | 321 | # Number of spaces of indent required inside a hanging or continued line. 322 | indent-after-paren=4 323 | 324 | # String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 325 | # tab). 326 | indent-string=' ' 327 | 328 | # Maximum number of characters on a single line. 329 | max-line-length=100 330 | 331 | # Maximum number of lines in a module. 332 | max-module-lines=1000 333 | 334 | # List of optional constructs for which whitespace checking is disabled. `dict- 335 | # separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. 336 | # `trailing-comma` allows a space between comma and closing bracket: (a, ). 337 | # `empty-line` allows space-only lines. 338 | no-space-check=trailing-comma, 339 | dict-separator 340 | 341 | # Allow the body of a class to be on the same line as the declaration if body 342 | # contains single statement. 343 | single-line-class-stmt=no 344 | 345 | # Allow the body of an if to be on the same line as the test if there is no 346 | # else. 347 | single-line-if-stmt=no 348 | 349 | 350 | [SIMILARITIES] 351 | 352 | # Ignore comments when computing similarities. 353 | ignore-comments=yes 354 | 355 | # Ignore docstrings when computing similarities. 356 | ignore-docstrings=yes 357 | 358 | # Ignore imports when computing similarities. 359 | ignore-imports=no 360 | 361 | # Minimum lines number of a similarity. 362 | min-similarity-lines=4 363 | 364 | 365 | [BASIC] 366 | 367 | # Naming style matching correct argument names. 368 | argument-naming-style=snake_case 369 | 370 | # Regular expression matching correct argument names. Overrides argument- 371 | # naming-style. 372 | #argument-rgx= 373 | 374 | # Naming style matching correct attribute names. 375 | attr-naming-style=snake_case 376 | 377 | # Regular expression matching correct attribute names. Overrides attr-naming- 378 | # style. 379 | #attr-rgx= 380 | 381 | # Bad variable names which should always be refused, separated by a comma. 382 | bad-names=foo, 383 | baz, 384 | toto, 385 | tutu, 386 | tata 387 | 388 | # Naming style matching correct class attribute names. 389 | class-attribute-naming-style=any 390 | 391 | # Regular expression matching correct class attribute names. Overrides class- 392 | # attribute-naming-style. 393 | #class-attribute-rgx= 394 | 395 | # Naming style matching correct class names. 396 | class-naming-style=PascalCase 397 | 398 | # Regular expression matching correct class names. Overrides class-naming- 399 | # style. 400 | #class-rgx= 401 | 402 | # Naming style matching correct constant names. 403 | const-naming-style=UPPER_CASE 404 | 405 | # Regular expression matching correct constant names. Overrides const-naming- 406 | # style. 407 | #const-rgx= 408 | 409 | # Minimum line length for functions/classes that require docstrings, shorter 410 | # ones are exempt. 411 | docstring-min-length=-1 412 | 413 | # Naming style matching correct function names. 414 | function-naming-style=snake_case 415 | 416 | # Regular expression matching correct function names. Overrides function- 417 | # naming-style. 418 | #function-rgx= 419 | 420 | # Good variable names which should always be accepted, separated by a comma. 421 | good-names=i, 422 | j, 423 | k, 424 | ex, 425 | Run, 426 | _ 427 | 428 | # Include a hint for the correct naming format with invalid-name. 429 | include-naming-hint=no 430 | 431 | # Naming style matching correct inline iteration names. 432 | inlinevar-naming-style=any 433 | 434 | # Regular expression matching correct inline iteration names. Overrides 435 | # inlinevar-naming-style. 436 | #inlinevar-rgx= 437 | 438 | # Naming style matching correct method names. 439 | method-naming-style=snake_case 440 | 441 | # Regular expression matching correct method names. Overrides method-naming- 442 | # style. 443 | #method-rgx= 444 | 445 | # Naming style matching correct module names. 446 | module-naming-style=snake_case 447 | 448 | # Regular expression matching correct module names. Overrides module-naming- 449 | # style. 450 | #module-rgx= 451 | 452 | # Colon-delimited sets of names that determine each other's naming style when 453 | # the name regexes allow several styles. 454 | name-group= 455 | 456 | # Regular expression which should only match function or class names that do 457 | # not require a docstring. 458 | no-docstring-rgx=^_ 459 | 460 | # List of decorators that produce properties, such as abc.abstractproperty. Add 461 | # to this list to register other decorators that produce valid properties. 462 | # These decorators are taken in consideration only for invalid-name. 463 | property-classes=abc.abstractproperty 464 | 465 | # Naming style matching correct variable names. 466 | variable-naming-style=snake_case 467 | 468 | # Regular expression matching correct variable names. Overrides variable- 469 | # naming-style. 470 | #variable-rgx= 471 | 472 | 473 | [STRING] 474 | 475 | # This flag controls whether the implicit-str-concat-in-sequence should 476 | # generate a warning on implicit string concatenation in sequences defined over 477 | # several lines. 478 | check-str-concat-over-line-jumps=no 479 | 480 | 481 | [IMPORTS] 482 | 483 | # Allow wildcard imports from modules that define __all__. 484 | allow-wildcard-with-all=no 485 | 486 | # Analyse import fallback blocks. This can be used to support both Python 2 and 487 | # 3 compatible code, which means that the block might have code that exists 488 | # only in one or another interpreter, leading to false positives when analysed. 489 | analyse-fallback-blocks=no 490 | 491 | # Deprecated modules which should not be used, separated by a comma. 492 | deprecated-modules=optparse,tkinter.tix 493 | 494 | # Create a graph of external dependencies in the given file (report RP0402 must 495 | # not be disabled). 496 | ext-import-graph= 497 | 498 | # Create a graph of every (i.e. internal and external) dependencies in the 499 | # given file (report RP0402 must not be disabled). 500 | import-graph= 501 | 502 | # Create a graph of internal dependencies in the given file (report RP0402 must 503 | # not be disabled). 504 | int-import-graph= 505 | 506 | # Force import order to recognize a module as part of the standard 507 | # compatibility libraries. 508 | known-standard-library= 509 | 510 | # Force import order to recognize a module as part of a third party library. 511 | known-third-party=enchant 512 | 513 | 514 | [CLASSES] 515 | 516 | # List of method names used to declare (i.e. assign) instance attributes. 517 | defining-attr-methods=__init__, 518 | __new__, 519 | setUp 520 | 521 | # List of member names, which should be excluded from the protected access 522 | # warning. 523 | exclude-protected=_asdict, 524 | _fields, 525 | _replace, 526 | _source, 527 | _make 528 | 529 | # List of valid names for the first argument in a class method. 530 | valid-classmethod-first-arg=cls 531 | 532 | # List of valid names for the first argument in a metaclass class method. 533 | valid-metaclass-classmethod-first-arg=cls 534 | 535 | 536 | [DESIGN] 537 | 538 | # Maximum number of arguments for function / method. 539 | max-args=5 540 | 541 | # Maximum number of attributes for a class (see R0902). 542 | max-attributes=7 543 | 544 | # Maximum number of boolean expressions in an if statement. 545 | max-bool-expr=5 546 | 547 | # Maximum number of branch for function / method body. 548 | max-branches=12 549 | 550 | # Maximum number of locals for function / method body. 551 | max-locals=15 552 | 553 | # Maximum number of parents for a class (see R0901). 554 | max-parents=7 555 | 556 | # Maximum number of public methods for a class (see R0904). 557 | max-public-methods=20 558 | 559 | # Maximum number of return / yield for function / method body. 560 | max-returns=6 561 | 562 | # Maximum number of statements in function / method body. 563 | max-statements=50 564 | 565 | # Minimum number of public methods for a class (see R0903). 566 | min-public-methods=2 567 | 568 | 569 | [EXCEPTIONS] 570 | 571 | # Exceptions that will emit a warning when being caught. Defaults to 572 | # "BaseException, Exception". 573 | overgeneral-exceptions=BaseException, 574 | Exception 575 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | sudo: false 4 | 5 | services: 6 | - xvfb 7 | 8 | addons: 9 | apt: 10 | packages: 11 | - herbstluftwm 12 | - libxkbcommon-x11-0 13 | - qt5-default 14 | 15 | env: 16 | global: 17 | - OFFICIAL_REPO="klauer/qtpydocking" 18 | # Doctr deploy key for klauer/qtpydocking 19 | - secure: "qqYfoPOJWnlAEhLfXUiQqHxMFTuo4WibiTy1HiBpyImkhyLXeb7nP0bHn1AzhXcnYAYV7p8IjsIqyqkeklgGqufNfmwrz9kPoN1IZW53GxPYoVcThGpWOEeUavXsNUlT+ixHV245Y2DTFQ3B+44uH9SHW6r5EdGGYXTmqo6TZfLGlKcexeqEyhuaFww0b93THHxIyGdyhJ1SdmOOLHzswSrW/aUR8JbZhYl/j23Ou+g1uFIwv5iVx2pnWJibIyD80oQONMNqO+0gP4EfAy9XMMSABllojidK4So1oyyeLjUcNPyDSzvnrQVyEaptwc//eS55MFxn21oL4pUSH1cXprJpZAJUOMZzsNIGJNcIl+iG29ISha41Vn9bJy18lZqpeNe5jNKqNVO9tL3Vh2jplOK2E0tqa4HJx9n9aVizBh1j1M/RvH0XHaLqaQ+eg1Nl0owqG9nhPRCkV43LLnH3o8mmoqSQ+Qn2NgtAIqx1DjyUe/3PVhFZClnyFfXFgO8o+NYSsfyAr99H89tCh/l6Sry9Awk1VOy+HGgMtq/BAfTNB7gR97s0mNTnYetgYymdJncnI3f7I09IT448hHEX+dpRfNeRBtx9U1aErTEGgZexCTI2Lf1D88vRkoBaHr0NFTnKYHap9vOUsLb8O7j0VpdaKZfhjXg9HLw62E82fb0=" 20 | 21 | cache: 22 | directories: 23 | - $HOME/.cache/pip 24 | - $HOME/.ccache # https://github.com/travis-ci/travis-ci/issues/5853 25 | 26 | matrix: 27 | fast_finish: true 28 | include: 29 | - python: 3.6 30 | env: 31 | - CONDA_UPLOAD=1 32 | - BUILD_DOCS=1 33 | - QT_API=pyqt5 34 | - python: 3.7 35 | env: 36 | - QT_API=pyqt5 37 | - python: 3.6 38 | env: 39 | - QT_API=PySide2 40 | - python: 3.7 41 | env: 42 | - QT_API=PySide2 43 | allow_failures: 44 | - python: 3.6 45 | env: 46 | - QT_API=PySide2 47 | - python: 3.7 48 | env: 49 | - QT_API=PySide2 50 | 51 | 52 | install: 53 | # Install and configure miniconda 54 | - wget https://repo.continuum.io/miniconda/Miniconda3-latest-Linux-x86_64.sh -O miniconda.sh 55 | - bash miniconda.sh -b -p $HOME/miniconda 56 | - export PATH="$HOME/miniconda/bin:$PATH" 57 | - hash -r 58 | - conda config --set always_yes yes --set changeps1 no 59 | 60 | # Ensure all packages are up-to-date 61 | - conda update -q conda 62 | - conda install conda-build anaconda-client 63 | - conda config --append channels conda-forge 64 | - conda info -a 65 | 66 | # Build the conda recipe for this package 67 | - conda build -q conda-recipe --python=$TRAVIS_PYTHON_VERSION --output-folder bld-dir 68 | - conda config --add channels "file://`pwd`/bld-dir" 69 | 70 | # Create the test environment 71 | - conda create -q -n test-environment python=$TRAVIS_PYTHON_VERSION qtpydocking --file requirements.txt 72 | - source deactivate 73 | - source activate test-environment 74 | 75 | # Install additional development requirements 76 | - pip install -Ur dev-requirements.txt 77 | 78 | # Install the package 79 | - pip install -e . 80 | 81 | # Install the specific binding we're testing against 82 | - pip install "${QT_API}" 83 | 84 | 85 | before_script: 86 | # Run the window manager 87 | - "herbstluftwm &" 88 | - sleep 1 89 | 90 | 91 | script: 92 | - flake8 qtpydocking 93 | - coverage run run_tests.py 94 | - set -e 95 | 96 | - | 97 | if [[ "$BUILD_DOCS" == "1" ]]; then 98 | # Create HTML documentation 99 | pushd docs 100 | make html 101 | popd 102 | #Publish docs. 103 | doctr deploy . --built-docs docs/build/html --deploy-branch-name gh-pages --command "touch .nojekyll; git add .nojekyll" 104 | fi 105 | 106 | 107 | after_success: 108 | - coverage report -m 109 | - codecov 110 | 111 | - | 112 | if [[ $TRAVIS_PULL_REQUEST == false && $TRAVIS_REPO_SLUG == $OFFICIAL_REPO && "$CONDA_UPLOAD" == "1" ]]; then 113 | if [[ $TRAVIS_BRANCH == $TRAVIS_TAG && $TRAVIS_TAG != '' ]]; then 114 | export ANACONDA_API_TOKEN=$CONDA_UPLOAD_TOKEN_TAG 115 | anaconda upload bld-dir/linux-64/*.tar.bz2 116 | fi 117 | fi 118 | -------------------------------------------------------------------------------- /AUTHORS.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | Credits 3 | ======= 4 | 5 | Maintainer 6 | ---------- 7 | 8 | * Ken Lauer @klauer 9 | 10 | Contributors 11 | ------------ 12 | 13 | Interested? See: CONTRIBUTING.rst 14 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | ============ 2 | Contributing 3 | ============ 4 | 5 | Contributions are welcome, and they are greatly appreciated! Every little bit 6 | helps, and credit will always be given. 7 | 8 | You can contribute in many ways: 9 | 10 | Types of Contributions 11 | ---------------------- 12 | 13 | Report Bugs 14 | ~~~~~~~~~~~ 15 | 16 | Report bugs at https://github.com/klauer/qtpydocking/issues. 17 | 18 | If you are reporting a bug, please include: 19 | 20 | * Any details about your local setup that might be helpful in troubleshooting. 21 | * Detailed steps to reproduce the bug. 22 | 23 | Fix Bugs 24 | ~~~~~~~~ 25 | 26 | Look through the GitHub issues for bugs. Anything tagged with "bug" 27 | is open to whoever wants to implement it. 28 | 29 | Implement Features 30 | ~~~~~~~~~~~~~~~~~~ 31 | 32 | Look through the GitHub issues for features. Anything tagged with "feature" 33 | is open to whoever wants to implement it. 34 | 35 | Write Documentation 36 | ~~~~~~~~~~~~~~~~~~~ 37 | 38 | qtpydocking could always use more documentation, whether 39 | as part of the official qtpydocking docs, in docstrings, 40 | or even on the web in blog posts, articles, and such. 41 | 42 | Submit Feedback 43 | ~~~~~~~~~~~~~~~ 44 | 45 | The best way to send feedback is to file an issue at https://github.com/klauer/qtpydocking/issues. 46 | 47 | If you are proposing a feature: 48 | 49 | * Explain in detail how it would work. 50 | * Keep the scope as narrow as possible, to make it easier to implement. 51 | * Remember that this is a volunteer-driven project, and that contributions 52 | are welcome :) 53 | 54 | Get Started! 55 | ------------ 56 | 57 | Ready to contribute? Here's how to set up `qtpydocking` for local development. 58 | 59 | 1. Fork the `qtpydocking` repo on GitHub. 60 | 2. Clone your fork locally:: 61 | 62 | $ git clone git@github.com:your_name_here/qtpydocking.git 63 | 64 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 65 | 66 | $ mkvirtualenv qtpydocking 67 | $ cd qtpydocking/ 68 | $ python setup.py develop 69 | 70 | 4. Create a branch for local development:: 71 | 72 | $ git checkout -b name-of-your-bugfix-or-feature 73 | 74 | Now you can make your changes locally. 75 | 76 | 5. When you're done making changes, check that your changes pass flake8 and the tests, including testing other Python versions with tox:: 77 | 78 | $ flake8 qtpydocking tests 79 | $ python setup.py test 80 | $ tox 81 | 82 | To get flake8 and tox, just pip install them into your virtualenv. 83 | 84 | 6. Commit your changes and push your branch to GitHub:: 85 | 86 | $ git add . 87 | $ git commit -m "Your detailed description of your changes." 88 | $ git push origin name-of-your-bugfix-or-feature 89 | 90 | 7. Submit a pull request through the GitHub website. 91 | 92 | Pull Request Guidelines 93 | ----------------------- 94 | 95 | Before you submit a pull request, check that it meets these guidelines: 96 | 97 | 1. The pull request should include tests. 98 | 2. If the pull request adds functionality, the docs should be updated. Put your 99 | new functionality into a function with a docstring, and add the feature to 100 | the list in README.rst. 101 | 3. The pull request should work for Python 3.6 and up. Check 102 | https://travis-ci.org/klauer/qtpydocking/pull_requests 103 | and make sure that the tests pass for all supported Python versions. 104 | 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2019, K Lauer 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include AUTHORS.rst 2 | include CONTRIBUTING.rst 3 | include LICENSE 4 | include README.rst 5 | include requirements.txt 6 | include dev-requirements.txt 7 | 8 | recursive-exclude * __pycache__ 9 | recursive-exclude * *.py[co] 10 | 11 | recursive-include docs *.rst conf.py Makefile make.bat 12 | 13 | include versioneer.py 14 | include qtpydocking/_version.py 15 | include qtpydocking/default.css 16 | include qtpydocking/default_linux.css -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **DEPRECATED** 2 | 3 | Qt-Advanced-Docking-System provides a Python wrapper directly, implemented with sip. 4 | It's easily available through conda-forge, and you should use it instead of qtpydocking: 5 | 6 | https://github.com/conda-forge/pyqtads-feedstock/#installing-pyqtads 7 | 8 | **DEPRECATED** 9 | 10 | 11 | .. image:: https://img.shields.io/travis/klauer/qtpydocking.svg 12 | :target: https://travis-ci.org/klauer/qtpydocking 13 | 14 | .. image:: https://img.shields.io/pypi/v/qtpydocking.svg 15 | :target: https://pypi.python.org/pypi/qtpydocking 16 | 17 | =============================== 18 | qtpydocking 19 | =============================== 20 | 21 | Pure Python port of the `Qt-Advanced-Docking-System `_, 22 | supporting PyQt5 and PySide through `qtpy `_. 23 | 24 | Requirements 25 | ------------ 26 | 27 | * Python 3.6+ 28 | * qtpy 29 | * PyQt5 / PySide 30 | 31 | 32 | Documentation 33 | ------------- 34 | 35 | `Sphinx-generated documentation `_ 36 | 37 | 38 | Installation 39 | ------------ 40 | :: 41 | 42 | $ conda create -n docking -c conda-forge python=3.6 pyqt qt qtpy 43 | $ conda activate docking 44 | $ git clone https://github.com/klauer/qtpydocking 45 | $ cd qtpydocking 46 | $ python setup.py install 47 | 48 | Running the Tests 49 | ----------------- 50 | :: 51 | 52 | $ python run_tests.py 53 | -------------------------------------------------------------------------------- /conda-recipe/build.sh: -------------------------------------------------------------------------------- 1 | $PYTHON setup.py install --single-version-externally-managed --record=record.txt 2 | -------------------------------------------------------------------------------- /conda-recipe/meta.yaml: -------------------------------------------------------------------------------- 1 | {% set data = load_setup_py_data() %} 2 | 3 | build: 4 | noarch: python 5 | 6 | package: 7 | name: qtpydocking 8 | version: {{ data.get('version') }} 9 | 10 | 11 | source: 12 | path: .. 13 | 14 | requirements: 15 | build: 16 | - python >=3.6 17 | - setuptools 18 | 19 | run: 20 | - python >=3.6 21 | - pyqt >=5 22 | - qtpy 23 | 24 | test: 25 | imports: 26 | - qtpydocking 27 | 28 | requires: 29 | - pytest 30 | - pyside2 31 | - pyqt 32 | 33 | 34 | about: 35 | home: https://github.com/klauer/qtpydocking 36 | license: BSD 3-clause 37 | summary: Pure Python port of the Qt-Advanced-Docking-System 38 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | # These are required for developing the package (running the tests, building 2 | # the documentation) but not necessarily required for _using_ it. 3 | codecov 4 | coverage 5 | flake8 6 | pytest 7 | pytest-qt 8 | pytest-faulthandler 9 | sphinx 10 | doctr 11 | # These are dependencies of various sphinx extensions for documentation. 12 | ipython 13 | matplotlib 14 | numpydoc 15 | sphinx-copybutton 16 | sphinx_rtd_theme 17 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SPHINXPROJ = qtpydocking 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | set SPHINXPROJ=qtpydocking 13 | 14 | if "%1" == "" goto help 15 | 16 | %SPHINXBUILD% >NUL 2>NUL 17 | if errorlevel 9009 ( 18 | echo. 19 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 20 | echo.installed, then set the SPHINXBUILD environment variable to point 21 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 22 | echo.may add the Sphinx directory to PATH. 23 | echo. 24 | echo.If you don't have Sphinx installed, grab it from 25 | echo.http://sphinx-doc.org/ 26 | exit /b 1 27 | ) 28 | 29 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 30 | goto end 31 | 32 | :help 33 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% 34 | 35 | :end 36 | popd 37 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | 5 | qtpydocking.dock_area_layout 6 | ============================ 7 | 8 | .. automodule:: qtpydocking.dock_area_layout 9 | :show-inheritance: 10 | :members: 11 | 12 | 13 | qtpydocking.dock_area_tab_bar 14 | ============================= 15 | 16 | .. automodule:: qtpydocking.dock_area_tab_bar 17 | :show-inheritance: 18 | :members: 19 | 20 | 21 | qtpydocking.dock_area_title_bar 22 | ===================================== 23 | 24 | .. automodule:: qtpydocking.dock_area_title_bar 25 | :show-inheritance: 26 | :members: 27 | 28 | 29 | qtpydocking.dock_area_widget 30 | ============================= 31 | 32 | .. automodule:: qtpydocking.dock_area_widget 33 | :show-inheritance: 34 | :members: 35 | 36 | 37 | qtpydocking.dock_container_widget 38 | ===================================== 39 | 40 | .. automodule:: qtpydocking.dock_container_widget 41 | :show-inheritance: 42 | :members: 43 | 44 | 45 | qtpydocking.dock_manager 46 | ============================= 47 | 48 | .. automodule:: qtpydocking.dock_manager 49 | :show-inheritance: 50 | :members: 51 | 52 | 53 | qtpydocking.dock_overlay 54 | ============================= 55 | 56 | .. automodule:: qtpydocking.dock_overlay 57 | :show-inheritance: 58 | :members: 59 | 60 | 61 | qtpydocking.dock_splitter 62 | ============================= 63 | 64 | .. automodule:: qtpydocking.dock_splitter 65 | :show-inheritance: 66 | :members: 67 | 68 | 69 | qtpydocking.dock_widget 70 | ============================= 71 | 72 | .. automodule:: qtpydocking.dock_widget 73 | :show-inheritance: 74 | :members: 75 | 76 | 77 | qtpydocking.dock_widget_tab 78 | ============================= 79 | 80 | .. automodule:: qtpydocking.dock_widget_tab 81 | :show-inheritance: 82 | :members: 83 | 84 | 85 | qtpydocking.eliding_label 86 | ============================= 87 | 88 | .. automodule:: qtpydocking.eliding_label 89 | :show-inheritance: 90 | :members: 91 | 92 | 93 | qtpydocking.enums 94 | ================= 95 | 96 | .. automodule:: qtpydocking.enums 97 | :show-inheritance: 98 | :members: 99 | 100 | 101 | qtpydocking.floating_dock_container 102 | =================================== 103 | 104 | .. automodule:: qtpydocking.floating_dock_container 105 | :show-inheritance: 106 | :members: 107 | 108 | 109 | qtpydocking.util 110 | ===================== 111 | 112 | .. automodule:: qtpydocking.util 113 | :show-inheritance: 114 | :members: 115 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | import os 16 | import sys 17 | import sphinx_rtd_theme 18 | module_path = os.path.join(os.path.dirname(os.path.abspath(__file__)),'../../') 19 | sys.path.insert(0,module_path) 20 | 21 | # -- Project information ----------------------------------------------------- 22 | 23 | project = 'qtpydocking' 24 | copyright = '2019, Ken Lauer' 25 | author = 'Ken Lauer' 26 | 27 | # The short X.Y version 28 | version = '' 29 | # The full version, including alpha/beta/rc tags 30 | release = '' 31 | 32 | 33 | # -- General configuration --------------------------------------------------- 34 | 35 | # If your documentation needs a minimal Sphinx version, state it here. 36 | # 37 | # needs_sphinx = '1.0' 38 | 39 | # Add any Sphinx extension module names here, as strings. They can be 40 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 41 | # ones. 42 | extensions = [ 43 | 'sphinx.ext.autodoc', 44 | 'sphinx.ext.todo', 45 | 'sphinx.ext.coverage', 46 | 'sphinx.ext.mathjax', 47 | 'sphinx.ext.viewcode', 48 | 'sphinx.ext.githubpages', 49 | 'numpydoc', 50 | ] 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ['_templates'] 54 | 55 | autosummary_generate = True 56 | numpydoc_show_class_members = False 57 | 58 | # The suffix(es) of source filenames. 59 | # You can specify multiple suffix as a list of string: 60 | # 61 | # source_suffix = ['.rst', '.md'] 62 | source_suffix = '.rst' 63 | 64 | # The master toctree document. 65 | master_doc = 'index' 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | # This pattern also affects html_static_path and html_extra_path . 77 | exclude_patterns = [] 78 | 79 | # The name of the Pygments (syntax highlighting) style to use. 80 | pygments_style = 'sphinx' 81 | 82 | 83 | # -- Options for HTML output ------------------------------------------------- 84 | 85 | # The theme to use for HTML and HTML Help pages. See the documentation for 86 | # a list of builtin themes. 87 | # 88 | html_theme = 'sphinx_rtd_theme' 89 | 90 | # Theme options are theme-specific and customize the look and feel of a theme 91 | # further. For a list of options available for each theme, see the 92 | # documentation. 93 | # 94 | # html_theme_options = {} 95 | 96 | # Add any paths that contain custom static files (such as style sheets) here, 97 | # relative to this directory. They are copied after the builtin static files, 98 | # so a file named "default.css" will overwrite the builtin "default.css". 99 | html_static_path = ['_static'] 100 | 101 | # Custom sidebar templates, must be a dictionary that maps document names 102 | # to template names. 103 | # 104 | # The default sidebars (for documents that don't match any pattern) are 105 | # defined by theme itself. Builtin themes are using these templates by 106 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 107 | # 'searchbox.html']``. 108 | # 109 | # html_sidebars = {} 110 | 111 | 112 | # -- Options for HTMLHelp output --------------------------------------------- 113 | 114 | # Output file base name for HTML help builder. 115 | htmlhelp_basename = 'qtpydocking' 116 | 117 | 118 | # -- Options for LaTeX output ------------------------------------------------ 119 | 120 | latex_elements = { 121 | # The paper size ('letterpaper' or 'a4paper'). 122 | # 123 | # 'papersize': 'letterpaper', 124 | 125 | # The font size ('10pt', '11pt' or '12pt'). 126 | # 127 | # 'pointsize': '10pt', 128 | 129 | # Additional stuff for the LaTeX preamble. 130 | # 131 | # 'preamble': '', 132 | 133 | # Latex figure (float) alignment 134 | # 135 | # 'figure_align': 'htbp', 136 | } 137 | 138 | # Grouping the document tree into LaTeX files. List of tuples 139 | # (source start file, target name, title, 140 | # author, documentclass [howto, manual, or own class]). 141 | latex_documents = [ 142 | (master_doc, 'qtpydocking.tex', 'qtpydocking Documentation', 143 | 'Ken Lauer', 'manual'), 144 | ] 145 | 146 | 147 | # -- Options for manual page output ------------------------------------------ 148 | 149 | # One entry per manual page. List of tuples 150 | # (source start file, name, description, authors, manual section). 151 | man_pages = [ 152 | (master_doc, 'qtpydocking', 'qtpydocking Documentation', 153 | [author], 1) 154 | ] 155 | 156 | 157 | # -- Options for Texinfo output ---------------------------------------------- 158 | 159 | # Grouping the document tree into Texinfo files. List of tuples 160 | # (source start file, target name, title, author, 161 | # dir menu entry, description, category) 162 | texinfo_documents = [ 163 | (master_doc, 'qtpydocking', 'qtpydocking Documentation', 164 | author, 'qtpydocking', 'Python Qt Docking', 'Miscellaneous'), 165 | ] 166 | 167 | 168 | # -- Extension configuration ------------------------------------------------- 169 | 170 | # -- Options for todo extension ---------------------------------------------- 171 | 172 | # If true, `todo` and `todoList` produce output, else they produce nothing. 173 | todo_include_todos = True 174 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | =========== 2 | qtpydocking 3 | =========== 4 | 5 | A pure Python port of the `Qt-Advanced-Docking-System 6 | `_, supporting 7 | PyQt5 and PySide through qtpy. 8 | 9 | 10 | Contents 11 | ======== 12 | 13 | .. toctree:: 14 | :maxdepth: 3 15 | :caption: API Documentation 16 | :hidden: 17 | 18 | api.rst 19 | 20 | .. toctree:: 21 | :maxdepth: 1 22 | :caption: Links 23 | :hidden: 24 | 25 | Github Repository 26 | 27 | 28 | Indices and tables 29 | ================== 30 | 31 | * :ref:`genindex` 32 | * :ref:`modindex` 33 | * :ref:`search` 34 | -------------------------------------------------------------------------------- /github_deploy_key_klauer_qtpydocking.enc: -------------------------------------------------------------------------------- 1 | gAAAAABdP5MrE0LmHB0R_WtgqNxuoewpCCsuW_BVMOGM0l1A1MvM64gZA43n7g73V1eS8ilG9c8T-ZwMJkndrqUySwJwu9giQbf4h2U489LZ0ruVHnj4S-D1KCZbTwCp4Hysdysv8HchV-3Qxl4EgF9ZX92LWzwz6BgzZQyEL9vR-g2V_uehZC_Q_6SWgCEj_pv3QZAXXww25MrrLq3URCZrEtj-9XB8p840alOwNqqs-ACNUdDZQwR0mhifJTSEFgmrMOd2Dh8-RHGwZcGd8CkQWL-2IfURE0zkU0ZoJi3hrA31hFUorAF8cGku_HLnT9oloENTXL-l6HdbFHDFDyKS7tOjC8mnb1FeKO0TsiaraprwuLbzaKaorhsvOq9iF97EamZo4dC-d_u3FDNn2hwm7sVQqB3i0VeK40mabixYMIQm5WtCvASksUdnvlwofVU6wfDu9O-o5b0qcWUDvkOIde4aXCJslYAun5bVFoXrCDSK3tUmjvaoWeXYoG4NA5KNuKtfvTtIPe8EA8FBLajH1EKHPf2QudgAi7Jn4K_1EmBogbN8S9YCV4NRCm4pxknhXwwy2VIaiw_4Zpr0ErrwJrlcs1gkSQMCfGu5_Of4pV4AwtOtrIDE2o-uCRG2Yh1JLGlv9OlEy8dHevONN5wY0GvcnC7Mw1VF4-qIUg5YQY4EQ2Cp6guqviaLxRdXI50RYAVmrMMExjgdY47ELVAeqHqJ84CCtNP7HflBRqFBSWagtA6rCka0CtfOa_pqDyRHg9e_klJkS_8NvmJYgjFq3GdenPkWiVeQMu8ouuXiznmw25nbfPqTOF1simKVcmhYWOd91wzNxSIlKc7BARB7qqVvset2pzJ-wwxD1z92Lk8IEFjM4lae4_3x5EYXw_EH1wZLOwZYU_uFrVC58_3pLy4r9Y5zan-TpwdYK2Gh9wFrYP6akbilH-954VnMqT5UDa3t3NXIj-PgaWd--NFlUKbqsah0X9Wfp4D_BXDpig3xy5Ji-fr_BGu7ZLM9srwRMq7OI2I963uTtzvLuT2L2ocSHTPuXTPUgHnehQ_sSjPku6InghLqJ7vqX9tE4FtYVIeG6u7_-DFoDSWTAVmZAPsefzcpQqqOjhuR3lrtgamdO4kpGKU8xsnLakbhAJW-8Y63MEl43iqwsgBvziZK66fVtYTsXxZx_5xfXA1y_d3S-U6bRFbscrfjnOXHvLBIqPptuoX_5ugVL8-GmkYQ6-wfEF-b34UKF2E_23LGANwH1V6zVw_wvTu-dX0corgFv1uHNlJuJvm1dWvrvdLYubnyVCkM1H2NojFC2QvVJVntHtIEtyTfPGk6HjRTBbtHXJIYvz_5yAL4C6rJlgX3FiqcAXlV6s7UIPVpaa9gyvrSuxodsQvWpSoMVse-tDqe4taibGDShws_4Ox65xn95cax7_KTp6SbqAHSe2mfQBKNfFUlRdawXPq_unuyXGE3Qjsvn6oRW0HlKYP57TkJSwjYzOqc5WrDyzRJrOUgeF2jcWbAmZSBFuZvDt6arbS12ZFOOAlicXCmG0nL44HnYxMc3rIkgOL-me0GLYacFKd9I02bdT5JP8ItgC-kjfU1_w7BynLdHmdGpEZ3CRCYexQbjot_KJGRcEuZReBjSPSFTR5q2eqh8YJ_TpT8brywSPJq39mCTeObPvg_eNPVXGIR9REBu5re5VBpF4c9mt2JCBHfuIPsqEwVHTa3cEwJM7gPawwi7DKrMWwquWTFMZHzgjPuhlIfvDzag8CwrubP7nf1CljYp-L_698ez4bQVH11H1JEZ5-liVxmAg7JxLJtAW-UHPtpV3L7YUvSR3XMmShNvcqtQRpEVRURt9WiGHJdvQB101e0077bBS3BZiocdYpkUZjqlhunOljjX3QooFttyQL9h6m1Dasld41QqxwABELXhD2mQjE6d-3_dXbhKteigl_EpfyIKs8tY7o967OB4pBI0FT7N9N5Y0akvzB1v_5jearuo5GmwfCLnb_X3gNiUDbvbthqYuNj2bPln9jc6TTGCgzydocb6N0nrpae-5xhvbxwVhxBp_AoiKby68HYj8D5k5BR3pI9uzx3OXvGdmIVskpoK9HqCm40nVNZ4WwvJTMkyfnb5CtoQ30f8lTfHoXfBrax91r1p-jBzimRn5P8QnkGQKOO1Pdw2tJdJ4wg17pxZ0qtnyKTtNiaMh7AZQbw3-6Z_jS85dHCrSD8EByPKFMdJS9CvbA6IvWik05VwzD8-MgWtPaYc4Rq1uIVH5hgpKk7imXexMPWTVGzDWNKmt1wmHcWFbJKMXi8SUHJrr6Mio3d5AR_JR-WKGiThMCC6j04KFe7MXb9_OGofwOw-q-p0RKf-tNLtuB8oKkfNx3M-y0DsJ6Q75QkwEOwWtpn41TIoEa2fnYG6HeOb6rT76l_wtqF1FU7KRcvTCWp_zdDrGddtSF0HerEna7l4Vr4rxyPiWRiY8U9iIwFVokodmMD2B4dtCteX_Iu3H54gR3oKiL2foUa3LlzrhxflGSdp6IjaKZYwK0rdboK0Ws8NSJ3oT0ean2d29SRtWQKtz3-Tgh7DiwhvQxraBVf6V9YAvGA8M74Gyl62YKZYu5xS9ugT8Qn9rwqoneIW3tMpxgBF7O4srNjFGZYvHCXiGgywF8k0vAvmy1b7WTqCMy-6npTXo10Inln4KgDFI-zDZrIDci2v4ADIy2qxIFCpXbHNMCspxJgUUwe0p3AIbV4Ubc_IGtXwFLrmvqKh6yJB7uohQYBDjQFI5pBgVNBMYIGCz6wiajl8jgRyO0Spn7CUWcLm8Grmpdn8iwL_4LJTjhCzCqhxPYaHeCUGcCWoOHBk8Gd0fZJLc6DAcgZQUF5B15fpiTpCpCi5k4nCwX-2j-6qYoBg9xY3Lh4UkRNVLT1Q7zbUHfqif19layprjuzahEK93SrvZEAom7RmRAAdz7_JutXqPT58ktSv_-X-an1SyM0iQTFp8TtJo98u4n31qRVP7CA8T4p6t-X8LD9UFvWulm9qepWPZ7YTl_74335Ge71rLLh9NVuK_vCVspNr0IxT2HCSQdumI5ZWJXObCnVI2VbyerzG-Lh2SmwJ0rMeRM9darDMIMm5TEDuIc6AN06nwb5leA0gP9f7xGW08xaAbgvss_G68CvDENBsDLrwAWIP6Y5xq74gTx-1dJi6VJ8bptYNmvWX_7mqoZugGM96B_oV7L3n0RIStbw1Hpm-82Ql_ev_FdlNB72BeXYv99eT6ES3aziIz80gEzoehgnpqV9n9Ti5AZHJKmW1Sz_fQsM03BKu3RFEa3oQT71KZiOKs73_uRoMkp5Bok29Et9lbdn7BvveX0gT1iPg_OQLYsITGyPcWX4LhWwEhK2oujXojLxZsLg99MaGPNS5xMCN2nCtX4bDMjK369Jqjh_K9eNd_8TxT8iSpXkZFusYD3Rlad1K_WBYvjsamTnEW7eTHEpPSllCNULy984rww8q878BQQhQfoBaBmQOEMaVMEg_Xkx6CoK_61Ly39CnQX2HviJ5W3qPmreoZjAaNz62sCER0VV79hjlN1ZHtuS1x7ImcVGmArNgeFI1wYG_Ej5VZ-u3IU0RhjKHf2KG9f2l8Qn0M3JAw2Y8zHoPXxKyIcOxDYio6uv8R0TbeBfyvQGKXxb-RCnmDvxTBZRhzLoCKzJsKbw5aiLp7pf1b3JQFO5tqcw-3ZlgVdR4BGIVhWaadqVEw6he5KKGo90Qh4sN04GGZEQNmenzm_OOoQXu_xupUnqtlzhMlnqvPoqh2iF3yuZ0v-3EcA9KnVUfhYvvQASAbn6EuRvxvkOfsxqf8abUaVmU0RYtlztzuLL8AXPqcyTJ7mp_QKCfaOxrY2wx6xlVcMQveRZHJoZFw7wyOz8rV8EnT891MirI0OSjpfc_Wl4xgzNPs-8PZHNasb-YdVuNIUyY-vJwkS5GGXb-SBq1HumidZpMNFkTUfASrKvOmRwvvZv8v7F6oBZoCmGBN2XpNz6yXUnTJzks8oRSa5lPuzE5pbYj5ZDp2wOTV6Zj8xyme5Ucdi8EHulyEDWZbHrUSc6flZDax0UpHHhIsQZBYJVQbOjlg0VXyFoSHo6J-zaiG-_tghHsiKEKjdK2j9NeEC7_vbkc_9Yj2ZaN010ZGPVKT95p5_k7Rqu7VKv3PReUvx_yYLKtb2uFkmhpEPVVE-hJhTE2DeHxWVQU2OlSUfLQCi_iHfGhWYcE_spJsewDJzh6dsaSLQhVNoEUfYcyEgC0LOxYBHo7SG29vzYF7l6b9SpcPji3LVH8-GjOZMuHcqp8zWsWyhNBzNvWdTe4T6UD1gEJaTV9Z--VsdwHPixpex4_bbgS-1w65zxzs0kBMCM6pElo1LRFoZAXOZ5BeqJd_eMl8tL1CjsnE5hVs5TBSb3HhQLDwYpAgF6GrNrW3Ci3c3--naKDg== -------------------------------------------------------------------------------- /qtpydocking/__init__.py: -------------------------------------------------------------------------------- 1 | # TODO: Q_PROPERTY 2 | from ._version import get_versions 3 | __version__ = get_versions()['version'] 4 | del get_versions 5 | 6 | from .enums import DockInsertParam 7 | from .enums import DockWidgetArea 8 | from .enums import DockWidgetFeature 9 | from .enums import TitleBarButton 10 | from .enums import DockFlags 11 | from .enums import DragState 12 | from .enums import IconColor 13 | from .enums import InsertMode 14 | from .enums import OverlayMode 15 | from .enums import WidgetState 16 | from .enums import ToggleViewActionMode 17 | from .enums import InsertionOrder 18 | 19 | from . import util 20 | 21 | from .eliding_label import ElidingLabel 22 | from .floating_dock_container import FloatingDockContainer 23 | from .dock_area_layout import DockAreaLayout 24 | from .dock_area_tab_bar import DockAreaTabBar 25 | from .dock_area_title_bar import DockAreaTitleBar 26 | from .dock_area_widget import DockAreaWidget 27 | from .dock_container_widget import DockContainerWidget 28 | from .dock_manager import DockManager 29 | from .dock_overlay import DockOverlay, DockOverlayCross 30 | from .dock_splitter import DockSplitter 31 | from .dock_widget import DockWidget 32 | from .dock_widget_tab import DockWidgetTab 33 | 34 | from . import examples 35 | 36 | 37 | __all__ = [ 38 | '__version__', 39 | 'DockAreaLayout', 40 | 'DockAreaTabBar', 41 | 'DockAreaTitleBar', 42 | 'DockAreaWidget', 43 | 'DockContainerWidget', 44 | 'DockInsertParam', 45 | 'DockManager', 46 | 'DockOverlay', 47 | 'DockOverlayCross', 48 | 'DockSplitter', 49 | 'DockWidget', 50 | 'DockWidgetArea', 51 | 'DockWidgetFeature', 52 | 'DockWidgetTab', 53 | 'ElidingLabel', 54 | 'FloatingDockContainer', 55 | 'TitleBarButton', 56 | 'DockFlags', 57 | 'DragState', 58 | 'IconColor', 59 | 'InsertMode', 60 | 'OverlayMode', 61 | 'WidgetState', 62 | 'ToggleViewActionMode', 63 | 'InsertionOrder', 64 | 'examples', 65 | 'util', 66 | ] 67 | -------------------------------------------------------------------------------- /qtpydocking/_version.py: -------------------------------------------------------------------------------- 1 | 2 | # This file helps to compute a version number in source trees obtained from 3 | # git-archive tarball (such as those provided by githubs download-from-tag 4 | # feature). Distribution tarballs (built by setup.py sdist) and build 5 | # directories (produced by setup.py build) will contain a much shorter file 6 | # that just contains the computed version number. 7 | 8 | # This file is released into the public domain. Generated by 9 | # versioneer-0.18 (https://github.com/warner/python-versioneer) 10 | 11 | """Git implementation of _version.py.""" 12 | 13 | import errno 14 | import os 15 | import re 16 | import subprocess 17 | import sys 18 | 19 | 20 | def get_keywords(): 21 | """Get the keywords needed to look up the version information.""" 22 | # these strings will be replaced by git during git-archive. 23 | # setup.py/versioneer.py will grep for the variable names, so they must 24 | # each be defined on a line of their own. _version.py will just call 25 | # get_keywords(). 26 | git_refnames = " (HEAD -> master)" 27 | git_full = "7159a21a83c7ea0711e90fef9258296b98118f28" 28 | git_date = "2020-10-19 15:45:10 -0700" 29 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 30 | return keywords 31 | 32 | 33 | class VersioneerConfig: 34 | """Container for Versioneer configuration parameters.""" 35 | 36 | 37 | def get_config(): 38 | """Create, populate and return the VersioneerConfig() object.""" 39 | # these strings are filled in when 'setup.py versioneer' creates 40 | # _version.py 41 | cfg = VersioneerConfig() 42 | cfg.VCS = "git" 43 | cfg.style = "pep440" 44 | cfg.tag_prefix = "v" 45 | cfg.parentdir_prefix = "None" 46 | cfg.versionfile_source = "qtpydocking/_version.py" 47 | cfg.verbose = False 48 | return cfg 49 | 50 | 51 | class NotThisMethod(Exception): 52 | """Exception raised if a method is not valid for the current scenario.""" 53 | 54 | 55 | LONG_VERSION_PY = {} 56 | HANDLERS = {} 57 | 58 | 59 | def register_vcs_handler(vcs, method): # decorator 60 | """Decorator to mark a method as the handler for a particular VCS.""" 61 | def decorate(f): 62 | """Store f in HANDLERS[vcs][method].""" 63 | if vcs not in HANDLERS: 64 | HANDLERS[vcs] = {} 65 | HANDLERS[vcs][method] = f 66 | return f 67 | return decorate 68 | 69 | 70 | def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, 71 | env=None): 72 | """Call the given command(s).""" 73 | assert isinstance(commands, list) 74 | p = None 75 | for c in commands: 76 | try: 77 | dispcmd = str([c] + args) 78 | # remember shell=False, so use git.cmd on windows, not just git 79 | p = subprocess.Popen([c] + args, cwd=cwd, env=env, 80 | stdout=subprocess.PIPE, 81 | stderr=(subprocess.PIPE if hide_stderr 82 | else None)) 83 | break 84 | except EnvironmentError: 85 | e = sys.exc_info()[1] 86 | if e.errno == errno.ENOENT: 87 | continue 88 | if verbose: 89 | print("unable to run %s" % dispcmd) 90 | print(e) 91 | return None, None 92 | else: 93 | if verbose: 94 | print("unable to find command, tried %s" % (commands,)) 95 | return None, None 96 | stdout = p.communicate()[0].strip() 97 | if sys.version_info[0] >= 3: 98 | stdout = stdout.decode() 99 | if p.returncode != 0: 100 | if verbose: 101 | print("unable to run %s (error)" % dispcmd) 102 | print("stdout was %s" % stdout) 103 | return None, p.returncode 104 | return stdout, p.returncode 105 | 106 | 107 | def versions_from_parentdir(parentdir_prefix, root, verbose): 108 | """Try to determine the version from the parent directory name. 109 | 110 | Source tarballs conventionally unpack into a directory that includes both 111 | the project name and a version string. We will also support searching up 112 | two directory levels for an appropriately named parent directory 113 | """ 114 | rootdirs = [] 115 | 116 | for i in range(3): 117 | dirname = os.path.basename(root) 118 | if dirname.startswith(parentdir_prefix): 119 | return {"version": dirname[len(parentdir_prefix):], 120 | "full-revisionid": None, 121 | "dirty": False, "error": None, "date": None} 122 | else: 123 | rootdirs.append(root) 124 | root = os.path.dirname(root) # up a level 125 | 126 | if verbose: 127 | print("Tried directories %s but none started with prefix %s" % 128 | (str(rootdirs), parentdir_prefix)) 129 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 130 | 131 | 132 | @register_vcs_handler("git", "get_keywords") 133 | def git_get_keywords(versionfile_abs): 134 | """Extract version information from the given file.""" 135 | # the code embedded in _version.py can just fetch the value of these 136 | # keywords. When used from setup.py, we don't want to import _version.py, 137 | # so we do it with a regexp instead. This function is not used from 138 | # _version.py. 139 | keywords = {} 140 | try: 141 | f = open(versionfile_abs, "r") 142 | for line in f.readlines(): 143 | if line.strip().startswith("git_refnames ="): 144 | mo = re.search(r'=\s*"(.*)"', line) 145 | if mo: 146 | keywords["refnames"] = mo.group(1) 147 | if line.strip().startswith("git_full ="): 148 | mo = re.search(r'=\s*"(.*)"', line) 149 | if mo: 150 | keywords["full"] = mo.group(1) 151 | if line.strip().startswith("git_date ="): 152 | mo = re.search(r'=\s*"(.*)"', line) 153 | if mo: 154 | keywords["date"] = mo.group(1) 155 | f.close() 156 | except EnvironmentError: 157 | pass 158 | return keywords 159 | 160 | 161 | @register_vcs_handler("git", "keywords") 162 | def git_versions_from_keywords(keywords, tag_prefix, verbose): 163 | """Get version information from git keywords.""" 164 | if not keywords: 165 | raise NotThisMethod("no keywords at all, weird") 166 | date = keywords.get("date") 167 | if date is not None: 168 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 169 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 170 | # -like" string, which we must then edit to make compliant), because 171 | # it's been around since git-1.5.3, and it's too difficult to 172 | # discover which version we're using, or to work around using an 173 | # older one. 174 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 175 | refnames = keywords["refnames"].strip() 176 | if refnames.startswith("$Format"): 177 | if verbose: 178 | print("keywords are unexpanded, not using") 179 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 180 | refs = set([r.strip() for r in refnames.strip("()").split(",")]) 181 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 182 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 183 | TAG = "tag: " 184 | tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) 185 | if not tags: 186 | # Either we're using git < 1.8.3, or there really are no tags. We use 187 | # a heuristic: assume all version tags have a digit. The old git %d 188 | # expansion behaves like git log --decorate=short and strips out the 189 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 190 | # between branches and tags. By ignoring refnames without digits, we 191 | # filter out many common branch names like "release" and 192 | # "stabilization", as well as "HEAD" and "master". 193 | tags = set([r for r in refs if re.search(r'\d', r)]) 194 | if verbose: 195 | print("discarding '%s', no digits" % ",".join(refs - tags)) 196 | if verbose: 197 | print("likely tags: %s" % ",".join(sorted(tags))) 198 | for ref in sorted(tags): 199 | # sorting will prefer e.g. "2.0" over "2.0rc1" 200 | if ref.startswith(tag_prefix): 201 | r = ref[len(tag_prefix):] 202 | if verbose: 203 | print("picking %s" % r) 204 | return {"version": r, 205 | "full-revisionid": keywords["full"].strip(), 206 | "dirty": False, "error": None, 207 | "date": date} 208 | # no suitable tags, so version is "0+unknown", but full hex is still there 209 | if verbose: 210 | print("no suitable tags, using unknown + full revision id") 211 | return {"version": "0+unknown", 212 | "full-revisionid": keywords["full"].strip(), 213 | "dirty": False, "error": "no suitable tags", "date": None} 214 | 215 | 216 | @register_vcs_handler("git", "pieces_from_vcs") 217 | def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): 218 | """Get version from 'git describe' in the root of the source tree. 219 | 220 | This only gets called if the git-archive 'subst' keywords were *not* 221 | expanded, and _version.py hasn't already been rewritten with a short 222 | version string, meaning we're inside a checked out source tree. 223 | """ 224 | GITS = ["git"] 225 | if sys.platform == "win32": 226 | GITS = ["git.cmd", "git.exe"] 227 | 228 | out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, 229 | hide_stderr=True) 230 | if rc != 0: 231 | if verbose: 232 | print("Directory %s not under git control" % root) 233 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 234 | 235 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 236 | # if there isn't one, this yields HEX[-dirty] (no NUM) 237 | describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", 238 | "--always", "--long", 239 | "--match", "%s*" % tag_prefix], 240 | cwd=root) 241 | # --long was added in git-1.5.5 242 | if describe_out is None: 243 | raise NotThisMethod("'git describe' failed") 244 | describe_out = describe_out.strip() 245 | full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) 246 | if full_out is None: 247 | raise NotThisMethod("'git rev-parse' failed") 248 | full_out = full_out.strip() 249 | 250 | pieces = {} 251 | pieces["long"] = full_out 252 | pieces["short"] = full_out[:7] # maybe improved later 253 | pieces["error"] = None 254 | 255 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 256 | # TAG might have hyphens. 257 | git_describe = describe_out 258 | 259 | # look for -dirty suffix 260 | dirty = git_describe.endswith("-dirty") 261 | pieces["dirty"] = dirty 262 | if dirty: 263 | git_describe = git_describe[:git_describe.rindex("-dirty")] 264 | 265 | # now we have TAG-NUM-gHEX or HEX 266 | 267 | if "-" in git_describe: 268 | # TAG-NUM-gHEX 269 | mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) 270 | if not mo: 271 | # unparseable. Maybe git-describe is misbehaving? 272 | pieces["error"] = ("unable to parse git-describe output: '%s'" 273 | % describe_out) 274 | return pieces 275 | 276 | # tag 277 | full_tag = mo.group(1) 278 | if not full_tag.startswith(tag_prefix): 279 | if verbose: 280 | fmt = "tag '%s' doesn't start with prefix '%s'" 281 | print(fmt % (full_tag, tag_prefix)) 282 | pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" 283 | % (full_tag, tag_prefix)) 284 | return pieces 285 | pieces["closest-tag"] = full_tag[len(tag_prefix):] 286 | 287 | # distance: number of commits since tag 288 | pieces["distance"] = int(mo.group(2)) 289 | 290 | # commit: short hex revision ID 291 | pieces["short"] = mo.group(3) 292 | 293 | else: 294 | # HEX: no tags 295 | pieces["closest-tag"] = None 296 | count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], 297 | cwd=root) 298 | pieces["distance"] = int(count_out) # total number of commits 299 | 300 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 301 | date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], 302 | cwd=root)[0].strip() 303 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 304 | 305 | return pieces 306 | 307 | 308 | def plus_or_dot(pieces): 309 | """Return a + if we don't already have one, else return a .""" 310 | if "+" in pieces.get("closest-tag", ""): 311 | return "." 312 | return "+" 313 | 314 | 315 | def render_pep440(pieces): 316 | """Build up version string, with post-release "local version identifier". 317 | 318 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 319 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 320 | 321 | Exceptions: 322 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 323 | """ 324 | if pieces["closest-tag"]: 325 | rendered = pieces["closest-tag"] 326 | if pieces["distance"] or pieces["dirty"]: 327 | rendered += plus_or_dot(pieces) 328 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 329 | if pieces["dirty"]: 330 | rendered += ".dirty" 331 | else: 332 | # exception #1 333 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], 334 | pieces["short"]) 335 | if pieces["dirty"]: 336 | rendered += ".dirty" 337 | return rendered 338 | 339 | 340 | def render_pep440_pre(pieces): 341 | """TAG[.post.devDISTANCE] -- No -dirty. 342 | 343 | Exceptions: 344 | 1: no tags. 0.post.devDISTANCE 345 | """ 346 | if pieces["closest-tag"]: 347 | rendered = pieces["closest-tag"] 348 | if pieces["distance"]: 349 | rendered += ".post.dev%d" % pieces["distance"] 350 | else: 351 | # exception #1 352 | rendered = "0.post.dev%d" % pieces["distance"] 353 | return rendered 354 | 355 | 356 | def render_pep440_post(pieces): 357 | """TAG[.postDISTANCE[.dev0]+gHEX] . 358 | 359 | The ".dev0" means dirty. Note that .dev0 sorts backwards 360 | (a dirty tree will appear "older" than the corresponding clean one), 361 | but you shouldn't be releasing software with -dirty anyways. 362 | 363 | Exceptions: 364 | 1: no tags. 0.postDISTANCE[.dev0] 365 | """ 366 | if pieces["closest-tag"]: 367 | rendered = pieces["closest-tag"] 368 | if pieces["distance"] or pieces["dirty"]: 369 | rendered += ".post%d" % pieces["distance"] 370 | if pieces["dirty"]: 371 | rendered += ".dev0" 372 | rendered += plus_or_dot(pieces) 373 | rendered += "g%s" % pieces["short"] 374 | else: 375 | # exception #1 376 | rendered = "0.post%d" % pieces["distance"] 377 | if pieces["dirty"]: 378 | rendered += ".dev0" 379 | rendered += "+g%s" % pieces["short"] 380 | return rendered 381 | 382 | 383 | def render_pep440_old(pieces): 384 | """TAG[.postDISTANCE[.dev0]] . 385 | 386 | The ".dev0" means dirty. 387 | 388 | Eexceptions: 389 | 1: no tags. 0.postDISTANCE[.dev0] 390 | """ 391 | if pieces["closest-tag"]: 392 | rendered = pieces["closest-tag"] 393 | if pieces["distance"] or pieces["dirty"]: 394 | rendered += ".post%d" % pieces["distance"] 395 | if pieces["dirty"]: 396 | rendered += ".dev0" 397 | else: 398 | # exception #1 399 | rendered = "0.post%d" % pieces["distance"] 400 | if pieces["dirty"]: 401 | rendered += ".dev0" 402 | return rendered 403 | 404 | 405 | def render_git_describe(pieces): 406 | """TAG[-DISTANCE-gHEX][-dirty]. 407 | 408 | Like 'git describe --tags --dirty --always'. 409 | 410 | Exceptions: 411 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 412 | """ 413 | if pieces["closest-tag"]: 414 | rendered = pieces["closest-tag"] 415 | if pieces["distance"]: 416 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 417 | else: 418 | # exception #1 419 | rendered = pieces["short"] 420 | if pieces["dirty"]: 421 | rendered += "-dirty" 422 | return rendered 423 | 424 | 425 | def render_git_describe_long(pieces): 426 | """TAG-DISTANCE-gHEX[-dirty]. 427 | 428 | Like 'git describe --tags --dirty --always -long'. 429 | The distance/hash is unconditional. 430 | 431 | Exceptions: 432 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 433 | """ 434 | if pieces["closest-tag"]: 435 | rendered = pieces["closest-tag"] 436 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 437 | else: 438 | # exception #1 439 | rendered = pieces["short"] 440 | if pieces["dirty"]: 441 | rendered += "-dirty" 442 | return rendered 443 | 444 | 445 | def render(pieces, style): 446 | """Render the given version pieces into the requested style.""" 447 | if pieces["error"]: 448 | return {"version": "unknown", 449 | "full-revisionid": pieces.get("long"), 450 | "dirty": None, 451 | "error": pieces["error"], 452 | "date": None} 453 | 454 | if not style or style == "default": 455 | style = "pep440" # the default 456 | 457 | if style == "pep440": 458 | rendered = render_pep440(pieces) 459 | elif style == "pep440-pre": 460 | rendered = render_pep440_pre(pieces) 461 | elif style == "pep440-post": 462 | rendered = render_pep440_post(pieces) 463 | elif style == "pep440-old": 464 | rendered = render_pep440_old(pieces) 465 | elif style == "git-describe": 466 | rendered = render_git_describe(pieces) 467 | elif style == "git-describe-long": 468 | rendered = render_git_describe_long(pieces) 469 | else: 470 | raise ValueError("unknown style '%s'" % style) 471 | 472 | return {"version": rendered, "full-revisionid": pieces["long"], 473 | "dirty": pieces["dirty"], "error": None, 474 | "date": pieces.get("date")} 475 | 476 | 477 | def get_versions(): 478 | """Get version information or return default if unable to do so.""" 479 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 480 | # __file__, we can work backwards from there to the root. Some 481 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 482 | # case we can only use expanded keywords. 483 | 484 | cfg = get_config() 485 | verbose = cfg.verbose 486 | 487 | try: 488 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, 489 | verbose) 490 | except NotThisMethod: 491 | pass 492 | 493 | try: 494 | root = os.path.realpath(__file__) 495 | # versionfile_source is the relative path from the top of the source 496 | # tree (where the .git directory might live) to this file. Invert 497 | # this to find the root from __file__. 498 | for i in cfg.versionfile_source.split('/'): 499 | root = os.path.dirname(root) 500 | except NameError: 501 | return {"version": "0+unknown", "full-revisionid": None, 502 | "dirty": None, 503 | "error": "unable to find root of source tree", 504 | "date": None} 505 | 506 | try: 507 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 508 | return render(pieces, cfg.style) 509 | except NotThisMethod: 510 | pass 511 | 512 | try: 513 | if cfg.parentdir_prefix: 514 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 515 | except NotThisMethod: 516 | pass 517 | 518 | return {"version": "0+unknown", "full-revisionid": None, 519 | "dirty": None, 520 | "error": "unable to compute version", "date": None} 521 | -------------------------------------------------------------------------------- /qtpydocking/default.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was originally from: 3 | * 4 | * Qt Advanced Docking System 5 | * Copyright (C) 2017 Uwe Kindler 6 | * 7 | * This library is free software; you can redistribute it and/or 8 | * modify it under the terms of the GNU Lesser General Public 9 | * License as published by the Free Software Foundation; either 10 | * version 2.1 of the License, or (at your option) any later version. 11 | * 12 | * This library is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | * Lesser General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Lesser General Public 18 | * License along with this library; If not, see . 19 | */ 20 | 21 | /* 22 | * Default style sheet on Windows Platforms 23 | */ 24 | 25 | DockContainerWidget 26 | { 27 | background: palette(dark); 28 | } 29 | 30 | DockContainerWidget QSplitter::handle 31 | { 32 | background: palette(dark); 33 | } 34 | 35 | DockAreaWidget 36 | { 37 | background: palette(window); 38 | border: 1px solid white; 39 | } 40 | 41 | DockAreaWidget #tabsMenuButton::menu-indicator 42 | { 43 | image: none; 44 | } 45 | 46 | 47 | DockWidgetTab 48 | { 49 | background: palette(window); 50 | border-color: palette(light); 51 | border-style: solid; 52 | border-width: 0 1px 0 0; 53 | padding: 0 -2px; 54 | } 55 | 56 | DockWidgetTab[activeTab="true"] 57 | { 58 | background: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:0.5, stop:0 palette(window), stop:1 palette(light)); 59 | /*background: palette(highlight);*/ 60 | } 61 | 62 | DockWidgetTab QLabel 63 | { 64 | color: palette(dark); 65 | } 66 | 67 | DockWidgetTab[activeTab="true"] QLabel 68 | { 69 | color: palette(foreground); 70 | } 71 | 72 | DockWidget 73 | { 74 | background: palette(light); 75 | border-color: palette(light); 76 | border-style: solid; 77 | border-width: 1px 0 0 0; 78 | } 79 | 80 | #tabsMenuButton, 81 | #closeButton, 82 | #undockButton 83 | { 84 | padding: 0 -2px; 85 | } 86 | 87 | 88 | QScrollArea#dockWidgetScrollArea 89 | { 90 | padding: 0px; 91 | border: none; 92 | } 93 | 94 | 95 | #tabCloseButton 96 | { 97 | margin-top: 2px; 98 | background: none; 99 | border: none; 100 | padding: 0px -2px; 101 | } 102 | 103 | #tabCloseButton:hover 104 | { 105 | border: 1px solid rgba(0, 0, 0, 32); 106 | background: rgba(0, 0, 0, 16); 107 | } 108 | 109 | #tabCloseButton:pressed 110 | { 111 | background: rgba(0, 0, 0, 32); 112 | } 113 | 114 | 115 | -------------------------------------------------------------------------------- /qtpydocking/default_linux.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This file was originally from: 3 | * 4 | * Qt Advanced Docking System 5 | * Copyright (C) 2017 Uwe Kindler 6 | * 7 | * This library is free software; you can redistribute it and/or 8 | * modify it under the terms of the GNU Lesser General Public 9 | * License as published by the Free Software Foundation; either 10 | * version 2.1 of the License, or (at your option) any later version. 11 | * 12 | * This library is distributed in the hope that it will be useful, 13 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 15 | * Lesser General Public License for more details. 16 | * 17 | * You should have received a copy of the GNU Lesser General Public 18 | * License along with this library; If not, see . 19 | */ 20 | 21 | /* 22 | * Default style sheet on Linux Platforms 23 | */ 24 | 25 | DockContainerWidget 26 | { 27 | background: palette(dark); 28 | } 29 | 30 | DockContainerWidget QSplitter::handle 31 | { 32 | background: palette(dark); 33 | } 34 | 35 | DockAreaWidget 36 | { 37 | background: palette(window); 38 | border: 1px solid white; 39 | } 40 | 41 | DockAreaWidget #tabsMenuButton::menu-indicator 42 | { 43 | image: none; 44 | } 45 | 46 | 47 | DockWidgetTab 48 | { 49 | background: palette(window); 50 | border-color: palette(light); 51 | border-style: solid; 52 | border-width: 0 1px 0 0; 53 | padding: 0 0px; 54 | } 55 | 56 | DockWidgetTab[activeTab="true"] 57 | { 58 | background: qlineargradient(spread:pad, x1:0, y1:0, x2:0, y2:0.5, stop:0 palette(window), stop:1 palette(light)); 59 | /*background: palette(highlight);*/ 60 | } 61 | 62 | DockWidgetTab QLabel 63 | { 64 | color: palette(dark); 65 | } 66 | 67 | DockWidgetTab[activeTab="true"] QLabel 68 | { 69 | color: palette(foreground); 70 | } 71 | 72 | DockWidget 73 | { 74 | background: palette(light); 75 | border-color: palette(light); 76 | border-style: solid; 77 | border-width: 1px 0 0 0; 78 | } 79 | 80 | #tabsMenuButton, 81 | #closeButton, 82 | #undockButton 83 | { 84 | padding: 0px -2px; 85 | } 86 | 87 | 88 | QScrollArea#dockWidgetScrollArea 89 | { 90 | padding: 0px; 91 | border: none; 92 | } 93 | 94 | 95 | #tabCloseButton 96 | { 97 | margin-top: 2px; 98 | background: none; 99 | border: none; 100 | padding: 0px -2px; 101 | } 102 | 103 | #tabCloseButton:hover 104 | { 105 | border: 1px solid rgba(0, 0, 0, 32); 106 | background: rgba(0, 0, 0, 16); 107 | } 108 | 109 | #tabCloseButton:pressed 110 | { 111 | background: rgba(0, 0, 0, 32); 112 | } 113 | -------------------------------------------------------------------------------- /qtpydocking/dock_area_layout.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import List 3 | 4 | from qtpy.QtCore import QRect 5 | from qtpy.QtWidgets import QBoxLayout, QWidget 6 | 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | 11 | class DockAreaLayout: 12 | _parent_layout: QBoxLayout 13 | _widgets: List[QWidget] 14 | _current_widget: QWidget 15 | _current_index: int 16 | 17 | def __init__(self, parent_layout: QBoxLayout): 18 | ''' 19 | Creates an instance with the given parent layout 20 | 21 | Parameters 22 | ---------- 23 | parent_layout : QBoxLayout 24 | ''' 25 | self._parent_layout = parent_layout 26 | self._widgets = [] 27 | self._current_index = -1 28 | self._current_widget = None 29 | 30 | def count(self) -> int: 31 | ''' 32 | Returns the number of widgets in this layout 33 | 34 | Returns 35 | ------- 36 | value : int 37 | ''' 38 | return len(self._widgets) 39 | 40 | def insert_widget(self, index: int, widget: QWidget): 41 | ''' 42 | Inserts the widget at the given index position into the internal widget 43 | list 44 | 45 | Parameters 46 | ---------- 47 | index : int 48 | widget : QWidget 49 | ''' 50 | logger.debug('%s setParent None', widget) 51 | widget.setParent(None) 52 | if index < 0: 53 | index = len(self._widgets) 54 | 55 | self._widgets.insert(index, widget) 56 | if self._current_index < 0: 57 | self.set_current_index(index) 58 | elif index <= self._current_index: 59 | self._current_index += 1 60 | 61 | def remove_widget(self, widget: QWidget): 62 | ''' 63 | Removes the given widget from the lyout 64 | 65 | Parameters 66 | ---------- 67 | widget : QWidget 68 | ''' 69 | if self.current_widget() == widget: 70 | layout_item = self._parent_layout.takeAt(1) 71 | if layout_item: 72 | widget = layout_item.widget() 73 | logger.debug('%s setParent None', widget) 74 | widget.setParent(None) 75 | 76 | self._current_widget = None 77 | self._current_index = -1 78 | 79 | self._widgets.remove(widget) 80 | 81 | def current_widget(self) -> QWidget: 82 | ''' 83 | Returns the current selected widget 84 | 85 | Returns 86 | ------- 87 | value : QWidget 88 | ''' 89 | return self._current_widget 90 | 91 | def set_current_index(self, index: int): 92 | ''' 93 | Activates the widget with the give index. 94 | 95 | Parameters 96 | ---------- 97 | index : int 98 | ''' 99 | prev = self.current_widget() 100 | next_ = self.widget(index) 101 | if not next_ or (next_ is prev and not self._current_widget): 102 | return 103 | 104 | reenable_updates = False 105 | parent = self._parent_layout.parentWidget() 106 | if parent and parent.updatesEnabled(): 107 | reenable_updates = True 108 | parent.setUpdatesEnabled(False) 109 | 110 | layout_item = self._parent_layout.takeAt(1) 111 | if layout_item: 112 | widget = layout_item.widget() 113 | logger.debug('%s setParent None', widget) 114 | widget.setParent(None) 115 | 116 | self._parent_layout.addWidget(next_) 117 | if prev: 118 | prev.hide() 119 | 120 | self._current_index = index 121 | self._current_widget = next_ 122 | if reenable_updates: 123 | parent.setUpdatesEnabled(True) 124 | 125 | def current_index(self) -> int: 126 | ''' 127 | Returns the index of the current active widget 128 | 129 | Returns 130 | ------- 131 | value : int 132 | ''' 133 | return self._current_index 134 | 135 | def is_empty(self) -> bool: 136 | ''' 137 | Returns true if there are no widgets in the layout 138 | 139 | Returns 140 | ------- 141 | value : bool 142 | ''' 143 | return len(self._widgets) == 0 144 | 145 | def index_of(self, widget: QWidget) -> int: 146 | ''' 147 | Returns the index of the given widget 148 | 149 | Parameters 150 | ---------- 151 | widget : QWidget 152 | 153 | Returns 154 | ------- 155 | value : int 156 | ''' 157 | return self._widgets.index(widget) 158 | 159 | def widget(self, index: int) -> QWidget: 160 | ''' 161 | Returns the widget for the given index 162 | 163 | Parameters 164 | ---------- 165 | index : int 166 | 167 | Returns 168 | ------- 169 | value : QWidget 170 | ''' 171 | try: 172 | return self._widgets[index] 173 | except IndexError: 174 | return None 175 | 176 | def geometry(self) -> QRect: 177 | ''' 178 | Returns the geometry of the current active widget 179 | 180 | Returns 181 | ------- 182 | value : QRect 183 | ''' 184 | if not self._widgets: 185 | return QRect() 186 | return self.current_widget().geometry() 187 | -------------------------------------------------------------------------------- /qtpydocking/dock_area_tab_bar.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Optional 2 | import logging 3 | 4 | from qtpy.QtCore import QEvent, QObject, QPoint, Qt, Signal 5 | from qtpy.QtGui import QMouseEvent, QWheelEvent 6 | from qtpy.QtWidgets import QBoxLayout, QFrame, QScrollArea, QSizePolicy, QWidget 7 | 8 | from .util import start_drag_distance, event_filter_decorator 9 | from .enums import DragState, DockWidgetArea 10 | from .dock_widget_tab import DockWidgetTab 11 | from .floating_dock_container import FloatingDockContainer 12 | 13 | 14 | if TYPE_CHECKING: 15 | from . import DockAreaWidget 16 | 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | class DockAreaTabBarPrivate: 22 | public: 'DockAreaWidget' 23 | drag_start_mouse_pos: QPoint 24 | dock_area: 'DockAreaWidget' 25 | floating_widget: Optional['FloatingDockContainer'] 26 | tabs_container_widget: QWidget 27 | tabs_layout: QBoxLayout 28 | current_index: int 29 | 30 | def __init__(self, public: 'DockAreaTabBar'): 31 | ''' 32 | Private data for DockAreaTabBar 33 | 34 | Parameters 35 | ---------- 36 | public : DockAreaTabBar 37 | ''' 38 | self.public = public 39 | self.drag_start_mouse_pos = QPoint() 40 | self.dock_area = None 41 | self.floating_widget = None 42 | self.tabs_container_widget = None 43 | self.tabs_layout = None 44 | self.current_index = -1 45 | 46 | def update_tabs(self): 47 | ''' 48 | Update tabs after current index changed or when tabs are removed. The 49 | function reassigns the stylesheet to update the tabs 50 | ''' 51 | # Set active TAB and update all other tabs to be inactive 52 | for i in range(self.public.count()): 53 | tab_widget = self.public.tab(i) 54 | if not tab_widget: 55 | continue 56 | 57 | if i == self.current_index: 58 | tab_widget.show() 59 | tab_widget.set_active_tab(True) 60 | self.public.ensureWidgetVisible(tab_widget) 61 | else: 62 | tab_widget.set_active_tab(False) 63 | 64 | def connect_tab_signals(self, tab): 65 | tab.clicked.connect(self.public.on_tab_clicked) 66 | tab.close_requested.connect(self.public.on_tab_close_requested) 67 | tab.close_other_tabs_requested.connect( 68 | self.public.on_close_other_tabs_requested) 69 | tab.moved.connect(self.public.on_tab_widget_moved) 70 | 71 | def disconnect_tab_signals(self, tab): 72 | tab.clicked.disconnect(self.public.on_tab_clicked) 73 | tab.close_requested.disconnect(self.public.on_tab_close_requested) 74 | tab.close_other_tabs_requested.disconnect( 75 | self.public.on_close_other_tabs_requested) 76 | tab.moved.disconnect(self.public.on_tab_widget_moved) 77 | 78 | 79 | class DockAreaTabBar(QScrollArea): 80 | # This signal is emitted when the tab bar's current tab is about to be 81 | # changed. The new current has the given index, or -1 if there isn't a new 82 | # one. 83 | current_changing = Signal(int) 84 | 85 | # This signal is emitted when the tab bar's current tab changes. The new 86 | # current has the given index, or -1 if there isn't a new one 87 | current_changed = Signal(int) 88 | 89 | # This signal is emitted when user clicks on a tab 90 | tab_bar_clicked = Signal(int) 91 | 92 | # This signal is emitted when the close button on a tab is clicked. The 93 | # index is the index that should be closed. 94 | tab_close_requested = Signal(int) 95 | 96 | # This signal is emitted if a tab has been closed 97 | tab_closed = Signal(int) 98 | 99 | # This signal is emitted if a tab has been opened. A tab is opened if it 100 | # has been made visible 101 | tab_opened = Signal(int) 102 | 103 | # This signal is emitted when the tab has moved the tab at index position 104 | tab_moved = Signal(int, int) 105 | 106 | # This signal is emitted, just before the tab with the given index is 107 | # removed 108 | removing_tab = Signal(int) 109 | 110 | # This signal is emitted if a tab has been inserted 111 | tab_inserted = Signal(int) 112 | 113 | def __init__(self, parent: 'DockAreaWidget'): 114 | ''' 115 | Default Constructor 116 | 117 | Parameters 118 | ---------- 119 | parent : DockAreaWidget 120 | ''' 121 | super().__init__(parent) 122 | 123 | self.d = DockAreaTabBarPrivate(self) 124 | self.d.dock_area = parent 125 | 126 | self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) 127 | self.setFrameStyle(QFrame.NoFrame) 128 | self.setWidgetResizable(True) 129 | self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 130 | self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) 131 | 132 | self.d.tabs_container_widget = QWidget() 133 | self.d.tabs_container_widget.setObjectName("tabsContainerWidget") 134 | self.setWidget(self.d.tabs_container_widget) 135 | 136 | self.d.tabs_layout = QBoxLayout(QBoxLayout.LeftToRight) 137 | self.d.tabs_layout.setContentsMargins(0, 0, 0, 0) 138 | self.d.tabs_layout.setSpacing(0) 139 | self.d.tabs_layout.addStretch(1) 140 | self.d.tabs_container_widget.setLayout(self.d.tabs_layout) 141 | 142 | def on_tab_clicked(self): 143 | tab = self.sender() 144 | if not tab or not isinstance(tab, DockWidgetTab): 145 | return 146 | 147 | index = self.d.tabs_layout.indexOf(tab) 148 | if index < 0: 149 | return 150 | 151 | self.set_current_index(index) 152 | self.tab_bar_clicked.emit(index) 153 | 154 | def on_tab_close_requested(self): 155 | tab = self.sender() 156 | index = self.d.tabs_layout.indexOf(tab) 157 | self.close_tab(index) 158 | 159 | def on_close_other_tabs_requested(self): 160 | sender = self.sender() 161 | 162 | for i in range(self.count()): 163 | tab = self.tab(i) 164 | if tab.is_closable() and not tab.isHidden() and tab != sender: 165 | self.close_tab(i) 166 | 167 | def on_tab_widget_moved(self, global_pos: QPoint): 168 | ''' 169 | On tab widget moved 170 | 171 | Parameters 172 | ---------- 173 | global_pos : QPoint 174 | ''' 175 | moving_tab = self.sender() 176 | if not moving_tab or not isinstance(moving_tab, DockWidgetTab): 177 | return 178 | 179 | from_index = self.d.tabs_layout.indexOf(moving_tab) 180 | mouse_pos = self.mapFromGlobal(global_pos) 181 | to_index = -1 182 | 183 | # Find tab under mouse 184 | for i in range(self.count()): 185 | drop_tab = self.tab(i) 186 | if (drop_tab == moving_tab or not drop_tab.isVisibleTo(self) or 187 | not drop_tab.geometry().contains(mouse_pos)): 188 | continue 189 | 190 | to_index = self.d.tabs_layout.indexOf(drop_tab) 191 | if to_index == from_index: 192 | to_index = -1 193 | continue 194 | elif to_index < 0: 195 | to_index = 0 196 | 197 | break 198 | 199 | # Now check if the mouse is behind the last tab 200 | if to_index < 0: 201 | if mouse_pos.x() > self.tab(self.count()-1).geometry().right(): 202 | logger.debug('after all tabs') 203 | to_index = self.count()-1 204 | else: 205 | to_index = from_index 206 | 207 | self.d.tabs_layout.removeWidget(moving_tab) 208 | self.d.tabs_layout.insertWidget(to_index, moving_tab) 209 | if to_index >= 0: 210 | logger.debug('tabMoved from %s to %s', from_index, to_index) 211 | self.tab_moved.emit(from_index, to_index) 212 | self.set_current_index(to_index) 213 | 214 | def wheelEvent(self, event: QWheelEvent): 215 | ''' 216 | Wheelevent 217 | 218 | Parameters 219 | ---------- 220 | event : QWheelEvent 221 | ''' 222 | event.accept() 223 | direction = event.angleDelta().y() 224 | horizontal_bar = self.horizontalScrollBar() 225 | delta = (20 if direction < 0 226 | else -20) 227 | horizontal_bar.setValue(self.horizontalScrollBar().value() + delta) 228 | 229 | def mousePressEvent(self, ev: QMouseEvent): 230 | ''' 231 | Stores mouse position to detect dragging 232 | 233 | Parameters 234 | ---------- 235 | ev : QMouseEvent 236 | ''' 237 | if ev.button() == Qt.LeftButton: 238 | ev.accept() 239 | self.d.drag_start_mouse_pos = ev.pos() 240 | return 241 | 242 | super().mousePressEvent(ev) 243 | 244 | def mouseReleaseEvent(self, ev: QMouseEvent): 245 | ''' 246 | Stores mouse position to detect dragging 247 | 248 | Parameters 249 | ---------- 250 | ev : QMouseEvent 251 | ''' 252 | if ev.button() != Qt.LeftButton: 253 | return super().mouseReleaseEvent(ev) 254 | 255 | logger.debug('DockAreaTabBar.mouseReleaseEvent') 256 | ev.accept() 257 | self.d.floating_widget = None 258 | self.d.drag_start_mouse_pos = QPoint() 259 | 260 | def mouseMoveEvent(self, ev: QMouseEvent): 261 | ''' 262 | Starts floating the complete docking area including all dock widgets, 263 | if it is not the last dock area in a floating widget 264 | 265 | Parameters 266 | ---------- 267 | ev : QMouseEvent 268 | ''' 269 | super().mouseMoveEvent(ev) 270 | if ev.buttons() != Qt.LeftButton: 271 | return 272 | if self.d.floating_widget: 273 | self.d.floating_widget.move_floating() 274 | return 275 | 276 | # If this is the last dock area in a dock container it does not make 277 | # sense to move it to a new floating widget and leave this one empty 278 | container = self.d.dock_area.dock_container() 279 | if container.is_floating() and container.visible_dock_area_count() == 1: 280 | return 281 | 282 | # If one single dock widget in this area is not floatable, then the 283 | # whole area isn't floatable 284 | if not self.d.dock_area.floatable: 285 | return 286 | 287 | drag_distance = (self.d.drag_start_mouse_pos - 288 | ev.pos()).manhattanLength() 289 | if drag_distance >= start_drag_distance(): 290 | logger.debug('DockAreaTabBar.startFloating') 291 | self.start_floating(self.d.drag_start_mouse_pos) 292 | overlay = self.d.dock_area.dock_manager().container_overlay() 293 | overlay.set_allowed_areas(DockWidgetArea.outer_dock_areas) 294 | 295 | def mouseDoubleClickEvent(self, event: QMouseEvent): 296 | ''' 297 | Double clicking the title bar also starts floating of the complete area 298 | 299 | Parameters 300 | ---------- 301 | event : QMouseEvent 302 | ''' 303 | # If this is the last dock area in a dock container it does not make 304 | # sense to move it to a new floating widget and leave this one empty 305 | container = self.d.dock_area.dock_container() 306 | if container.is_floating() and container.dock_area_count() == 1: 307 | return 308 | if not self.d.dock_area.floatable: 309 | return 310 | 311 | self.make_area_floating(event.pos(), DragState.inactive) 312 | 313 | def start_floating(self, offset: QPoint): 314 | ''' 315 | Starts floating 316 | 317 | Parameters 318 | ---------- 319 | offset : QPoint 320 | ''' 321 | self.d.floating_widget = self.make_area_floating( 322 | offset, DragState.floating_widget) 323 | 324 | def make_area_floating(self, offset: QPoint, 325 | drag_state: DragState) -> FloatingDockContainer: 326 | ''' 327 | Makes the dock area floating 328 | 329 | Parameters 330 | ---------- 331 | offset : QPoint 332 | drag_state : DragState 333 | 334 | Returns 335 | ------- 336 | value : FloatingDockContainer 337 | ''' 338 | size = self.d.dock_area.size() 339 | 340 | floating_widget = FloatingDockContainer(dock_area=self.d.dock_area) 341 | floating_widget.start_floating(offset, size, drag_state) 342 | top_level_dock_widget = floating_widget.top_level_dock_widget() 343 | if top_level_dock_widget is not None: 344 | top_level_dock_widget.emit_top_level_changed(True) 345 | 346 | return floating_widget 347 | 348 | def insert_tab(self, index: int, tab: 'DockWidgetTab'): 349 | ''' 350 | Inserts the given dock widget tab at the given position. Inserting a 351 | new tab at an index less than or equal to the current index will 352 | increment the current index, but keep the current tab. 353 | 354 | Parameters 355 | ---------- 356 | index : int 357 | tab : DockWidgetTab 358 | ''' 359 | self.d.tabs_layout.insertWidget(index, tab) 360 | 361 | self.d.connect_tab_signals(tab) 362 | tab.installEventFilter(self) 363 | self.tab_inserted.emit(index) 364 | if index <= self.d.current_index: 365 | self.set_current_index(self.d.current_index+1) 366 | 367 | def remove_tab(self, tab: 'DockWidgetTab'): 368 | ''' 369 | Removes the given DockWidgetTab from the tabbar 370 | 371 | Parameters 372 | ---------- 373 | tab : DockWidgetTab 374 | ''' 375 | if not self.count(): 376 | return 377 | 378 | logger.debug('DockAreaTabBar.removeTab') 379 | new_current_index = self.current_index() 380 | remove_index = self.d.tabs_layout.indexOf(tab) 381 | if self.count() == 1: 382 | new_current_index = -1 383 | 384 | if new_current_index > remove_index: 385 | new_current_index -= 1 386 | elif new_current_index == remove_index: 387 | new_current_index = -1 388 | 389 | # First we walk to the right to search for the next visible tab 390 | for i in range(remove_index + 1, self.count()): 391 | if self.tab(i).isVisibleTo(self): 392 | new_current_index = i-1 393 | break 394 | 395 | # If there is no visible tab right to this tab then we walk to 396 | # the left to find a visible tab 397 | if new_current_index < 0: 398 | for i in range(remove_index - 1, -1, -1): 399 | if self.tab(i).isVisibleTo(self): 400 | new_current_index = i 401 | break 402 | 403 | self.removing_tab.emit(remove_index) 404 | self.d.tabs_layout.removeWidget(tab) 405 | self.d.disconnect_tab_signals(tab) 406 | 407 | tab.removeEventFilter(self) 408 | logger.debug('NewCurrentIndex %s', new_current_index) 409 | 410 | if new_current_index != self.d.current_index: 411 | self.set_current_index(new_current_index) 412 | else: 413 | self.d.update_tabs() 414 | 415 | def count(self) -> int: 416 | ''' 417 | Returns the number of tabs in this tabbar 418 | 419 | Returns 420 | ------- 421 | value : int 422 | ''' 423 | # The tab bar contains a stretch item as last item 424 | return self.d.tabs_layout.count() - 1 425 | 426 | def current_index(self) -> int: 427 | ''' 428 | Returns the current index or -1 if no tab is selected 429 | 430 | Returns 431 | ------- 432 | value : int 433 | ''' 434 | return self.d.current_index 435 | 436 | def current_tab(self) -> Optional['DockWidgetTab']: 437 | ''' 438 | Returns the current tab or a nullptr if no tab is selected. 439 | 440 | Returns 441 | ------- 442 | value : DockWidgetTab 443 | ''' 444 | if self.d.current_index < 0: 445 | return None 446 | return self.d.tabs_layout.itemAt(self.d.current_index).widget() 447 | 448 | def tab(self, index: int) -> Optional['DockWidgetTab']: 449 | ''' 450 | Returns the tab with the given index 451 | 452 | Parameters 453 | ---------- 454 | index : int 455 | 456 | Returns 457 | ------- 458 | value : DockWidgetTab 459 | ''' 460 | if index >= self.count() or index < 0: 461 | return None 462 | 463 | return self.d.tabs_layout.itemAt(index).widget() 464 | 465 | @event_filter_decorator 466 | def eventFilter(self, tab: QObject, event: QEvent) -> bool: 467 | ''' 468 | Filters the tab widget events 469 | 470 | Parameters 471 | ---------- 472 | tab : QObject 473 | event : QEvent 474 | 475 | Returns 476 | ------- 477 | value : bool 478 | ''' 479 | result = super().eventFilter(tab, event) 480 | if isinstance(tab, DockWidgetTab): 481 | if event.type() == QEvent.Hide: 482 | self.tab_closed.emit(self.d.tabs_layout.indexOf(tab)) 483 | elif event.type() == QEvent.Show: 484 | self.tab_opened.emit(self.d.tabs_layout.indexOf(tab)) 485 | 486 | return result 487 | 488 | def is_tab_open(self, index: int) -> bool: 489 | ''' 490 | This function returns true if the tab is open, that means if it is 491 | visible to the user. If the function returns false, the tab is closed 492 | 493 | Parameters 494 | ---------- 495 | index : int 496 | 497 | Returns 498 | ------- 499 | value : bool 500 | ''' 501 | if index < 0 or index >= self.count(): 502 | return False 503 | 504 | return not self.tab(index).isHidden() 505 | 506 | def set_current_index(self, index: int): 507 | ''' 508 | This property sets the index of the tab bar's visible tab 509 | 510 | Parameters 511 | ---------- 512 | index : int 513 | ''' 514 | if index == self.d.current_index: 515 | return 516 | if index < -1 or index > (self.count()-1): 517 | logger.warning('Invalid index %s', index) 518 | return 519 | 520 | self.current_changing.emit(index) 521 | self.d.current_index = index 522 | self.d.update_tabs() 523 | self.current_changed.emit(index) 524 | 525 | def close_tab(self, index: int): 526 | ''' 527 | This function will close the tab given in Index param. Closing a tab 528 | means, the tab will be hidden, it will not be removed 529 | 530 | Parameters 531 | ---------- 532 | index : int 533 | ''' 534 | if index < 0 or index >= self.count(): 535 | return 536 | 537 | tab = self.tab(index) 538 | if tab.isHidden(): 539 | return 540 | 541 | self.tab_close_requested.emit(index) 542 | tab.hide() 543 | -------------------------------------------------------------------------------- /qtpydocking/dock_area_title_bar.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, Optional 2 | import logging 3 | 4 | from qtpy.QtCore import QPoint, Qt, Signal, QSize 5 | from qtpy.QtGui import QCursor 6 | from qtpy.QtWidgets import (QAbstractButton, QAction, QBoxLayout, QFrame, 7 | QMenu, QSizePolicy, QStyle, QToolButton) 8 | 9 | 10 | from .enums import DockFlags, DragState, DockWidgetFeature, TitleBarButton 11 | from .util import set_button_icon 12 | 13 | 14 | if TYPE_CHECKING: 15 | from . import DockAreaWidget, DockAreaTabBar, DockManager 16 | 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | class DockAreaTitleBarPrivate: 22 | public: 'DockAreaTitleBar' 23 | tabs_menu_button: QToolButton 24 | undock_button: QToolButton 25 | close_button: QToolButton 26 | top_layout: QBoxLayout 27 | dock_area: 'DockAreaWidget' 28 | tab_bar: 'DockAreaTabBar' 29 | menu_outdated: bool 30 | tabs_menu: QMenu 31 | 32 | def __init__(self, public: 'DockAreaTitleBar'): 33 | self.public = public 34 | self.tabs_menu_button = None 35 | self.undock_button = None 36 | self.close_button = None 37 | self.top_layout = None 38 | self.dock_area = None 39 | self.tab_bar = None 40 | self.menu_outdated = True 41 | self.tabs_menu = None 42 | 43 | def create_buttons(self): 44 | ''' 45 | Creates the title bar close and menu buttons 46 | ''' 47 | self.tabs_menu_button = QToolButton() 48 | self.tabs_menu_button.setObjectName("tabsMenuButton") 49 | self.tabs_menu_button.setAutoRaise(True) 50 | self.tabs_menu_button.setPopupMode(QToolButton.InstantPopup) 51 | 52 | style = self.public.style() 53 | set_button_icon(style, self.tabs_menu_button, QStyle.SP_TitleBarUnshadeButton) 54 | 55 | self.tabs_menu = QMenu(self.tabs_menu_button) 56 | self.tabs_menu.setToolTipsVisible(True) 57 | self.tabs_menu.aboutToShow.connect( 58 | self.public.on_tabs_menu_about_to_show) 59 | self.tabs_menu_button.setMenu(self.tabs_menu) 60 | self.tabs_menu_button.setToolTip("List all tabs") 61 | 62 | self.tabs_menu_button.setSizePolicy( 63 | QSizePolicy.Fixed, QSizePolicy.Expanding) 64 | self.top_layout.addWidget(self.tabs_menu_button, 0) 65 | self.tabs_menu_button.menu().triggered.connect( 66 | self.public.on_tabs_menu_action_triggered) 67 | 68 | # Undock button 69 | self.undock_button = QToolButton() 70 | self.undock_button.setObjectName("undockButton") 71 | self.undock_button.setAutoRaise(True) 72 | self.undock_button.setToolTip("Detach Group") 73 | 74 | set_button_icon(style, self.undock_button, 75 | QStyle.SP_TitleBarNormalButton) 76 | 77 | self.undock_button.setSizePolicy( 78 | QSizePolicy.Fixed, QSizePolicy.Expanding) 79 | self.top_layout.addWidget(self.undock_button, 0) 80 | self.undock_button.clicked.connect( 81 | self.public.on_undock_button_clicked) 82 | 83 | # Close button 84 | self.close_button = QToolButton() 85 | self.close_button.setObjectName("closeButton") 86 | self.close_button.setAutoRaise(True) 87 | 88 | set_button_icon(style, self.close_button, QStyle.SP_TitleBarCloseButton) 89 | 90 | if self.test_config_flag(DockFlags.dock_area_close_button_closes_tab): 91 | self.close_button.setToolTip("Close Active Tab") 92 | else: 93 | self.close_button.setToolTip("Close Group") 94 | 95 | self.close_button.setSizePolicy( 96 | QSizePolicy.Fixed, QSizePolicy.Expanding) 97 | self.close_button.setIconSize(QSize(16, 16)) 98 | self.top_layout.addWidget(self.close_button, 0) 99 | self.close_button.clicked.connect(self.public.on_close_button_clicked) 100 | 101 | def create_tab_bar(self): 102 | ''' 103 | Creates the internal TabBar 104 | ''' 105 | 106 | from .dock_area_tab_bar import DockAreaTabBar 107 | self.tab_bar = DockAreaTabBar(self.dock_area) 108 | self.top_layout.addWidget(self.tab_bar) 109 | 110 | self.tab_bar.tab_closed.connect(self.public.mark_tabs_menu_outdated) 111 | self.tab_bar.tab_opened.connect(self.public.mark_tabs_menu_outdated) 112 | self.tab_bar.tab_inserted.connect(self.public.mark_tabs_menu_outdated) 113 | self.tab_bar.removing_tab.connect(self.public.mark_tabs_menu_outdated) 114 | self.tab_bar.tab_moved.connect(self.public.mark_tabs_menu_outdated) 115 | self.tab_bar.current_changed.connect(self.public.on_current_tab_changed) 116 | self.tab_bar.tab_bar_clicked.connect(self.public.tab_bar_clicked) 117 | 118 | self.tab_bar.setContextMenuPolicy(Qt.CustomContextMenu) 119 | self.tab_bar.customContextMenuRequested.connect( 120 | self.public.show_context_menu) 121 | 122 | def dock_manager(self) -> 'DockManager': 123 | ''' 124 | Convenience function for DockManager access 125 | 126 | Returns 127 | ------- 128 | value : DockManager 129 | ''' 130 | return self.dock_area.dock_manager() 131 | 132 | def test_config_flag(self, flag: DockFlags) -> bool: 133 | ''' 134 | Returns true if the given config flag is set 135 | 136 | Parameters 137 | ---------- 138 | flag : DockFlags 139 | 140 | Returns 141 | ------- 142 | value : bool 143 | ''' 144 | return flag in self.dock_area.dock_manager().config_flags() 145 | 146 | 147 | class DockAreaTitleBar(QFrame): 148 | # This signal is emitted if a tab in the tab bar is clicked by the user or 149 | # if the user clicks on a tab item in the title bar tab menu. 150 | tab_bar_clicked = Signal(int) 151 | 152 | def __init__(self, parent: 'DockAreaWidget'): 153 | ''' 154 | Default Constructor 155 | 156 | Parameters 157 | ---------- 158 | parent : DockAreaWidget 159 | ''' 160 | super().__init__(parent) 161 | self.d = DockAreaTitleBarPrivate(self) 162 | self.d.dock_area = parent 163 | self.setObjectName("dockAreaTitleBar") 164 | 165 | self.d.top_layout = QBoxLayout(QBoxLayout.LeftToRight) 166 | self.d.top_layout.setContentsMargins(0, 0, 0, 0) 167 | self.d.top_layout.setSpacing(0) 168 | self.setLayout(self.d.top_layout) 169 | self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) 170 | self.d.create_tab_bar() 171 | self.d.create_buttons() 172 | 173 | def __repr__(self): 174 | return f'<{self.__class__.__name__}>' 175 | 176 | def on_tabs_menu_about_to_show(self): 177 | if not self.d.menu_outdated: 178 | return 179 | 180 | menu = self.d.tabs_menu_button.menu() 181 | if menu is not None: 182 | menu.clear() 183 | 184 | for i in range(self.d.tab_bar.count()): 185 | if not self.d.tab_bar.is_tab_open(i): 186 | continue 187 | 188 | tab = self.d.tab_bar.tab(i) 189 | # TODO icon None? 190 | action = menu.addAction(tab.text()) # QAction(tab.icon(), tab.text())) 191 | action.setToolTip(tab.toolTip()) 192 | action.setData(i) 193 | 194 | self.d.menu_outdated = False 195 | 196 | def on_close_button_clicked(self): 197 | logger.debug('DockAreaTitleBar.onCloseButtonClicked') 198 | if self.d.test_config_flag(DockFlags.dock_area_close_button_closes_tab): 199 | self.d.tab_bar.close_tab(self.d.tab_bar.current_index()) 200 | else: 201 | self.d.dock_area.close_area() 202 | 203 | def on_undock_button_clicked(self): 204 | if self.d.dock_area.floatable: 205 | self.d.tab_bar.make_area_floating( 206 | self.mapFromGlobal(QCursor.pos()), DragState.inactive) 207 | 208 | def on_tabs_menu_action_triggered(self, action: QAction): 209 | ''' 210 | On tabs menu action triggered 211 | 212 | Parameters 213 | ---------- 214 | action : QAction 215 | ''' 216 | index = action.data() 217 | self.d.tab_bar.set_current_index(index) 218 | self.tab_bar_clicked.emit(index) 219 | 220 | def on_current_tab_changed(self, index: int): 221 | ''' 222 | On current tab changed 223 | 224 | Parameters 225 | ---------- 226 | index : int 227 | ''' 228 | if index < 0: 229 | return 230 | 231 | if self.d.test_config_flag(DockFlags.dock_area_close_button_closes_tab): 232 | dock_widget = self.d.tab_bar.tab(index).dock_widget() 233 | enabled = DockWidgetFeature.closable in dock_widget.features() 234 | self.d.close_button.setEnabled(enabled) 235 | 236 | def show_context_menu(self, pos: QPoint): 237 | ''' 238 | Show context menu 239 | 240 | Parameters 241 | ---------- 242 | pos : QPoint 243 | ''' 244 | menu = QMenu(self) 245 | menu.addAction("Detach Area", self.on_undock_button_clicked) 246 | menu.addSeparator() 247 | action = menu.addAction("Close Area", self.on_close_button_clicked) 248 | action.setEnabled(self.d.dock_area.closable) 249 | 250 | menu.addAction("Close Other Areas", self.d.dock_area.close_other_areas) 251 | menu.exec_(self.mapToGlobal(pos)) 252 | 253 | def mark_tabs_menu_outdated(self): 254 | self.d.menu_outdated = True 255 | 256 | def tab_bar(self) -> 'DockAreaTabBar': 257 | ''' 258 | Returns the pointer to the tabBar 259 | 260 | Returns 261 | ------- 262 | value : DockAreaTabBar 263 | ''' 264 | return self.d.tab_bar 265 | 266 | def button(self, which: TitleBarButton) -> Optional[QAbstractButton]: 267 | ''' 268 | Returns the button corresponding to the given title bar button identifier 269 | 270 | Parameters 271 | ---------- 272 | which : TitleBarButton 273 | 274 | Returns 275 | ------- 276 | value : QAbstractButton 277 | ''' 278 | if which == TitleBarButton.tabs_menu: 279 | return self.d.tabs_menu_button 280 | if which == TitleBarButton.undock: 281 | return self.d.undock_button 282 | if which == TitleBarButton.close: 283 | return self.d.close_button 284 | 285 | return None 286 | 287 | def setVisible(self, visible: bool): 288 | ''' 289 | This function is here for debug reasons 290 | 291 | Parameters 292 | ---------- 293 | visible : bool 294 | ''' 295 | super().setVisible(visible) 296 | self.mark_tabs_menu_outdated() 297 | -------------------------------------------------------------------------------- /qtpydocking/dock_area_widget.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import TYPE_CHECKING, Optional 3 | 4 | from qtpy.QtCore import QRect, QXmlStreamWriter, Signal 5 | from qtpy.QtWidgets import QAbstractButton, QAction, QBoxLayout, QFrame 6 | 7 | from .util import (find_parent, DEBUG_LEVEL, hide_empty_parent_splitters, 8 | emit_top_level_event_for_widget) 9 | from .enums import TitleBarButton, DockWidgetFeature 10 | from .dock_area_layout import DockAreaLayout 11 | 12 | if TYPE_CHECKING: 13 | from . import (DockContainerWidget, DockManager, DockWidget, DockWidgetTab, 14 | DockAreaTabBar, DockAreaTitleBar) 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class DockAreaWidgetPrivate: 20 | public: 'DockAreaWidget' 21 | layout: QBoxLayout 22 | contents_layout: DockAreaLayout 23 | title_bar: 'DockAreaTitleBar' 24 | dock_manager: 'DockManager' 25 | update_title_bar_buttons: bool 26 | 27 | def __init__(self, public): 28 | ''' 29 | Private data constructor 30 | 31 | Parameters 32 | ---------- 33 | public : DockAreaWidget 34 | ''' 35 | self.public = public 36 | self.layout = None 37 | self.contents_layout = None 38 | self.title_bar = None 39 | self.dock_manager = None 40 | self.update_title_bar_buttons = False 41 | 42 | def create_title_bar(self): 43 | ''' 44 | Creates the layout for top area with tabs and close button 45 | ''' 46 | from .dock_area_title_bar import DockAreaTitleBar 47 | self.title_bar = DockAreaTitleBar(self.public) 48 | self.layout.addWidget(self.title_bar) 49 | 50 | tab_bar = self.tab_bar() 51 | tab_bar.tab_close_requested.connect( 52 | self.public.on_tab_close_requested) 53 | self.title_bar.tab_bar_clicked.connect(self.public.set_current_index) 54 | tab_bar.tab_moved.connect(self.public.reorder_dock_widget) 55 | 56 | def dock_widget_at(self, index: int) -> 'DockWidget': 57 | ''' 58 | Returns the dock widget with the given index 59 | 60 | Parameters 61 | ---------- 62 | index : int 63 | 64 | Returns 65 | ------- 66 | value : DockWidget 67 | ''' 68 | return self.contents_layout.widget(index) 69 | 70 | def tab_widget_at(self, index: int) -> 'DockWidgetTab': 71 | ''' 72 | Convenience function to ease title widget access by index 73 | 74 | Parameters 75 | ---------- 76 | index : int 77 | 78 | Returns 79 | ------- 80 | value : DockWidgetTab 81 | ''' 82 | return self.dock_widget_at(index).tab_widget() 83 | 84 | def dock_widget_tab_action(self, dock_widget: 'DockWidget') -> QAction: 85 | ''' 86 | Returns the tab action of the given dock widget 87 | 88 | Parameters 89 | ---------- 90 | dock_widget : DockWidget 91 | 92 | Returns 93 | ------- 94 | value : QAction 95 | ''' 96 | return dock_widget.property('action') 97 | 98 | def dock_widget_index(self, dock_widget: 'DockWidget') -> int: 99 | ''' 100 | Returns the index of the given dock widget 101 | 102 | Parameters 103 | ---------- 104 | dock_widget : DockWidget 105 | 106 | Returns 107 | ------- 108 | value : int 109 | ''' 110 | return dock_widget.property('index') 111 | 112 | def tab_bar(self) -> 'DockAreaTabBar': 113 | ''' 114 | Convenience function for tabbar access 115 | 116 | Returns 117 | ------- 118 | value : DockAreaTabBar 119 | ''' 120 | return self.title_bar.tab_bar() 121 | 122 | def update_title_bar_button_states(self): 123 | ''' 124 | Udpates the enable state of the close/detach buttons 125 | ''' 126 | if self.public.isHidden(): 127 | self.update_title_bar_buttons = True 128 | return 129 | 130 | close_button = self.title_bar.button(TitleBarButton.close) 131 | close_button.setEnabled(self.public.closable) 132 | 133 | undock_button = self.title_bar.button(TitleBarButton.undock) 134 | undock_button.setEnabled(self.public.floatable) 135 | 136 | self.update_title_bar_buttons = False 137 | 138 | 139 | class DockAreaWidget(QFrame): 140 | # This signal is emitted when user clicks on a tab at an index. 141 | tab_bar_clicked = Signal(int) 142 | # This signal is emitted when the tab bar's current tab is about to be 143 | # changed. The new current has the given index, or -1 if there isn't a new one. 144 | current_changing = Signal(int) 145 | 146 | # This signal is emitted when the tab bar's current tab changes. The new 147 | # current has the given index, or -1 if there isn't a new one 148 | current_changed = Signal(int) 149 | 150 | # This signal is emitted if the visibility of this dock area is toggled via 151 | # toggle view function 152 | view_toggled = Signal(bool) 153 | 154 | def __init__(self, dock_manager: 'DockManager', 155 | parent: 'DockContainerWidget'): 156 | ''' 157 | Default Constructor 158 | 159 | Parameters 160 | ---------- 161 | dock_manager : DockManager 162 | parent : DockContainerWidget 163 | ''' 164 | super().__init__(parent) 165 | self.d = DockAreaWidgetPrivate(self) 166 | self.d.dock_manager = dock_manager 167 | self.d.layout = QBoxLayout(QBoxLayout.TopToBottom) 168 | self.d.layout.setContentsMargins(0, 0, 0, 0) 169 | self.d.layout.setSpacing(0) 170 | self.setLayout(self.d.layout) 171 | self.d.create_title_bar() 172 | self.d.contents_layout = DockAreaLayout(self.d.layout) 173 | 174 | def __repr__(self): 175 | return f'<{self.__class__.__name__}>' 176 | 177 | def on_tab_close_requested(self, index: int): 178 | ''' 179 | On tab close requested 180 | 181 | Parameters 182 | ---------- 183 | index : int 184 | ''' 185 | logger.debug('DockAreaWidget.onTabCloseRequested %s', index) 186 | self.dock_widget(index).toggle_view(False) 187 | 188 | def reorder_dock_widget(self, from_index: int, to_index: int): 189 | ''' 190 | Reorder the index position of DockWidget at fromIndx to toIndex if a 191 | tab in the tabbar is dragged from one index to another one 192 | 193 | Parameters 194 | ---------- 195 | from_index : int 196 | to_index : int 197 | ''' 198 | logger.debug('DockAreaWidget.reorderDockWidget') 199 | if (from_index >= self.d.contents_layout.count() or 200 | from_index < 0 or 201 | to_index >= self.d.contents_layout.count() or 202 | to_index < 0 or 203 | from_index == to_index): 204 | logger.debug('Invalid index for tab movement %s:%s', from_index, 205 | to_index) 206 | return 207 | 208 | widget = self.d.contents_layout.widget(from_index) 209 | self.d.contents_layout.remove_widget(widget) 210 | self.d.contents_layout.insert_widget(to_index, widget) 211 | self.set_current_index(to_index) 212 | 213 | def insert_dock_widget(self, index: int, dock_widget: 'DockWidget', 214 | activate: bool = True): 215 | ''' 216 | Inserts a dock widget into dock area. 217 | 218 | All dockwidgets in the dock area tabified in a stacked layout with 219 | tabs. The index indicates the index of the new dockwidget in the tabbar 220 | and in the stacked layout. If the Activate parameter is true, the new 221 | DockWidget will be the active one in the stacked layout 222 | 223 | Parameters 224 | ---------- 225 | index : int 226 | dock_widget : DockWidget 227 | activate : bool, optional 228 | ''' 229 | self.d.contents_layout.insert_widget(index, dock_widget) 230 | dock_widget.tab_widget().set_dock_area_widget(self) 231 | tab_widget = dock_widget.tab_widget() 232 | 233 | # Inserting the tab will change the current index which in turn will 234 | # make the tab widget visible in the slot 235 | tab_bar = self.d.tab_bar() 236 | tab_bar.blockSignals(True) 237 | tab_bar.insert_tab(index, tab_widget) 238 | tab_bar.blockSignals(False) 239 | 240 | tab_widget.setVisible(not dock_widget.is_closed()) 241 | dock_widget.setProperty('index', index) 242 | if activate: 243 | self.set_current_index(index) 244 | 245 | dock_widget.set_dock_area(self) 246 | self.d.update_title_bar_button_states() 247 | 248 | def add_dock_widget(self, dock_widget: 'DockWidget'): 249 | ''' 250 | Add a new dock widget to dock area. All dockwidgets in the dock area tabified in a stacked layout with tabs 251 | 252 | Parameters 253 | ---------- 254 | dock_widget : DockWidget 255 | ''' 256 | self.insert_dock_widget(self.d.contents_layout.count(), dock_widget) 257 | 258 | def remove_dock_widget(self, dock_widget: 'DockWidget'): 259 | ''' 260 | Removes the given dock widget from the dock area 261 | 262 | Parameters 263 | ---------- 264 | dock_widget : DockWidget 265 | ''' 266 | logger.debug('DockAreaWidget.removeDockWidget') 267 | next_open_dock_widget = self.next_open_dock_widget(dock_widget) 268 | self.d.contents_layout.remove_widget(dock_widget) 269 | tab_widget = dock_widget.tab_widget() 270 | tab_widget.hide() 271 | self.d.tab_bar().remove_tab(tab_widget) 272 | dock_container = self.dock_container() 273 | if next_open_dock_widget is not None: 274 | self.set_current_dock_widget(next_open_dock_widget) 275 | elif (self.d.contents_layout.is_empty() and 276 | dock_container.dock_area_count() > 1): 277 | logger.debug('Dock Area empty') 278 | dock_container.remove_dock_area(self) 279 | self.deleteLater() 280 | else: 281 | # if contents layout is not empty but there are no more open dock 282 | # widgets, then we need to hide the dock area because it does not 283 | # contain any visible content 284 | self.hide_area_with_no_visible_content() 285 | 286 | self.d.update_title_bar_button_states() 287 | self.update_title_bar_visibility() 288 | top_level_dock_widget = dock_container.top_level_dock_widget() 289 | if top_level_dock_widget is not None: 290 | top_level_dock_widget.emit_top_level_changed(True) 291 | 292 | if DEBUG_LEVEL > 0: 293 | dock_container.dump_layout() 294 | 295 | def toggle_dock_widget_view(self, dock_widget: 'DockWidget', open_: bool): 296 | ''' 297 | Called from dock widget if it is opened or closed 298 | 299 | Parameters 300 | ---------- 301 | dock_widget : DockWidget 302 | Unused 303 | open : bool 304 | Unused 305 | ''' 306 | #pylint: disable=unused-argument 307 | self.update_title_bar_visibility() 308 | 309 | def next_open_dock_widget(self, dock_widget: 'DockWidget' 310 | ) -> Optional['DockWidget']: 311 | ''' 312 | This is a helper function to get the next open dock widget to activate 313 | if the given DockWidget will be closed or removed. The function returns 314 | the next widget that should be activated or nullptr in case there are 315 | no more open widgets in this area. 316 | 317 | Parameters 318 | ---------- 319 | dock_widget : DockWidget 320 | 321 | Returns 322 | ------- 323 | value : DockWidget 324 | ''' 325 | open_dock_widgets = self.opened_dock_widgets() 326 | count = len(open_dock_widgets) 327 | if count > 1 or (count == 1 and open_dock_widgets[0] != dock_widget): 328 | if open_dock_widgets[-1] == dock_widget: 329 | next_dock_widget = open_dock_widgets[-2] 330 | else: 331 | next_index = open_dock_widgets.index(dock_widget)+1 332 | next_dock_widget = open_dock_widgets[next_index] 333 | return next_dock_widget 334 | return None 335 | 336 | def index(self, dock_widget: 'DockWidget') -> int: 337 | ''' 338 | Returns the index of the given DockWidget in the internal layout 339 | 340 | Parameters 341 | ---------- 342 | dock_widget : DockWidget 343 | 344 | Returns 345 | ------- 346 | value : int 347 | ''' 348 | return self.d.contents_layout.index_of(dock_widget) 349 | 350 | def hide_area_with_no_visible_content(self): 351 | ''' 352 | Call this function, if you already know, that the dock does not contain 353 | any visible content (any open dock widgets). 354 | ''' 355 | self.toggle_view(False) 356 | 357 | # Hide empty parent splitters 358 | from .dock_splitter import DockSplitter 359 | splitter = find_parent(DockSplitter, self) 360 | hide_empty_parent_splitters(splitter) 361 | 362 | # Hide empty floating widget 363 | container = self.dock_container() 364 | if not container.is_floating(): 365 | return 366 | 367 | self.update_title_bar_visibility() 368 | top_level_widget = container.top_level_dock_widget() 369 | floating_widget = container.floating_widget() 370 | if top_level_widget is not None: 371 | floating_widget.update_window_title() 372 | emit_top_level_event_for_widget(top_level_widget, True) 373 | 374 | elif not container.opened_dock_areas(): 375 | floating_widget.hide() 376 | 377 | def update_title_bar_visibility(self): 378 | ''' 379 | Updates the dock area layout and components visibility 380 | ''' 381 | container = self.dock_container() 382 | if not container: 383 | return 384 | 385 | if self.d.title_bar: 386 | visible = not container.is_floating() or not container.has_top_level_dock_widget() 387 | self.d.title_bar.setVisible(visible) 388 | 389 | def internal_set_current_dock_widget(self, dock_widget: 'DockWidget'): 390 | ''' 391 | This is the internal private function for setting the current widget. 392 | This function is called by the public setCurrentDockWidget() function 393 | and by the dock manager when restoring the state 394 | 395 | Parameters 396 | ---------- 397 | dock_widget : DockWidget 398 | ''' 399 | index = self.index(dock_widget) 400 | if index < 0: 401 | return 402 | 403 | self.set_current_index(index) 404 | 405 | def mark_title_bar_menu_outdated(self): 406 | ''' 407 | Marks tabs menu to update 408 | ''' 409 | if self.d.title_bar: 410 | self.d.title_bar.mark_tabs_menu_outdated() 411 | 412 | def toggle_view(self, open_: bool): 413 | ''' 414 | Toggle view 415 | 416 | Parameters 417 | ---------- 418 | open_ : bool 419 | ''' 420 | self.setVisible(open_) 421 | self.view_toggled.emit(open_) 422 | 423 | def dock_manager(self) -> 'DockManager': 424 | ''' 425 | Returns the dock manager object this dock area belongs to 426 | 427 | Returns 428 | ------- 429 | value : DockManager 430 | ''' 431 | return self.d.dock_manager 432 | 433 | def dock_container(self) -> 'DockContainerWidget': 434 | ''' 435 | Returns the dock container widget this dock area widget belongs to or 0 if there is no 436 | 437 | Returns 438 | ------- 439 | value : DockContainerWidget 440 | ''' 441 | from .dock_container_widget import DockContainerWidget 442 | return find_parent(DockContainerWidget, self) 443 | 444 | def title_bar_geometry(self) -> QRect: 445 | ''' 446 | Returns the rectangle of the title area 447 | 448 | Returns 449 | ------- 450 | value : QRect 451 | ''' 452 | return self.d.title_bar.geometry() 453 | 454 | def content_area_geometry(self) -> QRect: 455 | ''' 456 | Returns the rectangle of the content 457 | 458 | Returns 459 | ------- 460 | value : QRect 461 | ''' 462 | return self.d.contents_layout.geometry() 463 | 464 | def dock_widgets_count(self) -> int: 465 | ''' 466 | Returns the number of dock widgets in this area 467 | 468 | Returns 469 | ------- 470 | value : int 471 | ''' 472 | return self.d.contents_layout.count() 473 | 474 | def dock_widgets(self) -> list: 475 | ''' 476 | Returns a list of all dock widgets in this dock area. This list 477 | contains open and closed dock widgets. 478 | 479 | Returns 480 | ------- 481 | value : list of DockWidget 482 | ''' 483 | return [ 484 | self.dock_widget(i) 485 | for i in range(self.d.contents_layout.count()) 486 | ] 487 | 488 | def open_dock_widgets_count(self) -> int: 489 | ''' 490 | Returns the number of dock widgets in this area 491 | 492 | Returns 493 | ------- 494 | value : int 495 | ''' 496 | return len(self.opened_dock_widgets()) 497 | 498 | def opened_dock_widgets(self) -> list: 499 | ''' 500 | Returns a list of dock widgets that are not closed 501 | 502 | Returns 503 | ------- 504 | value : list of DockWidget 505 | ''' 506 | return [w for w in self.dock_widgets() 507 | if not w.is_closed() 508 | ] 509 | 510 | def dock_widget(self, index: int) -> 'DockWidget': 511 | ''' 512 | Returns a dock widget by its index 513 | 514 | Parameters 515 | ---------- 516 | index : int 517 | 518 | Returns 519 | ------- 520 | value : DockWidget 521 | ''' 522 | return self.d.contents_layout.widget(index) 523 | 524 | def current_index(self) -> int: 525 | ''' 526 | Returns the index of the current active dock widget or -1 if there are 527 | is no active dock widget (ie.e if all dock widgets are closed) 528 | 529 | Returns 530 | ------- 531 | value : int 532 | ''' 533 | return self.d.contents_layout.current_index() 534 | 535 | def index_of_first_open_dock_widget(self) -> int: 536 | ''' 537 | Returns the index of the first open dock widgets in the list of dock 538 | widgets. 539 | 540 | This function is here for performance reasons. Normally it would be 541 | possible to take the first dock widget from the list returned by 542 | openedDockWidgets() function. But that function enumerates all dock widgets 543 | while this functions stops after the first open dock widget. If there are no 544 | open dock widgets, the function returns -1. 545 | 546 | Returns 547 | ------- 548 | value : int 549 | ''' 550 | for i in range(self.d.contents_layout.count()): 551 | if not self.dock_widget(i).is_closed(): 552 | return i 553 | 554 | return -1 555 | 556 | def current_dock_widget(self) -> Optional['DockWidget']: 557 | ''' 558 | Returns the current active dock widget or a nullptr if there is no 559 | active dock widget (i.e. if all dock widgets are closed) 560 | 561 | Returns 562 | ------- 563 | value : DockWidget 564 | ''' 565 | current_index = self.current_index() 566 | if current_index < 0: 567 | return None 568 | 569 | return self.dock_widget(current_index) 570 | 571 | def set_current_dock_widget(self, dock_widget: 'DockWidget'): 572 | ''' 573 | Shows the tab with the given dock widget 574 | 575 | Parameters 576 | ---------- 577 | dock_widget : DockWidget 578 | ''' 579 | if self.dock_manager().is_restoring_state(): 580 | return 581 | 582 | self.internal_set_current_dock_widget(dock_widget) 583 | 584 | def save_state(self, stream: QXmlStreamWriter): 585 | ''' 586 | Saves the state into the given stream 587 | 588 | Parameters 589 | ---------- 590 | stream : QXmlStreamWriter 591 | ''' 592 | stream.writeStartElement("Area") 593 | stream.writeAttribute("Tabs", str(self.d.contents_layout.count())) 594 | current_dock_widget = self.current_dock_widget() 595 | name = current_dock_widget.objectName() if current_dock_widget else '' 596 | stream.writeAttribute("Current", name) 597 | logger.debug('DockAreaWidget.saveState TabCount: %s current: %s', 598 | self.d.contents_layout.count(), name) 599 | 600 | for i in range(self.d.contents_layout.count()): 601 | self.dock_widget(i).save_state(stream) 602 | 603 | stream.writeEndElement() 604 | 605 | @property 606 | def closable(self): 607 | ''' 608 | Is the dock area widget closable? 609 | ''' 610 | return DockWidgetFeature.closable in self.features() 611 | 612 | @property 613 | def floatable(self): 614 | ''' 615 | Is the dock area widget floatable? 616 | ''' 617 | return DockWidgetFeature.floatable in self.features() 618 | 619 | def features(self) -> DockWidgetFeature: 620 | ''' 621 | This functions returns the dock widget features of all dock widget in 622 | this area. A bitwise and is used to combine the flags of all dock 623 | widgets. That means, if only dock widget does not support a certain 624 | flag, the whole dock are does not support the flag. 625 | 626 | Returns 627 | ------- 628 | value : DockWidgetFeature 629 | ''' 630 | features = DockWidgetFeature.all_features 631 | for dock_widget in self.dock_widgets(): 632 | features &= dock_widget.features() 633 | 634 | return features 635 | 636 | def title_bar_button(self, which: TitleBarButton) -> QAbstractButton: 637 | ''' 638 | Returns the title bar button corresponding to the given title bar button identifier 639 | 640 | Parameters 641 | ---------- 642 | which : TitleBarButton 643 | 644 | Returns 645 | ------- 646 | value : QAbstractButton 647 | ''' 648 | return self.d.title_bar.button(which) 649 | 650 | def setVisible(self, visible: bool): 651 | ''' 652 | Update the close button if visibility changed 653 | 654 | Parameters 655 | ---------- 656 | visible : bool 657 | ''' 658 | super().setVisible(visible) 659 | if self.d.update_title_bar_buttons: 660 | self.d.update_title_bar_button_states() 661 | 662 | def set_current_index(self, index: int): 663 | ''' 664 | This activates the tab for the given tab index. If the dock widget for 665 | the given tab is not visible, the this function call will make it visible. 666 | 667 | Parameters 668 | ---------- 669 | index : int 670 | ''' 671 | tab_bar = self.d.tab_bar() 672 | if index < 0 or index > (tab_bar.count()-1): 673 | logger.warning('Invalid index %s', index) 674 | return 675 | 676 | self.current_changing.emit(index) 677 | tab_bar.set_current_index(index) 678 | self.d.contents_layout.set_current_index(index) 679 | self.d.contents_layout.current_widget().show() 680 | self.current_changed.emit(index) 681 | 682 | def close_area(self): 683 | ''' 684 | Closes the dock area and all dock widgets in this area 685 | ''' 686 | for dock_widget in self.opened_dock_widgets(): 687 | dock_widget.toggle_view(False) 688 | 689 | def close_other_areas(self): 690 | ''' 691 | This function closes all other areas except of this area 692 | ''' 693 | self.dock_container().close_other_areas(self) 694 | -------------------------------------------------------------------------------- /qtpydocking/dock_splitter.py: -------------------------------------------------------------------------------- 1 | from qtpy.QtCore import Qt 2 | from qtpy.QtWidgets import QSplitter, QWidget 3 | 4 | 5 | class DockSplitter(QSplitter): 6 | def __init__(self, orientation: Qt.Orientation = None, 7 | parent: QWidget = None): 8 | ''' 9 | init 10 | 11 | Parameters 12 | ---------- 13 | parent : QWidget 14 | ''' 15 | if orientation is not None: 16 | super().__init__(orientation, parent) 17 | else: 18 | super().__init__(parent) 19 | 20 | self.setProperty("ads-splitter", True) 21 | self.setChildrenCollapsible(False) 22 | 23 | def __repr__(self): 24 | return f'<{self.__class__.__name__} {self.orientation()}>' 25 | 26 | def has_visible_content(self) -> bool: 27 | ''' 28 | Returns true, if any of the internal widgets is visible 29 | 30 | Returns 31 | ------- 32 | value : bool 33 | ''' 34 | # TODO_UPSTREAM Cache or precalculate this to speed up 35 | 36 | for i in range(self.count()): 37 | if not self.widget(i).isHidden(): 38 | return True 39 | 40 | return False 41 | -------------------------------------------------------------------------------- /qtpydocking/dock_widget_tab.py: -------------------------------------------------------------------------------- 1 | from typing import TYPE_CHECKING, no_type_check 2 | import logging 3 | 4 | from qtpy.QtCore import QEvent, QPoint, QSize, Qt, Signal 5 | from qtpy.QtGui import QContextMenuEvent, QCursor, QFontMetrics, QIcon, QMouseEvent 6 | from qtpy.QtWidgets import (QBoxLayout, QFrame, QLabel, QMenu, QSizePolicy, 7 | QStyle, QWidget, QPushButton) 8 | 9 | from .util import start_drag_distance, set_button_icon 10 | from .enums import DragState, DockFlags, DockWidgetArea, DockWidgetFeature 11 | from .eliding_label import ElidingLabel 12 | 13 | if TYPE_CHECKING: 14 | from . import (DockWidget, DockAreaWidget, FloatingDockContainer) 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | 19 | class DockWidgetTabPrivate: 20 | public: 'DockWidgetTab' 21 | dock_widget: 'DockWidget' 22 | icon_label: QLabel 23 | title_label: QLabel 24 | drag_start_mouse_position: QPoint 25 | is_active_tab: bool 26 | dock_area: 'DockAreaWidget' 27 | drag_state: DragState 28 | floating_widget: 'FloatingDockContainer' 29 | icon: QIcon 30 | close_button: QPushButton 31 | 32 | @no_type_check 33 | def __init__(self, public: 'DockWidgetTab'): 34 | ''' 35 | Private data constructor 36 | 37 | Parameters 38 | ---------- 39 | public : DockWidgetTab 40 | ''' 41 | self.public = public 42 | self.dock_widget = None 43 | self.icon_label = None 44 | self.title_label = None 45 | self.drag_start_mouse_position = None 46 | self.is_active_tab = False 47 | self.dock_area = None 48 | self.drag_state = DragState.inactive 49 | self.floating_widget = None 50 | self.icon = None 51 | self.close_button = None 52 | 53 | def create_layout(self): 54 | ''' 55 | Creates the complete layout including all controls 56 | ''' 57 | self.title_label = ElidingLabel(text=self.dock_widget.windowTitle()) 58 | self.title_label.set_elide_mode(Qt.ElideRight) 59 | self.title_label.setObjectName("dockWidgetTabLabel") 60 | self.title_label.setAlignment(Qt.AlignCenter) 61 | self.close_button = QPushButton() 62 | self.close_button.setObjectName("tabCloseButton") 63 | 64 | set_button_icon(self.public.style(), self.close_button, 65 | QStyle.SP_TitleBarCloseButton) 66 | 67 | self.close_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 68 | self.close_button.setVisible(False) 69 | self.close_button.setToolTip("Close Tab") 70 | self.close_button.clicked.connect(self.public.close_requested) 71 | 72 | fm = QFontMetrics(self.title_label.font()) 73 | spacing = round(fm.height()/4.0) 74 | 75 | # Fill the layout 76 | layout = QBoxLayout(QBoxLayout.LeftToRight) 77 | layout.setContentsMargins(2*spacing, 0, 0, 0) 78 | layout.setSpacing(0) 79 | self.public.setLayout(layout) 80 | layout.addWidget(self.title_label, 1) 81 | layout.addSpacing(spacing) 82 | layout.addWidget(self.close_button) 83 | layout.addSpacing(round(spacing*4.0/3.0)) 84 | layout.setAlignment(Qt.AlignCenter) 85 | self.title_label.setVisible(True) 86 | 87 | def move_tab(self, ev: QMouseEvent): 88 | ''' 89 | Moves the tab depending on the position in the given mouse event 90 | 91 | Parameters 92 | ---------- 93 | ev : QMouseEvent 94 | ''' 95 | ev.accept() 96 | # left, top, right, bottom = self.public.getContentsMargins() 97 | move_to_pos = self.public.mapToParent(ev.pos())-self.drag_start_mouse_position 98 | move_to_pos.setY(0) 99 | 100 | self.public.move(move_to_pos) 101 | self.public.raise_() 102 | 103 | def is_dragging_state(self, drag_state: DragState) -> bool: 104 | ''' 105 | Test function for current drag state 106 | 107 | Parameters 108 | ---------- 109 | drag_state : DragState 110 | 111 | Returns 112 | ------- 113 | value : bool 114 | ''' 115 | return self.drag_state == drag_state 116 | 117 | def title_area_geometry_contains(self, global_pos: QPoint) -> bool: 118 | ''' 119 | Returns true if the given global point is inside the title area 120 | geometry rectangle. The position is given as global position. 121 | 122 | Parameters 123 | ---------- 124 | global_pos : QPoint 125 | 126 | Returns 127 | ------- 128 | value : bool 129 | ''' 130 | return self.dock_area.title_bar_geometry().contains(self.dock_area.mapFromGlobal(global_pos)) 131 | 132 | def start_floating( 133 | self, 134 | dragging_state: DragState = DragState.floating_widget 135 | ) -> bool: 136 | ''' 137 | Starts floating of the dock widget that belongs to this title bar 138 | Returns true, if floating has been started and false if floating is not 139 | possible for any reason 140 | 141 | Parameters 142 | ---------- 143 | dragging_state : DragState 144 | 145 | Returns 146 | ------- 147 | value : bool 148 | ''' 149 | dock_container = self.dock_widget.dock_container() 150 | if dock_container is None: 151 | return 152 | 153 | logger.debug('is_floating %s', 154 | dock_container.is_floating()) 155 | logger.debug('area_count %s', 156 | dock_container.dock_area_count()) 157 | logger.debug('widget_count %s', 158 | self.dock_widget.dock_area_widget().dock_widgets_count()) 159 | 160 | # if this is the last dock widget inside of this floating widget, 161 | # then it does not make any sense, to make it floating because 162 | # it is already floating 163 | if (dock_container.is_floating() 164 | and (dock_container.visible_dock_area_count() == 1) 165 | and (self.dock_widget.dock_area_widget().dock_widgets_count() == 1)): 166 | return False 167 | 168 | logger.debug('startFloating') 169 | self.drag_state = dragging_state 170 | size = self.dock_area.size() 171 | 172 | from .floating_dock_container import FloatingDockContainer 173 | 174 | if self.dock_area.dock_widgets_count() > 1: 175 | # If section widget has multiple tabs, we take only one tab 176 | self.floating_widget = FloatingDockContainer(dock_widget=self.dock_widget) 177 | else: 178 | # If section widget has only one content widget, we can move the complete 179 | # dock area into floating widget 180 | self.floating_widget = FloatingDockContainer(dock_area=self.dock_area) 181 | 182 | if dragging_state == DragState.floating_widget: 183 | self.floating_widget.start_dragging(self.drag_start_mouse_position, 184 | size, self.public) 185 | overlay = self.dock_widget.dock_manager().container_overlay() 186 | overlay.set_allowed_areas(DockWidgetArea.outer_dock_areas) 187 | self.floating_widget = self.floating_widget 188 | else: 189 | self.floating_widget.init_floating_geometry(self.drag_start_mouse_position, size) 190 | 191 | self.dock_widget.emit_top_level_changed(True) 192 | return True 193 | 194 | def test_config_flag(self, flag: DockFlags) -> bool: 195 | ''' 196 | Returns true if the given config flag is set 197 | 198 | Parameters 199 | ---------- 200 | flag : DockFlags 201 | 202 | Returns 203 | ------- 204 | value : bool 205 | ''' 206 | return flag in self.dock_area.dock_manager().config_flags() 207 | 208 | @property 209 | def floatable(self): 210 | ''' 211 | Is the dock widget floatable? 212 | ''' 213 | return DockWidgetFeature.floatable in self.dock_widget.features() 214 | 215 | 216 | class DockWidgetTab(QFrame): 217 | active_tab_changed = Signal() 218 | clicked = Signal() 219 | close_requested = Signal() 220 | close_other_tabs_requested = Signal() 221 | moved = Signal(QPoint) 222 | 223 | def __init__(self, dock_widget: 'DockWidget', parent: QWidget): 224 | ''' 225 | Parameters 226 | ---------- 227 | dock_widget : DockWidget 228 | The dock widget this title bar 229 | parent : QWidget 230 | The parent widget of this title bar 231 | ''' 232 | super().__init__(parent) 233 | self.d = DockWidgetTabPrivate(self) 234 | self.setAttribute(Qt.WA_NoMousePropagation, True) 235 | self.d.dock_widget = dock_widget 236 | self.d.create_layout() 237 | 238 | def on_detach_action_triggered(self): 239 | if self.d.floatable: 240 | self.d.drag_start_mouse_position = self.mapFromGlobal(QCursor.pos()) 241 | self.d.start_floating(DragState.inactive) 242 | 243 | def mousePressEvent(self, ev: QMouseEvent): 244 | ''' 245 | Mousepressevent 246 | 247 | Parameters 248 | ---------- 249 | ev : QMouseEvent 250 | ''' 251 | if ev.button() == Qt.LeftButton: 252 | ev.accept() 253 | self.d.drag_start_mouse_position = ev.pos() 254 | self.d.drag_state = DragState.mouse_pressed 255 | self.clicked.emit() 256 | return 257 | 258 | super().mousePressEvent(ev) 259 | 260 | def mouseReleaseEvent(self, ev: QMouseEvent): 261 | ''' 262 | Mouse release event 263 | 264 | Parameters 265 | ---------- 266 | ev : QMouseEvent 267 | ''' 268 | # End of tab moving, emit signal 269 | if self.d.is_dragging_state(DragState.tab) and self.d.dock_area: 270 | self.moved.emit(ev.globalPos()) 271 | 272 | self.d.drag_start_mouse_position = QPoint() 273 | self.d.drag_state = DragState.inactive 274 | super().mouseReleaseEvent(ev) 275 | 276 | def mouseMoveEvent(self, ev: QMouseEvent): 277 | ''' 278 | Mousemoveevent 279 | 280 | Parameters 281 | ---------- 282 | ev : QMouseEvent 283 | ''' 284 | if (not (ev.buttons() & Qt.LeftButton) 285 | or self.d.is_dragging_state(DragState.inactive)): 286 | self.d.drag_state = DragState.inactive 287 | return super().mouseMoveEvent(ev) 288 | 289 | # move floating window 290 | if self.d.is_dragging_state(DragState.floating_widget): 291 | self.d.floating_widget.move_floating() 292 | return super().mouseMoveEvent(ev) 293 | 294 | # move tab 295 | if self.d.is_dragging_state(DragState.tab): 296 | # Moving the tab is always allowed because it does not mean moving 297 | # the dock widget around 298 | self.d.move_tab(ev) 299 | 300 | # Maybe a fixed drag distance is better here ? 301 | drag_distance_y = abs(self.d.drag_start_mouse_position.y()-ev.pos().y()) 302 | start_dist = start_drag_distance() 303 | if drag_distance_y >= start_dist: 304 | # If this is the last dock area in a dock container with only 305 | # one single dock widget it does not make sense to move it to a new 306 | # floating widget and leave this one empty 307 | if (self.d.dock_area.dock_container().is_floating() 308 | and self.d.dock_area.open_dock_widgets_count() == 1 309 | and self.d.dock_area.dock_container().visible_dock_area_count() == 1): 310 | return 311 | 312 | 313 | # Floating is only allowed for widgets that are movable 314 | if self.d.floatable: 315 | self.d.start_floating() 316 | elif (self.d.dock_area.open_dock_widgets_count() > 1 317 | and (ev.pos()-self.d.drag_start_mouse_position).manhattanLength() >= start_dist): 318 | # Wait a few pixels before start moving 319 | self.d.drag_state = DragState.tab 320 | else: 321 | return super().mouseMoveEvent(ev) 322 | 323 | def contextMenuEvent(self, ev: QContextMenuEvent): 324 | ''' 325 | Context menu event 326 | 327 | Parameters 328 | ---------- 329 | ev : QContextMenuEvent 330 | ''' 331 | ev.accept() 332 | self.d.drag_start_mouse_position = ev.pos() 333 | menu = QMenu(self) 334 | detach = menu.addAction("Detach", self.on_detach_action_triggered) 335 | detach.setEnabled(self.d.floatable) 336 | 337 | menu.addSeparator() 338 | 339 | action = menu.addAction("Close", self.close_requested) 340 | action.setEnabled(self.is_closable()) 341 | menu.addAction("Close Others", self.close_other_tabs_requested) 342 | menu.exec(self.mapToGlobal(ev.pos())) 343 | 344 | def mouseDoubleClickEvent(self, event: QMouseEvent): 345 | ''' 346 | Double clicking the tab widget makes the assigned dock widget floating 347 | 348 | Parameters 349 | ---------- 350 | event : QMouseEvent 351 | ''' 352 | # If this is the last dock area in a dock container it does not make 353 | # sense to move it to a new floating widget and leave this one 354 | # empty 355 | if (self.d.floatable and 356 | (not self.d.dock_area.dock_container().is_floating() 357 | or self.d.dock_area.dock_widgets_count() > 1)): 358 | self.d.drag_start_mouse_position = event.pos() 359 | self.d.start_floating(DragState.inactive) 360 | 361 | super().mouseDoubleClickEvent(event) 362 | 363 | def is_active_tab(self) -> bool: 364 | ''' 365 | Returns true, if this is the active tab 366 | 367 | Returns 368 | ------- 369 | value : bool 370 | ''' 371 | return self.d.is_active_tab 372 | 373 | def set_active_tab(self, active: bool): 374 | ''' 375 | Set this true to make this tab the active tab 376 | 377 | Parameters 378 | ---------- 379 | active : bool 380 | ''' 381 | closable = DockWidgetFeature.closable in self.d.dock_widget.features() 382 | tab_has_close_button = self.d.test_config_flag(DockFlags.active_tab_has_close_button) 383 | self.d.close_button.setVisible(active and closable and tab_has_close_button) 384 | if self.d.is_active_tab == active: 385 | return 386 | 387 | self.d.is_active_tab = active 388 | self.style().unpolish(self) 389 | self.style().polish(self) 390 | self.d.title_label.style().unpolish(self.d.title_label) 391 | self.d.title_label.style().polish(self.d.title_label) 392 | self.update() 393 | 394 | self.active_tab_changed.emit() 395 | 396 | def dock_widget(self) -> 'DockWidget': 397 | ''' 398 | Returns the dock widget this title widget belongs to 399 | 400 | Returns 401 | ------- 402 | value : DockWidget 403 | ''' 404 | return self.d.dock_widget 405 | 406 | def set_dock_area_widget(self, dock_area: 'DockAreaWidget'): 407 | ''' 408 | Sets the dock area widget the dockWidget returned by dockWidget() function belongs to. 409 | 410 | Parameters 411 | ---------- 412 | dock_area : DockAreaWidget 413 | ''' 414 | self.d.dock_area = dock_area 415 | 416 | def dock_area_widget(self) -> 'DockAreaWidget': 417 | ''' 418 | Returns the dock area widget this title bar belongs to. 419 | 420 | Returns 421 | ------- 422 | value : DockAreaWidget 423 | ''' 424 | return self.d.dock_area 425 | 426 | def set_icon(self, icon: QIcon): 427 | ''' 428 | Sets the icon to show in title bar 429 | 430 | Parameters 431 | ---------- 432 | icon : QIcon 433 | ''' 434 | layout = self.layout() 435 | if not self.d.icon_label and icon.isNull(): 436 | return 437 | 438 | if not self.d.icon_label: 439 | self.d.icon_label = QLabel() 440 | self.d.icon_label.setAlignment(Qt.AlignVCenter) 441 | self.d.icon_label.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) 442 | self.d.icon_label.setToolTip(self.d.title_label.toolTip()) 443 | layout.insertWidget(0, self.d.icon_label, Qt.AlignVCenter) 444 | layout.insertSpacing(1, round(1.5*layout.contentsMargins().left()/2.0)) 445 | 446 | elif icon.isNull(): 447 | # Remove icon label and spacer item 448 | layout.removeWidget(self.d.icon_label) 449 | layout.removeItem(layout.itemAt(0)) 450 | self.d.icon_label.deleteLater() 451 | self.d.icon_label = None 452 | 453 | self.d.icon = icon 454 | if self.d.icon_label: 455 | self.d.icon_label.setPixmap(icon.pixmap(self.windowHandle(), QSize(16, 16))) 456 | self.d.icon_label.setVisible(True) 457 | 458 | def icon(self) -> QIcon: 459 | ''' 460 | Returns the icon 461 | 462 | Returns 463 | ------- 464 | value : QIcon 465 | ''' 466 | return self.d.icon 467 | 468 | def text(self) -> str: 469 | ''' 470 | Returns the tab text 471 | 472 | Returns 473 | ------- 474 | value : str 475 | ''' 476 | return self.d.title_label.text() 477 | 478 | def set_text(self, title: str): 479 | ''' 480 | Sets the tab text 481 | 482 | Parameters 483 | ---------- 484 | title : str 485 | ''' 486 | self.d.title_label.setText(title) 487 | 488 | def is_closable(self) -> bool: 489 | ''' 490 | This function returns true if the assigned dock widget is closeable 491 | 492 | Returns 493 | ------- 494 | value : bool 495 | ''' 496 | return (self.d.dock_widget and 497 | DockWidgetFeature.closable in self.d.dock_widget.features()) 498 | 499 | def event(self, e: QEvent) -> bool: 500 | ''' 501 | Track event ToolTipChange and set child ToolTip 502 | 503 | Parameters 504 | ---------- 505 | e : QEvent 506 | 507 | Returns 508 | ------- 509 | value : bool 510 | ''' 511 | if e.type() == QEvent.ToolTipChange: 512 | text = self.toolTip() 513 | self.d.title_label.setToolTip(text) 514 | 515 | return super().event(e) 516 | 517 | # def setVisible(self, visible: bool): 518 | # ''' 519 | # Set visible 520 | # 521 | # Parameters 522 | # ---------- 523 | # visible : bool 524 | # ''' 525 | # super().setVisible(visible) 526 | -------------------------------------------------------------------------------- /qtpydocking/eliding_label.py: -------------------------------------------------------------------------------- 1 | from qtpy.QtCore import QSize, Qt, Signal 2 | from qtpy.QtGui import QMouseEvent, QResizeEvent 3 | from qtpy.QtWidgets import QLabel, QWidget 4 | 5 | from .util import PYSIDE, PYSIDE2 6 | 7 | 8 | class ElidingLabelPrivate: 9 | def __init__(self, public): 10 | ''' 11 | init 12 | 13 | Parameters 14 | ---------- 15 | public : CElidingLabel 16 | ''' 17 | self.public = public 18 | self.elide_mode = Qt.ElideNone 19 | self.text = '' 20 | 21 | def elide_text(self, width: int): 22 | ''' 23 | Elide text 24 | 25 | Parameters 26 | ---------- 27 | width : int 28 | ''' 29 | if self.is_mode_elide_none(): 30 | return 31 | 32 | fm = self.public.fontMetrics() 33 | text = fm.elidedText(self.text, self.elide_mode, 34 | width-self.public.margin()*2-self.public.indent()) 35 | if text == "…": 36 | text = self.text[0] 37 | 38 | QLabel.setText(self.public, text) 39 | 40 | def is_mode_elide_none(self) -> bool: 41 | ''' 42 | Convenience function to check if the 43 | 44 | Returns 45 | ------- 46 | value : bool 47 | ''' 48 | return Qt.ElideNone == self.elide_mode 49 | 50 | 51 | class ElidingLabel(QLabel): 52 | # This signal is emitted if the user clicks on the label (i.e. pressed down 53 | # then released while the mouse cursor is inside the label) 54 | clicked = Signal() 55 | # This signal is emitted if the user does a double click on the label 56 | double_clicked = Signal() 57 | 58 | def __init__(self, text='', parent: QWidget = None, 59 | flags: Qt.WindowFlags = Qt.Widget): 60 | ''' 61 | init 62 | 63 | Parameters 64 | ---------- 65 | parent : QWidget 66 | flags : Qt.WindowFlags 67 | ''' 68 | if PYSIDE or PYSIDE2: 69 | kwarg = {'f': flags} 70 | else: 71 | kwarg = {'flags': flags} 72 | super().__init__(text, parent=parent, **kwarg) 73 | self.d = ElidingLabelPrivate(self) 74 | 75 | if text: 76 | self.d.text = text 77 | self.setToolTip(text) 78 | 79 | def mouseReleaseEvent(self, event: QMouseEvent): 80 | ''' 81 | Mousereleaseevent 82 | 83 | Parameters 84 | ---------- 85 | event : QMouseEvent 86 | ''' 87 | super().mouseReleaseEvent(event) 88 | if event.button() != Qt.LeftButton: 89 | return 90 | 91 | self.clicked.emit() 92 | 93 | def resizeEvent(self, event: QResizeEvent): 94 | ''' 95 | Resizeevent 96 | 97 | Parameters 98 | ---------- 99 | event : QResizeEvent 100 | ''' 101 | if not self.d.is_mode_elide_none(): 102 | self.d.elide_text(event.size().width()) 103 | 104 | super().resizeEvent(event) 105 | 106 | def mouseDoubleClickEvent(self, ev: QMouseEvent): 107 | ''' 108 | Mousedoubleclickevent 109 | 110 | Parameters 111 | ---------- 112 | ev : QMouseEvent 113 | Unused 114 | ''' 115 | self.double_clicked.emit() 116 | super().mouseDoubleClickEvent(ev) 117 | 118 | def elide_mode(self) -> Qt.TextElideMode: 119 | ''' 120 | Returns the text elide mode. The default mode is ElideNone 121 | 122 | Returns 123 | ------- 124 | value : Qt.TextElideMode 125 | ''' 126 | return self.d.elide_mode 127 | 128 | def set_elide_mode(self, mode: Qt.TextElideMode): 129 | ''' 130 | Sets the text elide mode 131 | 132 | Parameters 133 | ---------- 134 | mode : Qt.TextElideMode 135 | ''' 136 | self.d.elide_mode = mode 137 | self.d.elide_text(self.size().width()) 138 | 139 | def minimumSizeHint(self) -> QSize: 140 | ''' 141 | Minimumsizehint 142 | 143 | Returns 144 | ------- 145 | value : QSize 146 | ''' 147 | if self.pixmap() is not None or self.d.is_mode_elide_none(): 148 | return super().minimumSizeHint() 149 | 150 | fm = self.fontMetrics() 151 | return QSize(fm.width(self.d.text[:2]+"…"), fm.height()) 152 | 153 | def sizeHint(self) -> QSize: 154 | ''' 155 | Sizehint 156 | 157 | Returns 158 | ------- 159 | value : QSize 160 | ''' 161 | if self.pixmap() is not None or self.d.is_mode_elide_none(): 162 | return super().sizeHint() 163 | 164 | fm = self.fontMetrics() 165 | return QSize(fm.width(self.d.text), super().sizeHint().height()) 166 | 167 | def setText(self, text: str): 168 | ''' 169 | Settext 170 | 171 | Parameters 172 | ---------- 173 | text : str 174 | ''' 175 | if self.d.is_mode_elide_none(): 176 | super().setText(text) 177 | else: 178 | self.d.text = text 179 | self.setToolTip(text) 180 | self.d.elide_text(self.size().width()) 181 | 182 | def text(self) -> str: 183 | ''' 184 | Text 185 | 186 | Returns 187 | ------- 188 | value : str 189 | ''' 190 | return self.d.text 191 | -------------------------------------------------------------------------------- /qtpydocking/enums.py: -------------------------------------------------------------------------------- 1 | import enum 2 | from collections import namedtuple 3 | 4 | from qtpy.QtCore import Qt 5 | 6 | 7 | class DockInsertParam(namedtuple('DockInsertParam', ('orientation', 8 | 'append'))): 9 | @property 10 | def insert_offset(self): 11 | return 1 if self.append else 0 12 | 13 | 14 | class DockWidgetArea(enum.IntFlag): 15 | no_area = 0x00 16 | left = 0x01 17 | right = 0x02 18 | top = 0x04 19 | bottom = 0x08 20 | center = 0x10 21 | 22 | invalid = no_area 23 | outer_dock_areas = (top | left | right | bottom) 24 | all_dock_areas = (outer_dock_areas | center) 25 | 26 | 27 | area_alignment = { 28 | DockWidgetArea.top: Qt.AlignHCenter | Qt.AlignBottom, 29 | DockWidgetArea.right: Qt.AlignLeft | Qt.AlignVCenter, 30 | DockWidgetArea.bottom: Qt.AlignHCenter | Qt.AlignTop, 31 | DockWidgetArea.left: Qt.AlignRight | Qt.AlignVCenter, 32 | DockWidgetArea.center: Qt.AlignCenter, 33 | 34 | DockWidgetArea.invalid: Qt.AlignCenter, 35 | DockWidgetArea.outer_dock_areas: Qt.AlignCenter, 36 | DockWidgetArea.all_dock_areas: Qt.AlignCenter, 37 | } 38 | 39 | 40 | class TitleBarButton(enum.Enum): 41 | tabs_menu = enum.auto() 42 | undock = enum.auto() 43 | close = enum.auto() 44 | 45 | 46 | class DragState(enum.Enum): 47 | inactive = enum.auto() 48 | mouse_pressed = enum.auto() 49 | tab = enum.auto() 50 | floating_widget = enum.auto() 51 | 52 | 53 | class InsertionOrder(enum.Enum): 54 | by_insertion = enum.auto() 55 | by_spelling = enum.auto() 56 | 57 | 58 | class DockFlags(enum.IntFlag): 59 | ''' 60 | These global configuration flags configure some global dock manager 61 | settings. 62 | ''' 63 | # If this flag is set, the active tab in a tab area has a close button 64 | active_tab_has_close_button = 0x01 65 | # If the flag is set each dock area has a close button 66 | dock_area_has_close_button = 0x02 67 | # If the flag is set, the dock area close button closes the active tab, if 68 | # not set, it closes the complete cock area 69 | dock_area_close_button_closes_tab = 0x04 70 | # See QSplitter.setOpaqueResize() documentation 71 | opaque_splitter_resize = 0x08 72 | # If enabled, the XML writer automatically adds line-breaks and indentation 73 | # to empty sections between elements (ignorable whitespace). 74 | xml_auto_formatting = 0x10 75 | # If enabled, the XML output will be compressed and is not human readable 76 | # anymore 77 | xml_compression = 0x20 78 | # the default configuration 79 | default_config = (active_tab_has_close_button 80 | | dock_area_has_close_button 81 | | opaque_splitter_resize 82 | | xml_auto_formatting 83 | ) 84 | 85 | 86 | class OverlayMode(enum.Enum): 87 | dock_area = enum.auto() 88 | container = enum.auto() 89 | 90 | 91 | class IconColor(enum.Enum): 92 | # the color of the frame of the small window icon 93 | frame_color = enum.auto() 94 | # the background color of the small window in the icon 95 | window_background_color = enum.auto() 96 | # the color that shows the overlay (the dock side) in the icon 97 | overlay_color = enum.auto() 98 | # the arrow that points into the direction 99 | arrow_color = enum.auto() 100 | # the color of the shadow rectangle that is painted below the icons 101 | shadow_color = enum.auto() 102 | 103 | 104 | class DockWidgetFeature(enum.IntFlag): 105 | closable = 0x01 106 | movable = 0x02 # not yet implemented 107 | floatable = 0x04 108 | all_features = (closable | movable | floatable) 109 | no_features = 0 110 | 111 | 112 | class WidgetState(enum.Enum): 113 | hidden = enum.auto() 114 | docked = enum.auto() 115 | floating = enum.auto() 116 | 117 | 118 | class InsertMode(enum.Enum): 119 | ''' 120 | Sets the widget for the dock widget to widget. 121 | 122 | The InsertMode defines how the widget is inserted into the dock widget. 123 | The content of a dock widget should be resizable do a very small size to 124 | prevent the dock widget from blocking the resizing. To ensure, that a dock 125 | widget can be resized very well, it is better to insert the content+ widget 126 | into a scroll area or to provide a widget that is already a scroll area or 127 | that contains a scroll area. 128 | 129 | If the InsertMode is AutoScrollArea, the DockWidget tries to automatically 130 | detect how to insert the given widget. If the widget is derived from 131 | QScrollArea (i.e. an QAbstractItemView), then the widget is inserted 132 | directly. If the given widget is not a scroll area, the widget will be 133 | inserted into a scroll area. 134 | 135 | To force insertion into a scroll area, you can also provide the InsertMode 136 | ForceScrollArea. To prevent insertion into a scroll area, you can provide 137 | the InsertMode ForceNoScrollArea 138 | ''' 139 | auto_scroll_area = enum.auto() 140 | force_scroll_area = enum.auto() 141 | force_no_scroll_area = enum.auto() 142 | 143 | 144 | class ToggleViewActionMode(enum.Enum): 145 | ''' 146 | This mode configures the behavior of the toggle view action. 147 | 148 | If the mode if ActionModeToggle, then the toggle view action is a checkable 149 | action to show / hide the dock widget. If the mode is ActionModeShow, then 150 | the action is not checkable an it will always show the dock widget if 151 | clicked. If the mode is ActionModeShow, the user can only close the 152 | DockWidget with the close button. 153 | ''' 154 | toggle = enum.auto() 155 | show = enum.auto() 156 | -------------------------------------------------------------------------------- /qtpydocking/examples/__init__.py: -------------------------------------------------------------------------------- 1 | from . import simple 2 | from . import demo 3 | 4 | 5 | __all__ = ['simple', 'demo'] 6 | -------------------------------------------------------------------------------- /qtpydocking/examples/demo.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | 4 | from qtpy.QtCore import (QCoreApplication, QDir, Qt, QSettings, QSignalBlocker, 5 | QRect) 6 | from qtpy.QtGui import QGuiApplication 7 | from qtpy.QtWidgets import (QCalendarWidget, QFileSystemModel, QFrame, QLabel, 8 | QMenu, QTreeView, QAction, QWidgetAction, 9 | QComboBox, QStyle, QSizePolicy, QInputDialog) 10 | 11 | from qtpy import QtWidgets 12 | 13 | from qtpydocking import (DockManager, DockWidget, DockWidgetArea, 14 | ToggleViewActionMode, DockWidgetFeature) 15 | 16 | 17 | class _State: 18 | label_count = 0 19 | calendar_count = 0 20 | file_system_count = 0 21 | 22 | 23 | def create_long_text_label_dock_widget(view_menu: QMenu) -> DockWidget: 24 | ''' 25 | Create long text label dock widget 26 | 27 | Parameters 28 | ---------- 29 | view_menu : QMenu 30 | 31 | Returns 32 | ------- 33 | value : DockWidget 34 | ''' 35 | label = QLabel() 36 | label.setWordWrap(True) 37 | label.setAlignment(Qt.AlignTop | Qt.AlignLeft) 38 | _State.label_count += 1 39 | label.setText('''\ 40 | Label {} {} - Lorem ipsum dolor sit amet, consectetuer 41 | adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum 42 | sociis natoque penatibus et magnis dis parturient montes, nascetur 43 | ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium 44 | quis, sem. Nulla consequat massa quis enim. Donec pede justo, fringilla 45 | vel, aliquet nec, vulputate eget, arcu. In enim justo, rhoncus ut, 46 | imperdiet a, venenatis vitae, justo. Nullam dictum felis eu pede mollis 47 | pretium. Integer tincidunt. Cras dapibus. Vivamus elementum semper nisi. 48 | Aenean vulputate eleifend tellus. Aenean leo ligula, porttitor eu, 49 | consequat vitae, eleifend ac, enim. Aliquam lorem ante, dapibus in, viverra 50 | quis, feugiat a, tellus. Phasellus viverra nulla ut metus varius laoreet. 51 | '''.format(_State.label_count, str(datetime.datetime.now()))) 52 | 53 | dock_widget = DockWidget("Label {}".format(_State.label_count)) 54 | dock_widget.set_widget(label) 55 | 56 | view_menu.addAction(dock_widget.toggle_view_action()) 57 | return dock_widget 58 | 59 | 60 | def create_calendar_dock_widget(view_menu: QMenu) -> DockWidget: 61 | ''' 62 | Create calendar dock widget 63 | 64 | Parameters 65 | ---------- 66 | view_menu : QMenu 67 | 68 | Returns 69 | ------- 70 | value : DockWidget 71 | ''' 72 | widget = QCalendarWidget() 73 | 74 | _State.calendar_count += 1 75 | dock_widget = DockWidget("Calendar {}".format(_State.calendar_count)) 76 | dock_widget.set_widget(widget) 77 | dock_widget.set_toggle_view_action_mode(ToggleViewActionMode.show) 78 | view_menu.addAction(dock_widget.toggle_view_action()) 79 | return dock_widget 80 | 81 | 82 | def create_file_system_tree_dock_widget(view_menu: QMenu) -> DockWidget: 83 | ''' 84 | Create file system tree dock widget 85 | 86 | Parameters 87 | ---------- 88 | view_menu : QMenu 89 | 90 | Returns 91 | ------- 92 | value : DockWidget 93 | ''' 94 | widget = QTreeView() 95 | widget.setFrameShape(QFrame.NoFrame) 96 | 97 | m = QFileSystemModel(widget) 98 | m.setRootPath(QDir.currentPath()) 99 | widget.setModel(m) 100 | 101 | _State.file_system_count += 1 102 | dock_widget = DockWidget("Filesystem {}".format(_State.file_system_count)) 103 | dock_widget.set_widget(widget) 104 | view_menu.addAction(dock_widget.toggle_view_action()) 105 | return dock_widget 106 | 107 | 108 | class MainWindow(QtWidgets.QMainWindow): 109 | save_perspective_action: QAction 110 | perspective_list_action: QWidgetAction 111 | perspective_combo_box: QComboBox 112 | dock_manager: DockManager 113 | 114 | def __init__(self, parent=None): 115 | super().__init__(parent) 116 | self.save_perspective_action = None 117 | self.perspective_list_action = None 118 | self.perspective_combo_box = None 119 | self.dock_manager = None 120 | 121 | self.setup_ui() 122 | 123 | self.dock_manager = DockManager(self) 124 | self.perspective_combo_box.activated.connect(self.dock_manager.open_perspective) 125 | self.create_content() 126 | self.resize(1280, 720) 127 | self.restore_state() 128 | self.restore_perspectives() 129 | 130 | def setup_ui(self): 131 | self.setObjectName("MainWindow") 132 | self.setDockOptions(QtWidgets.QMainWindow.AllowTabbedDocks) 133 | self.centralWidget = QtWidgets.QWidget(self) 134 | self.centralWidget.setObjectName("centralWidget") 135 | self.setCentralWidget(self.centralWidget) 136 | self.status_bar = QtWidgets.QStatusBar(self) 137 | self.status_bar.setObjectName("statusBar") 138 | self.setStatusBar(self.status_bar) 139 | self.menu_bar = QtWidgets.QMenuBar(self) 140 | self.menu_bar.setGeometry(QRect(0, 0, 400, 21)) 141 | self.menu_bar.setObjectName("menuBar") 142 | self.menu_file = QtWidgets.QMenu(self.menu_bar) 143 | self.menu_file.setObjectName("menuFile") 144 | self.menu_view = QtWidgets.QMenu(self.menu_bar) 145 | self.menu_view.setObjectName("menuView") 146 | self.menu_about = QtWidgets.QMenu(self.menu_bar) 147 | self.menu_about.setObjectName("menuAbout") 148 | self.setMenuBar(self.menu_bar) 149 | self.tool_bar = QtWidgets.QToolBar(self) 150 | self.tool_bar.setObjectName("toolBar") 151 | self.addToolBar(Qt.TopToolBarArea, self.tool_bar) 152 | self.action_exit = QtWidgets.QAction(self) 153 | self.action_exit.setObjectName("actionExit") 154 | self.action_save_state = QtWidgets.QAction(self) 155 | self.action_save_state.setObjectName("actionSaveState") 156 | self.action_save_state.triggered.connect(self.save_state) 157 | 158 | self.action_restore_state = QtWidgets.QAction(self) 159 | self.action_restore_state.setObjectName("actionRestoreState") 160 | self.action_restore_state.triggered.connect(self.restore_state) 161 | 162 | self.menu_file.addAction(self.action_exit) 163 | self.menu_file.addAction(self.action_save_state) 164 | self.menu_file.addAction(self.action_restore_state) 165 | self.menu_bar.addAction(self.menu_file.menuAction()) 166 | self.menu_bar.addAction(self.menu_view.menuAction()) 167 | self.menu_bar.addAction(self.menu_about.menuAction()) 168 | 169 | self.setWindowTitle("MainWindow") 170 | self.menu_file.setTitle("File") 171 | self.menu_view.setTitle("View") 172 | self.menu_about.setTitle("About") 173 | self.tool_bar.setWindowTitle("toolBar") 174 | self.action_exit.setText("Exit") 175 | self.action_save_state.setText("Save State") 176 | self.action_restore_state.setText("Restore State") 177 | self.create_actions() 178 | 179 | def create_actions(self): 180 | ''' 181 | Creates the toolbar actions 182 | ''' 183 | self.tool_bar.addAction(self.action_save_state) 184 | self.action_save_state.setIcon(self.style().standardIcon(QStyle.SP_DialogSaveButton)) 185 | self.tool_bar.addAction(self.action_restore_state) 186 | self.action_restore_state.setIcon(self.style().standardIcon(QStyle.SP_DialogOpenButton)) 187 | self.save_perspective_action = QAction("Save Perspective", self) 188 | self.save_perspective_action.triggered.connect(self.save_perspective) 189 | 190 | self.perspective_list_action = QWidgetAction(self) 191 | self.perspective_combo_box = QComboBox(self) 192 | self.perspective_combo_box.setSizeAdjustPolicy(QComboBox.AdjustToContents) 193 | self.perspective_combo_box.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred) 194 | self.perspective_list_action.setDefaultWidget(self.perspective_combo_box) 195 | self.tool_bar.addSeparator() 196 | self.tool_bar.addAction(self.perspective_list_action) 197 | self.tool_bar.addAction(self.save_perspective_action) 198 | 199 | def create_content(self): 200 | ''' 201 | Fill the dock manager with dock widgets 202 | ''' 203 | # Test container docking 204 | view_menu = self.menu_view 205 | dock_widget = create_calendar_dock_widget(view_menu) 206 | dock_widget.set_icon(self.style().standardIcon(QStyle.SP_DialogOpenButton)) 207 | dock_widget.set_feature(DockWidgetFeature.closable, False) 208 | self.dock_manager.add_dock_widget(DockWidgetArea.left, dock_widget) 209 | self.dock_manager.add_dock_widget(DockWidgetArea.left, create_long_text_label_dock_widget(view_menu)) 210 | 211 | file_system_widget = create_file_system_tree_dock_widget(view_menu) 212 | tool_bar = file_system_widget.create_default_tool_bar() 213 | tool_bar.addAction(self.action_save_state) 214 | tool_bar.addAction(self.action_restore_state) 215 | self.dock_manager.add_dock_widget(DockWidgetArea.bottom, file_system_widget) 216 | 217 | file_system_widget = create_file_system_tree_dock_widget(view_menu) 218 | tool_bar = file_system_widget.create_default_tool_bar() 219 | tool_bar.addAction(self.action_save_state) 220 | tool_bar.addAction(self.action_restore_state) 221 | file_system_widget.set_feature(DockWidgetFeature.movable, False) 222 | file_system_widget.set_feature(DockWidgetFeature.floatable, False) 223 | 224 | top_dock_area = self.dock_manager.add_dock_widget(DockWidgetArea.top, file_system_widget) 225 | dock_widget = create_calendar_dock_widget(view_menu) 226 | dock_widget.set_feature(DockWidgetFeature.closable, False) 227 | dock_widget.set_tab_tool_tip("Tab ToolTip\nHodie est dies magna") 228 | self.dock_manager.add_dock_widget(DockWidgetArea.center, dock_widget, top_dock_area) 229 | 230 | # Test dock area docking 231 | right_dock_area = self.dock_manager.add_dock_widget( 232 | DockWidgetArea.right, 233 | create_long_text_label_dock_widget(view_menu), top_dock_area) 234 | self.dock_manager.add_dock_widget( 235 | DockWidgetArea.top, 236 | create_long_text_label_dock_widget(view_menu), right_dock_area) 237 | 238 | bottom_dock_area = self.dock_manager.add_dock_widget( 239 | DockWidgetArea.bottom, 240 | create_long_text_label_dock_widget(view_menu), right_dock_area) 241 | 242 | self.dock_manager.add_dock_widget( 243 | DockWidgetArea.right, 244 | create_long_text_label_dock_widget(view_menu), right_dock_area) 245 | self.dock_manager.add_dock_widget( 246 | DockWidgetArea.center, 247 | create_long_text_label_dock_widget(view_menu), bottom_dock_area) 248 | 249 | def save_state(self): 250 | ''' 251 | Saves the dock manager state and the main window geometry 252 | ''' 253 | settings = QSettings("Settings.ini", QSettings.IniFormat) 254 | settings.setValue("mainWindow/Geometry", self.saveGeometry()) 255 | settings.setValue("mainWindow/State", self.saveState()) 256 | settings.setValue("mainWindow/DockingState", self.dock_manager.save_state()) 257 | 258 | def save_perspectives(self): 259 | ''' 260 | Save the list of perspectives 261 | ''' 262 | settings = QSettings("Settings.ini", QSettings.IniFormat) 263 | self.dock_manager.save_perspectives(settings) 264 | 265 | def restore_state(self): 266 | ''' 267 | Restores the dock manager state 268 | ''' 269 | settings = QSettings("Settings.ini", QSettings.IniFormat) 270 | geom = settings.value("mainWindow/Geometry") 271 | if geom is not None: 272 | self.restoreGeometry(geom) 273 | 274 | state = settings.value("mainWindow/State") 275 | if state is not None: 276 | self.restoreState(state) 277 | 278 | state = settings.value("mainWindow/DockingState") 279 | if state is not None: 280 | self.dock_manager.restore_state(state) 281 | 282 | def restore_perspectives(self): 283 | ''' 284 | Restore the perspective listo of the dock manager 285 | ''' 286 | settings = QSettings("Settings.ini", QSettings.IniFormat) 287 | self.dock_manager.load_perspectives(settings) 288 | self.perspective_combo_box.clear() 289 | self.perspective_combo_box.addItems(self.dock_manager.perspective_names()) 290 | 291 | def save_perspective(self): 292 | perspective_name = QInputDialog.getText(self, 'Save perspective', 'Enter unique name:') 293 | if perspective_name: 294 | self.dock_manager.add_perspective(perspective_name) 295 | _ = QSignalBlocker(self.perspective_combo_box) 296 | self.perspective_combo_box.clear() 297 | self.perspective_combo_box.addItems(self.dock_manager.perspective_names()) 298 | self.perspective_combo_box.setCurrentIndex(perspective_name) 299 | self.save_perspectives() 300 | 301 | 302 | def main(app_): 303 | main_window = MainWindow() 304 | main_window.show() 305 | state = main_window.dock_manager.save_state() 306 | print('This is what the saved state looks like in XML:') 307 | print(str(state, 'utf-8')) 308 | print() 309 | main_window.dock_manager.restore_state(state) 310 | return main_window 311 | 312 | 313 | if __name__ == '__main__': 314 | logging.basicConfig(level='DEBUG') 315 | QCoreApplication.setAttribute(Qt.AA_UseHighDpiPixmaps) 316 | QGuiApplication.setAttribute(Qt.AA_EnableHighDpiScaling) 317 | app = QtWidgets.QApplication([]) 318 | window = main(app) 319 | window.show() 320 | app.exec_() 321 | -------------------------------------------------------------------------------- /qtpydocking/examples/simple.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from qtpy import QtWidgets, QtCore 4 | from qtpy.QtCore import Qt 5 | import qtpydocking 6 | 7 | 8 | class MainWindow(QtWidgets.QMainWindow): 9 | def __init__(self, parent=None): 10 | super().__init__(parent) 11 | self.setup_ui() 12 | self.dock_manager = qtpydocking.DockManager(self) 13 | 14 | self.dock_widgets = [] 15 | 16 | for label_text, area in ( 17 | ('1 Top', qtpydocking.DockWidgetArea.top), 18 | ('2 Bottom', qtpydocking.DockWidgetArea.bottom), 19 | ('3 Left', qtpydocking.DockWidgetArea.left), 20 | ('4 Right', qtpydocking.DockWidgetArea.right), 21 | ): 22 | # Create example content label - this can be any application specific 23 | # widget 24 | label = QtWidgets.QLabel() 25 | label.setWordWrap(True) 26 | label.setAlignment(Qt.AlignTop | Qt.AlignLeft) 27 | label.setText(f"{label_text}: Lorem ipsum dolor sit amet, consectetuer adipiscing elit. ") 28 | 29 | # Create a dock widget with the title Label 1 and set the created label 30 | # as the dock widget content 31 | dock_widget = qtpydocking.DockWidget(label_text) 32 | dock_widget.set_widget(label) 33 | self.dock_widgets.append(dock_widget) 34 | 35 | # Add the toggleViewAction of the dock widget to the menu to give 36 | # the user the possibility to show the dock widget if it has been closed 37 | self.menu_view.addAction(dock_widget.toggle_view_action()) 38 | 39 | # Add the dock widget to the top dock widget area 40 | self.dock_manager.add_dock_widget(area, dock_widget) 41 | 42 | def setup_ui(self): 43 | self.setWindowTitle("MainWindow") 44 | self.setObjectName("MainWindow") 45 | self.resize(400, 300) 46 | self.central_widget = QtWidgets.QWidget(self) 47 | self.central_widget.setObjectName("central_widget") 48 | self.setCentralWidget(self.central_widget) 49 | 50 | self.menu_bar = QtWidgets.QMenuBar(self) 51 | self.menu_bar.setGeometry(QtCore.QRect(0, 0, 400, 21)) 52 | self.menu_bar.setObjectName("menuBar") 53 | 54 | self.menu_view = QtWidgets.QMenu(self.menu_bar) 55 | self.menu_view.setObjectName("menu_view") 56 | self.menu_view.setTitle("View") 57 | self.setMenuBar(self.menu_bar) 58 | 59 | self.status_bar = QtWidgets.QStatusBar(self) 60 | self.status_bar.setObjectName("statusBar") 61 | self.setStatusBar(self.status_bar) 62 | self.menu_bar.addAction(self.menu_view.menuAction()) 63 | 64 | 65 | def main(app): 66 | main = MainWindow() 67 | main.show() 68 | state = main.dock_manager.save_state() 69 | print('This is what the saved state looks like in XML:') 70 | print(str(state, 'utf-8')) 71 | print() 72 | main.dock_manager.restore_state(state) 73 | return main 74 | 75 | 76 | if __name__ == '__main__': 77 | logging.basicConfig(level='DEBUG') 78 | app = QtWidgets.QApplication([]) 79 | window = main(app) 80 | window.show() 81 | print('shown') 82 | app.exec_() 83 | -------------------------------------------------------------------------------- /qtpydocking/floating_widget_title_bar.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import TYPE_CHECKING 3 | import math 4 | 5 | from qtpy.QtCore import Qt, Signal 6 | from qtpy.QtGui import QMouseEvent, QFontMetrics 7 | from qtpy.QtWidgets import QBoxLayout, QSizePolicy, QStyle, QPushButton, QLabel, QWidget 8 | 9 | from .enums import DragState 10 | from .eliding_label import ElidingLabel 11 | from .util import set_button_icon 12 | 13 | 14 | if TYPE_CHECKING: 15 | from . import FloatingDockContainer 16 | 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | 21 | class FloatingWidgetTitleBarPrivate: 22 | public: 'FloatingWidgetTitleBar' 23 | icon_label: QLabel 24 | title_label: 'ElidingLabel' 25 | close_button: QPushButton 26 | floating_widget: 'FloatingDockContainer' 27 | drag_state: DragState 28 | 29 | def __init__(self, public: 'FloatingWidgetTitleBar'): 30 | ''' 31 | Private data constructor 32 | 33 | Parameters 34 | ---------- 35 | public : FloatingWidgetTitleBar 36 | ''' 37 | self.public = public 38 | self.icon_label = None 39 | self.title_label = None 40 | self.close_button = None 41 | self.floating_widget = None 42 | self.drag_state = DragState.inactive 43 | 44 | def create_layout(self): 45 | ''' 46 | Creates the complete layout including all controls 47 | ''' 48 | self.title_label = ElidingLabel() 49 | self.title_label.set_elide_mode(Qt.ElideRight) 50 | self.title_label.setText("DockWidget->windowTitle()") 51 | self.title_label.setObjectName("floatingTitleLabel") 52 | self.title_label.setAlignment(Qt.AlignLeft) 53 | 54 | self.close_button = QPushButton() 55 | self.close_button.setObjectName("floatingTitleCloseButton") 56 | self.close_button.setFlat(True) 57 | 58 | # self.close_button.setAutoRaise(True) 59 | set_button_icon(self.public.style(), self.close_button, QStyle.SP_TitleBarCloseButton) 60 | 61 | self.close_button.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) 62 | self.close_button.setVisible(True) 63 | self.close_button.setFocusPolicy(Qt.NoFocus) 64 | self.close_button.clicked.connect(self.public.close_requested) 65 | 66 | fm = QFontMetrics(self.title_label.font()) 67 | spacing = round(fm.height() / 4.0) 68 | 69 | # Fill the layout 70 | layout = QBoxLayout(QBoxLayout.LeftToRight) 71 | layout.setContentsMargins(6, 0, 0, 0) 72 | layout.setSpacing(0) 73 | self.public.setLayout(layout) 74 | layout.addWidget(self.title_label, 1) 75 | layout.addSpacing(spacing) 76 | layout.addWidget(self.close_button) 77 | layout.setAlignment(Qt.AlignCenter) 78 | self.title_label.setVisible(True) 79 | 80 | 81 | class FloatingWidgetTitleBar(QWidget): 82 | close_requested = Signal() 83 | 84 | def __init__(self, parent: 'FloatingDockContainer'): 85 | super().__init__(parent) 86 | 87 | self.d = FloatingWidgetTitleBarPrivate(self) 88 | self.d.floating_widget = parent 89 | self.d.create_layout() 90 | 91 | def mousePressEvent(self, ev: QMouseEvent): 92 | if ev.button() == Qt.LeftButton: 93 | self.d.drag_state = DragState.floating_widget 94 | self.d.floating_widget.start_dragging( 95 | ev.pos(), self.d.floating_widget.size(), self) 96 | return 97 | 98 | super().mousePressEvent(ev) 99 | 100 | def mouseReleaseEvent(self, ev: QMouseEvent): 101 | logger.debug('FloatingWidgetTitleBar.mouseReleaseEvent') 102 | self.d.drag_state = DragState.inactive 103 | super().mouseReleaseEvent(ev) 104 | 105 | def mouseMoveEvent(self, ev: QMouseEvent): 106 | if not (ev.buttons() & Qt.LeftButton) or self.d.drag_state == DragState.inactive: 107 | self.d.drag_state = DragState.inactive 108 | elif self.d.drag_state == DragState.floating_widget: 109 | # Move floating window 110 | self.d.floating_widget.move_floating() 111 | super().mouseMoveEvent(ev) 112 | 113 | def enable_close_button(self, enable: bool): 114 | self.d.close_button.setEnabled(enable) 115 | 116 | def set_title(self, text: str): 117 | self.d.title_label.setText(text) 118 | -------------------------------------------------------------------------------- /qtpydocking/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/klauer/qtpydocking/7159a21a83c7ea0711e90fef9258296b98118f28/qtpydocking/tests/__init__.py -------------------------------------------------------------------------------- /qtpydocking/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pytest # noqa 4 | from pytestqt.qt_compat import qt_api # noqa 5 | 6 | 7 | logger = logging.getLogger('qtpydocking') 8 | logger.setLevel('DEBUG') 9 | -------------------------------------------------------------------------------- /qtpydocking/tests/test_examples.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from qtpy import QtGui, QtCore, QtWidgets 3 | 4 | import qtpydocking 5 | from qtpydocking import examples, DockWidgetArea 6 | 7 | 8 | @pytest.fixture(scope='function', 9 | params=['simple', 'demo'] 10 | ) 11 | def example(qtbot, qapp, request): 12 | example_module = getattr(examples, request.param) 13 | main = example_module.main(qapp) 14 | qtbot.addWidget(main) 15 | yield main 16 | 17 | 18 | @pytest.fixture(scope='function') 19 | def manager(example): 20 | return example.dock_manager 21 | 22 | 23 | @pytest.fixture(scope='function') 24 | def containers(manager): 25 | return manager.dock_containers() 26 | 27 | 28 | def test_smoke_example(qtbot, manager: qtpydocking.DockManager): 29 | # DockManager 30 | manager.container_overlay() 31 | manager.dock_area_overlay() 32 | manager.set_config_flags(manager.config_flags()) 33 | manager.find_dock_widget('') 34 | manager.dock_widgets_map() 35 | manager.dock_containers() 36 | manager.floating_widgets() 37 | manager.floating_widgets() 38 | manager.restore_state(manager.save_state()) 39 | manager.add_perspective('test') 40 | assert manager.perspective_names() == ['test'] 41 | settings = QtCore.QSettings() 42 | manager.save_perspectives(settings) 43 | manager.remove_perspectives('test') 44 | manager.load_perspectives(settings) 45 | manager.view_menu() 46 | manager.open_perspective('test') 47 | manager.set_view_menu_insertion_order(qtpydocking.InsertionOrder.by_spelling) 48 | 49 | # DockContainerWidget 50 | manager.create_root_splitter() 51 | manager.root_splitter() 52 | manager.last_added_dock_area_widget(DockWidgetArea.left) 53 | manager.has_top_level_dock_widget() 54 | manager.top_level_dock_widget() 55 | manager.top_level_dock_area() 56 | manager.dock_widgets() 57 | assert not manager.is_in_front_of(manager) 58 | manager.dock_area_at(QtCore.QPoint(0, 0)) 59 | manager.dock_area(0) 60 | assert manager.dock_area(999) is None 61 | manager.opened_dock_areas() 62 | manager.dock_area_count() 63 | manager.visible_dock_area_count() 64 | manager.dump_layout() 65 | manager.features() 66 | manager.floating_widget() 67 | manager.close_other_areas(DockWidgetArea.top) 68 | 69 | 70 | def test_smoke_example_adding(qtbot, manager: qtpydocking.DockManager): 71 | for area in (DockWidgetArea.left, 72 | DockWidgetArea.right, 73 | DockWidgetArea.top, 74 | DockWidgetArea.bottom, 75 | ): 76 | widget = qtpydocking.DockWidget('test') 77 | widget.set_widget(QtWidgets.QLabel('test')) 78 | manager.add_dock_widget_tab(area, widget) 79 | 80 | widget = qtpydocking.DockWidget('test') 81 | qtbot.addWidget(widget) 82 | widget.set_widget(QtWidgets.QLabel('test')) 83 | manager.add_dock_widget_tab_to_area(widget, manager.top_level_dock_area()) 84 | 85 | 86 | def test_smoke_example_add_floating(qtbot, manager: qtpydocking.DockManager): 87 | widget = qtpydocking.DockWidget('test') 88 | qtbot.addWidget(widget) 89 | widget.set_widget(QtWidgets.QLabel('test')) 90 | 91 | # tests: manager.add_dock_area() with dock_widget set 92 | floating = qtpydocking.FloatingDockContainer( 93 | dock_widget=widget, dock_manager=manager) 94 | 95 | 96 | def test_smoke_example_add_floating_container(qtbot, manager: qtpydocking.DockManager): 97 | widget = qtpydocking.DockWidget('test') 98 | qtbot.addWidget(widget) 99 | widget.set_widget(QtWidgets.QLabel('test')) 100 | 101 | area = manager.opened_dock_areas()[0] 102 | floating = qtpydocking.FloatingDockContainer( 103 | dock_widget=widget, dock_area=area) 104 | 105 | for area in manager.opened_dock_areas(): 106 | manager.remove_dock_area(area) 107 | 108 | 109 | def test_smoke_make_area_floating(example): 110 | for container in example.dock_manager.dock_containers(): 111 | print('container', container.z_order_index()) 112 | floating_widget = container.floating_widget() 113 | 114 | 115 | # def test_container_saving(containers): 116 | # for container in containers: 117 | # container.top_level_dock_widget() 118 | # container.restore_state(container.save_state()) 119 | # assert floating.dock_container() is container 120 | # floating.is_closable() 121 | # floating.has_top_level_dock_widget() 122 | # floating.top_level_dock_widget() 123 | # floating.dock_widgets() 124 | 125 | 126 | def test_smoke_widget(qtbot, manager: qtpydocking.DockManager): 127 | widget = qtpydocking.DockWidget('test') 128 | qtbot.addWidget(widget) 129 | 130 | label = QtWidgets.QLabel('test') 131 | widget.set_widget(label) 132 | assert widget.widget() is label 133 | 134 | widget.create_default_tool_bar() 135 | assert widget.tool_bar() 136 | widget.set_toolbar_floating_style(False) 137 | widget.set_toolbar_floating_style(True) 138 | 139 | tool_bar = QtWidgets.QToolBar() 140 | widget.set_tool_bar(tool_bar) 141 | widget.set_tool_bar_style(QtCore.Qt.ToolButtonTextUnderIcon, 142 | state=qtpydocking.WidgetState.floating) 143 | assert widget.tool_bar_style(qtpydocking.WidgetState.floating) == QtCore.Qt.ToolButtonTextUnderIcon 144 | 145 | widget.set_tool_bar_style(QtCore.Qt.ToolButtonIconOnly, 146 | state=qtpydocking.WidgetState.hidden) 147 | assert widget.tool_bar_style(qtpydocking.WidgetState.hidden) == QtCore.Qt.ToolButtonIconOnly 148 | 149 | widget.set_tool_bar_icon_size(QtCore.QSize(25, 25), 150 | state=qtpydocking.WidgetState.floating) 151 | assert widget.tool_bar_icon_size(qtpydocking.WidgetState.floating) == QtCore.QSize(25, 25) 152 | widget.set_tool_bar_icon_size(QtCore.QSize(26, 26), 153 | state=qtpydocking.WidgetState.hidden) 154 | assert widget.tool_bar_icon_size(qtpydocking.WidgetState.hidden) == QtCore.QSize(26, 26) 155 | 156 | widget.set_tab_tool_tip('tooltip') 157 | widget.toggle_view(True) 158 | widget.toggle_view(False) 159 | 160 | widget.set_feature(widget.features(), on=True) 161 | widget.set_features(widget.features()) 162 | 163 | widget.set_dock_manager(manager) 164 | assert widget.dock_manager() is manager 165 | 166 | widget.flag_as_unassigned() 167 | 168 | 169 | def test_smoke_floating(qtbot, manager: qtpydocking.DockManager): 170 | widget = qtpydocking.DockWidget('test') 171 | qtbot.addWidget(widget) 172 | 173 | widget.set_widget(QtWidgets.QLabel('test')) 174 | floating = qtpydocking.FloatingDockContainer( 175 | dock_widget=widget, dock_manager=manager) 176 | 177 | floating.on_dock_areas_added_or_removed() 178 | floating.on_dock_area_current_changed(0) 179 | floating.init_floating_geometry(QtCore.QPoint(0, 0), QtCore.QSize(100, 180 | 100)) 181 | floating.start_floating(QtCore.QPoint(0, 0), QtCore.QSize(100, 100), 182 | qtpydocking.DragState.inactive) 183 | 184 | floating.start_dragging(QtCore.QPoint(10, 0), QtCore.QSize(100, 100)) 185 | 186 | floating.update_window_title() 187 | 188 | widget.set_feature(qtpydocking.DockWidgetFeature.closable, False) 189 | floating.close() 190 | 191 | widget.set_feature(qtpydocking.DockWidgetFeature.closable, True) 192 | floating.close() 193 | 194 | floating.dock_container() 195 | floating.has_top_level_dock_widget() 196 | floating.top_level_dock_widget() 197 | floating.dock_widgets() 198 | 199 | floating.deleteLater() 200 | 201 | 202 | def test_smoke_xml_settings(qtbot, manager: qtpydocking.DockManager): 203 | manager.set_config_flags(qtpydocking.DockFlags.xml_compression) 204 | manager.restore_state(manager.save_state()) 205 | 206 | manager.set_config_flags(qtpydocking.DockFlags.xml_auto_formatting) 207 | manager.restore_state(manager.save_state()) 208 | -------------------------------------------------------------------------------- /qtpydocking/util.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import functools 3 | 4 | from typing import Optional, Any, Union, Type 5 | 6 | from qtpy.QtCore import Qt, QEvent, QObject, QRegExp 7 | from qtpy.QtGui import QPainter, QPixmap, QIcon 8 | from qtpy.QtWidgets import QApplication, QStyle, QAbstractButton 9 | from qtpy import QT_VERSION 10 | # if needed, you can import specific boolean API variables from this module 11 | # when implementing API code elsewhere 12 | from qtpy import API, PYQT4, PYQT5, PYSIDE, PYSIDE2 13 | 14 | from .dock_splitter import DockSplitter 15 | 16 | DEBUG_LEVEL = 0 17 | QT_VERSION_TUPLE = tuple(int(i) for i in QT_VERSION.split('.')[:3]) 18 | del QT_VERSION 19 | 20 | 21 | LINUX = sys.platform.startswith('linux') 22 | 23 | 24 | def emit_top_level_event_for_widget(widget: Optional['DockWidget'], 25 | floating: bool): 26 | ''' 27 | Call this function to emit a topLevelChanged() signal and to update the 28 | dock area tool bar visibility 29 | 30 | Parameters 31 | ---------- 32 | widget : DockWidget 33 | The top-level dock widget 34 | floating : bool 35 | ''' 36 | if widget is None: 37 | return 38 | 39 | widget.dock_area_widget().update_title_bar_visibility() 40 | widget.emit_top_level_changed(floating) 41 | 42 | 43 | def start_drag_distance() -> int: 44 | ''' 45 | The distance the user needs to move the mouse with the left button hold 46 | down before a dock widget start floating 47 | 48 | Returns 49 | ------- 50 | value : int 51 | ''' 52 | return int(QApplication.startDragDistance() * 1.5) 53 | 54 | 55 | def create_transparent_pixmap(source: QPixmap, opacity: float) -> QPixmap: 56 | ''' 57 | Creates a semi transparent pixmap from the given pixmap Source. The Opacity 58 | parameter defines the opacity from completely transparent (0.0) to 59 | completely opaque (1.0) 60 | 61 | Parameters 62 | ---------- 63 | source : QPixmap 64 | opacity : qreal 65 | 66 | Returns 67 | ------- 68 | value : QPixmap 69 | ''' 70 | transparent_pixmap = QPixmap(source.size()) 71 | transparent_pixmap.fill(Qt.transparent) 72 | 73 | painter = QPainter(transparent_pixmap) 74 | painter.setOpacity(opacity) 75 | painter.drawPixmap(0, 0, source) 76 | return transparent_pixmap 77 | 78 | 79 | def make_icon_pair(style, parent, standard_pixmap, 80 | transparent_role=QIcon.Disabled, *, 81 | transparency=0.25): 82 | ''' 83 | Using a standard pixmap (e.g., close button), create two pixmaps and set 84 | parent icon 85 | ''' 86 | icon = QIcon() 87 | normal_pixmap = style.standardPixmap(standard_pixmap, None, parent) 88 | icon.addPixmap(create_transparent_pixmap(normal_pixmap, transparency), 89 | transparent_role) 90 | icon.addPixmap(normal_pixmap, QIcon.Normal) 91 | parent.setIcon(icon) 92 | return icon 93 | 94 | 95 | def set_button_icon(style: QStyle, button: QAbstractButton, 96 | standard_pixmap: QStyle.StandardPixmap) -> QIcon: 97 | ''' 98 | Set a button icon 99 | 100 | Parameters 101 | ---------- 102 | style : QStyle 103 | button : QAbstractButton 104 | standard_pixmap: QStyle.StandardPixmap 105 | 106 | Returns 107 | ------- 108 | icon : QIcon 109 | ''' 110 | if LINUX: 111 | icon = style.standardIcon(standard_pixmap) 112 | button.setIcon(icon) 113 | return icon 114 | 115 | return make_icon_pair( 116 | style, parent=button, standard_pixmap=standard_pixmap, 117 | transparent_role=QIcon.Disabled) 118 | 119 | 120 | def hide_empty_parent_splitters(splitter: DockSplitter): 121 | ''' 122 | This function walks the splitter tree upwards to hides all splitters that 123 | do not have visible content 124 | 125 | Parameters 126 | ---------- 127 | splitter : DockSplitter 128 | ''' 129 | while splitter and splitter.isVisible(): 130 | if not splitter.has_visible_content(): 131 | splitter.hide() 132 | 133 | splitter = find_parent(DockSplitter, splitter) 134 | 135 | 136 | def find_parent(parent_type, widget): 137 | ''' 138 | Searches for the parent widget of the given type. 139 | Returns the parent widget of the given widget or 0 if the widget is not 140 | child of any widget of type T 141 | 142 | It is not safe to use this function in in DockWidget because only 143 | the current dock widget has a parent. All dock widgets that are not the 144 | current dock widget in a dock area have no parent. 145 | ''' 146 | parent_widget = widget.parentWidget() 147 | while parent_widget: 148 | if isinstance(parent_widget, parent_type): 149 | return parent_widget 150 | 151 | parent_widget = parent_widget.parentWidget() 152 | 153 | 154 | def find_child(parent: Type[QObject], type: Type[QObject], name: str = '', 155 | options: Qt.FindChildOptions = Qt.FindChildrenRecursively) -> Optional[QObject]: 156 | ''' 157 | Returns the child of this object that can be cast into type T and that is called name, or nullptr if there is no 158 | such object. Omitting the name argument causes all object names to be matched. The search is performed recursively, 159 | unless options specifies the option FindDirectChildrenOnly. 160 | 161 | If there is more than one child matching the search, the most direct ancestor is returned. If there are several 162 | direct ancestors, it is undefined which one will be returned. In that case, findChildren() should be used. 163 | 164 | WARNING: If you're using PySide, PySide2 or PyQt4, the options parameter will be discarded. 165 | ''' 166 | 167 | if PYQT5: 168 | return parent.findChild(type, name, options) 169 | else: 170 | # every other API (PySide, PySide2, PyQt4) has no options parameter 171 | return parent.findChild(type, name) 172 | 173 | 174 | def find_children(parent: Type[QObject], type: Type[QObject], name: Union[str, QRegExp] = '', 175 | options: Qt.FindChildOptions = Qt.FindChildrenRecursively) -> Optional[Any]: 176 | ''' 177 | Returns all children of this object with the given name that can be cast to type T, or an empty list if there are no 178 | such objects. Omitting the name argument causes all object names to be matched. The search is performed recursively, 179 | unless options specifies the option FindDirectChildrenOnly. 180 | 181 | WARNING: If you're using PySide, PySide2 or PyQt4, the options parameter will be discarded. 182 | ''' 183 | 184 | if PYQT5: 185 | return parent.findChildren(type, name, options) 186 | else: 187 | # every other API (PySide, PySide2, PyQt4) has no options parameter 188 | return parent.findChildren(type, name) 189 | 190 | 191 | def event_filter_decorator(method): 192 | ''' 193 | PySide2 exhibits some strange behavior where an eventFilter may get a 194 | 'PySide2.QtWidgets.QWidgetItem` as the `event` argument. This wrapper 195 | effectively just makes those specific cases a no-operation. 196 | 197 | NOTE:: 198 | This is considered a work-around until the source of the issue can be 199 | determined. 200 | ''' 201 | if PYSIDE or PYSIDE2: 202 | @functools.wraps(method) 203 | def wrapped(self, obj: QObject, event: QEvent): 204 | if not isinstance(event, QEvent): 205 | return True 206 | return method(self, obj, event) 207 | return wrapped 208 | return method 209 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | versioneer 2 | qtpy 3 | -------------------------------------------------------------------------------- /run_tests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys 4 | import pytest 5 | 6 | 7 | if __name__ == '__main__': 8 | # Show output results from every test function 9 | # Show the message output for skipped and expected failures 10 | args = ['-v', '-vrxs'] 11 | 12 | # Add extra arguments 13 | if len(sys.argv) > 1: 14 | args.extend(sys.argv[1:]) 15 | 16 | # Show coverage 17 | if '--show-cov' in args: 18 | args.extend(['--cov=qtpydocking', '--cov-report', 'term-missing']) 19 | args.remove('--show-cov') 20 | 21 | sys.exit(pytest.main(args)) 22 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [versioneer] 2 | VCS = git 3 | style = pep440 4 | versionfile_source = qtpydocking/_version.py 5 | versionfile_build = qtpydocking/_version.py 6 | tag_prefix = v 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import pathlib 3 | from setuptools import setup, find_packages 4 | 5 | import versioneer 6 | 7 | min_version = (3, 6) 8 | 9 | if sys.version_info < min_version: 10 | error = """ 11 | qtpydocking does not support Python {0}.{1}. 12 | Python {2}.{3} and above is required. Check your Python version like so: 13 | 14 | python3 --version 15 | 16 | This may be due to an out-of-date pip. Make sure you have pip >= 9.0.1. 17 | Upgrade pip like so: 18 | 19 | pip install --upgrade pip 20 | """.format(*sys.version_info[:2], *min_version) 21 | sys.exit(error) 22 | 23 | 24 | here = pathlib.Path(__file__).parent.absolute() 25 | 26 | with open(here / 'README.rst', encoding='utf-8') as readme_file: 27 | readme = readme_file.read() 28 | 29 | with open(here / 'requirements.txt', 'rt') as requirements_file: 30 | # Parse requirements.txt, ignoring any commented-out lines. 31 | requirements = [line for line in requirements_file.read().splitlines() 32 | if not line.startswith('#')] 33 | 34 | 35 | setup( 36 | name='qtpydocking', 37 | version=versioneer.get_version(), 38 | cmdclass=versioneer.get_cmdclass(), 39 | license='BSD', 40 | author='Ken Lauer', 41 | packages=find_packages(exclude=['docs', 'tests']), 42 | description='Python Qt Advanced Docking System', 43 | long_description=readme, 44 | url='https://github.com/klauer/qtpydocking', 45 | entry_points={ 46 | 'console_scripts': [ 47 | # 'some.module:some_function', 48 | ], 49 | }, 50 | include_package_data=True, 51 | package_data={ 52 | 'qtpydocking': [ 53 | # When adding files here, remember to update MANIFEST.in as well, 54 | # or else they will not be included in the distribution on PyPI! 55 | # 'path/to/data_file', 56 | ] 57 | }, 58 | install_requires=requirements, 59 | classifiers=[ 60 | 'Development Status :: 2 - Pre-Alpha', 61 | 'Natural Language :: English', 62 | 'Programming Language :: Python :: 3', 63 | ], 64 | ) 65 | --------------------------------------------------------------------------------