From 94cf29d5bc56648e71202ff47b90d8f8ff02ed43 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 26 Nov 2025 18:07:42 +0000 Subject: [PATCH 01/70] Build(deps): Bump requests from 2.32.3 to 2.32.4 in /scripts (#105) Bumps [requests](https://github.com/psf/requests) from 2.32.3 to 2.32.4. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.3...v2.32.4) --- updated-dependencies: - dependency-name: requests dependency-version: 2.32.4 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- scripts/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/requirements.txt b/scripts/requirements.txt index c6526130..ca9730a2 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -7,7 +7,7 @@ pefile==2023.2.7 PyQt6==6.7.1 PyQt6-Qt6==6.7.2 PyQt6_sip==13.8.0 -requests==2.32.3 +requests==2.32.4 sdbus==0.12.0 sdbus-networkmanager==2.0.0 typing==3.7.4.3 From 3e3583b83f7fafe54fb35ec31cb270571c61ba59 Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Fri, 28 Nov 2025 11:26:35 +0000 Subject: [PATCH 02/70] Hugoclsc/feature/GitHub actions (#107) * initial actions * Update CodeQL workflow to ignore certain paths Ignore specific paths for CodeQL analysis on push and pull request events. * experimenting workflows * Add pylintrc configuration file, configure dev test code workflow * split into multiple jobs * Fail job on error * pylint rc file, jobs refactores for dev workflow * Remove upload artifacts for now, move pylintrc file to root dir * Added upload artifacts with different files * Rem: branch naming workflow, enviroment handles this * Added fail-fast to false * Run only on PR to dev * Change file name --- .github/workflows/dev-ci.yml | 89 +++++ .pylintrc.dev | 11 + scripts/dev-requirements.txt | 2 - scripts/requirements-dev.txt | 4 + tools/.pylintrc | 659 +++++++++++++++++++++++++++++++++++ 5 files changed, 763 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/dev-ci.yml create mode 100644 .pylintrc.dev delete mode 100644 scripts/dev-requirements.txt create mode 100644 scripts/requirements-dev.txt create mode 100644 tools/.pylintrc diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml new file mode 100644 index 00000000..8fa61f99 --- /dev/null +++ b/.github/workflows/dev-ci.yml @@ -0,0 +1,89 @@ +name: dev-test-code + +on: + push: + branches: + - dev + paths-ignore: + - "scripts/**" + - "BlocksScreen/lib/ui/**" + - "extras/**" + pull_request: + branches: + - dev + paths-ignore: + - "scripts/**" + - "BlocksScreen/lib/ui/**" + - "extras/**" +jobs: + ci-checks: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.11.2"] + test-type: [ruff, pylint, pytest] + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + cache: "pip" + - name: Install dependencies + run: | + echo "Installing dependencies" + python -m pip install --upgrade pip + pip install scripts -r scripts/requirements-dev.txt + + - name: Run Test ${{ matrix.test-type }} + run: | + echo "Starting test runs" + if [ "${{ matrix.test-type }}" == "ruff" ]; then + echo "Running Formatting Test" + ruff check --output-format=github --target-version=py311 --config=pyproject.toml > ruff-output.txt 2>&1 + ruff format --diff --target-version=py311 --config=pyproject.toml >> ruff-output.txt 2>&1 + echo "Ruff finished" + fi + if [ "${{ matrix.test-type }}" == "pylint" ]; then + echo "Running Code Test" + pylint -j$(nproc) --recursive=y --rcfile=.pylintrc.dev . > pylint-output.txt 2>&1 + echo "Pylint finished" + fi + + if [ "${{ matrix.test-type }}" == "pytest" ]; then + if [ -d "tests/" ] && [ "$(ls -A tests/)" ]; then + echo "Running Python unit tests" + pytest tests/'*.py' --doctest-modules --junitxml=junit/test-results.xml --cov=com --conv-report=xml --cov-report=html > pytest-output.txt 2>&1 + else + echo "No tests directory no need to proceed with tests" + fi + fi + + - name: Upload ruff artifact + if: always() && matrix.test-type == 'ruff' + uses: actions/upload-artifact@v4 + with: + name: ruff-results + path: ruff-output.txt + + - name: Upload Pylint Artifacts + if: always() && matrix.test-type == 'pylint' + uses: actions/upload-artifact@v4 + with: + name: pylint-results + path: pylint-output.txt + + - name: Upload Pytest Artifacts + if: always() && matrix.test-type == 'pytest' && hashFiles('pytest-output.txt', 'junit/test-results.xml', 'coverage.xml') + uses: actions/upload-artifact@v4 + with: + name: pytest-results + path: | + pytest_output.txt + junit/test-results.xml + coverage.xml + htmlcov/ + \ No newline at end of file diff --git a/.pylintrc.dev b/.pylintrc.dev new file mode 100644 index 00000000..6c910bac --- /dev/null +++ b/.pylintrc.dev @@ -0,0 +1,11 @@ +[MAIN] +fail-under=7 +jobs=16 +ignore=tests,scripts,ui,extras +ignore-paths=BlocksScreen/lib/ui +py-version=3.11 + + +[FORMAT] +max-line-length=88 + diff --git a/scripts/dev-requirements.txt b/scripts/dev-requirements.txt deleted file mode 100644 index f88d7484..00000000 --- a/scripts/dev-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pycodestyle -pygobject-stubs \ No newline at end of file diff --git a/scripts/requirements-dev.txt b/scripts/requirements-dev.txt new file mode 100644 index 00000000..5cbd2e7d --- /dev/null +++ b/scripts/requirements-dev.txt @@ -0,0 +1,4 @@ +ruff +pylint +pytest +pytest-cov diff --git a/tools/.pylintrc b/tools/.pylintrc new file mode 100644 index 00000000..603b7b30 --- /dev/null +++ b/tools/.pylintrc @@ -0,0 +1,659 @@ +[MAIN] + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Clear in-memory caches upon conclusion of linting. Useful if running pylint +# in a server-like mode. +clear-cache-post-run=no + +# Load and enable all available extensions. Use --list-extensions to see a list +# all available extensions. +#enable-all-extensions= + +# In error mode, messages with a category besides ERROR or FATAL are +# suppressed, and no reports are done by default. Error mode is compatible with +# disabling specific errors. +#errors-only= + +# Always return a 0 (non-error) status code, even if lint errors are found. +# This is primarily useful in continuous integration scripts. +#exit-zero= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-allow-list= + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. (This is an alternative name to extension-pkg-allow-list +# for backward compatibility.) +extension-pkg-whitelist= + +# Return non-zero exit code if any of these messages/categories are detected, +# even if score is above --fail-under value. Syntax same as enable. Messages +# specified are enabled, while categories only check already-enabled messages. +fail-on= + +# Specify a score threshold under which the program will exit with error. +fail-under=10 + +# Interpret the stdin as a python script, whose filename needs to be passed as +# the module_or_package argument. +#from-stdin= + +# Files or directories to be skipped. They should be base names, not paths. +ignore=CVS + +# Add files or directories matching the regular expressions patterns to the +# ignore-list. The regex matches against paths and can be in Posix or Windows +# format. Because '\\' represents the directory delimiter on Windows systems, +# it can't be used as an escape character. +ignore-paths= + +# Files or directories matching the regular expression patterns are skipped. +# The regex matches against base names, not paths. The default value ignores +# Emacs file locks +ignore-patterns=^\.# + +# List of module names for which member attributes should not be checked and +# will not be imported (useful for modules/projects where namespaces are +# manipulated during runtime and thus existing member attributes cannot be +# deduced by static analysis). It supports qualified module names, as well as +# Unix pattern matching. +ignored-modules= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use, and will cap the count on Windows to +# avoid hangs. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins= + +# Pickle collected data for later comparisons. +persistent=yes + +# Resolve imports to .pyi stubs if available. May reduce no-member messages and +# increase not-an-iterable messages. +prefer-stubs=no + +# Minimum Python version to use for version dependent checks. Will default to +# the version used to run pylint. +py-version=3.11 + +# Discover python modules and packages in the file system subtree. +recursive=no + +# Add paths to the list of the source roots. Supports globbing patterns. The +# source root is an absolute path or a path relative to the current working +# directory used to determine a package namespace for modules located under the +# source root. +source-roots= + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + +# In verbose mode, extra non-checker-related info will be displayed. +#verbose= + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. If left empty, argument names will be checked with the set +# naming style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. If left empty, attribute names will be checked with the set naming +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. If left empty, class attribute names will be checked +# with the set naming style. +#class-attribute-rgx= + +# Naming style matching correct class constant names. +class-const-naming-style=UPPER_CASE + +# Regular expression matching correct class constant names. Overrides class- +# const-naming-style. If left empty, class constant names will be checked with +# the set naming style. +#class-const-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. If left empty, class names will be checked with the set naming style. +class-rgx=[A-Z][a-z]+ + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. If left empty, constant names will be checked with the set naming +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. If left empty, function names will be checked with the set +# naming style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. If left empty, inline iteration names will be checked +# with the set naming style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. If left empty, method names will be checked with the set naming style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. If left empty, module names will be checked with the set naming style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=^_ + +# Regular expression matching correct parameter specification variable names. +# If left empty, parameter specification variable names will be checked with +# the set naming style. +#paramspec-rgx= + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Regular expression matching correct type alias names. If left empty, type +# alias names will be checked with the set naming style. +#typealias-rgx= + +# Regular expression matching correct type variable names. If left empty, type +# variable names will be checked with the set naming style. +#typevar-rgx= + +# Regular expression matching correct type variable tuple names. If left empty, +# type variable tuple names will be checked with the set naming style. +#typevartuple-rgx= + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. If left empty, variable names will be checked with the set +# naming style. +#variable-rgx= + + +[CLASSES] + +# Warn about protected attribute access inside special methods +check-protected-access-in-special-methods=no + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + asyncSetUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict,_fields,_replace,_source,_make,os._exit + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[DESIGN] + +# List of regular expressions of class ancestor names to ignore when counting +# public methods (see R0903) +exclude-too-few-public-methods= + +# List of qualified class names to ignore when counting class parents (see +# R0901) +ignored-parents= + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of positional arguments for function / method. +max-positional-arguments=5 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when caught. +overgeneral-exceptions=builtins.BaseException,builtins.Exception + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. Pylint's default of 100 is +# based on PEP 8's guidance that teams may choose line lengths up to 99 +# characters. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow explicit reexports by alias from a package __init__. +allow-reexport-from-package=no + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules= + +# Output a graph (.gv or any supported image format) of external dependencies +# to the given file (report RP0402 must not be disabled). +ext-import-graph= + +# Output a graph (.gv or any supported image format) of all (i.e. internal and +# external) dependencies to the given file (report RP0402 must not be +# disabled). +import-graph= + +# Output a graph (.gv or any supported image format) of internal dependencies +# to the given file (report RP0402 must not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, CONTROL_FLOW, INFERENCE, INFERENCE_FAILURE, +# UNDEFINED. +confidence=HIGH, + CONTROL_FLOW, + INFERENCE, + INFERENCE_FAILURE, + UNDEFINED + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then re-enable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + use-implicit-booleaness-not-comparison-to-string, + use-implicit-booleaness-not-comparison-to-zero, + bare-except, + invalid-name + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable= + + +[METHOD_ARGS] + +# List of qualified names (i.e., library.method) which require a timeout +# parameter e.g. 'requests.api.get,requests.api.post' +timeout-methods=requests.api.delete,requests.api.get,requests.api.head,requests.api.options,requests.api.patch,requests.api.post,requests.api.put,requests.api.request + + +[MISCELLANEOUS] + +# Whether or not to search for fixme's in docstrings. +check-fixme-in-docstring=no + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +notes-rgx= + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit,argparse.parse_error + +# Let 'consider-using-join' be raised when the separator to join on would be +# non-empty (resulting in expected fixes of the type: ``"- " + " - +# ".join(items)``) +suggest-join-with-non-empty-separator=yes + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'fatal', 'error', 'warning', 'refactor', +# 'convention', and 'info' which contain the number of messages in each +# category, as well as 'statement' which is the total number of statements +# analyzed. This score is used by the global evaluation report (RP0004). +evaluation=max(0, 0 if fatal else 10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +msg-template= + +# Set the output format. Available formats are: 'text', 'parseable', +# 'colorized', 'json2' (improved json format), 'json' (old json format), msvs +# (visual studio) and 'github' (GitHub actions). You can also give a reporter +# class, e.g. mypackage.mymodule.MyReporterClass. +#output-format= + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[SIMILARITIES] + +# Comments are removed from the similarity computation +ignore-comments=yes + +# Docstrings are removed from the similarity computation +ignore-docstrings=yes + +# Imports are removed from the similarity computation +ignore-imports=yes + +# Signatures are removed from the similarity computation +ignore-signatures=yes + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. No available dictionaries : You need to install +# both the python package and the system dependency for enchant to work. +spelling-dict= + +# List of comma separated words that should be considered directives if they +# appear at the beginning of a comment and should not be checked. +spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy: + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of symbolic message names to ignore for Mixin members. +ignored-checks-for-mixins=no-member, + not-async-context-manager, + not-context-manager, + attribute-defined-outside-init + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local,argparse.Namespace + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The maximum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# Regex pattern to define which classes are considered mixins. +mixin-class-rgx=.*[Mm]ixin + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of names allowed to shadow builtins +allowed-redefined-builtins= + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io From 420a3e197566c610ca42414c5590e5ffa368fa4c Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Fri, 5 Dec 2025 14:55:36 +0000 Subject: [PATCH 03/70] Hugoclsc/feature/GitHub actions (#113) * initial actions * Update CodeQL workflow to ignore certain paths Ignore specific paths for CodeQL analysis on push and pull request events. * experimenting workflows * Add pylintrc configuration file, configure dev test code workflow * split into multiple jobs * Fail job on error * pylint rc file, jobs refactores for dev workflow * Remove upload artifacts for now, move pylintrc file to root dir * Added upload artifacts with different files * Rem: branch naming workflow, enviroment handles this * Added fail-fast to false * Run only on PR to dev * Change file name * Initial stage-ci workflow configuration * Changed worflow name, added docstr-coverage tool to the workflow * Added dependencies cache, reduces redundant installations, fixes incorrect pytest file * Exclude F403 ruff error * Added docstr-coverage dependency * Fix incorrect requirements installation command * bugfix on cli command for docstr-coverage * Uncomment artifact upload * Update pyproject.toml * Added bandig config, bump requests version * Bump requests version * Bump requirement versions * Add dev, stage requirements, bump all requirements * Migrate pylint config options to pyproject.toml file * Deleted pylintrc.dev file * Remove unused dependencie * uncomment upload artifacts * Added bandit security tests * Standardize bandit output to json * Add environment --------- Signed-off-by: Hugo Costa --- .github/workflows/dev-ci.yml | 57 +++++++++++++++++++++++++-------- .github/workflows/stage-ci.yml | 23 ++++++++++++++ pyproject.toml | 58 ++++++++++++++++++++++++++-------- scripts/requirements-dev.txt | 2 ++ scripts/requirements.txt | 23 +++++++------- 5 files changed, 123 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/stage-ci.yml diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index 8fa61f99..609990a4 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -1,9 +1,9 @@ -name: dev-test-code +name: CI-dev-pipeline on: push: branches: - - dev + - dev paths-ignore: - "scripts/**" - "BlocksScreen/lib/ui/**" @@ -22,7 +22,9 @@ jobs: fail-fast: false matrix: python-version: ["3.11.2"] - test-type: [ruff, pylint, pytest] + test-type: [ruff, pylint, pytest, docstrcov, security] + environment: Dev + steps: - name: Checkout repo uses: actions/checkout@v4 @@ -31,13 +33,15 @@ jobs: uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - cache: "pip" + cache: pip + cache-dependency-path: scripts/requirements-dev.txt + - name: Install dependencies run: | echo "Installing dependencies" python -m pip install --upgrade pip - pip install scripts -r scripts/requirements-dev.txt - + pip install -r scripts/requirements-dev.txt + - name: Run Test ${{ matrix.test-type }} run: | echo "Starting test runs" @@ -48,21 +52,32 @@ jobs: echo "Ruff finished" fi if [ "${{ matrix.test-type }}" == "pylint" ]; then - echo "Running Code Test" - pylint -j$(nproc) --recursive=y --rcfile=.pylintrc.dev . > pylint-output.txt 2>&1 + echo "Running Pylint Code Test" + pylint -j$(nproc) --recursive=y BlocksScreen/ > pylint-output.txt 2>&1 echo "Pylint finished" fi if [ "${{ matrix.test-type }}" == "pytest" ]; then if [ -d "tests/" ] && [ "$(ls -A tests/)" ]; then echo "Running Python unit tests" - pytest tests/'*.py' --doctest-modules --junitxml=junit/test-results.xml --cov=com --conv-report=xml --cov-report=html > pytest-output.txt 2>&1 + pytest tests/*.py --doctest-modules --junitxml=junit/test-results.xml --cov=BlocksScreen --cov-report=xml --cov-report=html > pytest-output.txt 2>&1 else echo "No tests directory no need to proceed with tests" fi fi - - name: Upload ruff artifact + if [ "${{ matrix.test-type }}" == "docstrcov" ]; then + echo "Running docstring coverage test" + docstr-coverage BlocksScreen/ --exclude .*/BlocksScreen/lib/ui/.*?$ --fail-under=80 --skip-magic --skip-init --skip-private --skip-property > docstr-cov-output.txt 2>&1 + fi + + if [ "${{matrix.test-type }}" == "security" ]; then + echo "Running bandit security test" + bandit -c pyproject.toml -r . -f json -o bandit-output.json 2>&1 + fi + + + - name: Upload ruff artifact if: always() && matrix.test-type == 'ruff' uses: actions/upload-artifact@v4 with: @@ -75,15 +90,29 @@ jobs: with: name: pylint-results path: pylint-output.txt - + - name: Upload Pytest Artifacts - if: always() && matrix.test-type == 'pytest' && hashFiles('pytest-output.txt', 'junit/test-results.xml', 'coverage.xml') + if: always() && matrix.test-type == 'pytest' uses: actions/upload-artifact@v4 with: name: pytest-results path: | - pytest_output.txt + pytest-output.txt junit/test-results.xml coverage.xml htmlcov/ - \ No newline at end of file + continue-on-error: true + + - name: Upload docstr coverage report + if: always() && matrix.test-type == 'docstrcov' + uses: actions/upload-artifact@v4 + with: + name: docstr-coverage + path: docstr-cov-output.txt + + - name: Upload bandit security report + if: always() && matrix.test-type == 'security' + uses: actions/upload-artifact@v4 + with: + name: bandit-output + path: bandit-output.txt diff --git a/.github/workflows/stage-ci.yml b/.github/workflows/stage-ci.yml new file mode 100644 index 00000000..6d8de6be --- /dev/null +++ b/.github/workflows/stage-ci.yml @@ -0,0 +1,23 @@ +name: stage-ci + +on: + branches: + - stage + paths-ignore: + - "scripts/**" + - "BlocksScreen/lib/ui/**" + - "extras/**" + workflow_run: + workflows: ["dev-test-code"] + types: + - completed + jobs: + ci-stage: + if: ${{ github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Run staging pipeline + run: echo "Running staging integration tests..." \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index f3be8e2f..eaa6152e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,35 +5,65 @@ description = "GUI for BLOCKS Printers running Klipper" authors = [ { name = "Hugo do Carmo Costa", email = "hugo.santos.costa@gmail.com" }, ] +maintainers = [ + { name = "Guilherme Costa", email = "guilherme.costa@blockstec.com" }, + { name = "Roberto Martins ", email = "roberto.martins@blockstec.com" }, +] dependencies = [ 'altgraph==0.17.4', - 'certifi==2024.7.4', - 'charset-normalizer==3.3.2', - 'idna==3.8', - 'numpy==2.1.0', - 'pefile==2023.2.7', - 'PyQt6==6.7.1', - 'PyQt6-Qt6==6.7.2', - 'PyQt6_sip==13.8.0', - 'requests==2.32.3', - 'sdbus==0.12.0', + 'certifi==2025.10.5', + 'charset-normalizer==3.4.4', + 'idna==3.11', + 'numpy==2.3.4', + 'pefile==2024.8.26', + 'PyQt6==6.10.0', + 'PyQt6-Qt6==6.10.0', + 'PyQt6_sip==13.10.2', + 'requests>=2.32.5', + 'sdbus==0.14.1', 'sdbus-networkmanager==2.0.0', 'typing==3.7.4.3', - 'websocket-client==1.8.0', - 'opencv-python-headless==4.11.0.86', - 'qrcode==8.2' + 'websocket-client==1.9.0', + 'qrcode==8.2', ] -requires-python = ">=3.11.2" +requires-python = "==3.11.2" readme = "README.md" license = { text = "GNU Affero General Public License v3.0 or later" } keywords = ["GUI", "klipper", "BlocksScreen", "BLOCKS"] +[project.optional-dependencies] +dev = ["ruff", "pylint", "pytest", "pytest-cov", "docstr_coverage"] +stage = ["bandit"] +full-dev = ["BlockScreen[dev,stage]"] + + +[project.urls] +Homepage = "https://blockstec.com" +Issues = "https://github.com/BlocksTechnology/BlocksScreen/issues" + [tool.ruff] line-length = 88 indent-width = 4 +[tool.ruff.lint] +ignore = ["F403"] + [tool.ruff.format] indent-style = "space" line-ending = 'auto' docstring-code-format = true docstring-code-line-length = 94 + +[tool.pylint] +fail-under = 7 +jobs = 16 +ignore = ["tests", "scripts", "ui", "extras"] +ignore-paths = ["BlocksScreen/lib/ui"] +py-version = "3.11" +max-line-length = 88 + +[tool.pytest.ini_options] +addopts = "--cov=BlocksScreen --cov-report=html" + +[tool.bandit] +exclude_dirs = ["tests", "BlocksScreen/lib/ui/resources/"] diff --git a/scripts/requirements-dev.txt b/scripts/requirements-dev.txt index 5cbd2e7d..61706f13 100644 --- a/scripts/requirements-dev.txt +++ b/scripts/requirements-dev.txt @@ -2,3 +2,5 @@ ruff pylint pytest pytest-cov +docstr_coverage +bandit \ No newline at end of file diff --git a/scripts/requirements.txt b/scripts/requirements.txt index ca9730a2..9dfdbd57 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -1,16 +1,15 @@ altgraph==0.17.4 -certifi==2024.7.4 -charset-normalizer==3.3.2 -idna==3.8 -numpy==2.1.0 -pefile==2023.2.7 -PyQt6==6.7.1 -PyQt6-Qt6==6.7.2 -PyQt6_sip==13.8.0 -requests==2.32.4 -sdbus==0.12.0 +certifi==2025.10.5 +charset-normalizer==3.4.4 +idna==3.11 +numpy==2.3.4 +pefile==2024.8.26 +PyQt6==6.10.0 +PyQt6-Qt6==6.10.0 +PyQt6_sip==13.10.2 +requests>=2.32.5 +sdbus==0.14.1 sdbus-networkmanager==2.0.0 typing==3.7.4.3 -websocket-client==1.8.0 -opencv-python-headless==4.11.0.86 +websocket-client==1.9.0 qrcode==8.2 \ No newline at end of file From bc1156ccfabd449b05f3602e91cd9671922e70d8 Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Fri, 5 Dec 2025 15:19:51 +0000 Subject: [PATCH 04/70] Hugoclsc/feature/GitHub actions (#114) * initial actions * Update CodeQL workflow to ignore certain paths Ignore specific paths for CodeQL analysis on push and pull request events. * experimenting workflows * Add pylintrc configuration file, configure dev test code workflow * split into multiple jobs * Fail job on error * pylint rc file, jobs refactores for dev workflow * Remove upload artifacts for now, move pylintrc file to root dir * Added upload artifacts with different files * Rem: branch naming workflow, enviroment handles this * Added fail-fast to false * Run only on PR to dev * Change file name * Initial stage-ci workflow configuration * Changed worflow name, added docstr-coverage tool to the workflow * Added dependencies cache, reduces redundant installations, fixes incorrect pytest file * Exclude F403 ruff error * Added docstr-coverage dependency * Fix incorrect requirements installation command * bugfix on cli command for docstr-coverage * Uncomment artifact upload * Update pyproject.toml * Added bandig config, bump requests version * Bump requests version * Bump requirement versions * Add dev, stage requirements, bump all requirements * Migrate pylint config options to pyproject.toml file * Deleted pylintrc.dev file * Remove unused dependencie * uncomment upload artifacts * Added bandit security tests * Standardize bandit output to json * Add environment * Fix incorrect file extension for bandit --------- Signed-off-by: Hugo Costa --- .github/workflows/dev-ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index 609990a4..d6e0bebd 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -60,7 +60,7 @@ jobs: if [ "${{ matrix.test-type }}" == "pytest" ]; then if [ -d "tests/" ] && [ "$(ls -A tests/)" ]; then echo "Running Python unit tests" - pytest tests/*.py --doctest-modules --junitxml=junit/test-results.xml --cov=BlocksScreen --cov-report=xml --cov-report=html > pytest-output.txt 2>&1 + pytest tests/ --doctest-modules --junitxml=junit/test-results.xml --cov=BlocksScreen/ --cov-report=xml --cov-report=html > pytest-output.txt 2>&1 else echo "No tests directory no need to proceed with tests" fi @@ -115,4 +115,4 @@ jobs: uses: actions/upload-artifact@v4 with: name: bandit-output - path: bandit-output.txt + path: bandit-output.json From ca9eb864695afdb3f1ff205cce73415dbe842f82 Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Fri, 5 Dec 2025 16:39:20 +0000 Subject: [PATCH 05/70] Hugoclsc/feature/GitHub actions (#115) * initial actions * Update CodeQL workflow to ignore certain paths Ignore specific paths for CodeQL analysis on push and pull request events. * experimenting workflows * Add pylintrc configuration file, configure dev test code workflow * split into multiple jobs * Fail job on error * pylint rc file, jobs refactores for dev workflow * Remove upload artifacts for now, move pylintrc file to root dir * Added upload artifacts with different files * Rem: branch naming workflow, enviroment handles this * Added fail-fast to false * Run only on PR to dev * Change file name * Initial stage-ci workflow configuration * Changed worflow name, added docstr-coverage tool to the workflow * Added dependencies cache, reduces redundant installations, fixes incorrect pytest file * Exclude F403 ruff error * Added docstr-coverage dependency * Fix incorrect requirements installation command * bugfix on cli command for docstr-coverage * Uncomment artifact upload * Update pyproject.toml * Added bandig config, bump requests version * Bump requests version * Bump requirement versions * Add dev, stage requirements, bump all requirements * Migrate pylint config options to pyproject.toml file * Deleted pylintrc.dev file * Remove unused dependencie * uncomment upload artifacts * Added bandit security tests * Standardize bandit output to json * Add environment * Fix incorrect file extension for bandit * Add exclude section to ruff config * Fix docstr-converage exclude regex --------- Signed-off-by: Hugo Costa --- .github/workflows/dev-ci.yml | 2 +- pyproject.toml | 31 +++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index d6e0bebd..7cba017e 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -68,7 +68,7 @@ jobs: if [ "${{ matrix.test-type }}" == "docstrcov" ]; then echo "Running docstring coverage test" - docstr-coverage BlocksScreen/ --exclude .*/BlocksScreen/lib/ui/.*?$ --fail-under=80 --skip-magic --skip-init --skip-private --skip-property > docstr-cov-output.txt 2>&1 + docstr-coverage BlocksScreen/ --exclude '.*/BlocksScreen/lib/ui/.*?$' --fail-under=80 --skip-magic --skip-init --skip-private --skip-property > docstr-cov-output.txt 2>&1 fi if [ "${{matrix.test-type }}" == "security" ]; then diff --git a/pyproject.toml b/pyproject.toml index eaa6152e..9f7e1cfb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,37 @@ Issues = "https://github.com/BlocksTechnology/BlocksScreen/issues" [tool.ruff] line-length = 88 indent-width = 4 +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", + "BlocksScreen/lib/ui", + "extras", + "tests" +] [tool.ruff.lint] ignore = ["F403"] From 69deb3b8a9f608de4c1ca7376a5e989d334e7e74 Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Fri, 5 Dec 2025 17:07:28 +0000 Subject: [PATCH 06/70] Hugoclsc/feature/GitHub actions (#116) * initial actions * Update CodeQL workflow to ignore certain paths Ignore specific paths for CodeQL analysis on push and pull request events. * experimenting workflows * Add pylintrc configuration file, configure dev test code workflow * split into multiple jobs * Fail job on error * pylint rc file, jobs refactores for dev workflow * Remove upload artifacts for now, move pylintrc file to root dir * Added upload artifacts with different files * Rem: branch naming workflow, enviroment handles this * Added fail-fast to false * Run only on PR to dev * Change file name * Initial stage-ci workflow configuration * Changed worflow name, added docstr-coverage tool to the workflow * Added dependencies cache, reduces redundant installations, fixes incorrect pytest file * Exclude F403 ruff error * Added docstr-coverage dependency * Fix incorrect requirements installation command * bugfix on cli command for docstr-coverage * Uncomment artifact upload * Update pyproject.toml * Added bandig config, bump requests version * Bump requests version * Bump requirement versions * Add dev, stage requirements, bump all requirements * Migrate pylint config options to pyproject.toml file * Deleted pylintrc.dev file * Remove unused dependencie * uncomment upload artifacts * Added bandit security tests * Standardize bandit output to json * Add environment * Fix incorrect file extension for bandit * Add exclude section to ruff config * Fix docstr-converage exclude regex * Separate CI from CD --------- Signed-off-by: Hugo Costa --- .github/workflows/dev-ci.yml | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index 7cba017e..556119c2 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -1,13 +1,6 @@ name: CI-dev-pipeline on: - push: - branches: - - dev - paths-ignore: - - "scripts/**" - - "BlocksScreen/lib/ui/**" - - "extras/**" pull_request: branches: - dev @@ -23,7 +16,6 @@ jobs: matrix: python-version: ["3.11.2"] test-type: [ruff, pylint, pytest, docstrcov, security] - environment: Dev steps: - name: Checkout repo @@ -41,7 +33,7 @@ jobs: echo "Installing dependencies" python -m pip install --upgrade pip pip install -r scripts/requirements-dev.txt - + - name: Run Test ${{ matrix.test-type }} run: | echo "Starting test runs" @@ -70,13 +62,12 @@ jobs: echo "Running docstring coverage test" docstr-coverage BlocksScreen/ --exclude '.*/BlocksScreen/lib/ui/.*?$' --fail-under=80 --skip-magic --skip-init --skip-private --skip-property > docstr-cov-output.txt 2>&1 fi - + if [ "${{matrix.test-type }}" == "security" ]; then echo "Running bandit security test" bandit -c pyproject.toml -r . -f json -o bandit-output.json 2>&1 fi - - name: Upload ruff artifact if: always() && matrix.test-type == 'ruff' uses: actions/upload-artifact@v4 From 8229438f9f9eb61e33ec29e071283aa28b6f2cd5 Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Wed, 10 Dec 2025 14:18:11 +0000 Subject: [PATCH 07/70] Refactor/tests compliance (#117) * Build(deps): Bump requests from 2.32.3 to 2.32.4 in /scripts (#112) Bumps [requests](https://github.com/psf/requests) from 2.32.3 to 2.32.4. - [Release notes](https://github.com/psf/requests/releases) - [Changelog](https://github.com/psf/requests/blob/main/HISTORY.md) - [Commits](https://github.com/psf/requests/compare/v2.32.3...v2.32.4) --- updated-dependencies: - dependency-name: requests dependency-version: 2.32.4 dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * Removed unused method * Remove unused imports * Remove unused import, f-string without placeholders * Remove unused import * Removed unused module * Removed f-string without placeholder * Removed unused code on method * Removed placeholder file for new module * Removed assigned variable but never used * Removed unused import * Comment unused variable * Removed unused code line * Removed unused file * Explicity re-raise with from * Removed unused code line * Refactor text box painting * Removed unused import * Removed unused code line * Removed unused module * Removed unused module placeholder * Added docstrings * Add docstrings to methods * Add docstring to methods and class * Add docstring to methods * Removed unused module * Added docstrings * Added docstrings * Added docstrings, deleted commented code * Added docstrings, deleted commented code * Added docstrings, deleted commented code * Added docstrings, deleted commented code * deleted commented code * Added docstrings, deleted commented code * Deleted code from unused window * Added docstring to methods, deleted unused code * Added docstring to methods, deleted unused code * Add docsctring, delete commented code * Add docsctring * Add docsctring, delete unused code * Add docstring, change method name * Add docstring, change method name, delete unused code * Add docstring, delete unused code * Add docstring, delete unused code, changed method name * Add docstring, changed method name * Add docstring, changed method name * Add docstring, changed method name * Add docstring, changed method name * Add docstring, changed method name * Formatting * Add docstring * Add docstring, delete unused code, changed method name * Add docstring, delete unused code, changed method name * Add docstring, changed method name * Add docstring, changed method name * Add docstring, delete commented code, change method name * Add docstring, delete commented code, change method name * Let troubleshoot page decide where it wants to go * Add docstring, change method name * Add docstring, delete unused and untested code * Add docstring * Add docstring * Add docstring, delete unused code * Add docstring * Add docstring * Add docstring * Add docstring * Add docstring * Add docstrings * Add docstring, change method name * Add docstring * Add docstring, delete unused code * Add docstring, delete unused code * Add docstring * Change list item docstring * Add docstring * Add docstring * Deleted unused module * Add docstring * Deleted unused method * Deleted unused code line * Delete unused code line * Delete unused code line * Security patch subprocess shell = True, security issue * Remove argument from method * Ruff formatting * Ruff formatting * Surpress Reviewed security issues * Formatting --------- Signed-off-by: dependabot[bot] Signed-off-by: Hugo Costa Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- BlocksScreen/BlocksScreen.py | 7 +- BlocksScreen/configfile.py | 134 ++++--- BlocksScreen/events.py | 50 ++- BlocksScreen/helper_methods.py | 52 ++- BlocksScreen/lib/async_network_monitor.py | 157 -------- BlocksScreen/lib/filament.py | 19 +- BlocksScreen/lib/files.py | 90 ++--- BlocksScreen/lib/machine.py | 84 ++-- BlocksScreen/lib/moonrakerComm.py | 142 +++---- BlocksScreen/lib/moonrest.py | 9 +- BlocksScreen/lib/network.py | 39 +- BlocksScreen/lib/panels/controlTab.py | 31 +- BlocksScreen/lib/panels/filamentTab.py | 32 +- BlocksScreen/lib/panels/instructionsWindow.py | 30 -- BlocksScreen/lib/panels/mainWindow.py | 42 +- BlocksScreen/lib/panels/networkWindow.py | 126 +++--- BlocksScreen/lib/panels/printTab.py | 110 ++--- BlocksScreen/lib/panels/userauthWindow.py | 6 - BlocksScreen/lib/panels/utilitiesTab.py | 54 ++- .../lib/panels/widgets/babystepPage.py | 103 ++--- .../lib/panels/widgets/confirmPage.py | 8 +- .../lib/panels/widgets/connectionPage.py | 34 +- BlocksScreen/lib/panels/widgets/dialogPage.py | 46 +-- BlocksScreen/lib/panels/widgets/fansPage.py | 14 +- BlocksScreen/lib/panels/widgets/filesPage.py | 80 ++-- .../lib/panels/widgets/jobStatusPage.py | 28 +- .../lib/panels/widgets/keyboardPage.py | 18 +- BlocksScreen/lib/panels/widgets/loadPage.py | 40 +- BlocksScreen/lib/panels/widgets/loadWidget.py | 36 +- BlocksScreen/lib/panels/widgets/numpadPage.py | 153 +++---- .../lib/panels/widgets/optionCardWidget.py | 38 +- .../lib/panels/widgets/popupDialogWidget.py | 95 +++-- .../lib/panels/widgets/printcorePage.py | 62 +-- .../lib/panels/widgets/probeHelperPage.py | 298 +++++--------- .../lib/panels/widgets/sensorWidget.py | 45 +-- .../lib/panels/widgets/sensorsPanel.py | 94 ++--- .../panels/widgets/slider_selector_page.py | 51 +-- .../lib/panels/widgets/troubleshootPage.py | 73 ++-- BlocksScreen/lib/panels/widgets/tunePage.py | 108 ++--- BlocksScreen/lib/panels/widgets/updatePage.py | 2 +- BlocksScreen/lib/printer.py | 2 +- BlocksScreen/lib/qrcode_gen.py | 8 +- BlocksScreen/lib/ui/instructionsWindow.ui | 377 ------------------ BlocksScreen/lib/ui/instructionsWindow_ui.py | 162 -------- BlocksScreen/lib/utils/RepeatedTimer.py | 14 +- BlocksScreen/lib/utils/RoutingQueue.py | 7 +- BlocksScreen/lib/utils/blocks_Scrollbar.py | 10 +- BlocksScreen/lib/utils/blocks_button.py | 44 +- BlocksScreen/lib/utils/blocks_frame.py | 9 +- BlocksScreen/lib/utils/blocks_label.py | 31 +- BlocksScreen/lib/utils/blocks_linedit.py | 16 +- BlocksScreen/lib/utils/blocks_progressbar.py | 25 +- BlocksScreen/lib/utils/blocks_slider.py | 23 +- BlocksScreen/lib/utils/blocks_tabwidget.py | 7 + BlocksScreen/lib/utils/blocks_togglebutton.py | 39 +- BlocksScreen/lib/utils/display_button.py | 53 ++- BlocksScreen/lib/utils/group_button.py | 22 +- BlocksScreen/lib/utils/icon_button.py | 11 +- BlocksScreen/lib/utils/list_button.py | 33 +- BlocksScreen/lib/utils/list_model.py | 2 +- BlocksScreen/lib/utils/loadAnimatedLabel.py | 6 - BlocksScreen/lib/utils/numpad_button.py | 8 +- BlocksScreen/lib/utils/others.py | 48 --- .../lib/utils/toggleAnimatedButton.py | 52 ++- BlocksScreen/lib/utils/ui.py | 2 - BlocksScreen/lib/utils/url.py | 76 ---- BlocksScreen/logger.py | 36 +- BlocksScreen/screensaver.py | 17 +- 68 files changed, 1308 insertions(+), 2472 deletions(-) delete mode 100644 BlocksScreen/lib/async_network_monitor.py delete mode 100644 BlocksScreen/lib/panels/instructionsWindow.py delete mode 100644 BlocksScreen/lib/panels/userauthWindow.py delete mode 100644 BlocksScreen/lib/ui/instructionsWindow.ui delete mode 100644 BlocksScreen/lib/ui/instructionsWindow_ui.py delete mode 100644 BlocksScreen/lib/utils/loadAnimatedLabel.py delete mode 100644 BlocksScreen/lib/utils/others.py delete mode 100644 BlocksScreen/lib/utils/ui.py delete mode 100644 BlocksScreen/lib/utils/url.py diff --git a/BlocksScreen/BlocksScreen.py b/BlocksScreen/BlocksScreen.py index 83ec0a3b..a7a2098e 100644 --- a/BlocksScreen/BlocksScreen.py +++ b/BlocksScreen/BlocksScreen.py @@ -22,16 +22,15 @@ RESET = "\033[0m" -def setup_working_dir(): ... - - def setup_app_loggers(): - ql = logger.create_logger(name="logs/BlocksScreen.log", level=logging.DEBUG) + """Setup logger""" + _ = logger.create_logger(name="logs/BlocksScreen.log", level=logging.DEBUG) _logger = logging.getLogger(name="logs/BlocksScreen.log") _logger.info("============ BlocksScreen Initializing ============") def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): + """Show splash screen on app initialization""" logo = QtGui.QPixmap("BlocksScreen/BlocksScreen/lib/ui/resources/logoblocks.png") splash = QtWidgets.QSplashScreen(pixmap=logo) splash.setGeometry(QtCore.QRect(0, 0, 400, 200)) diff --git a/BlocksScreen/configfile.py b/BlocksScreen/configfile.py index 53ad0579..981ac4b2 100644 --- a/BlocksScreen/configfile.py +++ b/BlocksScreen/configfile.py @@ -47,10 +47,14 @@ class Sentinel(enum.Enum): + """Sentinel value to signify missing condition, absence of value""" + MISSING = object class ConfigError(Exception): + """Exception raised when Configfile errors exist""" + def __init__(self, msg) -> None: super().__init__(msg) self.msg = msg @@ -59,24 +63,10 @@ def __init__(self, msg) -> None: class BlocksScreenConfig: config = configparser.ConfigParser( allow_no_value=True, - # interpolation=configparser.ExtendedInterpolation(), - # delimiters=(":"), - # inline_comment_prefixes=("#"), - # comment_prefixes=("#", "#~#"), - # empty_lines_in_values=True, ) - update_pending: bool = False - _instance = None - # def __new__( - # cls, *args, **kwargs - # ) -> BlocksScreenConfig: # Singleton pattern - # if not cls._instance: - # cls._instance = super(BlocksScreenConfig, cls).__new__(cls) - # return cls._instance - def __init__( self, configfile: typing.Union[str, pathlib.Path], section: str ) -> None: @@ -93,24 +83,41 @@ def __contains__(self, key): return key in self.config def sections(self) -> typing.List[str]: + """Returns list of all sections""" return self.config.sections() def get_section( self, section: str, fallback: typing.Optional[T] = None ) -> BlocksScreenConfig: + """Get configfile section""" if not self.config.has_section(section): - raise configparser.NoSectionError( - f"No section with name: {section}" - ) + raise configparser.NoSectionError(f"No section with name: {section}") return BlocksScreenConfig(self.configfile, section) def get_options(self) -> list: + """Get section options""" return self.config.options(self.section) def has_section(self, section: str) -> bool: + """Check if config file has a section + + Args: + section (str): section name + + Returns: + bool: true if section exists, false otherwise + """ return bool(self.config.has_section(section)) def has_option(self, option: str) -> bool: + """Check if section has a option + + Args: + option (str): option name + + Returns: + bool: true if section exists, false otherwise + """ return bool(self.config.has_option(self.section, option)) def get( @@ -119,10 +126,18 @@ def get( parser: type = str, default: typing.Union[Sentinel, str, T] = Sentinel.MISSING, ) -> typing.Union[Sentinel, str]: + """Get option value + + Args: + option (str): option name + parser (type, optional): bool, float, int. Defaults to str. + default (typing.Union[Sentinel, str, T], optional): Default value for specified option. Defaults to Sentinel.MISSING. + + Returns: + typing.Union[Sentinel, str]: Requested option. Defaults to the specified default value + """ return parser( - self.config.get( - section=self.section, option=option, fallback=default - ) + self.config.get(section=self.section, option=option, fallback=default) ) def getint( @@ -130,15 +145,31 @@ def getint( option: str, default: typing.Union[Sentinel, int] = Sentinel.MISSING, ) -> typing.Union[Sentinel, int]: - return self.config.getint( - section=self.section, option=option, fallback=default - ) + """Get option value + + Args: + option (str): option name + default (typing.Union[Sentinel, int], optional): Default value for specified option. Defaults to Sentinel.MISSING. + + Returns: + typing.Union[Sentinel, int]: Requested option. + """ + return self.config.getint(section=self.section, option=option, fallback=default) def getfloat( self, option: str, default: typing.Union[Sentinel, float] = Sentinel.MISSING, ) -> typing.Union[Sentinel, float]: + """Get the value for the specified option + + Args: + option (str): option name + default (typing.Union[Sentinel, float], optional): Default value for specified option. Defaults to Sentinel.MISSING. + + Returns: + typing.Union[Sentinel, float]: _description_ + """ return self.config.getfloat( section=self.section, option=option, fallback=default ) @@ -148,6 +179,15 @@ def getboolean( option: str, default: typing.Union[Sentinel, bool] = Sentinel.MISSING, ) -> typing.Union[Sentinel, bool]: + """Get option value + + Args: + option (str): option name + default (typing.Union[Sentinel, bool], optional): Default value for specified option. Defaults to Sentinel.MISSING. + + Returns: + typing.Union[Sentinel, bool]: _description_ + """ return self.config.getboolean( section=self.section, option=option, fallback=default ) @@ -156,9 +196,7 @@ def _find_section_index(self, section: str) -> int: try: return self.raw_config.index("[" + section + "]") except ValueError as e: - raise configparser.Error( - f'Section "{section}" does not exist: {e}' - ) + raise configparser.Error(f'Section "{section}" does not exist: {e}') def _find_section_limits(self, section: str) -> typing.Tuple: try: @@ -189,6 +227,14 @@ def _find_option_index( ) def add_section(self, section: str) -> None: + """Add a section to configuration file + + Args: + section (str): section name + + Raises: + configparser.DuplicateSectionError: Exception thrown when section is duplicated + """ try: with self.file_lock: sec_string = f"[{section}]" @@ -209,9 +255,7 @@ def add_section(self, section: str) -> None: except configparser.DuplicateSectionError as e: logging.error(f'Section "{section}" already exists. {e}') except configparser.Error as e: - logging.error( - f'Unable to add "{section}" section to configuration: {e}' - ) + logging.error(f'Unable to add "{section}" section to configuration: {e}') def add_option( self, @@ -219,6 +263,13 @@ def add_option( option: str, value: typing.Union[str, None] = None, ) -> None: + """Add option with a value to a section + + Args: + section (str): section name + option (str): option name + value (typing.Union[str, None], optional): value for the specified option. Defaults to None. + """ try: with self.file_lock: section_start, section_end = self._find_section_limits(section) @@ -239,13 +290,12 @@ def add_option( ) def save_configuration(self) -> None: + """Save teh configuration to file""" try: if not self.update_pending: return with self.file_lock: - self.configfile.write_text( - "\n".join(self.raw_config), encoding="utf-8" - ) + self.configfile.write_text("\n".join(self.raw_config), encoding="utf-8") sio = io.StringIO() sio.writelines(self.raw_config) self.config.write(sio) @@ -257,13 +307,8 @@ def save_configuration(self) -> None: finally: self.update_pending = False - def _do_save(self, data) -> bool: - try: - return True - except Exception as e: - return False - def load_config(self): + """Load configuration file""" try: self.raw_config.clear() self.config.clear() # Reset configparser @@ -330,6 +375,7 @@ def _parse_file(self) -> typing.Tuple[typing.List[str], typing.Dict]: def get_configparser() -> BlocksScreenConfig: + """Loads configuration from file and returns that configuration""" wanted_target = os.path.join(DEFAULT_CONFIGFILE_PATH, "BlocksScreen.cfg") fallback = os.path.join(WORKING_DIR, "BlocksScreen.cfg") configfile = ( @@ -337,13 +383,9 @@ def get_configparser() -> BlocksScreenConfig: if check_file_on_path(DEFAULT_CONFIGFILE_PATH, "BlocksScreen.cfg") else fallback ) - try: - config_object = BlocksScreenConfig( - configfile=configfile, section="server" - ) - config_object.load_config() - if not config_object.has_section("server"): - raise ConfigError("Section [server] is missing from configuration") - except ConfigError: + config_object = BlocksScreenConfig(configfile=configfile, section="server") + config_object.load_config() + if not config_object.has_section("server"): logging.error("Error loading configuration file for the application.") + raise ConfigError("Section [server] is missing from configuration") return BlocksScreenConfig(configfile=configfile, section="server") diff --git a/BlocksScreen/events.py b/BlocksScreen/events.py index 05c5d28f..20a870e4 100644 --- a/BlocksScreen/events.py +++ b/BlocksScreen/events.py @@ -1,3 +1,5 @@ +"""Collection of all custom events used by the application""" + import typing from PyQt6.QtCore import QEvent @@ -21,6 +23,7 @@ def __init__(self, data, *args, **kwargs): @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(WebSocketConnecting.WebsocketConnectingEvent) @@ -48,9 +51,8 @@ def __init__( @staticmethod def type() -> QEvent.Type: - return QEvent.Type( - WebSocketMessageReceived.WebsocketMessageReceivedEvent - ) + """Return event type""" + return QEvent.Type(WebSocketMessageReceived.WebsocketMessageReceivedEvent) class WebSocketOpen(QEvent): @@ -70,6 +72,7 @@ def __init__(self, data, *args, **kwargs): @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(WebSocketOpen.WebsocketOpenEvent) @@ -83,15 +86,14 @@ class WebSocketError(QEvent): WebsocketErrorEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(WebSocketError, self).__init__( - WebSocketError.WebsocketErrorEvent - ) + super(WebSocketError, self).__init__(WebSocketError.WebsocketErrorEvent) self.data = data self.args = args self.kwargs = kwargs @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(WebSocketError.WebsocketErrorEvent) @@ -114,6 +116,7 @@ def __init__(self, data, *args, **kwargs): @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(WebSocketDisconnected.WebsocketDisconnectedEvent) @@ -128,15 +131,14 @@ class WebSocketClose(QEvent): WebsocketCloseEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(WebSocketClose, self).__init__( - WebSocketClose.WebsocketCloseEvent - ) + super(WebSocketClose, self).__init__(WebSocketClose.WebsocketCloseEvent) self.data = data self.args = args self.kwargs = kwargs @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(WebSocketClose.WebsocketCloseEvent) @@ -151,15 +153,14 @@ class KlippyShutdown(QEvent): def __init__(self, data, *args, **kwargs): QEvent.__instancecheck__(self) - super(KlippyShutdown, self).__init__( - KlippyShutdown.KlippyShutdownEvent - ) + super(KlippyShutdown, self).__init__(KlippyShutdown.KlippyShutdownEvent) self.data = data self.args = args self.kwargs = kwargs @staticmethod def type() -> QEvent.Type: + """Return event type""" return KlippyShutdown.KlippyShutdownEvent # def __instancecheck__(self, instance: Any) -> bool: @@ -186,6 +187,7 @@ def __init__(self, data, *args, **kwargs): @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(KlippyReady.KlippyReadyEvent) @@ -208,6 +210,7 @@ def __init__(self, data, *args, **kwargs): @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(KlippyDisconnected.KlippyDisconnectedEvent) @@ -227,6 +230,7 @@ def __init__(self, data, message, *args, **kwargs): @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(KlippyError.KlippyErrorEvent) @@ -244,9 +248,7 @@ class ReceivedFileData(QEvent): def __init__( self, data, method, params, /, *args, **kwargs ): # Positional-only arguments "data", "method", "params", these need to be inserted in order or it wont work - super(ReceivedFileData, self).__init__( - ReceivedFileData.ReceivedFileDataEvent - ) + super(ReceivedFileData, self).__init__(ReceivedFileData.ReceivedFileDataEvent) self.data = data self.method = method self.params = params @@ -255,6 +257,7 @@ def __init__( @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(ReceivedFileData.ReceivedFileDataEvent) @@ -276,6 +279,7 @@ def __init__(self, filename, *args, **kwargs): @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(PrintStart.PrintStartEvent) @@ -296,6 +300,7 @@ def __init__(self, data, *args, **kwargs): @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(PrintComplete.PrintCompleteEvent) @@ -317,6 +322,7 @@ def __init__(self, data, *args, **kwargs): @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(PrintPause.PrintPauseEvent) @@ -338,6 +344,7 @@ def __init__(self, data, *args, **kwargs): @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(PrintResume.PrintResumeEvent) @@ -351,9 +358,7 @@ class PrintCancelled(QEvent): PrintCancelledEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(PrintCancelled, self).__init__( - PrintCancelled.PrintCancelledEvent - ) + super(PrintCancelled, self).__init__(PrintCancelled.PrintCancelledEvent) self.data = data self.args = args @@ -361,6 +366,7 @@ def __init__(self, data, *args, **kwargs): @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(PrintCancelled.PrintCancelledEvent) @@ -381,6 +387,7 @@ def __init__(self, data, *args, **kwargs): @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(PrintError.PrintErrorEvent) @@ -401,6 +408,7 @@ def __init__(self, data, *args, **kwargs): @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(NetworkAdded.NetworkAddedEvent) @@ -414,15 +422,14 @@ class NetworkDeleted(QEvent): NetworkDeletedEvent = QEvent.Type(QEvent.registerEventType()) def __init__(self, data, *args, **kwargs): - super(NetworkDeleted, self).__init__( - NetworkDeleted.NetworkDeletedEvent - ) + super(NetworkDeleted, self).__init__(NetworkDeleted.NetworkDeletedEvent) self.data = data self.args = args self.kwargs = kwargs @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(NetworkDeleted) @@ -443,4 +450,5 @@ def __init__(self, data, *args, **kwargs): @staticmethod def type() -> QEvent.Type: + """Return event type""" return QEvent.Type(NetworkScan.NetworkScanEvent) diff --git a/BlocksScreen/helper_methods.py b/BlocksScreen/helper_methods.py index b086c751..0dd2b2db 100644 --- a/BlocksScreen/helper_methods.py +++ b/BlocksScreen/helper_methods.py @@ -1,7 +1,9 @@ +# Collection of useful methods +# # This file contains some methods derived from KlipperScreen # Original source: https://github.com/KlipperScreen/KlipperScreen # License: GNU General Public License v3 -# Modifications made by Hugo Costa (2025) for BlocksScreen +# Modifications made by Hugo Costa (2025) for BlocksScreen import ctypes @@ -18,6 +20,8 @@ libxext = ctypes.CDLL("libXext.so.6") class DPMSState(enum.Enum): + """Available DPMS states""" + FAIL = -1 ON = 0 STANDBY = 1 @@ -35,6 +39,7 @@ class DPMSState(enum.Enum): libxext.DPMSForceLevel.restype = ctypes.c_int def get_dpms_state(): + """Gets and returns DPMS state""" _dpms_state = DPMSState.FAIL _display_name = ctypes.c_char_p(b":0") libxext.XOpenDisplay.restype = ctypes.c_void_p @@ -59,6 +64,11 @@ def get_dpms_state(): return _dpms_state def set_dpms_mode(mode: DPMSState) -> None: + """Set DPMS state + + Args: + mode (DPMSState): State to set DPMS. Check available state on `DPMSState` + """ _display_name = ctypes.c_char_p(b":0") libxext.XOpenDisplay.restype = ctypes.c_void_p display = ctypes.c_void_p( @@ -76,6 +86,7 @@ def set_dpms_mode(mode: DPMSState) -> None: libxext.XCloseDisplay(display) def get_dpms_timeouts() -> typing.Dict: + """Get current DPMS timeouts""" _display_name = ctypes.c_char_p(b":0") libxext.XOpenDisplay.restype = ctypes.c_void_p display = ctypes.c_void_p( @@ -93,9 +104,7 @@ def get_dpms_timeouts() -> typing.Dict: suspend_p = ctypes.create_string_buffer(2) off_p = ctypes.create_string_buffer(2) - if libxext.DPMSGetTimeouts( - display, standby_p, suspend_p, off_p - ): + if libxext.DPMSGetTimeouts(display, standby_p, suspend_p, off_p): _standby_timeout = struct.unpack("H", standby_p.raw)[0] _suspend_timeout = struct.unpack("H", suspend_p.raw)[0] _off_timeout = struct.unpack("H", off_p.raw)[0] @@ -111,6 +120,7 @@ def get_dpms_timeouts() -> typing.Dict: def set_dpms_timeouts( suspend: int = 0, standby: int = 0, off: int = 0 ) -> typing.Dict: + """Set DPMS timeout""" _display_name = ctypes.c_char_p(b":0") libxext.XOpenDisplay.restype = ctypes.c_void_p display = ctypes.c_void_p( @@ -131,9 +141,7 @@ def set_dpms_timeouts( suspend_p = ctypes.create_string_buffer(2) off_p = ctypes.create_string_buffer(2) - if libxext.DPMSGetTimeouts( - display, standby_p, suspend_p, off_p - ): + if libxext.DPMSGetTimeouts(display, standby_p, suspend_p, off_p): _standby_timeout = struct.unpack("H", standby_p.raw)[0] _suspend_timeout = struct.unpack("H", suspend_p.raw)[0] _off_timeout = struct.unpack("H", off_p.raw)[0] @@ -147,6 +155,11 @@ def set_dpms_timeouts( } def get_dpms_info() -> typing.Dict: + """Get DPMS information + + Returns: + typing.Dict: Dpms state + """ _dpms_state = DPMSState.FAIL onoff = 0 _display_name = ctypes.c_char_p(b":0") @@ -176,6 +189,12 @@ def get_dpms_info() -> typing.Dict: return {"power_level": onoff, "state": DPMSState(_dpms_state)} def check_dpms_capable(display: int): + """Check if device has DPMS + + Args: + display (int): Display index + + """ _display_name = ctypes.c_char_p(b":%d" % (display)) libxext.XOpenDisplay.restype = ctypes.c_void_p @@ -198,6 +217,7 @@ def check_dpms_capable(display: int): return _capable def disable_dpms() -> None: + """Disable DPMS""" set_dpms_mode(DPMSState.OFF) except OSError as e: @@ -255,6 +275,7 @@ def estimate_print_time(seconds: int) -> list: def normalize(value, r_min=0.0, r_max=1.0, t_min=0.0, t_max=100): + """Normalize values between a rage""" # https://stats.stackexchange.com/questions/281162/scale-a-number-between-a-range c1 = (value - r_min) / (r_max - r_min) c2 = (t_max - t_min) + t_min @@ -290,6 +311,7 @@ def check_filepath_permission(filepath, access_type: int = os.R_OK) -> bool: def check_dir_existence( directory: typing.Union[str, pathlib.Path], ) -> bool: + """Check if a directory exists. Returns a true if it exists""" if isinstance(directory, pathlib.Path): return bool(directory.is_dir()) return bool(os.path.isdir(directory)) @@ -299,20 +321,6 @@ def check_file_on_path( path: typing.Union[typing.LiteralString, pathlib.Path], filename: typing.Union[typing.LiteralString, pathlib.Path], ) -> bool: + """Check if file exists on path. Returns true if file exists on that specified directory""" _filepath = os.path.join(path, filename) return os.path.exists(_filepath) - - -def get_file_loc(filename) -> pathlib.Path: - ... - - -# def get_hash(data) -> hashlib._Hash: -# hash = hashlib.sha256() -# hash.update(data.encode()) -# hash.digest() -# return hash - - - -def digest_hash() -> None: ... diff --git a/BlocksScreen/lib/async_network_monitor.py b/BlocksScreen/lib/async_network_monitor.py deleted file mode 100644 index 6063be30..00000000 --- a/BlocksScreen/lib/async_network_monitor.py +++ /dev/null @@ -1,157 +0,0 @@ -import asyncio -import logging -import threading -import typing - -import sdbus -from PyQt6 import QtCore -from sdbus_async import networkmanager - - -class SdbusNMMonitor(QtCore.QObject): - state_change: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, name="nm-state-changed" - ) - prop_changed: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - name="nm-properties-changed" - ) - added_conn: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, name="nm-conn-added" - ) - rem_con: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, name="nm-conn-added" - ) - - def __init__(self) -> None: - super().__init__() - - self._running: bool = False # control - # Run on separate thread - self.thread: threading.Thread = threading.Thread( - name="asyncio.NMonitor.run_forever", - target=self._run_loop, - ) - self.thread.daemon = False # Do not exit the thread - - # Create a new asyncio loop - self.loop = asyncio.new_event_loop() - - # Asyncio Event - self.stop_event = asyncio.Event() - self.stop_event.clear() - # open asd set system sdbus - self.system_dbus = sdbus.sd_bus_open_system() - if not self.system_dbus: - logging.info("No dbus found, async network monitor exiting") - del self - return - sdbus.set_default_bus(self.system_dbus) - - # Instantiate NetworkManager - self.nm = networkmanager.NetworkManager() - - # Start thread - self.thread.start() - - if self.thread.is_alive(): - logging.info( - f"Sdbus NetworkManager Monitor Thread {self.thread.name} Running" - ) - - def close(self) -> None: - self._running = False - if hasattr(self, "state_listener_task"): - self.state_listener_task.cancel() - if hasattr(self, "added_ap_listener_task"): - self.added_ap_listener_task.cancel() - if hasattr(self, "rem_ap_listener_task"): - self.rem_ap_listener_task.cancel() - if hasattr(self, "prop_changed_listener_task"): - self.prop_changed_listener_task.cancel() - try: - self.loop.run_until_complete(self.state_listener_task) - self.loop.run_until_complete(self.added_ap_listener_task) - self.loop.run_until_complete(self.rem_ap_listener_task) - except asyncio.CancelledError as e: - logging.error(f"Exception while cancelling {e}") - self.stop_event.set() - self.loop.call_soon_threadsafe(self.stop_event.set) - self.loop.close() - self.thread.join() - - def _run_loop(self) -> None: - try: - asyncio.set_event_loop(self.loop) - self.loop.run_until_complete(asyncio.gather(self.monitor())) - except Exception as e: - logging.error(f"Exception on loop coroutine: {e}") - - async def monitor(self) -> None: - try: - self._running = True - self.state_listener_task = self.loop.create_task( - self._state_change_listener() - ) - self.added_ap_listener_task = self.loop.create_task( - self._access_added_listener() - ) - self.rem_ap_listener_task = self.loop.create_task( - self._access_rem_listener() - ) - self.prop_changed_listener_task = self.loop.create_task( - self._properties_changed_listener() - ) - await ( - self.stop_event.wait() - ) # Wait until .set() is done on self.stop_event - except Exception as e: - logging.error(f"Exception on monitor coroutine: {e}") - - async def _state_change_listener(self) -> None: - while self._running: - try: - logging.debug( - "Listening coroutine for NetworkManager state signal..." - ) - async for state in self.nm.state_changed: - enum_state = networkmanager.NetworkManagerState(state) - logging.debug( - f"NM State Changed: {enum_state.name} ({state})" - ) - self.state_change.emit(state) - except Exception as e: - logging.error(f"Exception on NM state listener: {e}") - - async def _properties_changed_listener(self) -> None: - while self._running: - try: - logging.debug( - "Listening coroutine for NetworkManager state signal..." - ) - async for state in self.nm.properties_changed: - enum_state = networkmanager.NetworkManagerState(state) - logging.debug( - f"NM State Changed: {enum_state.name} ({state})" - ) - self.state_change.emit(state) - except Exception as e: - logging.error(f"Exception on NM state listener: {e}") - - async def _access_added_listener(self) -> None: - while self._running: - try: - logging.debug("Listening coroutine for added access points") - async for ac in self.nm.device_added: - logging.debug(f"Signal for device added received {ac}") - self.added_conn.emit(ac) - except Exception as e: - logging.error(f"Error for added access points listener: {e}") - - async def _access_rem_listener(self) -> None: - while self._running: - try: - logging.debug("Listening coroutine for removed access points") - async for ac in self.nm.device_removed: - self.rem_con.emit(ac) - except Exception as e: - logging.error(f"Error for removed access points listener: {e}") diff --git a/BlocksScreen/lib/filament.py b/BlocksScreen/lib/filament.py index cafb053e..cb4c0232 100644 --- a/BlocksScreen/lib/filament.py +++ b/BlocksScreen/lib/filament.py @@ -1,20 +1,23 @@ -from typing import Optional - +# Class that represents a filament spool +from typing import Optional import enum -# typing.Optional[type()] == typing.Union[type(), None] - - class Filament: + """Filament spool""" + class SpoolBaseWeights(enum.Enum): # XXX This enum will probably be unnecessary + """Spool base weights""" + MINI = 750 BASE = 1000 BIG = 3000 JUMBO = 5000 class SpoolMaterial(enum.Flag): + """Spool material types""" + PLASTIC = enum.auto() PAPER = enum.auto() UNKNOWN = -1 @@ -80,10 +83,4 @@ def spool_type(self, new): raise ValueError( "Spool Material type is invalid" ) # Correct type but invalid option - else: - raise TypeError("") # TODO: Finish this type raise self._spool_type = new - - def calc_remaining_weight(self): ... # TODO calculate remaining spool weight - - def calc_initial_weight(self): ... # TODO calculate initial spool weight diff --git a/BlocksScreen/lib/files.py b/BlocksScreen/lib/files.py index 02849191..68a399c2 100644 --- a/BlocksScreen/lib/files.py +++ b/BlocksScreen/lib/files.py @@ -1,3 +1,6 @@ +# +# Gcode File manager +# from __future__ import annotations import os @@ -15,9 +18,7 @@ class Files(QtCore.QObject): [], [str], [str, bool], name="api-get-dir-info" ) request_file_metadata = QtCore.pyqtSignal([str], name="get_file_metadata") - request_files_thumbnails = QtCore.pyqtSignal( - [str], name="request_files_thumbnail" - ) + request_files_thumbnails = QtCore.pyqtSignal([str], name="request_files_thumbnail") request_file_download = QtCore.pyqtSignal([str, str], name="file_download") on_dirs: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( list, name="on-dirs" @@ -44,16 +45,10 @@ def __init__( self.request_file_list.connect(slot=self.ws.api.get_file_list) self.request_file_list[str].connect(slot=self.ws.api.get_file_list) self.request_dir_info.connect(slot=self.ws.api.get_dir_information) - self.request_dir_info[str, bool].connect( - self.ws.api.get_dir_information - ) - self.request_dir_info[str].connect( - slot=self.ws.api.get_dir_information - ) + self.request_dir_info[str, bool].connect(self.ws.api.get_dir_information) + self.request_dir_info[str].connect(slot=self.ws.api.get_dir_information) self.request_file_metadata.connect(slot=self.ws.api.get_gcode_metadata) - self.request_files_thumbnails.connect( - slot=self.ws.api.get_gcode_thumbnail - ) + self.request_files_thumbnails.connect(slot=self.ws.api.get_gcode_thumbnail) self.request_file_download.connect(slot=self.ws.api.download_file) QtWidgets.QApplication.instance().installEventFilter(self) # type: ignore @@ -62,15 +57,13 @@ def file_list(self): return self.files def handle_message_received(self, method: str, data, params: dict) -> None: + """Handle file related messages received by moonraker""" if "server.files.list" in method: # Get all files in root and its subdirectories and # request their metadata self.files.clear() self.files = data - [ - self.request_file_metadata.emit(item["path"]) - for item in self.files - ] + [self.request_file_metadata.emit(item["path"]) for item in self.files] elif "server.files.metadata" in method: if data["filename"] in self.files_metadata.keys(): if not data.get("filename", None): @@ -89,8 +82,11 @@ def handle_message_received(self, method: str, data, params: dict) -> None: @QtCore.pyqtSlot(str, name="on_request_fileinfo") def on_request_fileinfo(self, filename: str) -> None: - # if not filename: - # return + """Requests metadata for a file + + Args: + filename (str): file + """ _data: dict = { "thumbnail_images": list, "filament_total": dict, @@ -122,34 +118,18 @@ def on_request_fileinfo(self, filename: str) -> None: _thumbnails, ) ) - _thumbnail_images = list( - map(lambda path: QtGui.QImage(path), _thumbnail_paths) - ) + _thumbnail_images = list(map(lambda path: QtGui.QImage(path), _thumbnail_paths)) _data.update({"thumbnail_images": _thumbnail_images}) - _data.update( - {"filament_total": _file_metadata.get("filament_total", "?")} - ) - _data.update( - {"estimated_time": _file_metadata.get("estimated_time", 0)} - ) + _data.update({"filament_total": _file_metadata.get("filament_total", "?")}) + _data.update({"estimated_time": _file_metadata.get("estimated_time", 0)}) _data.update({"layer_count": _file_metadata.get("layer_count", -1.0)}) _data.update({"total_layer": _file_metadata.get("total_layer", -1.0)}) + _data.update({"object_height": _file_metadata.get("object_height", -1.0)}) + _data.update({"nozzle_diameter": _file_metadata.get("nozzle_diameter", -1.0)}) + _data.update({"layer_height": _file_metadata.get("layer_height", -1.0)}) _data.update( - {"object_height": _file_metadata.get("object_height", -1.0)} - ) - _data.update( - {"nozzle_diameter": _file_metadata.get("nozzle_diameter", -1.0)} - ) - _data.update( - {"layer_height": _file_metadata.get("layer_height", -1.0)} - ) - _data.update( - { - "first_layer_height": _file_metadata.get( - "first_layer_height", -1.0 - ) - } + {"first_layer_height": _file_metadata.get("first_layer_height", -1.0)} ) _data.update( { @@ -159,37 +139,24 @@ def on_request_fileinfo(self, filename: str) -> None: } ) _data.update( - { - "first_layer_bed_temp": _file_metadata.get( - "first_layer_bed_temp", -1.0 - ) - } - ) - _data.update( - {"chamber_temp": _file_metadata.get("chamber_temp", -1.0)} + {"first_layer_bed_temp": _file_metadata.get("first_layer_bed_temp", -1.0)} ) + _data.update({"chamber_temp": _file_metadata.get("chamber_temp", -1.0)}) + _data.update({"filament_name": _file_metadata.get("filament_name", -1.0)}) + _data.update({"filament_type": _file_metadata.get("filament_type", -1.0)}) _data.update( - {"filament_name": _file_metadata.get("filament_name", -1.0)} - ) - _data.update( - {"filament_type": _file_metadata.get("filament_type", -1.0)} - ) - _data.update( - { - "filament_weight_total": _file_metadata.get( - "filament_weight_total", -1.0 - ) - } + {"filament_weight_total": _file_metadata.get("filament_weight_total", -1.0)} ) _data.update({"slicer": _file_metadata.get("slicer", -1.0)}) self.fileinfo.emit(_data) def eventFilter(self, a0: QtCore.QObject, a1: QtCore.QEvent) -> bool: + """Filter Klippy related events""" if a1.type() == events.KlippyDisconnected.type(): self.files_metadata.clear() self.files.clear() return False - elif a1.type() == events.KlippyReady.type(): + if a1.type() == events.KlippyReady.type(): # Request all files including in subdirectories # in order to get all metadata self.request_file_list.emit() @@ -199,6 +166,7 @@ def eventFilter(self, a0: QtCore.QObject, a1: QtCore.QEvent) -> bool: return super().eventFilter(a0, a1) def event(self, a0: QtCore.QEvent) -> bool: + """Filter ReceivedFileData event""" if a0.type() == ReceivedFileData.type(): if isinstance(a0, ReceivedFileData): self.handle_message_received(a0.method, a0.data, a0.params) diff --git a/BlocksScreen/lib/machine.py b/BlocksScreen/lib/machine.py index 6258a94b..e1c4a0ca 100644 --- a/BlocksScreen/lib/machine.py +++ b/BlocksScreen/lib/machine.py @@ -1,45 +1,55 @@ +# +# Machine manager +# import logging -import subprocess +import shlex +import subprocess # nosec: B404 import typing -from PyQt6.QtCore import QObject, pyqtSignal, pyqtSlot +from PyQt6 import QtCore -class MachineControl(QObject): - service_restart = pyqtSignal(str, name="service-restart") +class MachineControl(QtCore.QObject): + service_restart = QtCore.pyqtSignal(str, name="service-restart") - def __init__(self, parent: typing.Optional["QObject"]) -> None: + def __init__(self, parent: typing.Optional["QtCore.QObject"]) -> None: super(MachineControl, self).__init__(parent) self.setObjectName("MachineControl") - - @pyqtSlot(name="machine_restart") + + @QtCore.pyqtSlot(name="machine_restart") def machine_restart(self): + """Reboot machine""" return self._run_command("sudo reboot now") - @pyqtSlot(name="machine_shutdown") + @QtCore.pyqtSlot(name="machine_shutdown") def machine_shutdown(self): + """Shutdown machine""" return self._run_command("sudo shutdown now") - @pyqtSlot(name="restart_klipper_service") + @QtCore.pyqtSlot(name="restart_klipper_service") def restart_klipper_service(self): - # self.service_restart.emit("restart-klipper-service") + """Restart klipper service""" return self._run_command("sudo systemctl stop klipper.service") - - @pyqtSlot(name="restart_moonraker_service") + + @QtCore.pyqtSlot(name="restart_moonraker_service") def restart_moonraker_service(self): - # self.service_restart.emit("restart-moonraker-service") + """Restart moonraker service""" return self._run_command("sudo systemctl restart moonraker.service") - def restart_bo_service(self): - # TODO: Restart Blocks Screen service, implement it later on - pass - def check_service_state(self, service_name: str): + """Check service status + + Args: + service_name (str): service name + + Returns: + _type_: output of the command `systemctl is-active ` + """ if service_name is None: return None return self._run_command(f"systemctl is-active {service_name}") - - def _run_command(self, command): + + def _run_command(self, command: str): """Runs a shell command. Args: @@ -50,18 +60,26 @@ def _run_command(self, command): """ try: - # REVIEW: Safe way to run bash commands - # * Old way, it didn't let me use grep commands or use | on the command - # cmd = shlex.split(command,posix=False) - # exec = cmd[0] - # exec_options = cmd[1:] - # output = subprocess.run( - # ([exec] + exec_options), capture_output=True) - # TEST: is this safe to use like this, or is it susceptible to attacks and stuff - p = subprocess.Popen( - command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT + # Split command into a list of strings + cmd = shlex.split(command) + p = subprocess.run( # nosec: B603 + cmd, check=True, capture_output=True, text=True, timeout=5 + ) + return p.stdout.strip() + "\n" + p.stderr.strip() + except ValueError as e: + logging.error("Failed to parse command string '%s': '%s'", command, e) + raise RuntimeError(f"Invalid command format: {e}") from e + except subprocess.CalledProcessError as e: + logging.error( + "Caught exception (exit code %d) failed to run command: %s \nStderr: %s", + e.returncode, + command, + e.stderr.strip(), ) - output, e = p.communicate() - return output - except subprocess.SubprocessError: - logging.error("Error running commas : %s", command) + raise + except ( + subprocess.SubprocessError, + subprocess.TimeoutExpired, + FileNotFoundError, + ): + logging.error("Caught exception failed to run command %s", command) diff --git a/BlocksScreen/lib/moonrakerComm.py b/BlocksScreen/lib/moonrakerComm.py index 3519cf01..b2f41446 100644 --- a/BlocksScreen/lib/moonrakerComm.py +++ b/BlocksScreen/lib/moonrakerComm.py @@ -1,11 +1,10 @@ +# Moonraker api import json import logging import threading import websocket from events import ( - KlippyDisconnected, - KlippyShutdown, WebSocketDisconnected, WebSocketError, WebSocketMessageReceived, @@ -17,11 +16,6 @@ _logger = logging.getLogger(name="logs/BlocksScreen.log") -RED = "\033[31m" -GREEN = "\033[32m" -YELLOW = "\033[33m" -RESET = "\033[0m" - class OneShotTokenError(Exception): """Raised when unable to get oneshot token to connect to a websocket""" @@ -77,17 +71,20 @@ def __init__(self, parent: QtCore.QObject) -> None: @QtCore.pyqtSlot(name="retry_wb_conn") def retry_wb_conn(self): + """Retry websocket connection""" if self.connecting is True and self.connected is False: return False self._reconnect_count = 0 self.try_connection() def try_connection(self): + """Try connecting to websocket""" self.connecting = True self._retry_timer = RepeatedTimer(self.timeout, self.reconnect) return self.connect() def reconnect(self): + """Reconnect to websocket""" if self.connected: return True @@ -115,6 +112,7 @@ def reconnect(self): return self.connect() def connect(self) -> bool: + """Connect to websocket""" if self.connected: _logger.info("Connection established") return True @@ -320,7 +318,6 @@ def send_request(self, method: str, params: dict = {}) -> bool: return False self._request_id += 1 - # REVIEW: This data structure could be better, think about other implementations self.request_table[self._request_id] = [method, params] packet = { "jsonrpc": "2.0", @@ -331,18 +328,6 @@ def send_request(self, method: str, params: dict = {}) -> bool: self.ws.send(json.dumps(packet)) return True - # def customEvent(self, event: QtCore.QEvent | None) -> None: - # if not event: - # return - - # if ( - # event.type() == KlippyDisconnected.type() - # or event.type() == KlippyShutdown.type() - # ): - # # * Received notify_klippy_disconnected, start querying server information again to check if klipper is available - # self.evaluate_klippy_status() - # return super().customEvent(event) - class MoonAPI(QtCore.QObject): def __init__(self, ws: MoonWebSocket): @@ -351,12 +336,13 @@ def __init__(self, ws: MoonWebSocket): @QtCore.pyqtSlot(name="api_query_server_info") def api_query_server_info(self): - _logger.debug("Requested server.info") + """Query server information""" return self._ws.send_request(method="server.info") def identify_connection( self, client_name, version, type, url, access_token, api_key ): + """Request moonraker to identify connection""" return self._ws.send_request( method="server.connection.identify", params={ @@ -370,6 +356,7 @@ def identify_connection( ) def request_temperature_cached_data(self, include_monitors: bool = False): + """Request stored temperature monitors""" return self._ws.send_request( method="server.temperature_store", params={"include_monitors": include_monitors}, @@ -377,30 +364,36 @@ def request_temperature_cached_data(self, include_monitors: bool = False): @QtCore.pyqtSlot(name="query_printer_info") def request_printer_info(self): + """Requested printer information""" return self._ws.send_request(method="printer.info") @QtCore.pyqtSlot(name="get_available_objects") def get_available_objects(self): + """Request available printer objects""" return self._ws.send_request(method="printer.objects.list") @QtCore.pyqtSlot(dict, name="query_object") def object_query(self, objects: dict): + """Query printer object""" return self._ws.send_request( method="printer.objects.query", params={"objects": objects} ) @QtCore.pyqtSlot(dict, name="object_subscription") def object_subscription(self, objects: dict): + """Subscribe to printer object""" return self._ws.send_request( method="printer.objects.subscribe", params={"objects": objects} ) @QtCore.pyqtSlot(name="ws_query_endstops") def query_endstops(self): + """Query printer endstops""" return self._ws.send_request(method="printer.query_endstops.status") @QtCore.pyqtSlot(str, name="run_gcode") def run_gcode(self, gcode: str): + """Run Gcode""" if isinstance(gcode, str) is False or gcode is None: return False return self._ws.send_request( @@ -408,36 +401,45 @@ def run_gcode(self, gcode: str): ) def gcode_help(self): + """Request Gcode information""" return self._ws.send_request(method="printer.gcode.help") @QtCore.pyqtSlot(str, name="start_print") def start_print(self, filename): + """Start print job""" return self._ws.send_request( method="printer.print.start", params={"filename": filename} ) @QtCore.pyqtSlot(name="pause_print") def pause_print(self): + """Pause print job""" return self._ws.send_request(method="printer.print.pause") @QtCore.pyqtSlot(name="resume_print") def resume_print(self): + """Resume print job""" return self._ws.send_request(method="printer.print.resume") @QtCore.pyqtSlot(name="stop_print") def cancel_print(self): + """Cancel print job""" return self._ws.send_request(method="printer.print.cancel") - def machine_system(self): + def machine_shutdown(self): + """Request machine shutdown""" return self._ws.send_request(method="machine.shutdown") def machine_reboot(self): + """Request machine reboot""" return self._ws.send_request(method="machine.reboot") def restart_server(self): + """Request server restart""" return self._ws.send_request(method="server.restart") def restart_service(self, service): + """Request service restart""" if service is None or isinstance(service, str) is False: return False return self._ws.send_request( @@ -446,21 +448,16 @@ def restart_service(self, service): @QtCore.pyqtSlot(name="firmware_restart") def firmware_restart(self): - """firmware_restart + """Request Klipper firmware restart HTTP_REQUEST: POST /printer/firmware_restart JSON_RPC_REQUEST: printer.firmware_restart - Returns: - _type_: _description_ """ - # REVIEW: Whether i should send a websocket request or a post with http - # return self._ws._moonRest.firmware_restart() # With HTTP - return self._ws.send_request( - method="printer.firmware_restart" - ) # With Websocket + return self._ws.send_request(method="printer.firmware_restart") def stop_service(self, service): + """Request service stop""" if service is None or isinstance(service, str) is False: return False return self._ws.send_request( @@ -468,6 +465,7 @@ def stop_service(self, service): ) def start_service(self, service): + """Request service start""" if service is None or isinstance(service, str) is False: return False return self._ws.send_request( @@ -475,6 +473,7 @@ def start_service(self, service): ) def get_sudo_info(self, permission: bool = False): + """Request sudo privileges information""" if isinstance(permission, bool) is False: return False return self._ws.send_request( @@ -482,15 +481,19 @@ def get_sudo_info(self, permission: bool = False): ) def get_usb_devices(self): + """Request available usb devices""" return self._ws.send_request(method="machine.peripherals.usb") def get_serial_devices(self): + """Request available serial devices""" return self._ws.send_request(method="machine.peripherals.serial") def get_video_devices(self): + """Request available video devices""" return self._ws.send_request(method="machine.peripherals.video") def get_cabus_devices(self, interface: str = "can0"): + """Request available CAN devices""" return self._ws.send_request( method="machine.peripherals.canbus", params={"interface": interface}, @@ -499,16 +502,19 @@ def get_cabus_devices(self, interface: str = "can0"): @QtCore.pyqtSlot(name="api-request-file-list") @QtCore.pyqtSlot(str, name="api-request-file-list") def get_file_list(self, root_folder: str = "gcodes"): + """Get available files""" return self._ws.send_request( method="server.files.list", params={"root": root_folder} ) @QtCore.pyqtSlot(name="api-list-roots") def list_registered_roots(self): + """Get available root directories""" return self._ws.send_request(method="server.files.roots") @QtCore.pyqtSlot(str, name="api_request_file_list") def get_gcode_metadata(self, filename_dir: str): + """Request gcode metadata""" if not isinstance(filename_dir, str) or not filename_dir: return False return self._ws.send_request( @@ -517,6 +523,7 @@ def get_gcode_metadata(self, filename_dir: str): @QtCore.pyqtSlot(str, name="api-scan-gcode-metadata") def scan_gcode_metadata(self, filename_dir: str): + """Scan gcode metadata""" if isinstance(filename_dir, str) is False or filename_dir is None: return False return self._ws.send_request( @@ -525,6 +532,7 @@ def scan_gcode_metadata(self, filename_dir: str): @QtCore.pyqtSlot(name="api_get_gcode_thumbnail") def get_gcode_thumbnail(self, filename_dir: str): + """Request gcode thumbnail""" if isinstance(filename_dir, str) is False or filename_dir is None: return False return self._ws.send_request( @@ -534,6 +542,7 @@ def get_gcode_thumbnail(self, filename_dir: str): @QtCore.pyqtSlot(str, str, name="api-delete-file") @QtCore.pyqtSlot(str, name="api-delete-file") def delete_file(self, filename: str, root_dir: str = "gcodes"): + """Request file deletion""" filepath = f"{root_dir}/{filename}" return self._ws.send_request( method="server.files.delete_file", @@ -542,7 +551,7 @@ def delete_file(self, filename: str, root_dir: str = "gcodes"): @QtCore.pyqtSlot(str, str, name="api-file_download") def download_file(self, root: str, filename: str): - """download_file Retrieves file *filename* at root *root*, the filename must include the relative path if + """Retrieves file *filename* at root *root*, the filename must include the relative path if it is not in the root folder Args: @@ -561,6 +570,7 @@ def download_file(self, root: str, filename: str): @QtCore.pyqtSlot(str, name="api-get-dir-info") @QtCore.pyqtSlot(str, bool, name="api-get-dir-info") def get_dir_information(self, directory: str = "", extended: bool = True): + """Request directory information""" if not isinstance(directory, str): return False return self._ws.send_request( @@ -569,6 +579,7 @@ def get_dir_information(self, directory: str = "", extended: bool = True): ) def create_directory(self, directory: str): + """Create directory""" if isinstance(directory, str) is False or directory is None: return False return self._ws.send_request( @@ -579,6 +590,7 @@ def create_directory(self, directory: str): ) def delete_directory(self, directory: str): + """Delete directory""" if isinstance(directory, str) is False or directory is None: return False return self._ws.send_request( @@ -589,6 +601,7 @@ def delete_directory(self, directory: str): ) def move_file(self, source_dir: str, dest_dir: str): + """Move file""" if ( isinstance(source_dir, str) is False or isinstance(dest_dir, str) is False @@ -602,6 +615,7 @@ def move_file(self, source_dir: str, dest_dir: str): ) def copy_file(self, source_dir: str, dest_dir: str): + """Copy file""" if ( isinstance(source_dir, str) is False or isinstance(dest_dir, str) is False @@ -614,21 +628,19 @@ def copy_file(self, source_dir: str, dest_dir: str): params={"source": source_dir, "dest": dest_dir}, ) - def zip_archive(self, items: list): - raise NotImplementedError() - - # !Can implement a jog queueu - def list_announcements(self, include_dismissed: bool = False): + """Request available announcements""" return self._ws.send_request( method="server.announcements.list", params={"include_dismissed": include_dismissed}, ) def update_announcements(self): + """Request announcements update to moonraker""" return self._ws.send_request(method="server.announcements.update") def dismiss_announcements(self, entry_id: str, wake_time: int = 600): + """Dismiss announcements""" if ( isinstance(entry_id, str) is False or entry_id is None @@ -641,9 +653,11 @@ def dismiss_announcements(self, entry_id: str, wake_time: int = 600): ) def list_announcements_feeds(self): + """List announcement feeds""" return self._ws.send_request(method="server.announcements.feeds") def post_announcement_feed(self, announcement_name: str): + """Post annoucement feeds""" if isinstance(announcement_name, str) is False or announcement_name is None: return False return self._ws.send_request( @@ -652,6 +666,7 @@ def post_announcement_feed(self, announcement_name: str): ) def delete_announcement_feed(self, announcement_name: str): + """Delete announcement feeds""" if isinstance(announcement_name, str) is False or announcement_name is None: return False return self._ws.send_request( @@ -659,21 +674,20 @@ def delete_announcement_feed(self, announcement_name: str): params={"name": announcement_name}, ) - # * WEBCAM - def list_webcams(self): + """List available webcams""" return self._ws.send_request(method="server.webcams.list") def get_webcam_info(self, uid: str): + """Get webcamera information""" if isinstance(uid, str) is False or uid is None: return False return self._ws.send_request( method="server.webcams.get_info", params={"uid": uid} ) - # TODO: Can create a class that irs a URL type like i've done before to validate the links - # TODO: There are more options in this section, alot more options, later see if it's worth to implement or not def add_update_webcam(self, cam_name: str, snapshot_url: str, stream_url: str): + """Add or update webcamera""" if ( isinstance(cam_name, str) is False or isinstance(snapshot_url, str) is False @@ -693,6 +707,7 @@ def add_update_webcam(self, cam_name: str, snapshot_url: str, stream_url: str): ) def delete_webcam(self, uid: str): + """Delete webcamera""" if isinstance(uid, str) is False or uid is None: return False return self._ws.send_request( @@ -700,15 +715,18 @@ def delete_webcam(self, uid: str): ) def test_webcam(self, uid: str): + """Test webcamera connection""" if isinstance(uid, str) is False or uid is None: return False return self._ws.send_request(method="server.webcams.test", params={"uid": uid}) def list_notifiers(self): + """List configured notifiers""" return self._ws.send_request(method="server.notifiers.list") @QtCore.pyqtSlot(bool, name="update-status") def update_status(self, refresh: bool = False) -> bool: + """Get packages state""" return self._ws.send_request( method="machine.update.status", params={"refresh": refresh} ) @@ -716,6 +734,7 @@ def update_status(self, refresh: bool = False) -> bool: @QtCore.pyqtSlot(name="update-refresh") @QtCore.pyqtSlot(str, name="update-refresh") def refresh_update_status(self, name: str = "") -> bool: + """Refresh packages state""" if not isinstance(name, str) or not name: return False return self._ws.send_request( @@ -724,29 +743,35 @@ def refresh_update_status(self, name: str = "") -> bool: @QtCore.pyqtSlot(name="update-full") def full_update(self) -> bool: + """Issue full upgrade to all packages""" return self._ws.send_request(method="machine.update.full") @QtCore.pyqtSlot(name="update-moonraker") def update_moonraker(self) -> bool: + """Issue moonraker update""" return self._ws.send_request(method="machine.update.moonraker") @QtCore.pyqtSlot(name="update-klipper") def update_klipper(self) -> bool: + """Issue klipper update""" return self._ws.send_request(method="machine.update.klipper") @QtCore.pyqtSlot(str, name="update-client") def update_client(self, client_name: str = "") -> bool: + """Issue client update""" if not isinstance(client_name, str) or not client_name: return False return self._ws.send_request(method="machine.update.client") @QtCore.pyqtSlot(name="update-system") def update_system(self): + """Issue system update""" return self._ws.send_request(method="machine.update.system") @QtCore.pyqtSlot(str, name="recover-repo") @QtCore.pyqtSlot(str, bool, name="recover-repo") def recover_corrupt_repo(self, name: str, hard: bool = False): + """Issue package recovery""" if isinstance(name, str) is False or name is None: return False return self._ws.send_request( @@ -756,54 +781,29 @@ def recover_corrupt_repo(self, name: str, hard: bool = False): @QtCore.pyqtSlot(str, name="rollback-update") def rollback_update(self, name: str): + """Issue rollback update""" if not isinstance(name, str) or not name: return False return self._ws.send_request( method="machine,update.rollback", params={"name": name} ) - # If moonraker [history] is configured def history_list(self, limit, start, since, before, order): - # TODO: + """Request Job history list""" raise NotImplementedError - return self._ws.send_request( - method="server.history.list", - params={ - "limit": limit, - "start": start, - "since": since, - "before": before, - "order": order, - }, - ) def history_job_totals(self): + """Request total job history""" raise NotImplementedError - return self._ws.send_request(method="server.history.totals") def history_reset_totals(self): + """Request history reset""" raise NotImplementedError - return self._ws.send_request(method="server.history.reset_totals") def history_get_job(self, uid: str): + """Request job history""" raise NotImplementedError - return self._ws.send_request( - method="server.history.get_job", params={"uid": uid} - ) def history_delete_job(self, uid: str): + """Request delete job history""" raise NotImplementedError - # It is possible to replace the uid argument with all=true to delete all jobs in the history database. - return self._ws.send_request( - method="server.history.delete_job", params={"uid": uid} - ) - - -############################################################################################################################ -# TODO: WEBSOCKET NOTIFICATIONS - -# TODO: Pass the logger object instanteation to another class so that the main window defines and calls it -# TODO: make host, port and websocket name not static but a argument that can be feed in the class -# TODO: Create websocket connection for each user login, which means different api keys for each user - -# TEST: Try and use multiprocessing as it sidesteps the GIL diff --git a/BlocksScreen/lib/moonrest.py b/BlocksScreen/lib/moonrest.py index d85b5425..1e43552a 100644 --- a/BlocksScreen/lib/moonrest.py +++ b/BlocksScreen/lib/moonrest.py @@ -57,6 +57,7 @@ def __init__(self, host: str = "localhost", port: int = 7125, api_key=False): @property def build_endpoint(self): + """Build connection endpoint""" return f"http://{self._host}:{self._port}" def get_oneshot_token(self): @@ -93,11 +94,8 @@ def firmware_restart(self): """ return self.post_request(method="printer/firmware_restart") - def delete_request(self): - # TODO: Create a delete request, so the user is able to delete files from the pi, can also be made with websockets - pass - def post_request(self, method, data=None, json=None, json_response=True): + """POST request""" return self._request( request_type="post", method=method, @@ -107,6 +105,7 @@ def post_request(self, method, data=None, json=None, json_response=True): ) def get_request(self, method, json=True, timeout=timeout): + """GET request""" return self._request( request_type="get", method=method, @@ -123,8 +122,6 @@ def _request( json_response=True, timeout=timeout, ): - # TODO: Need to check if the header is actually correct or not - # TEST: Test the reliability of this _url = f"{self.build_endpoint}/{method}" _headers = {"x-api-key": self._api_key} if self._api_key else {} try: diff --git a/BlocksScreen/lib/network.py b/BlocksScreen/lib/network.py index ca8a5ded..312b60e8 100644 --- a/BlocksScreen/lib/network.py +++ b/BlocksScreen/lib/network.py @@ -1,6 +1,5 @@ import asyncio import enum -import hashlib import logging import threading import typing @@ -23,6 +22,8 @@ def __init__(self, error): class SdbusNetworkManagerAsync(QtCore.QObject): class ConnectionPriority(enum.Enum): + """Connection priorities""" + HIGH = 90 MEDIUM = 50 LOW = 20 @@ -111,6 +112,7 @@ def close(self) -> None: self.loop.close() async def listener_monitor(self) -> None: + """Monitor for NetworkManager properties""" try: self._listeners_running = True @@ -152,6 +154,7 @@ async def _nm_properties_listener(self) -> None: logging.error(f"Exception on Network Manager state listener: {e}") def check_nm_state(self) -> typing.Union[str, None]: + """Check NetworkManager state""" if not self.nm: return future = asyncio.run_coroutine_threadsafe(self.nm.state.get_async(), self.loop) @@ -194,6 +197,11 @@ def check_connectivity(self) -> str: return "" def check_wifi_interface(self) -> bool: + """Check if wifi interface is set + + Returns: + bool: true if it is. False otherwise + """ return bool(self.primary_wifi_interface) def get_available_interfaces(self) -> typing.Union[typing.List[str], None]: @@ -266,6 +274,7 @@ async def _toggle_networking(self, value: bool = True) -> None: logger.error(f"Exception Caught when toggling network : {result}") def disable_networking(self) -> None: + """Disable networking""" if not (self.primary_wifi_interface and self.primary_wired_interface): return if self.primary_wifi_interface == "/" and self.primary_wired_interface == "/": @@ -273,6 +282,7 @@ def disable_networking(self) -> None: asyncio.run_coroutine_threadsafe(self._toggle_networking(False), self.loop) def activate_networking(self) -> None: + """Activate networking""" if not (self.primary_wifi_interface and self.primary_wired_interface): return if self.primary_wifi_interface == "/" and self.primary_wired_interface == "/": @@ -327,7 +337,6 @@ def hotspot_enabled(self) -> typing.Optional["bool"]: Returns: bool: True if Hotspot is activated, False otherwise. """ - # REFACTOR: untested for all cases return bool(self.hotspot_ssid == self.get_current_ssid()) def get_wired_interfaces(self) -> typing.List[dbusNm.NetworkDeviceWired]: @@ -415,6 +424,11 @@ async def _gather_ssid(self) -> str: return "" def get_current_ssid(self) -> str: + """Get current ssid + + Returns: + str: ssid address + """ try: future = asyncio.run_coroutine_threadsafe(self._gather_ssid(), self.loop) return future.result(timeout=5) @@ -457,7 +471,7 @@ def get_current_ip_addr(self) -> str: addr_data = addr_data_fut.result(timeout=2) return [address_data["address"][1] for address_data in addr_data][0] except IndexError as e: - logger.error(f"List out of index %s", e) + logger.error("List out of index %s", e) return "" async def _gather_primary_interface( @@ -508,7 +522,6 @@ def get_primary_interface( If there is no wireless interface and no active connection return the first wired interface that is not (lo). - ### `TODO: Completely blocking and should be refactored` Returns: typing.List: """ @@ -607,6 +620,7 @@ async def _get_available_networks(self) -> typing.Union[typing.Dict, None]: return {} def get_available_networks(self) -> typing.Union[typing.Dict, None]: + """Get available networks""" future = asyncio.run_coroutine_threadsafe( self._get_available_networks(), self.loop ) @@ -1244,9 +1258,11 @@ def delete_network(self, ssid: str) -> None: logging.debug(f"Caught Exception while deleting network {ssid}: {e}") def get_hotspot_ssid(self) -> str: + """Get current hotspot ssid""" return self.hotspot_ssid def deactivate_connection(self, connection_path) -> None: + """Deactivate a connection, by connection path""" if not self.nm: return if not self.primary_wifi_interface: @@ -1269,6 +1285,7 @@ def deactivate_connection(self, connection_path) -> None: ) def deactivate_connection_by_ssid(self, ssid: str) -> None: + """Deactivate connection by ssid""" if not self.nm: return if not self.primary_wifi_interface: @@ -1287,6 +1304,12 @@ def deactivate_connection_by_ssid(self, ssid: str) -> None: def create_hotspot( self, ssid: str = "PrinterHotspot", password: str = "123456789" ) -> None: + """Create hostpot + + Args: + ssid (str, optional): Hotspot ssid. Defaults to "PrinterHotspot". + password (str, optional): connection password. Defaults to "123456789". + """ if self.is_known(ssid): self.delete_network(ssid) logger.debug("old hotspot deleted") @@ -1339,6 +1362,12 @@ def create_hotspot( def set_network_priority( self, ssid: str, priority: ConnectionPriority = ConnectionPriority.LOW ) -> None: + """Set network priority + + Args: + ssid (str): connection ssid + priority (ConnectionPriority, optional): Priority. Defaults to ConnectionPriority.LOW. + """ if not self.nm: return if not self.is_known(ssid): @@ -1408,4 +1437,4 @@ def update_connection_settings( if password != self.hotspot_password and password: self.hotspot_password = password except Exception as e: - logger.error(f"Caught Exception while updating network: %s", e) + logger.error("Caught Exception while updating network: %s", e) diff --git a/BlocksScreen/lib/panels/controlTab.py b/BlocksScreen/lib/panels/controlTab.py index a66ba1e6..bef67b3f 100644 --- a/BlocksScreen/lib/panels/controlTab.py +++ b/BlocksScreen/lib/panels/controlTab.py @@ -14,6 +14,7 @@ from lib.panels.widgets.popupDialogWidget import Popup + class ControlTab(QtWidgets.QStackedWidget): """Printer Control Stacked Widget""" @@ -108,12 +109,6 @@ def __init__( partial(self.change_page, self.indexOf(self.panel.temperature_page)) ) self.panel.cp_switch_print_core_btn.clicked.connect(self.show_swapcore) - # self.panel.cp_printer_settings_btn.clicked.connect( - # partial( - # self.change_page, - # self.indexOf(self.panel.printer_settings_page), - # ) - # ) self.panel.cp_nozzles_calibration_btn.clicked.connect( partial(self.change_page, self.indexOf(self.probe_helper_page)) ) @@ -258,9 +253,7 @@ def __init__( ) ) - self.panel.cp_z_tilt_btn.clicked.connect( - lambda: self.handle_ztilt() - ) + self.panel.cp_z_tilt_btn.clicked.connect(lambda: self.handle_ztilt()) self.printcores_page.pc_accept.clicked.connect(self.handle_swapcore) @@ -274,9 +267,7 @@ def __init__( self.panel.cooldown_btn.hide() self.panel.cp_switch_print_core_btn.hide() - - def handle_printcoreupdate(self, value:dict): - + def handle_printcoreupdate(self, value: dict): if value["swapping"] == "idle": return @@ -289,14 +280,10 @@ def handle_printcoreupdate(self, value:dict): ) if value["swapping"] == "unloading": self.loadpage.set_status_message("Unloading print core") - + if value["swapping"] == "cleaning": self.loadpage.set_status_message("Cleaning print core") - - - - def _handle_gcode_response(self, messages: list): """Handle gcode response for Z-tilt adjustment""" pattern = r"Retries:\s*(\d+)/(\d+).*?range:\s*([\d.]+)\s*tolerance:\s*([\d.]+)" @@ -305,7 +292,11 @@ def _handle_gcode_response(self, messages: list): if not msg_list: continue - if "Retries:" in msg_list and "range:" in msg_list and "tolerance:" in msg_list: + if ( + "Retries:" in msg_list + and "range:" in msg_list + and "tolerance:" in msg_list + ): print("Match candidate:", msg_list) match = re.search(pattern, msg_list) print("Regex match:", match) @@ -327,7 +318,6 @@ def _handle_gcode_response(self, messages: list): f"Retries: {retries_done}/{retries_total} | Range: {probed_range:.6f} | Tolerance: {tolerance:.6f}" ) - def handle_ztilt(self): """Handle Z-Tilt Adjustment""" self.loadpage.show() @@ -351,7 +341,6 @@ def show_swapcore(self): self.loadpage.show() self.loadpage.set_status_message("Preparing to swap print core") - def handle_swapcore(self): """Handle swap printcore routine finish""" self.printcores_page.setText("Executing \n Firmware Restart") @@ -521,7 +510,7 @@ def on_toolhead_update(self, field: str, values: list) -> None: self.panel.mva_y_value_label.setText(f"{values[1]:.2f}") self.panel.mva_z_value_label.setText(f"{values[2]:.3f}") - if values[0] == "252,50" and values[1] == "250" and values[2] == "50": + if values[0] == "252,50" and values[1] == "250" and values[2] == "50": self.loadpage.hide self.toolhead_info.update({f"{field}": values}) diff --git a/BlocksScreen/lib/panels/filamentTab.py b/BlocksScreen/lib/panels/filamentTab.py index ceb77a2a..4ab358fa 100644 --- a/BlocksScreen/lib/panels/filamentTab.py +++ b/BlocksScreen/lib/panels/filamentTab.py @@ -1,5 +1,4 @@ import enum -import typing from functools import partial @@ -52,7 +51,6 @@ def __init__(self, parent: QtWidgets.QWidget, printer: Printer, ws, /) -> None: partial(self.change_page, self.indexOf(self.panel.load_page)) ) self.panel.custom_filament_header_back_btn.clicked.connect(self.back_button) - # REFACTOR self.panel.load_custom_btn.clicked.connect(partial(self.change_page, 2)) self.panel.load_custom_btn.hide() self.panel.load_header_back_button.clicked.connect(self.back_button) self.panel.load_pla_btn.clicked.connect( @@ -95,9 +93,7 @@ def __init__(self, parent: QtWidgets.QWidget, printer: Printer, ws, /) -> None: @QtCore.pyqtSlot(str, float, name="on_print_stats_update") @QtCore.pyqtSlot(str, str, name="on_print_stats_update") def on_print_stats_update(self, field: str, value: dict | float | str) -> None: - """ - unblocks tabs if on standby - """ + """Handle print stats object update""" if isinstance(value, str): if "state" in field: if value in ("standby"): @@ -106,6 +102,7 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: @QtCore.pyqtSlot(str, str, bool, name="on_filament_sensor_update") def on_filament_sensor_update(self, sensor_name: str, parameter: str, value: bool): + """Handle filament sensor object update""" if parameter == "filament_detected": if not isinstance(value, bool): self._filament_state = self.FilamentStates.UNKNOWN @@ -126,6 +123,7 @@ def on_filament_sensor_update(self, sensor_name: str, parameter: str, value: boo def on_extruder_update( self, extruder_name: str, field: str, new_value: float ) -> None: + """Handle extruder update""" if not self.isVisible: return @@ -134,16 +132,17 @@ def on_extruder_update( self.loadscreen.set_status_message("Extruder heated up \n Please wait") return if field == "temperature": - self.current_temp = round(new_value, 0) # somehow this works + self.current_temp = round(new_value, 0) self.loadscreen.set_status_message( f"Heating up ({new_value}/{self.target_temp}) \n Please wait" ) if field == "target": - self.target_temp = round(new_value, 0) # somehow this works again + self.target_temp = round(new_value, 0) self.loadscreen.set_status_message("Heating up \n Please wait") @QtCore.pyqtSlot(bool, name="on_load_filament") def on_load_filament(self, status: bool): + """Handle load filament object updated""" if self.loadignore: self.loadignore = False return @@ -160,6 +159,7 @@ def on_load_filament(self, status: bool): @QtCore.pyqtSlot(bool, name="on_unload_filament") def on_unload_filament(self, status: bool): + """Handle unload filament object updated""" if self.unloadignore: self.unloadignore = False return @@ -177,6 +177,7 @@ def on_unload_filament(self, status: bool): @QtCore.pyqtSlot(int, int, name="load_filament") def load_filament(self, toolhead: int = 0, temp: int = 220) -> None: + """Handle load filament buttons clicked""" if not self.isVisible: return @@ -197,6 +198,7 @@ def load_filament(self, toolhead: int = 0, temp: int = 220) -> None: @QtCore.pyqtSlot(str, int, name="unload_filament") def unload_filament(self, toolhead: int = 0, temp: int = 220) -> None: + """Handle unload filament button clicked""" if not self.isVisible: return @@ -218,6 +220,7 @@ def unload_filament(self, toolhead: int = 0, temp: int = 220) -> None: self.run_gcode.emit(f"UNLOAD_FILAMENT TEMPERATURE={temp}") def handle_filament_state(self): + """Handle ui changes on filament states""" if self._filament_state == self.FilamentStates.LOADED: self.panel.filament_page_load_btn.setDisabled(True) self.panel.filament_page_load_btn.setDisabled(False) @@ -233,29 +236,22 @@ def filament_state(self): return self._filament_state def change_page(self, index): + """Issue a page change""" self.request_change_page.emit(1, index) def back_button(self): + """Go back a page""" self.request_back.emit() - def sizeHint(self) -> QtCore.QSize: - return super().sizeHint() - def paintEvent(self, a0: QtGui.QPaintEvent | None) -> None: + """Widget painting""" if self.panel.load_page.isVisible() and self.toolhead_count == 1: self.panel.load_header_page_title.setText("Load Toolhead") if a0 is not None: return super().paintEvent(a0) - def removeWidget(self, w: QtWidgets.QWidget | None) -> None: - if w is not None: - return super().removeWidget(w) - - def resizeEvent(self, a0: QtGui.QResizeEvent | None) -> None: - if a0 is not None: - return super().resizeEvent(a0) - def find_routine_objects(self): + """Check if printer has load/unload printer objects""" if not self.printer: return diff --git a/BlocksScreen/lib/panels/instructionsWindow.py b/BlocksScreen/lib/panels/instructionsWindow.py deleted file mode 100644 index cce27c46..00000000 --- a/BlocksScreen/lib/panels/instructionsWindow.py +++ /dev/null @@ -1,30 +0,0 @@ -from PyQt6.QtWidgets import QStackedWidget, QWidget -from PyQt6 import QtCore -import typing - -from lib.ui.instructionsWindow_ui import Ui_utilitiesStackedWidget - - -# TODO: Complete this panel - -class InstructionsWindow(QStackedWidget): - - def __init__(self, parent: typing.Optional[QWidget] = ...) -> None: - super().__init__(parent) - - self.panel = Ui_utilitiesStackedWidget() - self.panel.setupUi(self) - # self.show() - - self.index_stack = [] - - # Connecting the print_btn.clicked event to the change_page method - #self.panel.main_print_btn.clicked.connect(self.change_page) - #self.panel.files_back_folder_btn.clicked.connect(self.change_page) - - - def change_page(self, int): - self.setCurrentIndex(int) - self.index_stack.append(self.currentIndex()) - - \ No newline at end of file diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index f381febb..b7921e55 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -35,11 +35,12 @@ def api_handler(func): """Decorator for methods that handle api responses""" def wrapper(*args, **kwargs): + """Decorator for api_handler""" try: result = func(*args, **kwargs) return result except Exception as e: - _logger.error(f"Caught Exception in %s : %s ", func.__name__, e) + _logger.error("Caught Exception in %s : %s ", func.__name__, e) raise return wrapper @@ -156,25 +157,27 @@ def __init__(self): # @ Start websocket connection with moonraker self.bo_ws_startup.emit() self.reset_tab_indexes() + @QtCore.pyqtSlot(name="on-cancel-print") def on_cancel_print(self): - self.enable_tab_bar() - self.ui.extruder_temp_display.clicked.disconnect() - self.ui.bed_temp_display.clicked.disconnect() - self.ui.filament_type_icon.setDisabled(False) - self.ui.nozzle_size_icon.setDisabled(False) - self.ui.extruder_temp_display.clicked.connect( - lambda: self.global_change_page( - self.ui.main_content_widget.indexOf(self.ui.controlTab), - self.controlPanel.indexOf(self.controlPanel.panel.temperature_page), - ) + """Slot for cancel print signal""" + self.enable_tab_bar() + self.ui.extruder_temp_display.clicked.disconnect() + self.ui.bed_temp_display.clicked.disconnect() + self.ui.filament_type_icon.setDisabled(False) + self.ui.nozzle_size_icon.setDisabled(False) + self.ui.extruder_temp_display.clicked.connect( + lambda: self.global_change_page( + self.ui.main_content_widget.indexOf(self.ui.controlTab), + self.controlPanel.indexOf(self.controlPanel.panel.temperature_page), ) - self.ui.bed_temp_display.clicked.connect( - lambda: self.global_change_page( - self.ui.main_content_widget.indexOf(self.ui.controlTab), - self.controlPanel.indexOf(self.controlPanel.panel.temperature_page), - ) + ) + self.ui.bed_temp_display.clicked.connect( + lambda: self.global_change_page( + self.ui.main_content_widget.indexOf(self.ui.controlTab), + self.controlPanel.indexOf(self.controlPanel.panel.temperature_page), ) + ) @QtCore.pyqtSlot(bool, name="update-available") def on_update_available(self, state: bool = False): @@ -492,14 +495,7 @@ def _handle_notify_klippy_message(self, method, data, metadata) -> None: @api_handler def _handle_notify_filelist_changed_message(self, method, data, metadata) -> None: """Handle websocket file list messages""" - _file_change_list = data.get("params") - if _file_change_list: - fileaction = _file_change_list[0].get("action") - filepath = ( - _file_change_list[0].get("item").get("path") - ) # TODO : NOTIFY_FILELIST_CHANGED, I DON'T KNOW IF I REALLY WANT TO SEND NOTIFICATIONS ON FILE CHANGES. ... - # self.file_data.request_file_list.emit() @api_handler def _handle_notify_service_state_changed_message( diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 5df4b81d..57617efe 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -1,6 +1,6 @@ import logging import typing -import subprocess +import subprocess # nosec: B404 from functools import partial from lib.network import SdbusNetworkManagerAsync @@ -12,6 +12,7 @@ logger = logging.getLogger("logs/BlocksScreen.log") + class BuildNetworkList(QtCore.QThread): """Retrieves information from sdbus interface about scanned networks""" @@ -233,7 +234,7 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: partial( self.panel.saved_connection_change_password_field.setEchoMode, QtWidgets.QLineEdit.EchoMode.Normal, - ) + ) ) self.panel.saved_connection_change_password_view.released.connect( partial( @@ -315,13 +316,8 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") ) - - self.panel.network_activate_btn.clicked.connect( - lambda: self.saved_wifi_option_selected() - ) - self.panel.network_delete_btn.clicked.connect( - lambda: self.saved_wifi_option_selected() - ) + self.panel.network_activate_btn.clicked.connect(self.saved_wifi_option_selected) + self.panel.network_delete_btn.clicked.connect(self.saved_wifi_option_selected) self.network_list_worker.build() self.request_network_scan.emit() @@ -356,30 +352,41 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: ) def saved_wifi_option_selected(self): + """Handle connect/delete network button clicks""" _sender = self.sender() - self.panel.wifi_button.toggle_button.state = self.panel.wifi_button.toggle_button.State.ON - self.panel.hotspot_button.toggle_button.state = self.panel.hotspot_button.toggle_button.State.OFF + self.panel.wifi_button.toggle_button.state = ( + self.panel.wifi_button.toggle_button.State.ON + ) + self.panel.hotspot_button.toggle_button.state = ( + self.panel.hotspot_button.toggle_button.State.OFF + ) if _sender == self.panel.network_delete_btn: - self.sdbus_network.delete_network(self.panel.saved_connection_network_name.text()) + self.sdbus_network.delete_network( + self.panel.saved_connection_network_name.text() + ) self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) elif _sender == self.panel.network_activate_btn: self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) - self.sdbus_network.connect_network(self.panel.saved_connection_network_name.text()) + self.sdbus_network.connect_network( + self.panel.saved_connection_network_name.text() + ) self.info_box_load(True) - def on_show_keyboard(self, panel: QtWidgets.QWidget, field: QtWidgets.QLineEdit): + """Handle keyboard show""" self.previousPanel = panel self.currentField = field self.qwerty.set_value(field.text()) self.setCurrentIndex(self.indexOf(self.qwerty)) def on_qwerty_go_back(self): + """Hide keyboard""" self.setCurrentIndex(self.indexOf(self.previousPanel)) def on_qwerty_value_selected(self, value: str): + """Handle keyboard value input""" self.setCurrentIndex(self.indexOf(self.previousPanel)) if hasattr(self, "currentField") and self.currentField: self.currentField.setText(value) @@ -390,16 +397,15 @@ def info_box_load(self, toggle: bool = False) -> None: Sets a 30-second timeout to handle loading failures. """ self._show_loadscreen(toggle) - + self.panel.wifi_button.setEnabled(not toggle) self.panel.hotspot_button.setEnabled(not toggle) - + if toggle: if self._load_timer.isActive(): self._load_timer.stop() self._load_timer.start(30000) - def _handle_load_timeout(self): """ Logic to execute if the loading screen is still visible after 30 seconds.< @@ -416,12 +422,10 @@ def _handle_load_timeout(self): else: message = "Loading timed out.\n Please check your connection \n and try again." - - self.panel.mn_info_box.setText(message) self._show_loadscreen(False) self._expand_infobox(True) - + hotspot_btn.setEnabled(True) wifi_btn.setEnabled(True) @@ -448,10 +452,11 @@ def _show_loadscreen(self, toggle: bool = False): @QtCore.pyqtSlot(object, name="stateChange") def on_toggle_state(self, new_state) -> None: + """Handle toggle button changes""" sender_button = self.sender() wifi_btn = self.panel.wifi_button.toggle_button hotspot_btn = self.panel.hotspot_button.toggle_button - is_sender_now_on = (new_state == sender_button.State.ON) + is_sender_now_on = new_state == sender_button.State.ON _old_hotspot = None saved_network = self.sdbus_network.get_saved_networks() @@ -463,8 +468,13 @@ def on_toggle_state(self, new_state) -> None: if saved_network: try: ssid = next( - (n["ssid"] for n in saved_network if "ap" not in n['mode']and n["signal"] != 0), - None) + ( + n["ssid"] + for n in saved_network + if "ap" not in n["mode"] and n["signal"] != 0 + ), + None, + ) self.sdbus_network.connect_network(str(ssid)) except Exception as e: @@ -532,7 +542,7 @@ def evaluate_network_state(self, nm_state: str = "") -> None: break if _old_hotspot: self.panel.hotspot_name_input_field.setText(_old_hotspot["ssid"]) - + connection = self.sdbus_network.check_connectivity() if connection == "FULL": self.panel.wifi_button.toggle_button.state = ( @@ -547,23 +557,22 @@ def evaluate_network_state(self, nm_state: str = "") -> None: ) self.panel.hotspot_button.toggle_button.state = ( self.panel.hotspot_button.toggle_button.State.ON - ) - + ) if not self.sdbus_network.check_wifi_interface(): return if hotspot_btn.state == hotspot_btn.State.ON: - ipv4_addr = self.get_hotspot_ip_via_shell("wlan0") + ipv4_addr = self.get_hotspot_ip_via_shell() self.panel.netlist_ssuid.setText(self.panel.hotspot_name_input_field.text()) - self.panel.netlist_ip.setText(f"IP: {ipv4_addr or 'No IP Address'}") + self.panel.netlist_ip.setText(f"IP: {ipv4_addr or 'No IP Address'}") self.panel.netlist_strength.setText("--") - + self.panel.netlist_security.setText("--") - + self.panel.mn_info_box.setText("Hotspot On") if wifi_btn.state == wifi_btn.State.ON: @@ -589,7 +598,6 @@ def evaluate_network_state(self, nm_state: str = "") -> None: self.panel.hotspot_button.setEnabled(True) self.repaint() - if ( wifi_btn.state == wifi_btn.State.OFF and hotspot_btn.state == hotspot_btn.State.OFF @@ -600,37 +608,54 @@ def evaluate_network_state(self, nm_state: str = "") -> None: "Network connection required.\n\nConnect to Wi-Fi\nor\nTurn on Hotspot" ) - def get_hotspot_ip_via_shell(self, interface: str): + def get_hotspot_ip_via_shell(self): """ Executes a shell command to retrieve the IPv4 address for a specified interface. - Args: - interface: The name of the hotspot interface (e.g., 'wlan0'). Returns: The IP address string (e.g., '10.42.0.1') or None if not found. """ - command = ( - f"ip a show {interface} | grep 'inet ' | awk '{{print $2}}' | cut -d/ -f1" - ) + command = [ + "ip", + "a", + "show", + "wlan0", + " |", + "grep", + " 'inet '", + "|", + "awk", + " '{{print $2}}'", + "|", + "cut", + "-d/", + "-f1", + ] try: - result = subprocess.run( + result = subprocess.run( # nosec: B603 command, - shell=True, capture_output=True, text=True, check=True, timeout=5, ) - ip_addr = result.stdout.strip() if ip_addr and len(ip_addr.split(".")) == 4: return ip_addr - except Exception as e: - print(f"An unexpected error occurred: {e}") - - return None + except subprocess.CalledProcessError as e: + logging.error( + "Caught exception (exit code %d) failed to run command: %s \nStderr: %s", + e.returncode, + command, + e.stderr.strip(), + ) + raise + except subprocess.TimeoutExpired as e: + logging.error("Caught exception, failed to run command %s", e) + raise def close(self) -> bool: + """Close class, close network module""" self.sdbus_network.close() return super().close() @@ -656,14 +681,17 @@ def _expand_infobox(self, toggle: bool = False) -> None: @QtCore.pyqtSlot(str, name="delete-network") def delete_network(self, ssid: str) -> None: + """Delete network""" self.sdbus_network.delete_network(ssid=ssid) @QtCore.pyqtSlot(name="rescan-networks") def rescan_networks(self) -> None: + """Rescan for networks""" self.sdbus_network.rescan_networks() @QtCore.pyqtSlot(name="handle-hotspot-back") def handle_hotspot_back(self) -> None: + """Handle go back a page from hotspot page""" if ( self.panel.hotspot_password_input_field.text() != self.sdbus_network.hotspot_password @@ -709,7 +737,12 @@ def add_network(self) -> None: if not error_msg: # Assume it was a success QtCore.QTimer().singleShot(5000, self.network_list_worker.build) - QtCore.QTimer().singleShot(5000, lambda: self.sdbus_network.connect_network(self.panel.add_network_network_label.text())) + QtCore.QTimer().singleShot( + 5000, + lambda: self.sdbus_network.connect_network( + self.panel.add_network_network_label.text() + ), + ) self.info_box_load(True) self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) self.panel.add_network_validation_button.setEnabled(True) @@ -731,7 +764,6 @@ def add_network(self) -> None: self.panel.add_network_validation_button.setEnabled(True) self.panel.add_network_validation_button.repaint() self.popup.new_message(message_type=Popup.MessageType.ERROR, message=message) - @QtCore.pyqtSlot(QtWidgets.QListWidgetItem, name="ssid_item_clicked") def ssid_item_clicked(self, item: QtWidgets.QListWidgetItem) -> None: @@ -765,6 +797,7 @@ def update_network( password: typing.Union[str, None], new_ssid: typing.Union[str, None], ) -> None: + """Update network information""" if not self.sdbus_network.is_known(ssid): return @@ -778,6 +811,7 @@ def update_network( @QtCore.pyqtSlot(list, name="finished-network-list-build") def handle_network_list(self, data: typing.List[typing.Tuple]) -> None: + """Handle available network list update""" scroll_bar_position = self.network_list_widget.verticalScrollBar().value() self.network_list_widget.blockSignals(True) self.network_list_widget.clear() diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index d899c5e1..98301283 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -42,15 +42,15 @@ class PrintTab(QtWidgets.QStackedWidget): """ - request_query_print_stats: typing.ClassVar[QtCore.pyqtSignal] = ( - QtCore.pyqtSignal(dict, name="request_query_print_stats") + request_query_print_stats: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + dict, name="request_query_print_stats" ) request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="request-back" ) - request_change_page: typing.ClassVar[QtCore.pyqtSignal] = ( - QtCore.pyqtSignal(int, int, name="request_change_page") + request_change_page: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + int, int, name="request_change_page" ) run_gcode_signal: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( @@ -79,7 +79,6 @@ def __init__( self.gcode_path = os.path.expanduser("~/printer_data/gcodes") self.setMouseTracking(True) - self.sliderPage = SliderPage(self) self.addWidget(self.sliderPage) self.sliderPage.request_back.connect(self.back_button) @@ -95,7 +94,6 @@ def __init__( self.dialogPage = DialogPage(self) - self.confirmPage_widget = ConfirmWidget(self) self.addWidget(self.confirmPage_widget) self.confirmPage_widget.back_btn.clicked.connect(self.back_button) @@ -124,9 +122,7 @@ def __init__( self.filesPage_widget.request_dir_info[str].connect( self.file_data.request_dir_info[str] ) - self.filesPage_widget.request_dir_info.connect( - self.file_data.request_dir_info - ) + self.filesPage_widget.request_dir_info.connect(self.file_data.request_dir_info) self.file_data.on_file_list.connect(self.filesPage_widget.on_file_list) self.jobStatusPage_widget = JobStatusWidget(self) self.addWidget(self.jobStatusPage_widget) @@ -146,12 +142,8 @@ def __init__( ) self.file_data.fileinfo.connect(self.jobStatusPage_widget.on_fileinfo) self.jobStatusPage_widget.print_start.connect(self.ws.api.start_print) - self.jobStatusPage_widget.print_resume.connect( - self.ws.api.resume_print - ) - self.jobStatusPage_widget.print_cancel.connect( - self.handle_cancel_print - ) + self.jobStatusPage_widget.print_resume.connect(self.ws.api.resume_print) + self.jobStatusPage_widget.print_cancel.connect(self.handle_cancel_print) self.jobStatusPage_widget.print_pause.connect(self.ws.api.pause_print) self.jobStatusPage_widget.request_query_print_stats.connect( self.ws.api.object_query @@ -176,15 +168,9 @@ def __init__( self.jobStatusPage_widget.on_print_stats_update ) - self.printer.print_stats_update[str, str].connect( - self.on_print_stats_update - ) - self.printer.print_stats_update[str, dict].connect( - self.on_print_stats_update - ) - self.printer.print_stats_update[str, float].connect( - self.on_print_stats_update - ) + self.printer.print_stats_update[str, str].connect(self.on_print_stats_update) + self.printer.print_stats_update[str, dict].connect(self.on_print_stats_update) + self.printer.print_stats_update[str, float].connect(self.on_print_stats_update) self.printer.gcode_move_update[str, list].connect( self.jobStatusPage_widget.on_gcode_move_update @@ -222,12 +208,12 @@ def __init__( self.tune_page.request_sliderPage[str, int, "PyQt_PyObject"].connect( self.on_slidePage_request ) - self.tune_page.request_sliderPage[ - str, int, "PyQt_PyObject", int, int - ].connect(self.on_slidePage_request) - self.tune_page.request_numpad[ - str, int, "PyQt_PyObject", int, int - ].connect(self.on_numpad_request) + self.tune_page.request_sliderPage[str, int, "PyQt_PyObject", int, int].connect( + self.on_slidePage_request + ) + self.tune_page.request_numpad[str, int, "PyQt_PyObject", int, int].connect( + self.on_numpad_request + ) self.tune_page.request_numpad[ str, int, @@ -262,20 +248,14 @@ def __init__( self.run_gcode_signal.connect(self.ws.api.run_gcode) - self.confirmPage_widget.on_delete.connect( - self.delete_file - ) + self.confirmPage_widget.on_delete.connect(self.delete_file) - self.change_page( - self.indexOf(self.print_page) - ) # force set the initial page + self.change_page(self.indexOf(self.print_page)) # force set the initial page @QtCore.pyqtSlot(str, dict, name="on_print_stats_update") @QtCore.pyqtSlot(str, float, name="on_print_stats_update") @QtCore.pyqtSlot(str, str, name="on_print_stats_update") - def on_print_stats_update( - self, field: str, value: dict | float | str - ) -> None: + def on_print_stats_update(self, field: str, value: dict | float | str) -> None: """ unblocks tabs if on standby """ @@ -284,13 +264,8 @@ def on_print_stats_update( if value in ("standby"): self.on_cancel_print.emit() - - - @QtCore.pyqtSlot(str, int, "PyQt_PyObject", name="on_numpad_request") - @QtCore.pyqtSlot( - str, int, "PyQt_PyObject", int, int, name="on_numpad_request" - ) + @QtCore.pyqtSlot(str, int, "PyQt_PyObject", int, int, name="on_numpad_request") def on_numpad_request( self, name: str, @@ -299,6 +274,7 @@ def on_numpad_request( min_value: int = 0, max_value: int = 100, ) -> None: + """Handle numpad request""" self.numpadPage.value_selected.connect(callback) self.numpadPage.set_name(name) self.numpadPage.set_value(current_value) @@ -308,9 +284,7 @@ def on_numpad_request( self.change_page(self.indexOf(self.numpadPage)) @QtCore.pyqtSlot(str, int, "PyQt_PyObject", name="on_slidePage_request") - @QtCore.pyqtSlot( - str, int, "PyQt_PyObject", int, int, name="on_slidePage_request" - ) + @QtCore.pyqtSlot(str, int, "PyQt_PyObject", int, int, name="on_slidePage_request") def on_slidePage_request( self, name: str, @@ -319,6 +293,7 @@ def on_slidePage_request( min_value: int = 0, max_value: int = 100, ) -> None: + """Handle slider page request""" self.sliderPage.value_selected.connect(callback) self.sliderPage.set_name(name) self.sliderPage.set_slider_position(int(current_value)) @@ -326,28 +301,24 @@ def on_slidePage_request( self.sliderPage.set_slider_maximum(max_value) self.change_page(self.indexOf(self.sliderPage)) - def delete_file(self,direcotry:str,name:str): - self.directory:str = direcotry - self.filename:str = name + def delete_file(self, direcotry: str, name: str): + """Handle Delete file button clicked""" + self.directory: str = direcotry + self.filename: str = name self.dialogPage.set_message("Are you sure you want to delete this file?") self.dialogPage.button_clicked.connect(self.on_dialog_button_clicked) self.dialogPage.show() def on_dialog_button_clicked(self, button_name: str) -> None: - print(button_name) """Handle dialog button clicks""" if button_name == "Confirm": - self.ws.api.delete_file(self.filename,self.directory) + self.ws.api.delete_file(self.filename, self.directory) self.dialogPage.hide() else: self.dialogPage.hide() - def paintEvent(self, a0: QtGui.QPaintEvent) -> None: - """ - REFACTOR: Instead of using a background svg pixmap just draw the - background with with the correct styles and everything - """ + """Widget painting""" if self.babystepPage.isVisible(): _button_name_str = f"nozzle_offset_{self._z_offset}" if hasattr(self, _button_name_str): @@ -372,7 +343,7 @@ def setProperty(self, name: str, value: typing.Any) -> bool: if name == "backgroundPixmap": self.background = value return super().setProperty(name, value) - + def handle_cancel_print(self) -> None: """Handles the print cancel action""" self.ws.api.cancel_print() @@ -394,6 +365,7 @@ def back_button(self) -> None: self.request_back.emit() def setupMainPrintPage(self) -> None: + """Setup UI for print page""" self.setObjectName("printStackedWidget") self.setWindowModality(QtCore.Qt.WindowModality.WindowModal) self.resize(710, 410) @@ -409,9 +381,7 @@ def setupMainPrintPage(self) -> None: self.setMaximumSize(QtCore.QSize(720, 420)) self.setProperty( "backgroundPixmap", - QtGui.QPixmap( - ":/background/media/graphics/scroll_list_window.svg" - ), + QtGui.QPixmap(":/background/media/graphics/scroll_list_window.svg"), ) self.print_page = QtWidgets.QWidget() sizePolicy = QtWidgets.QSizePolicy( @@ -420,9 +390,7 @@ def setupMainPrintPage(self) -> None: ) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth( - self.print_page.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.print_page.sizePolicy().hasHeightForWidth()) self.print_page.setSizePolicy(sizePolicy) self.print_page.setMinimumSize(QtCore.QSize(710, 400)) self.print_page.setMaximumSize(QtCore.QSize(720, 420)) @@ -452,9 +420,7 @@ def setupMainPrintPage(self) -> None: self.main_print_btn.setContextMenuPolicy( QtCore.Qt.ContextMenuPolicy.NoContextMenu ) - self.main_print_btn.setLayoutDirection( - QtCore.Qt.LayoutDirection.LeftToRight - ) + self.main_print_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self.main_print_btn.setStyleSheet("") self.main_print_btn.setAutoDefault(False) self.main_print_btn.setFlat(True) @@ -481,9 +447,7 @@ def setupMainPrintPage(self) -> None: font.setFamily("Montserrat") font.setPointSize(14) self.main_text_label.setFont(font) - self.main_text_label.setStyleSheet( - "background: transparent; color: white;" - ) + self.main_text_label.setStyleSheet("background: transparent; color: white;") self.main_text_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.main_text_label.setTextInteractionFlags( QtCore.Qt.TextInteractionFlag.NoTextInteraction @@ -497,6 +461,4 @@ def setupMainPrintPage(self) -> None: self.main_print_btn.setProperty( "class", _translate("printStackedWidget", "menu_btn") ) - self.main_text_label.setText( - _translate("printStackedWidget", "Printer ready") - ) + self.main_text_label.setText(_translate("printStackedWidget", "Printer ready")) diff --git a/BlocksScreen/lib/panels/userauthWindow.py b/BlocksScreen/lib/panels/userauthWindow.py deleted file mode 100644 index de1a7e0b..00000000 --- a/BlocksScreen/lib/panels/userauthWindow.py +++ /dev/null @@ -1,6 +0,0 @@ -import logging - - -# TODO: Create user authentication panel -# TODO: Create change user login -# TODO: Create admin mode diff --git a/BlocksScreen/lib/panels/utilitiesTab.py b/BlocksScreen/lib/panels/utilitiesTab.py index f67bc22d..4797b2ff 100644 --- a/BlocksScreen/lib/panels/utilitiesTab.py +++ b/BlocksScreen/lib/panels/utilitiesTab.py @@ -41,6 +41,8 @@ def get_gcode(self, name: str) -> str: class Process(Enum): + """Printer Process""" + FAN = auto() AXIS = auto() BED_HEATER = auto() @@ -119,7 +121,7 @@ def __init__( self.update_page = UpdatePage(self) self.addWidget(self.update_page) - + self.panel.utilities_input_shaper_btn.hide() # --- Back Buttons --- for button in ( @@ -234,6 +236,7 @@ def __init__( @QtCore.pyqtSlot(list, name="on_object_list") def on_object_list(self, object_list: list) -> None: + """Handle receiving printer object list""" self.cg = object_list for obj in self.cg: base_name = obj.split()[0] @@ -246,6 +249,7 @@ def on_object_list(self, object_list: list) -> None: @QtCore.pyqtSlot(dict, name="on_object_config") @QtCore.pyqtSlot(list, name="on_object_config") def on_object_config(self, config: typing.Union[dict, list]) -> None: + """Handle receiving printer object configurations""" if not config: return config_items = [config] if isinstance(config, dict) else config @@ -269,6 +273,7 @@ def on_object_config(self, config: typing.Union[dict, list]) -> None: } def on_printer_config_received(self, config: dict) -> None: + """Handle printer configuration""" for axis in ("x", "y", "z"): self.subscribe_config[str, "PyQt_PyObject"].emit( f"stepper_{axis}", self.on_object_config @@ -276,6 +281,7 @@ def on_printer_config_received(self, config: dict) -> None: @QtCore.pyqtSlot(str, list, name="on_gcode_move_update") def on_gcode_move_update(self, name: str, value: list) -> None: + """Handle gcode move""" if not value: return if name == "gcode_position": @@ -290,12 +296,14 @@ def _connect_numpad_request(self, button: QtWidgets.QWidget, name: str, title: s ) def handle_numpad_change(self, name: str, new_value: typing.Union[int, float]): + """Handle numpad change""" if name == "frequency": self.panel.isui_fq.setText(f"Frequency: {new_value} Hz") elif name == "smoothing": self.panel.isui_sm.setText(f"Smoothing: {new_value}") def run_routine(self, process: Process): + """Run check routine for available processes""" self.current_process = process routine_configs = { Process.FAN: ("fans", "fan is spinning"), @@ -325,8 +333,7 @@ def run_routine(self, process: Process): message = "Please check if the temperature reaches 60°C. \n you may need to wait a few moments." self.set_routine_check_page( - f"Running routine for: {self.current_object}", - message + f"Running routine for: {self.current_object}", message ) self.show_waiting_page( self.indexOf(self.panel.rc_page), @@ -358,6 +365,7 @@ def _advance_routine_object(self, obj_list: list) -> bool: return True def on_routine_answer(self) -> None: + """Handle routine ongoing process""" if self.current_process is None or self.current_object is None: return if self.sender() == self.panel.rc_yes: @@ -391,8 +399,10 @@ def _send_routine_gcode(self): if fan_name == "fan": self.run_gcode_signal.emit("M106 S255\nM400") else: - self.run_gcode_signal.emit(f"SET_FAN_SPEED FAN={fan_name} SPEED=0.8\nM400") - + self.run_gcode_signal.emit( + f"SET_FAN_SPEED FAN={fan_name} SPEED=0.8\nM400" + ) + return gcode_map = { @@ -412,18 +422,16 @@ def _send_routine_gcode(self): if gcode := gcode_map.get(key): self.run_gcode_signal.emit(f"{gcode}\nM400") - def set_routine_check_page(self, title: str, label: str): + """Set text on routine page""" self.panel.rc_tittle.setText(title) self.panel.rc_label.setText(label) def update_led_values(self) -> None: + """Update led state and color values""" if self.current_object not in self.objects["leds"]: return led_state: LedState = self.objects["leds"][self.current_object] - # led_state.red = self.panel.leds_r_slider.value() - # led_state.green = self.panel.leds_g_slider.value() - # led_state.blue = self.panel.leds_b_slider.value() led_state.white = int(self.panel.leds_w_slider.value() * 255 / 100) self.save_led_state() @@ -469,10 +477,12 @@ def _update_leds_from_config(self): partial(self.handle_led_button, led_names[0]) ) else: - self._connect_page_change(self.panel.utilities_leds_btn, self.panel.leds_page) - + self._connect_page_change( + self.panel.utilities_leds_btn, self.panel.leds_page + ) def toggle_led_state(self) -> None: + """Toggle leds""" if self.current_object not in self.objects["leds"]: return led_state: LedState = self.objects["leds"][self.current_object] @@ -485,30 +495,25 @@ def toggle_led_state(self) -> None: self.save_led_state() def handle_led_button(self, name: str) -> None: + """Handle led button clicked""" self.current_object = name led_state: LedState = self.objects["leds"].get(name) if not led_state: return is_rgb = led_state.led_type == "rgb" - # self.panel.leds_r_slider.setVisible(is_rgb) - # self.panel.leds_g_slider.setVisible(is_rgb) - # self.panel.leds_b_slider.setVisible(is_rgb) self.panel.leds_w_slider.setVisible(not is_rgb) - #self.panel.leds_slider_tittle_label.setText(name) - # self.panel.leds_r_slider.setValue(led_state.red) - # self.panel.leds_g_slider.setValue(led_state.green) - # self.panel.leds_b_slider.setValue(led_state.blue) self.panel.leds_w_slider.setValue(led_state.white) self.change_page(self.indexOf(self.panel.leds_slider_page)) def save_led_state(self): + """Save led state""" if self.current_object: if self.current_object in self.objects["leds"]: led_state: LedState = self.objects["leds"][self.current_object] self.run_gcode_signal.emit(led_state.get_gcode(self.current_object)) - # input shapper def run_resonance_test(self, axis: str) -> None: + """Perform Input Shaper Measure resonances test""" self.axis_in = axis path_map = { "x": "/tmp/resonances_x_axis_data.csv", @@ -530,7 +535,7 @@ def run_resonance_test(self, axis: str) -> None: self.x_inputshaper[panel_attr] = entry self.change_page(self.indexOf(self.panel.is_page)) - def _parse_shaper_csv(self, file_path: str) -> list: + def _parse_shaper_csv(self, file_path: str) -> list: results = [] try: with open(file_path, newline="") as csvfile: @@ -551,11 +556,12 @@ def _parse_shaper_csv(self, file_path: str) -> list: ) except FileNotFoundError: ... - except csv.Error as e: + except csv.Error: ... return results def apply_input_shaper_selection(self) -> None: + """Apply input shaper results""" if not (checked_button := self.panel.is_btn_group.checkedButton()): return selected_name = checked_button.objectName() @@ -575,6 +581,7 @@ def apply_input_shaper_selection(self) -> None: self.change_page(self.indexOf(self.panel.utilities_page)) def axis_maintenance(self, axis: str) -> None: + """Routine, checks axis movement for printer debugging""" self.current_process = Process.AXIS_MAINTENANCE self.current_object = axis self.run_gcode_signal.emit(f"G28 {axis.upper()}\nM400") @@ -605,10 +612,11 @@ def _run_axis_maintenance_gcode(self, axis: str): self.change_page(self.indexOf(self.panel.axes_page)) def troubleshoot_request(self) -> None: - self.troubleshoot_page.geometry_calc() + """Show troubleshoot page""" self.troubleshoot_page.show() def show_waiting_page(self, page_to_go_to: int, label: str, time_ms: int): + """Show placeholder page""" self.loadPage.label.setText(label) self.loadPage.show() QtCore.QTimer.singleShot(time_ms, lambda: self.change_page(page_to_go_to)) @@ -618,6 +626,7 @@ def _connect_page_change(self, button: QtWidgets.QWidget, page: QtWidgets.QWidge button.clicked.connect(lambda: self.change_page(self.indexOf(page))) def change_page(self, index: int): + """Request change page by index""" self.loadPage.hide() self.troubleshoot_page.hide() if index < self.count(): @@ -625,4 +634,5 @@ def change_page(self, index: int): @QtCore.pyqtSlot(name="request-back") def back_button(self) -> None: + """Request back""" self.request_back.emit() diff --git a/BlocksScreen/lib/panels/widgets/babystepPage.py b/BlocksScreen/lib/panels/widgets/babystepPage.py index 4df7403e..b0fdfdca 100644 --- a/BlocksScreen/lib/panels/widgets/babystepPage.py +++ b/BlocksScreen/lib/panels/widgets/babystepPage.py @@ -34,11 +34,13 @@ def __init__(self, parent) -> None: self.bbp_nozzle_offset_05.toggled.connect(self.handle_z_offset_change) self.bbp_nozzle_offset_1.toggled.connect(self.handle_z_offset_change) - self.savebutton.clicked.connect(self.savevalue) + self.savebutton.clicked.connect(self.save_value) @QtCore.pyqtSlot(name="on_move_nozzle_close") def on_move_nozzle_close(self) -> None: - """Move the nozzle closer to the print plate by the amount set in **` self._z_offset`**""" + """Move the nozzle closer to the print plate + by the amount set in **` self._z_offset`** + """ self.run_gcode.emit( f"SET_GCODE_OFFSET Z_ADJUST=-{self._z_offset}" # Z_ADJUST adds the value to the existing offset ) @@ -46,7 +48,9 @@ def on_move_nozzle_close(self) -> None: @QtCore.pyqtSlot(name="on_move_nozzle_away") def on_move_nozzle_away(self) -> None: - """Slot for Babystep button to get far from the bed by **` self._z_offset`** amount""" + """Slot for Babystep button to get far from the + bed by **` self._z_offset`** amount + """ self.run_gcode.emit( f"SET_GCODE_OFFSET Z_ADJUST=+{self._z_offset}" # Z_ADJUST adds the value to the existing offset ) @@ -69,30 +73,25 @@ def handle_z_offset_change(self) -> None: return self._z_offset = float(_sender.text()[:-3]) - def savevalue(self): + def save_value(self): + """Save new z offset value""" self.run_gcode.emit("Z_OFFSET_APPLY_PROBE") self.savebutton.setVisible(False) - self.bbp_z_offset_title_label.setText( - self.bbp_z_offset_current_value.text() - ) - - return + self.bbp_z_offset_title_label.setText(self.bbp_z_offset_current_value.text()) def on_gcode_move_update(self, name: str, value: list) -> None: + """Handle gcode move updates""" if not value: return if name == "homing_origin": self._z_offset_text = value[2] - self.bbp_z_offset_current_value.setText( - f"Z: {self._z_offset_text:.3f}mm" - ) + self.bbp_z_offset_current_value.setText(f"Z: {self._z_offset_text:.3f}mm") if self.bbp_z_offset_title_label.text() == "smth": - self.bbp_z_offset_title_label.setText( - f"Z: {self._z_offset_text:.3f}mm" - ) + self.bbp_z_offset_title_label.setText(f"Z: {self._z_offset_text:.3f}mm") def setupUI(self): + """Setup babystep page ui""" self.bbp_offset_value_selector_group = QtWidgets.QButtonGroup(self) self.bbp_offset_value_selector_group.setExclusive(True) sizePolicy = QtWidgets.QSizePolicy( @@ -148,9 +147,7 @@ def setupUI(self): self.savebutton.setGeometry(QtCore.QRect(460, 340, 200, 60)) self.savebutton.setText("Save?") self.savebutton.setObjectName("savebutton") - self.savebutton.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/save.svg") - ) + self.savebutton.setPixmap(QtGui.QPixmap(":/ui/media/btn_icons/save.svg")) self.savebutton.setVisible(False) font = QtGui.QFont() font.setPointSize(15) @@ -178,16 +175,13 @@ def setupUI(self): self.babystep_back_btn.setMaximumSize(QtCore.QSize(60, 60)) self.babystep_back_btn.setText("") self.babystep_back_btn.setFlat(True) - self.babystep_back_btn.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/back.svg") - ) + self.babystep_back_btn.setPixmap(QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) self.babystep_back_btn.setObjectName("babystep_back_btn") self.bbp_header_layout.addWidget( self.babystep_back_btn, 0, - QtCore.Qt.AlignmentFlag.AlignRight - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter, ) self.bbp_header_layout.setStretch(0, 1) self.verticalLayout.addLayout(self.bbp_header_layout) @@ -233,14 +227,11 @@ def setupUI(self): self.bbp_nozzle_offset_1.setFlat(True) self.bbp_nozzle_offset_1.setProperty("button_type", "") self.bbp_nozzle_offset_1.setObjectName("bbp_nozzle_offset_1") - self.bbp_offset_value_selector_group.addButton( - self.bbp_nozzle_offset_1 - ) + self.bbp_offset_value_selector_group.addButton(self.bbp_nozzle_offset_1) self.bbp_offset_steps_buttons.addWidget( self.bbp_nozzle_offset_1, 0, - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) # Line separator for 0.1mm - set size policy to expanding horizontally @@ -262,14 +253,11 @@ def setupUI(self): self.bbp_nozzle_offset_01.setFlat(True) self.bbp_nozzle_offset_01.setProperty("button_type", "") self.bbp_nozzle_offset_01.setObjectName("bbp_nozzle_offset_01") - self.bbp_offset_value_selector_group.addButton( - self.bbp_nozzle_offset_01 - ) + self.bbp_offset_value_selector_group.addButton(self.bbp_nozzle_offset_01) self.bbp_offset_steps_buttons.addWidget( self.bbp_nozzle_offset_01, 0, - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) # 0.05mm button @@ -289,14 +277,11 @@ def setupUI(self): self.bbp_nozzle_offset_05.setFlat(True) self.bbp_nozzle_offset_05.setProperty("button_type", "") self.bbp_nozzle_offset_05.setObjectName("bbp_nozzle_offset_05") - self.bbp_offset_value_selector_group.addButton( - self.bbp_nozzle_offset_05 - ) + self.bbp_offset_value_selector_group.addButton(self.bbp_nozzle_offset_05) self.bbp_offset_steps_buttons.addWidget( self.bbp_nozzle_offset_05, 0, - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) # 0.025mm button @@ -316,22 +301,17 @@ def setupUI(self): self.bbp_nozzle_offset_025.setFlat(True) self.bbp_nozzle_offset_025.setProperty("button_type", "") self.bbp_nozzle_offset_025.setObjectName("bbp_nozzle_offset_025") - self.bbp_offset_value_selector_group.addButton( - self.bbp_nozzle_offset_025 - ) + self.bbp_offset_value_selector_group.addButton(self.bbp_nozzle_offset_025) self.bbp_offset_steps_buttons.addWidget( self.bbp_nozzle_offset_025, 0, - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) # Line separator for 0.025mm - set size policy to expanding horizontally # Set the layout for the group box - self.bbp_offset_steps_buttons_group_box.setLayout( - self.bbp_offset_steps_buttons - ) + self.bbp_offset_steps_buttons_group_box.setLayout(self.bbp_offset_steps_buttons) # Add the group box to the main content horizontal layout FIRST for left placement self.main_content_horizontal_layout.addWidget( self.bbp_offset_steps_buttons_group_box @@ -339,9 +319,7 @@ def setupUI(self): # Graphic and Current Value Frame (This will now be in the MIDDLE) self.frame_2 = QtWidgets.QFrame(parent=self) - sizePolicy.setHeightForWidth( - self.frame_2.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.frame_2.sizePolicy().hasHeightForWidth()) self.frame_2.setSizePolicy(sizePolicy) self.frame_2.setMinimumSize(QtCore.QSize(350, 160)) self.frame_2.setMaximumSize(QtCore.QSize(350, 160)) @@ -357,18 +335,14 @@ def setupUI(self): QtGui.QPixmap(":/graphics/media/graphics/babystep_graphic.png") ) self.bbp_babystep_graphic.setScaledContents(False) - self.bbp_babystep_graphic.setAlignment( - QtCore.Qt.AlignmentFlag.AlignCenter - ) + self.bbp_babystep_graphic.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.bbp_babystep_graphic.setObjectName("bbp_babystep_graphic") # === NEW LABEL ADDED HERE === # This is the title label that appears above the red value box. self.bbp_z_offset_title_label = QtWidgets.QLabel(parent=self) # Position it just above the red box. Red box is at y=70, so y=40 is appropriate. - self.bbp_z_offset_title_label.setGeometry( - QtCore.QRect(100, 40, 200, 30) - ) + self.bbp_z_offset_title_label.setGeometry(QtCore.QRect(100, 40, 200, 30)) font = QtGui.QFont() font.setPointSize(12) @@ -385,9 +359,7 @@ def setupUI(self): # === END OF NEW LABEL === self.bbp_z_offset_current_value = BlocksLabel(parent=self.frame_2) - self.bbp_z_offset_current_value.setGeometry( - QtCore.QRect(100, 70, 200, 60) - ) + self.bbp_z_offset_current_value.setGeometry(QtCore.QRect(100, 70, 200, 60)) sizePolicy.setHeightForWidth( self.bbp_z_offset_current_value.sizePolicy().hasHeightForWidth() ) @@ -407,15 +379,12 @@ def setupUI(self): self.bbp_z_offset_current_value.setAlignment( QtCore.Qt.AlignmentFlag.AlignCenter ) - self.bbp_z_offset_current_value.setObjectName( - "bbp_z_offset_current_value" - ) + self.bbp_z_offset_current_value.setObjectName("bbp_z_offset_current_value") # Add graphic frame AFTER the offset buttons group box self.main_content_horizontal_layout.addWidget( self.frame_2, 0, - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) # Move Buttons Layout (This will now be on the RIGHT) @@ -423,9 +392,7 @@ def setupUI(self): self.bbp_buttons_layout.setContentsMargins(5, 5, 5, 5) self.bbp_buttons_layout.setObjectName("bbp_buttons_layout") self.bbp_mvup = IconButton(parent=self) - sizePolicy.setHeightForWidth( - self.bbp_mvup.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.bbp_mvup.sizePolicy().hasHeightForWidth()) self.bbp_mvup.setSizePolicy(sizePolicy) self.bbp_mvup.setMinimumSize(QtCore.QSize(80, 80)) self.bbp_mvup.setMaximumSize(QtCore.QSize(80, 80)) @@ -442,9 +409,7 @@ def setupUI(self): self.bbp_mvup, 0, QtCore.Qt.AlignmentFlag.AlignRight ) self.bbp_mvdown = IconButton(parent=self) - sizePolicy.setHeightForWidth( - self.bbp_mvdown.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.bbp_mvdown.sizePolicy().hasHeightForWidth()) self.bbp_mvdown.setSizePolicy(sizePolicy) self.bbp_mvdown.setMinimumSize(QtCore.QSize(80, 80)) self.bbp_mvdown.setMaximumSize(QtCore.QSize(80, 80)) diff --git a/BlocksScreen/lib/panels/widgets/confirmPage.py b/BlocksScreen/lib/panels/widgets/confirmPage.py index 525b0bbe..95a12579 100644 --- a/BlocksScreen/lib/panels/widgets/confirmPage.py +++ b/BlocksScreen/lib/panels/widgets/confirmPage.py @@ -43,6 +43,7 @@ def __init__(self, parent) -> None: @QtCore.pyqtSlot(str, dict, name="on_show_widget") def on_show_widget(self, text: str, filedata: dict | None = None) -> None: + """Handle widget show""" directory = os.path.dirname(text) filename = os.path.basename(text) self.directory = directory @@ -101,11 +102,13 @@ def estimate_print_time(self, seconds: int) -> list: return [days, hours, minutes, seconds] def hide(self): + """Hide widget""" self.directory = "" self.filename = "" return super().hide() def paintEvent(self, event: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" if not self.isVisible(): self.directory = "" self.filename = "" @@ -144,14 +147,13 @@ def paintEvent(self, event: QtGui.QPaintEvent) -> None: self._scene.setSceneRect(graphics_rect) def showEvent(self, a0: QtGui.QShowEvent) -> None: + """Re-implemented method, Handle widget show event""" if not self.thumbnail: self.cf_thumbnail.close() return super().showEvent(a0) - def hideEvent(self, a0: QtGui.QHideEvent) -> None: - return super().hideEvent(a0) - def setupUI(self) -> None: + """Setup widget ui""" sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, diff --git a/BlocksScreen/lib/panels/widgets/connectionPage.py b/BlocksScreen/lib/panels/widgets/connectionPage.py index 7760cca5..33bd0576 100644 --- a/BlocksScreen/lib/panels/widgets/connectionPage.py +++ b/BlocksScreen/lib/panels/widgets/connectionPage.py @@ -3,20 +3,16 @@ from events import KlippyDisconnected, KlippyReady, KlippyShutdown from lib.moonrakerComm import MoonWebSocket from lib.ui.connectionWindow_ui import Ui_ConnectivityForm -from PyQt6 import QtCore, QtWidgets, QtGui +from PyQt6 import QtCore, QtWidgets class ConnectionPage(QtWidgets.QFrame): text_updated = QtCore.pyqtSignal(int, name="connection_text_updated") - retry_connection_clicked = QtCore.pyqtSignal( - name="retry_connection_clicked" - ) + retry_connection_clicked = QtCore.pyqtSignal(name="retry_connection_clicked") wifi_button_clicked = QtCore.pyqtSignal(name="call_network_page") reboot_clicked = QtCore.pyqtSignal(name="reboot_clicked") restart_klipper_clicked = QtCore.pyqtSignal(name="restart_klipper_clicked") - firmware_restart_clicked = QtCore.pyqtSignal( - name="firmware_restart_clicked" - ) + firmware_restart_clicked = QtCore.pyqtSignal(name="firmware_restart_clicked") def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket, /): super().__init__(parent) @@ -31,7 +27,7 @@ def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket, /): self.message = None self.dot_timer = QtCore.QTimer(self) self.dot_timer.setInterval(1000) - self.dot_timer.timeout.connect(self.add_dot) + self.dot_timer.timeout.connect(self._add_dot) self.installEventFilter(self.parent()) @@ -51,6 +47,7 @@ def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket, /): self.ws.klippy_state_signal.connect(self.on_klippy_state) def show_panel(self, reason: str | None = None): + """Show widget""" self.show() if reason is not None: self.text_update(reason) @@ -58,12 +55,9 @@ def show_panel(self, reason: str | None = None): self.text_update() return False - @QtCore.pyqtSlot(bool, name="klippy_connection") - def on_klippy_connection(self, state: bool): - pass - @QtCore.pyqtSlot(str, name="on_klippy_state") def on_klippy_state(self, state: str): + """Handle klippy state changes""" if state == "error": self.panel.connectionTextBox.setText("Klipper Connection Error") if not self.isVisible(): @@ -87,22 +81,24 @@ def on_klippy_state(self, state: str): @QtCore.pyqtSlot(int, name="on_websocket_connecting") @QtCore.pyqtSlot(str, name="on_websocket_connecting") def on_websocket_connecting(self, attempt: int): + """Handle websocket connecting state""" self.text_update(attempt) @QtCore.pyqtSlot(name="on_websocket_connection_achieved") def on_websocket_connection_achieved(self): - self.panel.connectionTextBox.setText( - "Moonraker Connected\n Klippy not ready" - ) + """Handle websocket connected state""" + self.panel.connectionTextBox.setText("Moonraker Connected\n Klippy not ready") self.hide() @QtCore.pyqtSlot(name="on_websocket_connection_lost") def on_websocket_connection_lost(self): + """Handle websocket connection lost state""" if not self.isVisible(): self.show() self.text_update(text="Websocket lost") def text_update(self, text: int | str | None = None): + """Update widget text""" if self.state == "shutdown" and self.message is not None: return False @@ -142,7 +138,7 @@ def text_update(self, text: int | str | None = None): return False - def add_dot(self): + def _add_dot(self): if self.state == "shutdown" and self.message is not None: self.dot_timer.stop() return False @@ -156,13 +152,13 @@ def add_dot(self): @QtCore.pyqtSlot(str, str, name="webhooks_update") def webhook_update(self, state: str, message: str): + """Handle websocket webhook updates""" self.state = state self.message = message self.text_update() - def eventFilter( - self, object: QtCore.QObject, event: QtCore.QEvent - ) -> bool: + def eventFilter(self, object: QtCore.QObject, event: QtCore.QEvent) -> bool: + """Re-implemented method, filter events""" if event.type() == KlippyDisconnected.type(): if not self.isVisible(): self.panel.connectionTextBox.setText("Klippy Disconnected") diff --git a/BlocksScreen/lib/panels/widgets/dialogPage.py b/BlocksScreen/lib/panels/widgets/dialogPage.py index 7ed3d42c..65f7c727 100644 --- a/BlocksScreen/lib/panels/widgets/dialogPage.py +++ b/BlocksScreen/lib/panels/widgets/dialogPage.py @@ -2,31 +2,28 @@ class DialogPage(QtWidgets.QDialog): - button_clicked = QtCore.pyqtSignal( - str - ) # Signal to emit which button was clicked + button_clicked = QtCore.pyqtSignal(str) # Signal to emit which button was clicked def __init__( self, parent: QtWidgets.QWidget, ) -> None: super().__init__(parent) - self.setWindowFlags( - QtCore.Qt.WindowType.Popup - | QtCore.Qt.WindowType.FramelessWindowHint + QtCore.Qt.WindowType.Popup | QtCore.Qt.WindowType.FramelessWindowHint ) self.setAttribute( QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True ) # Make background transparent - - self.setupUI() + self._setupUI() self.repaint() def set_message(self, message: str) -> None: + """Set dialog text message""" self.label.setText(message) - def geometry_calc(self) -> None: + def _geometry_calc(self) -> None: + """Calculate dialog widget position relative to the window""" app_instance = QtWidgets.QApplication.instance() main_window = app_instance.activeWindow() if app_instance else None if main_window is None and app_instance: @@ -42,14 +39,13 @@ def geometry_calc(self) -> None: self.testwidth = width self.testheight = height x = int(main_window.geometry().x() + (main_window.width() - width) / 2) - y = int( - main_window.geometry().y() + (main_window.height() - height) / 2 - ) + y = int(main_window.geometry().y() + (main_window.height() - height) / 2) self.setGeometry(x, y, width, height) def paintEvent(self, event: QtGui.QPaintEvent) -> None: - self.geometry_calc() + """Re-implemented method, paint widget""" + self._geometry_calc() painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) @@ -74,10 +70,10 @@ def paintEvent(self, event: QtGui.QPaintEvent) -> None: painter.end() def sizeHint(self) -> QtCore.QSize: + """Re-implemented method, widget size hint""" popup_width = int(self.geometry().width()) popup_height = int(self.geometry().height()) # Centering logic - popup_x = self.x() popup_y = self.y() + (self.height() - popup_height) // 2 self.move(popup_x, popup_y) @@ -85,10 +81,8 @@ def sizeHint(self) -> QtCore.QSize: self.setMinimumSize(popup_width, popup_height) return super().sizeHint() - def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: - return - def resizeEvent(self, event: QtGui.QResizeEvent) -> None: + """Re-implemented method, handle resize event""" super().resizeEvent(event) label_width = self.testwidth @@ -112,10 +106,11 @@ def resizeEvent(self, event: QtGui.QResizeEvent) -> None: ) def show(self) -> None: - self.geometry_calc() + """Re-implemented method, show widget""" + self._geometry_calc() return super().show() - def setupUI(self) -> None: + def _setupUI(self) -> None: self.label = QtWidgets.QLabel("Test", self) font = QtGui.QFont() font.setPointSize(25) @@ -166,17 +161,12 @@ def setupUI(self) -> None: ) # Connect button signals - self.confirm_button.clicked.connect( - lambda: self.on_button_clicked("Confirm") - ) - self.cancel_button.clicked.connect( - lambda: self.on_button_clicked("Cancel") - ) + self.confirm_button.clicked.connect(lambda: self.on_button_clicked("Confirm")) + self.cancel_button.clicked.connect(lambda: self.on_button_clicked("Cancel")) def on_button_clicked(self, button_name: str) -> None: - self.button_clicked.emit( - button_name - ) # Emit the signal with the button name + """Handle dialog buttons clicked""" + self.button_clicked.emit(button_name) # Emit the signal with the button name if button_name == "Confirm": self.accept() # Close the dialog with an accepted state elif button_name == "Cancel": diff --git a/BlocksScreen/lib/panels/widgets/fansPage.py b/BlocksScreen/lib/panels/widgets/fansPage.py index 5dffba42..925c0230 100644 --- a/BlocksScreen/lib/panels/widgets/fansPage.py +++ b/BlocksScreen/lib/panels/widgets/fansPage.py @@ -1,13 +1,15 @@ from PyQt6 import QtCore, QtWidgets -import typing +import typing -class FansPage(QtWidgets.QWidget): +class FansPage(QtWidgets.QWidget): def __init__( - self, parent: typing.Optional["QtWidgets.QWidget"], flags: typing.Optional["QtCore.Qt.WindowType"] + self, + parent: typing.Optional["QtWidgets.QWidget"], + flags: typing.Optional["QtCore.Qt.WindowType"], ) -> None: - if parent is not None and flags is not None: + if parent is not None and flags is not None: super(FansPage, self).__init__(parent, flags) - else : - super(FansPage, self).__init__() \ No newline at end of file + else: + super(FansPage, self).__init__() diff --git a/BlocksScreen/lib/panels/widgets/filesPage.py b/BlocksScreen/lib/panels/widgets/filesPage.py index 238c5eed..8de160c0 100644 --- a/BlocksScreen/lib/panels/widgets/filesPage.py +++ b/BlocksScreen/lib/panels/widgets/filesPage.py @@ -27,8 +27,8 @@ class FilesPage(QtWidgets.QWidget): request_file_list: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( [], [str], name="api-get-files-list" ) - request_file_metadata: typing.ClassVar[QtCore.pyqtSignal] = ( - QtCore.pyqtSignal(str, name="api-get-gcode-metadata") + request_file_metadata: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="api-get-gcode-metadata" ) file_list: list = [] files_data: dict = {} @@ -43,9 +43,7 @@ def __init__(self, parent) -> None: self.ReloadButton.clicked.connect( lambda: self.request_dir_info[str].emit(self.curr_dir) ) - self.listWidget.verticalScrollBar().valueChanged.connect( - self._handle_scrollbar - ) + self.listWidget.verticalScrollBar().valueChanged.connect(self._handle_scrollbar) self.scrollbar.valueChanged.connect(self._handle_scrollbar) self.scrollbar.valueChanged.connect( lambda value: self.listWidget.verticalScrollBar().setValue(value) @@ -54,31 +52,36 @@ def __init__(self, parent) -> None: @QtCore.pyqtSlot(name="reset-dir") def reset_dir(self) -> None: + """Reset current directory""" self.curr_dir = "" self.request_dir_info[str].emit(self.curr_dir) def showEvent(self, a0: QtGui.QShowEvent) -> None: + """Re-implemented method, handle widget show event""" self._build_file_list() return super().showEvent(a0) @QtCore.pyqtSlot(list, name="on-file-list") def on_file_list(self, file_list: list) -> None: + """Handle receiving files list from websocket""" self.files_data.clear() self.file_list = file_list - # if self.isVisible(): # Only build the list when directories come - # self._build_file_list() @QtCore.pyqtSlot(list, name="on-dirs") def on_directories(self, directories_data: list) -> None: + """Handle receiving available directories from websocket""" self.directories = directories_data if self.isVisible(): self._build_file_list() @QtCore.pyqtSlot(str, name="on-delete-file") - def on_delete_file(self, filename: str) -> None: ... + def on_delete_file(self, filename: str) -> None: + """Handle file deleted""" + ... @QtCore.pyqtSlot(dict, name="on-fileinfo") def on_fileinfo(self, filedata: dict) -> None: + """Handle receive file information/metadata""" if not filedata or not self.isVisible(): return filename = filedata.get("filename", "") @@ -86,11 +89,7 @@ def on_fileinfo(self, filedata: dict) -> None: return self.files_data.update({f"{filename}": filedata}) estimated_time = filedata.get("estimated_time", 0) - seconds = ( - int(estimated_time) - if isinstance(estimated_time, (int, float)) - else 0 - ) + seconds = int(estimated_time) if isinstance(estimated_time, (int, float)) else 0 filament_type = ( filedata.get("filament_type", "Unknown filament") if filedata.get("filament_type", "Unknown filament") != -1.0 @@ -110,9 +109,7 @@ def on_fileinfo(self, filedata: dict) -> None: else: time_str = f"{minutes}m" - list_items = [ - self.listWidget.item(i) for i in range(self.listWidget.count()) - ] + list_items = [self.listWidget.item(i) for i in range(self.listWidget.count())] if not list_items: return for list_item in list_items: @@ -131,17 +128,13 @@ def _fileItemClicked(self, item: QtWidgets.QListWidgetItem) -> None: widget = self.listWidget.itemWidget(item) for file in self.file_list: path = ( - file.get("path") - if "path" in file.keys() - else file.get("filename") + file.get("path") if "path" in file.keys() else file.get("filename") ) if not path: return if widget.text() in path: file_path = ( - path - if not self.curr_dir - else str(self.curr_dir + "/" + path) + path if not self.curr_dir else str(self.curr_dir + "/" + path) ) self.file_selected.emit( str(file_path.removeprefix("/")), @@ -151,9 +144,7 @@ def _fileItemClicked(self, item: QtWidgets.QListWidgetItem) -> None: ) @QtCore.pyqtSlot(QtWidgets.QListWidgetItem, str, name="dir-item-clicked") - def _dirItemClicked( - self, item: QtWidgets.QListWidgetItem, directory: str - ) -> None: + def _dirItemClicked(self, item: QtWidgets.QListWidgetItem, directory: str) -> None: self.curr_dir = self.curr_dir + directory self.request_dir_info[str].emit(self.curr_dir) @@ -172,9 +163,7 @@ def _build_file_list(self) -> None: if dir_data.get("dirname").startswith("."): continue self._add_directory_list_item(dir_data) - sorted_list = sorted( - self.file_list, key=lambda x: x["modified"], reverse=True - ) + sorted_list = sorted(self.file_list, key=lambda x: x["modified"], reverse=True) for item in sorted_list: self._add_file_list_item(item) self._add_spacer() @@ -188,9 +177,7 @@ def _add_directory_list_item(self, dir_data: dict) -> None: return button = ListCustomButton() button.setText(str(dir_data.get("dirname"))) - button.setSecondPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/folderIcon.svg") - ) + button.setSecondPixmap(QtGui.QPixmap(":/ui/media/btn_icons/folderIcon.svg")) button.setMinimumSize(600, 80) button.setMaximumSize(700, 80) button.setLeftFontSize(17) @@ -199,16 +186,12 @@ def _add_directory_list_item(self, dir_data: dict) -> None: list_item.setSizeHint(button.sizeHint()) self.listWidget.addItem(list_item) self.listWidget.setItemWidget(list_item, button) - button.clicked.connect( - lambda: self._dirItemClicked(list_item, "/" + dir_name) - ) + button.clicked.connect(lambda: self._dirItemClicked(list_item, "/" + dir_name)) def _add_back_folder_entry(self) -> None: button = ListCustomButton() button.setText("Go Back") - button.setSecondPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/back_folder.svg") - ) + button.setSecondPixmap(QtGui.QPixmap(":/ui/media/btn_icons/back_folder.svg")) button.setMinimumSize(600, 80) button.setMaximumSize(700, 80) button.setLeftFontSize(17) @@ -241,9 +224,7 @@ def _add_file_list_item(self, file_data_item) -> None: return button = ListCustomButton() button.setText(name[:-6]) - button.setPixmap( - QtGui.QPixmap(":/arrow_icons/media/btn_icons/right_arrow.svg") - ) + button.setPixmap(QtGui.QPixmap(":/arrow_icons/media/btn_icons/right_arrow.svg")) button.setMinimumSize(600, 80) button.setMaximumSize(700, 80) button.setLeftFontSize(17) @@ -274,8 +255,7 @@ def _add_placeholder(self) -> None: placeholder_label.setFont(font) placeholder_label.setStyleSheet("color: gray;") placeholder_label.setAlignment( - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter ) placeholder_label.setMinimumSize( QtCore.QSize(self.listWidget.width(), self.listWidget.height()) @@ -295,15 +275,9 @@ def _handle_scrollbar(self, value): self.scrollbar.blockSignals(False) def _setup_scrollbar(self) -> None: - self.scrollbar.setMinimum( - self.listWidget.verticalScrollBar().minimum() - ) - self.scrollbar.setMaximum( - self.listWidget.verticalScrollBar().maximum() - ) - self.scrollbar.setPageStep( - self.listWidget.verticalScrollBar().pageStep() - ) + self.scrollbar.setMinimum(self.listWidget.verticalScrollBar().minimum()) + self.scrollbar.setMaximum(self.listWidget.verticalScrollBar().maximum()) + self.scrollbar.setPageStep(self.listWidget.verticalScrollBar().pageStep()) self.scrollbar.show() def _setupUI(self): @@ -321,9 +295,7 @@ def _setupUI(self): self.setFont(font) self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self.setAutoFillBackground(False) - self.setStyleSheet( - "#file_page{\n background-color: transparent;\n}" - ) + self.setStyleSheet("#file_page{\n background-color: transparent;\n}") self.verticalLayout_5 = QtWidgets.QVBoxLayout(self) self.verticalLayout_5.setObjectName("verticalLayout_5") self.fp_header_layout = QtWidgets.QHBoxLayout() diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index e632dc2f..bb9754ea 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -12,12 +12,16 @@ class ClickableGraphicsView(QtWidgets.QGraphicsView): + """Re-implementation of QGraphicsView that adds clicked signal""" + clicked = QtCore.pyqtSignal() def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: + """Filter mouse press events""" if event.button() == QtCore.Qt.MouseButton.LeftButton: self.clicked.emit() + return True # Issue event handled super(ClickableGraphicsView, self).mousePressEvent(event) @@ -59,7 +63,7 @@ def __init__(self, parent) -> None: super().__init__(parent) self.canceldialog = dialogPage.DialogPage(self) - self.setupUI() + self._setupUI() self.tune_menu_btn.clicked.connect(self.tune_clicked.emit) self.pause_printing_btn.clicked.connect(self.pause_resume_print) self.stop_printing_btn.clicked.connect(self.handleCancel) @@ -77,6 +81,7 @@ def __init__(self, parent) -> None: self.CBVBigThumbnail.installEventFilter(self) def eventFilter(self, source, event): + """Re-implemented method, filter events""" if ( source == self.CBVSmallThumbnail and event.type() == QtCore.QEvent.Type.MouseButtonPress @@ -95,6 +100,7 @@ def eventFilter(self, source, event): @QtCore.pyqtSlot(name="show-thumbnail") def showthumbnail(self): + """Show print job fullscreen thumbnail""" self.contentWidget.hide() self.progressWidget.hide() self.headerWidget.hide() @@ -104,6 +110,7 @@ def showthumbnail(self): @QtCore.pyqtSlot(name="hide-thumbnail") def hidethumbnail(self): + """Hide print job fullscreen thumbnail""" self.contentWidget.show() self.progressWidget.show() self.headerWidget.show() @@ -113,7 +120,7 @@ def hidethumbnail(self): @QtCore.pyqtSlot(name="handle-cancel") def handleCancel(self) -> None: - """Handle the cancel print job dialog""" + """Handle cancel print job dialog""" self.canceldialog.set_message( "Are you sure you \n want to cancel \n this print job?" ) @@ -159,6 +166,7 @@ def on_print_start(self, file: str, thumbnails: list) -> None: @QtCore.pyqtSlot(dict, name="on_fileinfo") def on_fileinfo(self, fileinfo: dict) -> None: + """Handle received file information/metadata""" self.total_layers = str(fileinfo.get("layer_count", "?")) self.layer_display_button.setText("?") if ( @@ -175,6 +183,7 @@ def on_fileinfo(self, fileinfo: dict) -> None: @QtCore.pyqtSlot(name="pause_resume_print") def pause_resume_print(self) -> None: + """Handle pause/resume print job""" if not getattr(self, "_pause_locked", False): self._pause_locked = True self.pause_printing_btn.setEnabled(False) @@ -233,7 +242,6 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: self.file_metadata.clear() self.hide_request.emit() - if hasattr(events, str("Print" + value.capitalize())): event_obj = getattr(events, str("Print" + value.capitalize())) event = event_obj(self._current_file_name, self.file_metadata) @@ -278,13 +286,7 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: @QtCore.pyqtSlot(str, list, name="on_gcode_move_update") def on_gcode_move_update(self, field: str, value: list) -> None: - # """Processes the information that comes from the printer object "gcode_move" - - # Args: - # field (str): Name of the updated field - # value (list): New value for the field - # """ - + """Handle gcode move""" if isinstance(value, list): if "gcode_position" in field: # Without offsets if self._internal_print_status == "printing": @@ -307,7 +309,7 @@ def on_gcode_move_update(self, field: str, value: list) -> None: @QtCore.pyqtSlot(str, float, name="virtual_sdcard_update") @QtCore.pyqtSlot(str, bool, name="virtual_sdcard_update") def virtual_sdcard_update(self, field: str, value: float | bool) -> None: - """Slot for incoming printer object virtual_sdcard information update + """Handle virtual sdcard Args: field (str): Name of the updated field on the virtual_sdcard object @@ -321,6 +323,7 @@ def virtual_sdcard_update(self, field: str, value: float | bool) -> None: self.printing_progress_bar.setValue(self.print_progress) def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" _scene = QtWidgets.QGraphicsScene() if not self.smalthumbnail.isNull(): _graphics_rect = self.CBVSmallThumbnail.rect().toRectF() @@ -380,7 +383,8 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: _scene.addItem(_item_scaled) self.CBVBigThumbnail.setScene(_scene) - def setupUI(self) -> None: + def _setupUI(self) -> None: + """Setup widget ui""" sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding, diff --git a/BlocksScreen/lib/panels/widgets/keyboardPage.py b/BlocksScreen/lib/panels/widgets/keyboardPage.py index 07c9b338..4f1d13d8 100644 --- a/BlocksScreen/lib/panels/widgets/keyboardPage.py +++ b/BlocksScreen/lib/panels/widgets/keyboardPage.py @@ -4,7 +4,7 @@ class CustomQwertyKeyboard(QtWidgets.QWidget): - """A custom numpad for inserting integer values""" + """A custom keyboard for inserting integer values""" value_selected = QtCore.pyqtSignal(str, name="value_selected") request_back = QtCore.pyqtSignal(name="request_back") @@ -14,7 +14,7 @@ def __init__( parent, ) -> None: super().__init__(parent) - self.setupUi() + self._setupUi() self.current_value: str = "" self.symbolsrun = False self.setCursor( @@ -54,8 +54,8 @@ def __init__( self.inserted_value.setText("") - self.K_keychange.clicked.connect(self.handle_checkbuttons) - self.K_shift.clicked.connect(self.handle_checkbuttons) + self.K_keychange.clicked.connect(self.handle_keyboard_layout) + self.K_shift.clicked.connect(self.handle_keyboard_layout) self.numpad_back_btn.clicked.connect(lambda: self.request_back.emit()) @@ -75,9 +75,10 @@ def __init__( color: white; } """) - self.handle_checkbuttons() + self.handle_keyboard_layout() - def handle_checkbuttons(self): + def handle_keyboard_layout(self): + """Verifies if shift is toggled, changes layout accordingly""" shift = self.K_shift.isChecked() keychange = self.K_keychange.isChecked() @@ -207,7 +208,7 @@ def handle_checkbuttons(self): self.K_shift.setText("Shift") def value_inserted(self, value: str) -> None: - """Handle number insertion on the numpad + """Handle value insertion on the keyboard Args: value (int | str): value @@ -235,10 +236,11 @@ def value_inserted(self, value: str) -> None: self.inserted_value.setText(str(self.current_value)) def set_value(self, value: str) -> None: + """Set keyboard value""" self.current_value = value self.inserted_value.setText(value) - def setupUi(self): + def _setupUi(self): self.setObjectName("self") self.setEnabled(True) self.resize(800, 480) diff --git a/BlocksScreen/lib/panels/widgets/loadPage.py b/BlocksScreen/lib/panels/widgets/loadPage.py index d4ddcbcf..f5fd7963 100644 --- a/BlocksScreen/lib/panels/widgets/loadPage.py +++ b/BlocksScreen/lib/panels/widgets/loadPage.py @@ -5,9 +5,8 @@ class LoadScreen(QtWidgets.QDialog): class AnimationGIF(enum.Enum): - # [x]: WATHERE ARE NO GIFS IN LOADSCREEN PLEASE REMEMBER THIS IM WARNING + """Animation type""" - # TODO : add more types into LoadScreen DEFAULT = None PLACEHOLDER = "" @@ -31,10 +30,9 @@ def __init__( ) self.setWindowFlags( - QtCore.Qt.WindowType.Popup - | QtCore.Qt.WindowType.FramelessWindowHint + QtCore.Qt.WindowType.Popup | QtCore.Qt.WindowType.FramelessWindowHint ) - self.setupUI() + self._setupUI() config: BlocksScreenConfig = get_configparser() try: if config: @@ -61,10 +59,11 @@ def __init__( self.repaint() def set_status_message(self, message: str) -> None: + """Set widget status message""" self.label.setText(message) - def geometry_calc(self) -> None: - # REFACTOR: find another way to get mainwindow geometry , this version consumes too much ram + def _geometry_calc(self) -> None: + """Calculate widget position relative to the screen""" app_instance = QtWidgets.QApplication.instance() main_window = app_instance.activeWindow() if app_instance else None if main_window is None and app_instance: @@ -79,6 +78,7 @@ def geometry_calc(self) -> None: self.setGeometry(x, y, width, height) def close(self) -> bool: + """Re-implemented method, close widget""" self.timer.stop() self.label.setText("Loading...") self._angle = 0 @@ -102,10 +102,10 @@ def _update_animation(self) -> None: self.update() def sizeHint(self) -> QtCore.QSize: + """Re-implemented method, size hint""" popup_width = int(self.geometry().width()) popup_height = int(self.geometry().height()) # Centering logic - popup_x = self.x() popup_y = self.y() + (self.height() - popup_height) // 2 self.move(popup_x, popup_y) @@ -113,10 +113,8 @@ def sizeHint(self) -> QtCore.QSize: self.setMinimumSize(popup_width, popup_height) return super().sizeHint() - def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: - return - def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" painter = QtGui.QPainter(self) # loading circle draw if self.anim_type == LoadScreen.AnimationGIF.DEFAULT: @@ -124,12 +122,8 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: painter.setRenderHint( QtGui.QPainter.RenderHint.LosslessImageRendering, True ) - painter.setRenderHint( - QtGui.QPainter.RenderHint.SmoothPixmapTransform, True - ) - painter.setRenderHint( - QtGui.QPainter.RenderHint.TextAntialiasing, True - ) + painter.setRenderHint(QtGui.QPainter.RenderHint.SmoothPixmapTransform, True) + painter.setRenderHint(QtGui.QPainter.RenderHint.TextAntialiasing, True) pen = QtGui.QPen() pen.setWidth(8) pen.setColor(QtGui.QColor("#ffffff")) @@ -143,20 +137,17 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: painter.translate(center_x, center_y) painter.rotate(self._angle) - arc_rect = QtCore.QRectF( - -arc_size / 2, -arc_size / 2, arc_size, arc_size - ) + arc_rect = QtCore.QRectF(-arc_size / 2, -arc_size / 2, arc_size, arc_size) span_angle = int(self._span_angle * 16) painter.drawArc(arc_rect, 0, span_angle) def resizeEvent(self, event: QtGui.QResizeEvent) -> None: + """Re-implemented method, handle widget resize event""" super().resizeEvent(event) - label_width = self.width() label_height = 100 label_x = (self.width() - label_width) // 2 label_y = int(self.height() * 0.65) - margin = 20 # Center the GIF gifshow_width = self.width() - margin * 2 @@ -167,14 +158,15 @@ def resizeEvent(self, event: QtGui.QResizeEvent) -> None: self.label.setGeometry(label_x, label_y, label_width, label_height) def show(self) -> None: - self.geometry_calc() + """Re-implemented method, show widget""" + self._geometry_calc() # Start the animation timer only if no GIF is present if self.anim_type == LoadScreen.AnimationGIF.DEFAULT: self.timer.start() self.repaint() return super().show() - def setupUI(self) -> None: + def _setupUI(self) -> None: self.gifshow = QtWidgets.QLabel("", self) self.gifshow.setObjectName("gifshow") self.gifshow.setStyleSheet("background: transparent;") diff --git a/BlocksScreen/lib/panels/widgets/loadWidget.py b/BlocksScreen/lib/panels/widgets/loadWidget.py index 4001d900..9a5f3d68 100644 --- a/BlocksScreen/lib/panels/widgets/loadWidget.py +++ b/BlocksScreen/lib/panels/widgets/loadWidget.py @@ -1,8 +1,7 @@ - from PyQt6 import QtCore, QtGui, QtWidgets -class LoadingOverlayWidget(QtWidgets.QLabel): +class LoadingOverlayWidget(QtWidgets.QLabel): def __init__( self, parent: QtWidgets.QWidget, @@ -16,7 +15,7 @@ def __init__( self.max_length = 150.0 self.length_step = 2.5 - self.setupUI() + self._setupUI() self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self._update_animation) @@ -25,10 +24,11 @@ def __init__( self.repaint() def set_status_message(self, message: str) -> None: + """Set widget message""" self.label.setText(message) - def close(self) -> bool: + """Re-implemented method, close widget""" self.timer.stop() self.label.setText("Loading...") self._angle = 0 @@ -48,19 +48,13 @@ def _update_animation(self) -> None: self._is_span_growing = True self.update() - def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) - painter.setRenderHint( - QtGui.QPainter.RenderHint.LosslessImageRendering, True - ) - painter.setRenderHint( - QtGui.QPainter.RenderHint.SmoothPixmapTransform, True - ) - painter.setRenderHint( - QtGui.QPainter.RenderHint.TextAntialiasing, True - ) + painter.setRenderHint(QtGui.QPainter.RenderHint.LosslessImageRendering, True) + painter.setRenderHint(QtGui.QPainter.RenderHint.SmoothPixmapTransform, True) + painter.setRenderHint(QtGui.QPainter.RenderHint.TextAntialiasing, True) pen = QtGui.QPen() pen.setWidth(8) pen.setColor(QtGui.QColor("#ffffff")) @@ -74,35 +68,31 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: painter.translate(center_x, center_y) painter.rotate(self._angle) - arc_rect = QtCore.QRectF( - -arc_size / 2, -arc_size / 2, arc_size, arc_size - ) + arc_rect = QtCore.QRectF(-arc_size / 2, -arc_size / 2, arc_size, arc_size) span_angle = int(self._span_angle * 16) painter.drawArc(arc_rect, 0, span_angle) def resizeEvent(self, event: QtGui.QResizeEvent) -> None: + """Re-implemented method, handle resize event""" super().resizeEvent(event) - label_width = self.width() label_height = 100 label_x = (self.width() - label_width) // 2 label_y = int(self.height() * 0.65) - margin = 20 # Center the GIF gifshow_width = self.width() - margin * 2 gifshow_height = self.height() - (self.height() - label_y) - margin - self.gifshow.setGeometry(margin, margin, gifshow_width, gifshow_height) - self.label.setGeometry(label_x, label_y, label_width, label_height) def show(self) -> None: + """Re-implemented method, show widget""" self.timer.start() self.repaint() return super().show() - def setupUI(self) -> None: + def _setupUI(self) -> None: self.gifshow = QtWidgets.QLabel("", self) self.gifshow.setObjectName("gifshow") self.gifshow.setStyleSheet("background: transparent;") @@ -113,4 +103,4 @@ def setupUI(self) -> None: font.setPointSize(20) self.label.setFont(font) self.label.setStyleSheet("color: #ffffff; background: transparent;") - self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) \ No newline at end of file + self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) diff --git a/BlocksScreen/lib/panels/widgets/numpadPage.py b/BlocksScreen/lib/panels/widgets/numpadPage.py index dcfe6b5b..9674084d 100644 --- a/BlocksScreen/lib/panels/widgets/numpadPage.py +++ b/BlocksScreen/lib/panels/widgets/numpadPage.py @@ -17,7 +17,7 @@ def __init__( parent, ) -> None: super().__init__(parent) - self.setupUI() + self._setupUI() self.current_value: str = "0" self.name: str = "" self.min_value: int = 0 @@ -37,9 +37,7 @@ def __init__( self.numpad_enter.clicked.connect(lambda: self.value_inserted("enter")) self.numpad_clear.clicked.connect(lambda: self.value_inserted("clear")) self.numpad_back_btn.clicked.connect(self.back_button) - self.start_glow_animation.connect( - self.inserted_value.start_glow_animation - ) + self.start_glow_animation.connect(self.inserted_value.start_glow_animation) def value_inserted(self, value: str) -> None: """Handle number insertion on the numpad @@ -59,14 +57,8 @@ def value_inserted(self, value: str) -> None: if "enter" in value and self.current_value.isnumeric(): if len(self.current_value) == 0: self.current_value = "0" - if ( - self.min_value - <= int(self.current_value) - <= self.max_value - ): - self.value_selected.emit( - self.name, int(self.current_value) - ) + if self.min_value <= int(self.current_value) <= self.max_value: + self.value_selected.emit(self.name, int(self.current_value)) self.request_back.emit() elif "clear" in value: @@ -81,8 +73,9 @@ def value_inserted(self, value: str) -> None: self.inserted_value.glow_animation.stop() self.inserted_value.setText(str(self.current_value)) - + def back_button(self): + """Request back page""" self.request_back.emit() def set_name(self, name: str) -> None: @@ -93,24 +86,25 @@ def set_name(self, name: str) -> None: self.update() def set_value(self, value: int) -> None: + """Set numpad value""" self.current_value = str(value) self.inserted_value.setText(str(value)) def set_min_value(self, min_value: int) -> None: + """Set minimum allowed value""" self.min_value = min_value self.update_min_max_label() def set_max_value(self, max_value: int) -> None: + """Set maximum allowed value""" self.max_value = max_value self.update_min_max_label() def update_min_max_label(self) -> None: """Updates the text of the min/max label.""" - self.min_max_label.setText( - f"Range: {self.min_value} - {self.max_value}" - ) - - def setupUI(self) -> None: + self.min_max_label.setText(f"Range: {self.min_value} - {self.max_value}") + + def _setupUI(self) -> None: self.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) @@ -151,8 +145,7 @@ def setupUI(self) -> None: self.header_layout.addWidget( self.numpad_title, 0, - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) self.numpad_back_btn = IconButton(self) @@ -168,9 +161,7 @@ def setupUI(self) -> None: self.numpad_back_btn.setSizePolicy(sizePolicy) self.numpad_back_btn.setMinimumSize(QtCore.QSize(60, 60)) self.numpad_back_btn.setMaximumSize(QtCore.QSize(60, 60)) - self.numpad_back_btn.setPixmap( - QtGui.QPixmap(":ui/media/btn_icons/back.svg") - ) + self.numpad_back_btn.setPixmap(QtGui.QPixmap(":ui/media/btn_icons/back.svg")) self.numpad_back_btn.setObjectName("numpad_back_btn") self.header_layout.addWidget( self.numpad_back_btn, @@ -230,11 +221,9 @@ def setupUI(self) -> None: self.value_and_range_layout.addWidget( self.inserted_value, 0, QtCore.Qt.AlignmentFlag.AlignCenter ) - - self.main_content_layout.addLayout( - self.value_and_range_layout, 1 - ) - + + self.main_content_layout.addLayout(self.value_and_range_layout, 1) + self.inserted_value.setBackgroundRole(QtGui.QPalette.ColorRole.Window) self.setBackgroundRole(QtGui.QPalette.ColorRole.Window) self.line = QtWidgets.QFrame(self) @@ -258,9 +247,7 @@ def setupUI(self) -> None: font.setPointSize(28) font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) self.numpad_9 = NumpadButton(self) - sizePolicy.setHeightForWidth( - self.numpad_9.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.numpad_9.sizePolicy().hasHeightForWidth()) self.numpad_9.setSizePolicy(sizePolicy) self.numpad_9.setMinimumSize(QtCore.QSize(150, 60)) self.numpad_9.setFont(font) @@ -271,9 +258,7 @@ def setupUI(self) -> None: self.numpad_9, 0, 2, 1, 1, QtCore.Qt.AlignmentFlag.AlignLeft ) self.numpad_8 = NumpadButton(parent=self) - sizePolicy.setHeightForWidth( - self.numpad_8.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.numpad_8.sizePolicy().hasHeightForWidth()) self.numpad_8.setSizePolicy(sizePolicy) self.numpad_8.setMinimumSize(QtCore.QSize(150, 60)) self.numpad_8.setFont(font) @@ -284,9 +269,7 @@ def setupUI(self) -> None: self.numpad_8, 0, 1, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter ) self.numpad_7 = NumpadButton(self) - sizePolicy.setHeightForWidth( - self.numpad_7.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.numpad_7.sizePolicy().hasHeightForWidth()) self.numpad_7.setSizePolicy(sizePolicy) self.numpad_7.setMinimumSize(QtCore.QSize(150, 60)) self.numpad_7.setFont(font) @@ -297,9 +280,7 @@ def setupUI(self) -> None: self.numpad_7, 0, 0, 1, 1, QtCore.Qt.AlignmentFlag.AlignLeft ) self.numpad_6 = NumpadButton(self) - sizePolicy.setHeightForWidth( - self.numpad_6.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.numpad_6.sizePolicy().hasHeightForWidth()) self.numpad_6.setSizePolicy(sizePolicy) self.numpad_6.setMinimumSize(QtCore.QSize(150, 60)) self.numpad_6.setFont(font) @@ -311,9 +292,7 @@ def setupUI(self) -> None: self.numpad_6, 1, 2, 1, 1, QtCore.Qt.AlignmentFlag.AlignRight ) self.numpad_5 = NumpadButton(self) - sizePolicy.setHeightForWidth( - self.numpad_5.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.numpad_5.sizePolicy().hasHeightForWidth()) self.numpad_5.setSizePolicy(sizePolicy) self.numpad_5.setMinimumSize(QtCore.QSize(150, 60)) self.numpad_5.setFont(font) @@ -323,9 +302,7 @@ def setupUI(self) -> None: self.numpad_5, 1, 1, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter ) self.numpad_4 = NumpadButton(self) - sizePolicy.setHeightForWidth( - self.numpad_4.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.numpad_4.sizePolicy().hasHeightForWidth()) self.numpad_4.setSizePolicy(sizePolicy) self.numpad_4.setMinimumSize(QtCore.QSize(150, 60)) self.numpad_4.setFont(font) @@ -336,9 +313,7 @@ def setupUI(self) -> None: self.numpad_4, 1, 0, 1, 1, QtCore.Qt.AlignmentFlag.AlignLeft ) self.numpad_3 = NumpadButton(parent=self) - sizePolicy.setHeightForWidth( - self.numpad_3.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.numpad_3.sizePolicy().hasHeightForWidth()) self.numpad_3.setSizePolicy(sizePolicy) self.numpad_3.setMinimumSize(QtCore.QSize(150, 60)) self.numpad_3.setFont(font) @@ -349,9 +324,7 @@ def setupUI(self) -> None: self.numpad_3, 2, 2, 1, 1, QtCore.Qt.AlignmentFlag.AlignRight ) self.numpad_2 = NumpadButton(self) - sizePolicy.setHeightForWidth( - self.numpad_2.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.numpad_2.sizePolicy().hasHeightForWidth()) self.numpad_2.setSizePolicy(sizePolicy) self.numpad_2.setMinimumSize(QtCore.QSize(150, 60)) self.numpad_2.setFont(font) @@ -362,9 +335,7 @@ def setupUI(self) -> None: self.numpad_2, 2, 1, 1, 1, QtCore.Qt.AlignmentFlag.AlignCenter ) self.numpad_1 = NumpadButton(parent=self) - sizePolicy.setHeightForWidth( - self.numpad_1.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.numpad_1.sizePolicy().hasHeightForWidth()) self.numpad_1.setSizePolicy(sizePolicy) self.numpad_1.setMinimumSize(QtCore.QSize(150, 60)) self.numpad_1.setFont(font) @@ -375,9 +346,7 @@ def setupUI(self) -> None: self.numpad_1, 2, 0, 1, 1, QtCore.Qt.AlignmentFlag.AlignLeft ) self.numpad_0 = NumpadButton(parent=self) - sizePolicy.setHeightForWidth( - self.numpad_0.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.numpad_0.sizePolicy().hasHeightForWidth()) self.numpad_0.setSizePolicy(sizePolicy) self.numpad_0.setMinimumSize(QtCore.QSize(150, 60)) self.numpad_0.setFont(font) @@ -389,89 +358,57 @@ def setupUI(self) -> None: ) self.numpad_enter = IconButton(parent=self) self.numpad_enter.setEnabled(True) - sizePolicy.setHeightForWidth( - self.numpad_enter.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.numpad_enter.sizePolicy().hasHeightForWidth()) self.numpad_enter.setSizePolicy(sizePolicy) self.numpad_enter.setMinimumSize(QtCore.QSize(60, 60)) self.numpad_enter.setFlat(True) - self.numpad_enter.setPixmap( - QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") - ) + self.numpad_enter.setPixmap(QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg")) self.numpad_enter.setObjectName("numpad_enter") self.button_grid_layout.addWidget( self.numpad_enter, 3, 0, 1, 1, QtCore.Qt.AlignmentFlag.AlignCenter ) self.numpad_clear = IconButton(parent=self) - sizePolicy.setHeightForWidth( - self.numpad_clear.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.numpad_clear.sizePolicy().hasHeightForWidth()) self.numpad_clear.setSizePolicy(sizePolicy) self.numpad_clear.setMinimumSize(QtCore.QSize(60, 60)) self.numpad_clear.setFlat(True) - self.numpad_clear.setPixmap( - QtGui.QPixmap(":/dialog/media/btn_icons/no.svg") - ) + self.numpad_clear.setPixmap(QtGui.QPixmap(":/dialog/media/btn_icons/no.svg")) self.numpad_clear.setObjectName("numpad_clear") self.button_grid_layout.addWidget( self.numpad_clear, 3, 2, 1, 1, QtCore.Qt.AlignmentFlag.AlignCenter ) - self.button_grid_layout.setAlignment( - QtCore.Qt.AlignmentFlag.AlignCenter - ) + self.button_grid_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.main_content_layout.addLayout(self.button_grid_layout) - self.main_content_layout.setAlignment( - QtCore.Qt.AlignmentFlag.AlignCenter - ) + self.main_content_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.setLayout(self.main_content_layout) - self.retranslateUI() + self._retranslateUI() QtCore.QMetaObject.connectSlotsByName(self) - def retranslateUI(self) -> None: + def _retranslateUI(self) -> None: _translate = QtCore.QCoreApplication.translate self.setWindowTitle(_translate("customNumpad", "Form")) - self.numpad_title.setText( - _translate("customNumpad", "Target Temperature") - ) + self.numpad_title.setText(_translate("customNumpad", "Target Temperature")) self.numpad_back_btn.setProperty( "button_type", _translate("customNumpad", "icon") ) self.numpad_6.setText(_translate("customNumpad", "6")) - self.numpad_6.setProperty( - "position", _translate("customNumpad", "right") - ) + self.numpad_6.setProperty("position", _translate("customNumpad", "right")) self.numpad_9.setText(_translate("customNumpad", "9")) - self.numpad_9.setProperty( - "position", _translate("customNumpad", "right") - ) + self.numpad_9.setProperty("position", _translate("customNumpad", "right")) self.numpad_8.setText(_translate("customNumpad", "8")) self.numpad_2.setText(_translate("customNumpad", "2")) self.numpad_0.setText(_translate("customNumpad", "0")) - self.numpad_0.setProperty( - "position", _translate("customNumpad", "down") - ) + self.numpad_0.setProperty("position", _translate("customNumpad", "down")) self.numpad_3.setText(_translate("customNumpad", "3")) - self.numpad_3.setProperty( - "position", _translate("customNumpad", "right") - ) + self.numpad_3.setProperty("position", _translate("customNumpad", "right")) self.numpad_4.setText(_translate("customNumpad", "4")) - self.numpad_4.setProperty( - "position", _translate("customNumpad", "left") - ) + self.numpad_4.setProperty("position", _translate("customNumpad", "left")) self.numpad_5.setText(_translate("customNumpad", "5")) self.numpad_1.setText(_translate("customNumpad", "1")) - self.numpad_1.setProperty( - "position", _translate("customNumpad", "left") - ) - self.numpad_enter.setProperty( - "button_type", _translate("customNumpad", "icon") - ) + self.numpad_1.setProperty("position", _translate("customNumpad", "left")) + self.numpad_enter.setProperty("button_type", _translate("customNumpad", "icon")) self.numpad_7.setText(_translate("customNumpad", "7")) - self.numpad_7.setProperty( - "position", _translate("customNumpad", "left") - ) - self.numpad_clear.setProperty( - "button_type", _translate("customNumpad", "icon") - ) \ No newline at end of file + self.numpad_7.setProperty("position", _translate("customNumpad", "left")) + self.numpad_clear.setProperty("button_type", _translate("customNumpad", "icon")) diff --git a/BlocksScreen/lib/panels/widgets/optionCardWidget.py b/BlocksScreen/lib/panels/widgets/optionCardWidget.py index 81cb3bdb..711dcd02 100644 --- a/BlocksScreen/lib/panels/widgets/optionCardWidget.py +++ b/BlocksScreen/lib/panels/widgets/optionCardWidget.py @@ -25,30 +25,33 @@ def __init__( self.icon_background_color = QtGui.QColor(150, 150, 130, 80) self.name = name self.card_text = text - self.setupUi(self) - self.continue_button.clicked.connect( - lambda: self.continue_clicked.emit(self) - ) + self._setupUi(self) + self.continue_button.clicked.connect(lambda: self.continue_clicked.emit(self)) self.set_card_icon(icon) self.set_card_text(text) def disable_button(self) -> None: + """Disable widget button""" self.continue_button.setDisabled(True) self.repaint() def enable_button(self) -> None: + """Enable widget button""" self.continue_button.setEnabled(True) self.repaint() def set_card_icon(self, pixmap: QtGui.QPixmap) -> None: + """Set widget icon""" self.option_icon.setPixmap(pixmap) self.repaint() def set_card_text(self, text: str) -> None: + """Set widget text""" self.option_text.setText(text) self.repaint() def set_card_text_color(self, color: QtGui.QColor) -> None: + """Set widget text color""" self.text_color = color _palette = self.option_text.palette() _palette.setColor(QtGui.QPalette.ColorRole.WindowText, color) @@ -56,32 +59,31 @@ def set_card_text_color(self, color: QtGui.QColor) -> None: self.repaint() def set_background_color(self, color: QtGui.QColor) -> None: + """Set widget background color""" self.color = color self.repaint() - def sizeHint(self) -> QtCore.QSize: - return super().sizeHint() - - def underMouse(self) -> bool: - return super().underMouse() - def enterEvent(self, event: QtGui.QEnterEvent) -> None: + """Re-implemented method, highlight widget edges""" # Illuminate the edges to a lighter blue # To achieve this just Force update the widget self.update() return super().enterEvent(event) def leaveEvent(self, a0: QtCore.QEvent) -> None: + """Re-implemented method, disable widget edges highlight""" # Reset the color # Just as before force update the widget self.update() return super().leaveEvent(a0) def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: + """Re-implemented method, handle mouse press event""" self.update() return super().mousePressEvent(a0) def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" # Rounded background edges self.background_path = QtGui.QPainterPath() self.background_path.addRoundedRect( @@ -136,7 +138,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: painter.end() - def setupUi(self, option_card): + def _setupUi(self, option_card): option_card.setObjectName("option_card") option_card.resize(200, 300) sizePolicy = QtWidgets.QSizePolicy( @@ -145,9 +147,7 @@ def setupUi(self, option_card): ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth( - option_card.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(option_card.sizePolicy().hasHeightForWidth()) option_card.setSizePolicy(sizePolicy) option_card.setMinimumSize(QtCore.QSize(200, 300)) option_card.setMaximumSize(QtCore.QSize(200, 300)) @@ -169,8 +169,7 @@ def setupUi(self, option_card): self.verticalLayout.addWidget( self.line_separator, 0, - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) self.option_text = QtWidgets.QLabel(parent=option_card) self.option_text.setMinimumSize(QtCore.QSize(200, 50)) @@ -180,8 +179,7 @@ def setupUi(self, option_card): ) self.continue_button = IconButton(parent=option_card) self.option_text.setAlignment( - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter ) self.option_text.setWordWrap(True) _button_font = QtGui.QFont() @@ -211,10 +209,10 @@ def setupUi(self, option_card): self.continue_button.setObjectName("continue_button") self.verticalLayout.addWidget(self.continue_button) - self.retranslateUi(option_card) + self._retranslateUi(option_card) QtCore.QMetaObject.connectSlotsByName(option_card) - def retranslateUi(self, option_card): + def _retranslateUi(self, option_card): _translate = QtCore.QCoreApplication.translate option_card.setWindowTitle(_translate("option_card", "Frame")) self.option_text.setText(_translate("option_card", "TextLabel")) diff --git a/BlocksScreen/lib/panels/widgets/popupDialogWidget.py b/BlocksScreen/lib/panels/widgets/popupDialogWidget.py index 112d660a..f878f612 100644 --- a/BlocksScreen/lib/panels/widgets/popupDialogWidget.py +++ b/BlocksScreen/lib/panels/widgets/popupDialogWidget.py @@ -9,31 +9,31 @@ class Popup(QtWidgets.QDialog): class MessageType(enum.Enum): + """Popup Message type (level)""" + INFO = enum.auto() WARNING = enum.auto() ERROR = enum.auto() UNKNOWN = enum.auto() class ColorCode(enum.Enum): + """Popup message-color code""" + INFO = QtGui.QColor("#446CDB") WARNING = QtGui.QColor("#E7E147") ERROR = QtGui.QColor("#CA4949") def __init__(self, parent) -> None: super().__init__(parent) - - # Instance variables self.popup_timeout = BASE_POPUP_TIMEOUT self.timeout_timer = QtCore.QTimer(self) self.messages: Deque = deque() self.persistent_notifications: Deque = deque() - self.message_type: Popup.MessageType = Popup.MessageType.INFO self.default_background_color = QtGui.QColor(164, 164, 164) self.info_icon = QtGui.QPixmap(":ui/media/btn_icons/info.svg") self.warning_icon = QtGui.QPixmap(":ui/media/btn_icons/warning.svg") self.error_icon = QtGui.QPixmap(":ui/media/btn_icons/error.svg") - self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) self.setMouseTracking(True) self.setWindowFlags( @@ -41,31 +41,26 @@ def __init__(self, parent) -> None: | QtCore.Qt.WindowType.FramelessWindowHint | QtCore.Qt.WindowType.X11BypassWindowManagerHint ) - - self.setupUI() - - + self._setupUI() self.slide_in_animation = QtCore.QPropertyAnimation(self, b"geometry") self.slide_in_animation.setDuration(1000) self.slide_in_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutCubic) - - self.slide_out_animation = QtCore.QPropertyAnimation(self, b"geometry") self.slide_out_animation.setDuration(200) self.slide_out_animation.setEasingCurve(QtCore.QEasingCurve.Type.InCubic) - - self.slide_in_animation.finished.connect(self.on_slide_in_finished) self.slide_out_animation.finished.connect(self.on_slide_out_finished) self.timeout_timer.timeout.connect(self.slide_out_animation.start) def on_slide_in_finished(self): + """Handle slide in animation finished""" self.timeout_timer.start() def on_slide_out_finished(self): + """Handle slide out animation finished""" self.close() - self.add_popup() - + self._add_popup() + def _calculate_target_geometry(self) -> QtCore.QRect: app_instance = QtWidgets.QApplication.instance() main_window = app_instance.activeWindow() if app_instance else None @@ -74,28 +69,31 @@ def _calculate_target_geometry(self) -> QtCore.QRect: if isinstance(widget, QtWidgets.QMainWindow): main_window = widget break - + parent_rect = main_window.geometry() width = int(parent_rect.width() * 0.85) - height = min(self.text_label.rect().height(), self.icon_label.rect().height()) - - x = parent_rect.x() + (parent_rect.width() - width) // 2 + height = min(self.text_label.rect().height(), self.icon_label.rect().height()) + + x = parent_rect.x() + (parent_rect.width() - width) // 2 y = parent_rect.y() + 20 - + return QtCore.QRect(x, y, width, height) def updateMask(self) -> None: + """Update widget mask properties""" path = QtGui.QPainterPath() path.addRoundedRect(self.rect().toRectF(), 10, 10) region = QtGui.QRegion(path.toFillPolygon(QtGui.QTransform()).toPolygon()) self.setMask(region) def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: + """Re-implemented method, handle mouse press events""" self.timeout_timer.stop() self.slide_out_animation.start() def set_timeout(self, value: int) -> None: + """Set popup timeout""" if not isinstance(value, int): raise ValueError("Expected type int ") self.popup_timeout = value @@ -104,25 +102,36 @@ def new_message( self, message_type: MessageType = MessageType.INFO, message: str = "", - persistent: bool = False, timeout: int = 0, ): + """Create new popup message + + Args: + message_type (MessageType, optional): Message Level, See `MessageType` Types. Defaults to MessageType.INFO. + message (str, optional): The message. Defaults to "". + timeout (int, optional): How long the message stays for, in milliseconds. Defaults to 0. + + Returns: + _type_: _description_ + """ self.messages.append( {"message": message, "type": message_type, "timeout": timeout} ) - return self.add_popup() + return self._add_popup() - def add_popup(self) -> None: + def _add_popup(self) -> None: + """Add popup to queue""" if ( self.messages - and self.slide_in_animation.state() == QtCore.QPropertyAnimation.State.Stopped - and self.slide_out_animation.state() == QtCore.QPropertyAnimation.State.Stopped + and self.slide_in_animation.state() + == QtCore.QPropertyAnimation.State.Stopped + and self.slide_out_animation.state() + == QtCore.QPropertyAnimation.State.Stopped ): message_entry = self.messages.popleft() self.message_type = message_entry.get("type") message = message_entry.get("message") self.text_label.setText(message) - match self.message_type: case Popup.MessageType.INFO: self.icon_label.setPixmap(self.info_icon) @@ -130,36 +139,31 @@ def add_popup(self) -> None: self.icon_label.setPixmap(self.warning_icon) case Popup.MessageType.ERROR: self.icon_label.setPixmap(self.error_icon) - - self.timeout_timer.setInterval( - self.popup_timeout - ) - + self.timeout_timer.setInterval(self.popup_timeout) end_rect = self._calculate_target_geometry() - - start_rect = end_rect.translated(0, -end_rect.height()) - self.slide_in_animation.setStartValue(start_rect) self.slide_in_animation.setEndValue(end_rect) self.slide_out_animation.setStartValue(end_rect) self.slide_out_animation.setEndValue(start_rect) self.setGeometry(start_rect) - self.open() def showEvent(self, a0: QtGui.QShowEvent) -> None: + """Re-implementation, widget show""" self.slide_in_animation.start() super().showEvent(a0) def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: + """Re-implementation, handle resize event""" self.updateMask() super().resizeEvent(a0) def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) - + _base_color = self.default_background_color if self.message_type == Popup.MessageType.INFO: _base_color = Popup.ColorCode.INFO.value @@ -168,18 +172,17 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: elif self.message_type == Popup.MessageType.WARNING: _base_color = Popup.ColorCode.WARNING.value - center_point = QtCore.QPointF(self.rect().center()) gradient = QtGui.QRadialGradient(center_point, self.rect().width() / 2.0) - + gradient.setColorAt(0, _base_color) gradient.setColorAt(1.0, _base_color.darker(160)) painter.setBrush(gradient) painter.setPen(QtCore.Qt.PenStyle.NoPen) painter.drawRoundedRect(self.rect(), 10, 10) - - def setupUI(self) -> None: + + def _setupUI(self) -> None: self.vertical_layout = QtWidgets.QVBoxLayout(self) self.horizontal_layout = QtWidgets.QHBoxLayout() self.horizontal_layout.setContentsMargins(5, 5, 5, 5) @@ -192,19 +195,23 @@ def setupUI(self) -> None: self.text_label = QtWidgets.QLabel(self) self.text_label.setWordWrap(True) - self.text_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignVCenter | QtCore.Qt.AlignmentFlag.AlignHCenter) - + self.text_label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignVCenter | QtCore.Qt.AlignmentFlag.AlignHCenter + ) + font = self.text_label.font() font.setPixelSize(18) font.setFamily("sans-serif") palette = self.text_label.palette() - palette.setColor(QtGui.QPalette.ColorRole.WindowText, QtCore.Qt.GlobalColor.white) + palette.setColor( + QtGui.QPalette.ColorRole.WindowText, QtCore.Qt.GlobalColor.white + ) self.text_label.setPalette(palette) self.text_label.setFont(font) self.spacer = QtWidgets.QSpacerItem(60, 60) - + self.horizontal_layout.addWidget(self.text_label, 1) self.horizontal_layout.addItem(self.spacer) - self.vertical_layout.addLayout(self.horizontal_layout) \ No newline at end of file + self.vertical_layout.addLayout(self.horizontal_layout) diff --git a/BlocksScreen/lib/panels/widgets/printcorePage.py b/BlocksScreen/lib/panels/widgets/printcorePage.py index 6da6814f..c2683cd1 100644 --- a/BlocksScreen/lib/panels/widgets/printcorePage.py +++ b/BlocksScreen/lib/panels/widgets/printcorePage.py @@ -1,35 +1,33 @@ from lib.utils.blocks_button import BlocksCustomButton from PyQt6 import QtCore, QtGui, QtWidgets -class SwapPrintcorePage(QtWidgets.QDialog): - - +class SwapPrintcorePage(QtWidgets.QDialog): def __init__( - self, parent: QtWidgets.QWidget, + self, + parent: QtWidgets.QWidget, ) -> None: super().__init__(parent) self.setStyleSheet( "background-image: url(:/background/media/1st_background.png);" ) self.setWindowFlags( - QtCore.Qt.WindowType.Popup - | QtCore.Qt.WindowType.FramelessWindowHint + QtCore.Qt.WindowType.Popup | QtCore.Qt.WindowType.FramelessWindowHint ) - self.setupUI() + self._setupUI() self.repaint() def setText(self, text: str) -> None: + """Set widget text""" self.label.setText(text) self.repaint() - - def Text(self) -> str: - return self.label.text() - - + def text(self) -> str: + """Return current widget text""" + return self.label.text() - def geometry_calc(self) -> None: + def _geometry_calc(self) -> None: + """Calculate widget position relative to the screen""" app_instance = QtWidgets.QApplication.instance() main_window = app_instance.activeWindow() if app_instance else None if main_window is None and app_instance: @@ -44,6 +42,7 @@ def geometry_calc(self) -> None: self.setGeometry(x, y, width, height) def sizeHint(self) -> QtCore.QSize: + """Re-implemented method, handle widget size""" popup_width = int(self.geometry().width()) popup_height = int(self.geometry().height()) # Centering logic @@ -53,39 +52,36 @@ def sizeHint(self) -> QtCore.QSize: self.move(popup_x, popup_y) self.setFixedSize(popup_width, popup_height) self.setMinimumSize(popup_width, popup_height) - + return super().sizeHint() def resizeEvent(self, event: QtGui.QResizeEvent) -> None: + """Re-implemented method, handle widget resize event""" super().resizeEvent(event) - self.tittle.setGeometry(0, 0, self.width(), 60) - - # Calculate label geometry first label_margin = 20 label_height = int(self.height() * 0.65) - label_margin - self.label.setGeometry(label_margin, 60, self.width() - 2 * label_margin, label_height) - # Calculate button geometry based on the window's dimensions + self.label.setGeometry( + label_margin, 60, self.width() - 2 * label_margin, label_height + ) button_width = 250 button_height = 80 spacing = 100 total_button_width = 2 * button_width + spacing - # Center the buttons horizontally start_x = (self.width() - total_button_width) // 2 - button_y = self.height() - button_height -45 + button_y = self.height() - button_height - 45 self.pc_accept.setGeometry(start_x, button_y, button_width, button_height) - self.pc_cancel.setGeometry(start_x + button_width+100 , button_y, button_width, button_height) + self.pc_cancel.setGeometry( + start_x + button_width + 100, button_y, button_width, button_height + ) def show(self) -> None: - self.geometry_calc() + """Re-implemented method, widget show""" + self._geometry_calc() self.repaint() return super().show() - - - - - def setupUI(self) -> None: + def _setupUI(self) -> None: font = QtGui.QFont() font.setPointSize(20) @@ -104,7 +100,9 @@ def setupUI(self) -> None: self.pc_cancel = BlocksCustomButton(parent=self) self.pc_cancel.setMinimumSize(QtCore.QSize(250, 80)) self.pc_cancel.setMaximumSize(QtCore.QSize(250, 80)) - self.pc_cancel.setProperty("icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/no.svg")) + self.pc_cancel.setProperty( + "icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/no.svg") + ) self.pc_cancel.setObjectName("pc_cancel") self.pc_cancel.setFont(font) self.pc_cancel.setText("Cancel") @@ -112,7 +110,9 @@ def setupUI(self) -> None: self.pc_accept = BlocksCustomButton(parent=self) self.pc_accept.setMinimumSize(QtCore.QSize(250, 80)) self.pc_accept.setMaximumSize(QtCore.QSize(250, 80)) - self.pc_accept.setProperty("icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg")) + self.pc_accept.setProperty( + "icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") + ) self.pc_accept.setObjectName("pc_accept") self.pc_accept.setFont(font) - self.pc_accept.setText("Continue?") \ No newline at end of file + self.pc_accept.setText("Continue?") diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index e546afdd..5eeb3a3d 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -7,7 +7,7 @@ from lib.utils.group_button import GroupButton from lib.utils.blocks_button import BlocksCustomButton -from lib.panels.widgets.loadPage import LoadScreen +from lib.panels.widgets.loadPage import LoadScreen class ProbeHelper(QtWidgets.QWidget): @@ -18,8 +18,8 @@ class ProbeHelper(QtWidgets.QWidget): str, name="run_gcode" ) - query_printer_object: typing.ClassVar[QtCore.pyqtSignal] = ( - QtCore.pyqtSignal(dict, name="query_object") + query_printer_object: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + dict, name="query_object" ) subscribe_config: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( [ @@ -46,71 +46,47 @@ class ProbeHelper(QtWidgets.QWidget): z_offset_config_method: tuple = () z_offset_calibration_speed: int = 100 - - def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__(parent) - self.Loadscreen = LoadScreen(self) - self.setObjectName("probe_offset_page") - self.setupUi() - + self._setupUi() self.inductive_icon = QtGui.QPixmap( ":/z_levelling/media/btn_icons/inductive.svg" ) - self.bltouch_icon = QtGui.QPixmap( - ":/z_levelling/media/btn_icons/bltouch.svg" - ) + self.bltouch_icon = QtGui.QPixmap(":/z_levelling/media/btn_icons/bltouch.svg") self.endstop_icon = QtGui.QPixmap( ":/extruder_related/media/btn_icons/switch_zoom.svg" ) - self.eddy_icon = QtGui.QPixmap( - ":/z_levelling/media/btn_icons/eddy_mech.svg" - ) - + self.eddy_icon = QtGui.QPixmap(":/z_levelling/media/btn_icons/eddy_mech.svg") self._toggle_tool_buttons(False) self._setup_move_option_buttons() self.move_option_1.toggled.connect( - lambda: self.handle_zhopHeight_change( - new_value=float(self.distances[0]) - ) + lambda: self.handle_zhopHeight_change(new_value=float(self.distances[0])) ) self.move_option_2.toggled.connect( - lambda: self.handle_zhopHeight_change( - new_value=float(self.distances[1]) - ) + lambda: self.handle_zhopHeight_change(new_value=float(self.distances[1])) ) self.move_option_3.toggled.connect( - lambda: self.handle_zhopHeight_change( - new_value=float(self.distances[2]) - ) + lambda: self.handle_zhopHeight_change(new_value=float(self.distances[2])) ) self.move_option_4.toggled.connect( - lambda: self.handle_zhopHeight_change( - new_value=float(self.distances[3]) - ) + lambda: self.handle_zhopHeight_change(new_value=float(self.distances[3])) ) self.move_option_5.toggled.connect( - lambda: self.handle_zhopHeight_change( - new_value=float(self.distances[4]) - ) - ) - self.mb_raise_nozzle.clicked.connect( - lambda:self.handle_nozzle_move("raise") - ) - self.mb_lower_nozzle.clicked.connect( - lambda:self.handle_nozzle_move("lower") + lambda: self.handle_zhopHeight_change(new_value=float(self.distances[4])) ) + self.mb_raise_nozzle.clicked.connect(lambda: self.handle_nozzle_move("raise")) + self.mb_lower_nozzle.clicked.connect(lambda: self.handle_nozzle_move("lower")) self.po_back_button.clicked.connect(self.request_back) self.accept_button.clicked.connect(self.handle_accept) self.abort_button.clicked.connect(self.handle_abort) self.update() - self.block_z = False self.block_list = False def on_klippy_status(self, state: str): + """Handle Klippy status event change""" if state.lower() == "standby": self.block_z = False self.block_list = False @@ -138,9 +114,9 @@ def on_klippy_status(self, state: str): if child_widget is not None: child_widget.setParent(None) child_widget.deleteLater() - return def handle_nozzle_move(self, direction: str): + """Handle move z buttons click""" if direction == "raise": self._pending_gcode = f"TESTZ Z={self._zhop_height}" elif direction == "lower": @@ -177,7 +153,9 @@ def _configure_option_cards(self, probes_list: list[str]) -> None: _card = OptionCard(self, _card_text, str(probe), _icon) # type: ignore _card.setObjectName(str(probe)) self.card_options.update({str(probe): _card}) - self.main_content_horizontal_layout.addWidget(_card, alignment=QtCore.Qt.AlignmentFlag.AlignHCenter) + self.main_content_horizontal_layout.addWidget( + _card, alignment=QtCore.Qt.AlignmentFlag.AlignHCenter + ) if not hasattr(self.card_options.get(probe), "continue_clicked"): del _card self.card_options.pop(probe) @@ -194,22 +172,20 @@ def _hide_option_cards(self) -> None: def _show_option_cards(self) -> None: list(map(lambda x: x[1].show(), self.card_options.items())) - def init_probe_config(self) -> None: + def _init_probe_config(self) -> None: + """Initialize internal probe tracking""" if not self.z_offset_config_method: return - if self.z_offset_config_type != "endstop": self.z_offsets = tuple( map( - lambda axis: self.z_offset_config_method[1].get( - f"{axis}_offset" - ), + lambda axis: self.z_offset_config_method[1].get(f"{axis}_offset"), ["x", "y", "z"], ) ) - self.z_offset_calibration_speed = self.z_offset_config_method[ - 1 - ].get("speed") + self.z_offset_calibration_speed = self.z_offset_config_method[1].get( + "speed" + ) @QtCore.pyqtSlot(list, name="on_object_config") @QtCore.pyqtSlot(dict, name="on_object_config") @@ -224,29 +200,30 @@ def on_object_config(self, config: dict | list) -> None: return # BUG: If i don't add if not self.probe_config i'll just receive the configuration a bunch of times - if isinstance(config, list):... - # if self.block_list: - # return - # else: - # self.block_list = True - - # _keys = [] - # if not isinstance(config, list): - # return - - # list(map(lambda item: _keys.extend(item.keys()), config)) - - # probe, *_ = config[0].items() - # self.z_offset_method_type = probe[0] # The one found first - # self.z_offset_method_config = ( - # probe[1], - # "PROBE_CALIBRATE", - # "Z_OFFSET_APPLY_PROBE", - # ) - # self.init_probe_config() - # if not _keys: - # return - # self._configure_option_cards(_keys) + if isinstance(config, list): + ... + # if self.block_list: + # return + # else: + # self.block_list = True + + # _keys = [] + # if not isinstance(config, list): + # return + + # list(map(lambda item: _keys.extend(item.keys()), config)) + + # probe, *_ = config[0].items() + # self.z_offset_method_type = probe[0] # The one found first + # self.z_offset_method_config = ( + # probe[1], + # "PROBE_CALIBRATE", + # "Z_OFFSET_APPLY_PROBE", + # ) + # self.init_probe_config() + # if not _keys: + # return + # self._configure_option_cards(_keys) elif isinstance(config, dict): if config.get("stepper_z"): @@ -254,14 +231,12 @@ def on_object_config(self, config: dict | list) -> None: return else: self.block_z = True - + _virtual_endstop = "probe:z_virtual_endstop" _config = config.get("stepper_z") if not _config: return - if ( - _config.get("endstop_pin") == _virtual_endstop - ): # home with probe + if _config.get("endstop_pin") == _virtual_endstop: # home with probe return self.z_offset_config_type = "endstop" self.z_offset_config_method = ( @@ -304,6 +279,7 @@ def on_object_config(self, config: dict | list) -> None: @QtCore.pyqtSlot(dict, name="on_printer_config") def on_printer_config(self, config: dict) -> None: + """Handle received printer config""" _probe_types = [ "probe", "bltouch", @@ -326,6 +302,7 @@ def on_printer_config(self, config: dict) -> None: @QtCore.pyqtSlot(dict, name="on_available_gcode_cmds") def on_available_gcode_cmds(self, gcode_cmds: dict) -> None: + """Setup available probe calibration commands""" _available_commands = gcode_cmds.keys() if "PROBE_CALIBRATE" in _available_commands: self._calibration_commands.append("PROBE_CALIBRATE") @@ -411,7 +388,7 @@ def handle_start_tool(self, sender: typing.Type[OptionCard]) -> None: """ if not sender: return - + for i in self.card_options.values(): i.setDisabled(True) @@ -420,9 +397,7 @@ def handle_start_tool(self, sender: typing.Type[OptionCard]) -> None: if self.z_offset_safe_xy: self.run_gcode_signal.emit("G28\nM400") - self._move_to_pos( - self.z_offset_safe_xy[0], self.z_offset_safe_xy[1], 100 - ) + self._move_to_pos(self.z_offset_safe_xy[0], self.z_offset_safe_xy[1], 100) self.helper_initialize = True _timer = QtCore.QTimer() _timer.setSingleShot(True) @@ -462,7 +437,7 @@ def handle_abort(self) -> None: @QtCore.pyqtSlot(str, list, name="on_gcode_move_update") def on_gcode_move_update(self, name: str, value: list) -> None: - # TODO: catch the z distances and update the values on the window + """Handle gcode move update""" if not value: return @@ -477,6 +452,7 @@ def on_gcode_move_update(self, name: str, value: list) -> None: @QtCore.pyqtSlot(dict, name="on_manual_probe_update") def on_manual_probe_update(self, update: dict) -> None: + """Handle manual probe update""" if not update: return @@ -492,13 +468,9 @@ def on_manual_probe_update(self, update: dict) -> None: self._toggle_tool_buttons(True) if update.get("z_position_upper"): - self.old_offset_info.setText( - f"{update.get('z_position_upper'):.4f} mm" - ) + self.old_offset_info.setText(f"{update.get('z_position_upper'):.4f} mm") if update.get("z_position"): - self.current_offset_info.setText( - f"{update.get('z_position'):.4f} mm" - ) + self.current_offset_info.setText(f"{update.get('z_position'):.4f} mm") @QtCore.pyqtSlot(list, name="handle_gcode_response") def handle_gcode_response(self, data: list) -> None: @@ -508,13 +480,9 @@ def handle_gcode_response(self, data: list) -> None: data (list): A list containing the gcode that originated the response and the response """ - # TODO: Only check for messages if we are in the tool otherwise ignore them if self.isVisible(): if data[0].startswith("!!"): # An error occurred - if ( - "already in a manual z probe" - in data[0].strip("!! ").lower() - ): + if "already in a manual z probe" in data[0].strip("!! ").lower(): self._hide_option_cards() self.helper_start = True self._toggle_tool_buttons(True) @@ -527,6 +495,7 @@ def handle_gcode_response(self, data: list) -> None: @QtCore.pyqtSlot(list, name="handle_error_response") def handle_error_response(self, data: list) -> None: + """Handle received error response""" ... # _data, _metadata, *extra = data + [None] * max(0, 2 - len(data)) @@ -535,12 +504,6 @@ def _move_to_pos(self, x, y, speed) -> None: self.run_gcode_signal.emit(f"G90\nG1 X{x} Y{y} F{speed * 60}\nM400") return - ############################################################################### - ################################# UI RELATED ################################## - ############################################################################### - def show(self) -> None: - return super().show() - def _setup_move_option_buttons(self) -> None: """Change move_option_x buttons text for configured zhop values in stored in the class variable `distances` @@ -549,7 +512,6 @@ def _setup_move_option_buttons(self) -> None: """ if self.distances: return - self.move_option_1.setText(str(self.distances[0])) self.move_option_2.setText(str(self.distances[1])) self.move_option_3.setText(str(self.distances[2])) @@ -579,7 +541,12 @@ def _toggle_tool_buttons(self, state: bool) -> None: self.mb_raise_nozzle.show() self.mb_lower_nozzle.show() self.frame_2.show() - self.spacerItem.changeSize(40,20,QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + self.spacerItem.changeSize( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Minimum, + ) else: self.po_back_button.setEnabled(True) @@ -594,13 +561,17 @@ def _toggle_tool_buttons(self, state: bool) -> None: self.mb_raise_nozzle.hide() self.mb_lower_nozzle.hide() self.frame_2.hide() - self.spacerItem.changeSize(0,0,QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - + self.spacerItem.changeSize( + 0, + 0, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + self.update() return - - def setupUi(self) -> None: + def _setupUi(self) -> None: self.bbp_offset_value_selector_group = QtWidgets.QButtonGroup(self) self.bbp_offset_value_selector_group.setExclusive(True) sizePolicy = QtWidgets.QSizePolicy( @@ -656,9 +627,7 @@ def setupUi(self) -> None: self.accept_button.setGeometry(QtCore.QRect(480, 340, 170, 60)) self.accept_button.setText("Accept") self.accept_button.setObjectName("accept_button") - self.accept_button.setPixmap( - QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") - ) + self.accept_button.setPixmap(QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg")) self.accept_button.setVisible(False) font = QtGui.QFont() font.setPointSize(15) @@ -668,9 +637,7 @@ def setupUi(self) -> None: self.abort_button.setGeometry(QtCore.QRect(300, 340, 170, 60)) self.abort_button.setText("Abort") self.abort_button.setObjectName("accept_button") - self.abort_button.setPixmap( - QtGui.QPixmap(":/dialog/media/btn_icons/no.svg") - ) + self.abort_button.setPixmap(QtGui.QPixmap(":/dialog/media/btn_icons/no.svg")) self.abort_button.setVisible(False) font = QtGui.QFont() font.setPointSize(15) @@ -698,16 +665,13 @@ def setupUi(self) -> None: self.po_back_button.setMaximumSize(QtCore.QSize(60, 60)) self.po_back_button.setText("") self.po_back_button.setFlat(True) - self.po_back_button.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/back.svg") - ) + self.po_back_button.setPixmap(QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) self.po_back_button.setObjectName("po_back_button") self.bbp_header_layout.addWidget( self.po_back_button, 0, - QtCore.Qt.AlignmentFlag.AlignRight - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter, ) self.bbp_header_layout.setStretch(0, 1) self.verticalLayout.addLayout(self.bbp_header_layout) @@ -724,9 +688,6 @@ def setupUi(self) -> None: self.separator_line.setObjectName("separator_line") self.verticalLayout.addWidget(self.separator_line) - - - # Offset Steps Buttons Group Box (LEFT side of main_content_horizontal_layout) self.bbp_offset_steps_buttons_group_box = QtWidgets.QGroupBox(self) font = QtGui.QFont() @@ -748,9 +709,7 @@ def setupUi(self) -> None: self.bbp_offset_steps_buttons.setObjectName("bbp_offset_steps_buttons") # 0.1mm button - self.move_option_1 = GroupButton( - parent=self.bbp_offset_steps_buttons_group_box - ) + self.move_option_1 = GroupButton(parent=self.bbp_offset_steps_buttons_group_box) self.move_option_1.setMinimumSize(QtCore.QSize(100, 60)) self.move_option_1.setMaximumSize(QtCore.QSize(100, 60)) self.move_option_1.setText("0.01 mm") @@ -763,22 +722,15 @@ def setupUi(self) -> None: self.move_option_1.setFlat(True) self.move_option_1.setProperty("button_type", "") self.move_option_1.setObjectName("move_option_1") - self.bbp_offset_value_selector_group.addButton( - self.move_option_1 - ) + self.bbp_offset_value_selector_group.addButton(self.move_option_1) self.bbp_offset_steps_buttons.addWidget( self.move_option_1, 0, - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) - - # 0.01mm button - self.move_option_2 = GroupButton( - parent=self.bbp_offset_steps_buttons_group_box - ) + self.move_option_2 = GroupButton(parent=self.bbp_offset_steps_buttons_group_box) self.move_option_2.setMinimumSize(QtCore.QSize(100, 60)) self.move_option_2.setMaximumSize( QtCore.QSize(100, 60) @@ -792,20 +744,15 @@ def setupUi(self) -> None: self.move_option_2.setFlat(True) self.move_option_2.setProperty("button_type", "") self.move_option_2.setObjectName("move_option_2") - self.bbp_offset_value_selector_group.addButton( - self.move_option_2 - ) + self.bbp_offset_value_selector_group.addButton(self.move_option_2) self.bbp_offset_steps_buttons.addWidget( self.move_option_2, 0, - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) # 0.05mm button - self.move_option_3 = GroupButton( - parent=self.bbp_offset_steps_buttons_group_box - ) + self.move_option_3 = GroupButton(parent=self.bbp_offset_steps_buttons_group_box) self.move_option_3.setMinimumSize(QtCore.QSize(100, 60)) self.move_option_3.setMaximumSize( QtCore.QSize(100, 60) @@ -819,20 +766,15 @@ def setupUi(self) -> None: self.move_option_3.setFlat(True) self.move_option_3.setProperty("button_type", "") self.move_option_3.setObjectName("move_option_3") - self.bbp_offset_value_selector_group.addButton( - self.move_option_3 - ) + self.bbp_offset_value_selector_group.addButton(self.move_option_3) self.bbp_offset_steps_buttons.addWidget( self.move_option_3, 0, - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) # 0.025mm button - self.move_option_4 = GroupButton( - parent=self.bbp_offset_steps_buttons_group_box - ) + self.move_option_4 = GroupButton(parent=self.bbp_offset_steps_buttons_group_box) self.move_option_4.setMinimumSize(QtCore.QSize(100, 60)) self.move_option_4.setMaximumSize( QtCore.QSize(100, 60) @@ -846,20 +788,15 @@ def setupUi(self) -> None: self.move_option_4.setFlat(True) self.move_option_4.setProperty("button_type", "") self.move_option_4.setObjectName("move_option_4") - self.bbp_offset_value_selector_group.addButton( - self.move_option_4 - ) + self.bbp_offset_value_selector_group.addButton(self.move_option_4) self.bbp_offset_steps_buttons.addWidget( self.move_option_4, 0, - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) - # 0.01mm button - self.move_option_5 = GroupButton( - parent=self.bbp_offset_steps_buttons_group_box - ) + # 0.01mm button + self.move_option_5 = GroupButton(parent=self.bbp_offset_steps_buttons_group_box) self.move_option_5.setMinimumSize(QtCore.QSize(100, 60)) self.move_option_5.setMaximumSize( QtCore.QSize(100, 60) @@ -873,22 +810,17 @@ def setupUi(self) -> None: self.move_option_5.setFlat(True) self.move_option_5.setProperty("button_type", "") self.move_option_5.setObjectName("move_option_4") - self.bbp_offset_value_selector_group.addButton( - self.move_option_5 - ) + self.bbp_offset_value_selector_group.addButton(self.move_option_5) self.bbp_offset_steps_buttons.addWidget( self.move_option_5, 0, - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) # Line separator for 0.025mm - set size policy to expanding horizontally # Set the layout for the group box - self.bbp_offset_steps_buttons_group_box.setLayout( - self.bbp_offset_steps_buttons - ) + self.bbp_offset_steps_buttons_group_box.setLayout(self.bbp_offset_steps_buttons) # Add the group box to the main content horizontal layout FIRST for left placement self.main_content_horizontal_layout.addWidget( self.bbp_offset_steps_buttons_group_box @@ -896,9 +828,7 @@ def setupUi(self) -> None: # Graphic and Current Value Frame (This will now be in the MIDDLE) self.frame_2 = QtWidgets.QFrame(parent=self) - sizePolicy.setHeightForWidth( - self.frame_2.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.frame_2.sizePolicy().hasHeightForWidth()) self.frame_2.setSizePolicy(sizePolicy) self.frame_2.setMinimumSize(QtCore.QSize(350, 160)) self.frame_2.setMaximumSize(QtCore.QSize(350, 160)) @@ -907,33 +837,25 @@ def setupUi(self) -> None: self.frame_2.setObjectName("frame_2") self.tool_image = QtWidgets.QLabel(parent=self.frame_2) self.tool_image.setGeometry(QtCore.QRect(0, 30, 371, 121)) - self.tool_image.setLayoutDirection( - QtCore.Qt.LayoutDirection.RightToLeft - ) + self.tool_image.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) self.tool_image.setPixmap( QtGui.QPixmap(":/graphics/media/graphics/babystep_graphic.png") ) self.tool_image.setScaledContents(False) - self.tool_image.setAlignment( - QtCore.Qt.AlignmentFlag.AlignCenter - ) + self.tool_image.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.tool_image.setObjectName("tool_image") # === NEW LABEL ADDED HERE === # This is the title label that appears above the red value box. self.old_offset_info = QtWidgets.QLabel(parent=self.frame_2) # Position it just above the red box. Red box is at y=70, so y=40 is appropriate. - self.old_offset_info.setGeometry( - QtCore.QRect(240, 95, 200, 60) - ) + self.old_offset_info.setGeometry(QtCore.QRect(240, 95, 200, 60)) font = QtGui.QFont() font.setPointSize(12) self.old_offset_info.setFont(font) # Set color to white to be visible on the dark background - self.old_offset_info.setStyleSheet( - "color: gray; background: transparent;" - ) + self.old_offset_info.setStyleSheet("color: gray; background: transparent;") self.old_offset_info.setText("Z-Offset") self.old_offset_info.setObjectName("old_offset_info") self.old_offset_info.setText("0 mm") @@ -941,9 +863,7 @@ def setupUi(self) -> None: # === END OF NEW LABEL === self.current_offset_info = BlocksLabel(parent=self.frame_2) - self.current_offset_info.setGeometry( - QtCore.QRect(100, 70, 200, 60) - ) + self.current_offset_info.setGeometry(QtCore.QRect(100, 70, 200, 60)) sizePolicy.setHeightForWidth( self.current_offset_info.sizePolicy().hasHeightForWidth() ) @@ -953,25 +873,18 @@ def setupUi(self) -> None: font = QtGui.QFont() font.setPointSize(14) self.current_offset_info.setFont(font) - self.current_offset_info.setStyleSheet( - "background: transparent; color: white;" - ) + self.current_offset_info.setStyleSheet("background: transparent; color: white;") self.current_offset_info.setText("Z:0mm") self.current_offset_info.setPixmap( QtGui.QPixmap(":/graphics/media/btn_icons/z_offset_adjust.svg") ) - self.current_offset_info.setAlignment( - QtCore.Qt.AlignmentFlag.AlignCenter - ) - self.current_offset_info.setObjectName( - "current_offset_info" - ) + self.current_offset_info.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.current_offset_info.setObjectName("current_offset_info") # Add graphic frame AFTER the offset buttons group box self.main_content_horizontal_layout.addWidget( self.frame_2, 0, - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) # Move Buttons Layout (This will now be on the RIGHT) @@ -1025,7 +938,6 @@ def setupUi(self) -> None: # Add move buttons layout LAST for right placement self.main_content_horizontal_layout.addLayout(self.bbp_buttons_layout) - self.main_content_horizontal_layout.addItem(self.spacerItem) # Set stretch factors for main content horizontal layout diff --git a/BlocksScreen/lib/panels/widgets/sensorWidget.py b/BlocksScreen/lib/panels/widgets/sensorWidget.py index 109792fa..5223ac83 100644 --- a/BlocksScreen/lib/panels/widgets/sensorWidget.py +++ b/BlocksScreen/lib/panels/widgets/sensorWidget.py @@ -7,18 +7,26 @@ class SensorWidget(QtWidgets.QWidget): class SensorType(enum.Enum): + """Filament sensor type""" + SWITCH = enum.auto() MOTION = enum.auto() class SensorFlags(enum.Flag): + """Filament sensor flags""" + CLICKABLE = enum.auto() DISPLAY = enum.auto() class FilamentState(enum.Enum): + """Current filament state, sensor has or does not have filament""" + MISSING = 0 PRESENT = 1 class SensorState(enum.IntEnum): + """Current sensor filament state, if it's turned on or not""" + OFF = False ON = True @@ -40,14 +48,10 @@ def __init__(self, parent, sensor_name: str): self.filament_state: SensorWidget.FilamentState = ( SensorWidget.FilamentState.MISSING ) - self.sensor_state: SensorWidget.SensorState = ( - SensorWidget.SensorState.OFF - ) + self.sensor_state: SensorWidget.SensorState = SensorWidget.SensorState.OFF self._icon_label = None self._text_label = None - self._text: str = ( - str(self.sensor_type.name) + " Sensor: " + str(self.name) - ) + self._text: str = str(self.sensor_type.name) + " Sensor: " + str(self.name) self._item_rect: QtCore.QRect = QtCore.QRect() self.icon_pixmap_fp: QtGui.QPixmap = QtGui.QPixmap( ":/filament_related/media/btn_icons/filament_sensor_turn_on.svg" @@ -55,10 +59,11 @@ def __init__(self, parent, sensor_name: str): self.icon_pixmap_fnp: QtGui.QPixmap = QtGui.QPixmap( ":/filament_related/media/btn_icons/filament_sensor_off.svg" ) - self.setupUI() + self._setupUI() @property def type(self) -> SensorType: + """Sensor type""" return self._sensor_type @type.setter @@ -67,6 +72,7 @@ def type(self, type: SensorType): @property def flags(self) -> SensorFlags: + """Current filament sensor flags""" return self._flags @flags.setter @@ -75,6 +81,7 @@ def flags(self, flags: SensorFlags) -> None: @property def text(self) -> str: + """Filament sensor text""" return self._text @text.setter @@ -85,13 +92,12 @@ def text(self, new_text) -> None: @QtCore.pyqtSlot(bool, name="change_fil_sensor_state") def change_fil_sensor_state(self, state: FilamentState): + """Change filament sensor state""" if isinstance(state, SensorWidget.FilamentState): self.filament_state = state - def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: - return super().resizeEvent(a0) - def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" # if ( # self._scaled_select_on_pixmap is not None # and self._scaled_select_off_pixmap is not None @@ -103,9 +109,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: # ) style_painter = QtWidgets.QStylePainter(self) - style_painter.setRenderHint( - style_painter.RenderHint.Antialiasing, True - ) + style_painter.setRenderHint(style_painter.RenderHint.Antialiasing, True) style_painter.setRenderHint( style_painter.RenderHint.SmoothPixmapTransform, True ) @@ -142,6 +146,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: @property def toggle_sensor_gcode_command(self) -> str: + """Toggle filament sensor""" self.sensor_state = ( SensorWidget.SensorState.ON if self.sensor_state == SensorWidget.SensorState.OFF @@ -151,7 +156,7 @@ def toggle_sensor_gcode_command(self) -> str: f"SET_FILAMENT_SENSOR SENSOR={self.text} ENABLE={not self.sensor_state.value}" ) - def setupUI(self): + def _setupUI(self): _policy = QtWidgets.QSizePolicy.Policy.MinimumExpanding size_policy = QtWidgets.QSizePolicy(_policy, _policy) size_policy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) @@ -160,9 +165,7 @@ def setupUI(self): self.sensor_horizontal_layout.setGeometry(QtCore.QRect(0, 0, 640, 60)) self.sensor_horizontal_layout.setObjectName("sensorHorizontalLayout") self._icon_label = BlocksLabel(self) - size_policy.setHeightForWidth( - self._icon_label.sizePolicy().hasHeightForWidth() - ) + size_policy.setHeightForWidth(self._icon_label.sizePolicy().hasHeightForWidth()) self._icon_label.setSizePolicy(size_policy) self._icon_label.setMinimumSize(60, 60) self._icon_label.setMaximumSize(60, 60) @@ -173,18 +176,14 @@ def setupUI(self): ) self.sensor_horizontal_layout.addWidget(self._icon_label) self._text_label = QtWidgets.QLabel(parent=self) - size_policy.setHeightForWidth( - self._text_label.sizePolicy().hasHeightForWidth() - ) + size_policy.setHeightForWidth(self._text_label.sizePolicy().hasHeightForWidth()) self._text_label.setMinimumSize(100, 60) self._text_label.setMaximumSize(500, 60) _font = QtGui.QFont() _font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) _font.setPointSize(18) palette = self._text_label.palette() - palette.setColor( - palette.ColorRole.WindowText, QtGui.QColorConstants.White - ) + palette.setColor(palette.ColorRole.WindowText, QtGui.QColorConstants.White) self._text_label.setPalette(palette) self._text_label.setFont(_font) self._text_label.setText(str(self._text)) diff --git a/BlocksScreen/lib/panels/widgets/sensorsPanel.py b/BlocksScreen/lib/panels/widgets/sensorsPanel.py index a40247b8..51a5b2c1 100644 --- a/BlocksScreen/lib/panels/widgets/sensorsPanel.py +++ b/BlocksScreen/lib/panels/widgets/sensorsPanel.py @@ -9,10 +9,8 @@ class SensorsWindow(QtWidgets.QWidget): run_gcode_signal: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( str, name="run_gcode" ) - change_fil_sensor_state: typing.ClassVar[QtCore.pyqtSignal] = ( - QtCore.pyqtSignal( - SensorWidget.FilamentState, name="change_fil_sensor_state" - ) + change_fil_sensor_state: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + SensorWidget.FilamentState, name="change_fil_sensor_state" ) request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="request_back" @@ -21,10 +19,8 @@ class SensorsWindow(QtWidgets.QWidget): def __init__(self, parent): super(SensorsWindow, self).__init__(parent) - self.setupUi() - self.setAttribute( - QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True - ) + self._setupUi() + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) self.setTabletTracking(True) self.fs_sensors_list.itemClicked.connect(self.handle_sensor_clicked) @@ -33,33 +29,36 @@ def __init__(self, parent): @QtCore.pyqtSlot(dict, name="handle_available_fil_sensors") def handle_available_fil_sensors(self, sensors: dict) -> None: + """Handle available filament sensors, create `SensorWidget` for each detected + sensor + """ if not isinstance(sensors, dict): return - filtered_sensors = list( - filter( - lambda printer_obj: str(printer_obj).startswith("filament_switch_sensor") - or str(printer_obj).startswith("filament_motion_sensor"), - sensors.keys(), + filter( + lambda printer_obj: str(printer_obj).startswith( + "filament_switch_sensor" + ) + or str(printer_obj).startswith("filament_motion_sensor"), + sensors.keys(), ) ) - if filtered_sensors: self.fs_sensors_list.setRowHidden(self.fs_sensors_list.row(self.item), True) self.sensor_list = [ self.create_sensor_widget(name=sensor) for sensor in filtered_sensors ] else: - self.fs_sensors_list.setRowHidden(self.fs_sensors_list.row(self.item), False) - - + self.fs_sensors_list.setRowHidden( + self.fs_sensors_list.row(self.item), False + ) @QtCore.pyqtSlot(str, str, bool, name="handle_fil_state_change") def handle_fil_state_change( self, sensor_name: str, parameter: str, value: bool ) -> None: + """Handle filament state chage""" if sensor_name in self.sensor_list: - state = SensorWidget.FilamentState(value) _split = sensor_name.split(" ") _item = self.fs_sensors_list.findChild( SensorWidget, @@ -70,26 +69,21 @@ def handle_fil_state_change( if isinstance(_item, SensorWidget) and hasattr( _item, "change_fil_sensor_state" ): - _item.change_fil_sensor_state( - SensorWidget.FilamentState.PRESENT - ) + _item.change_fil_sensor_state(SensorWidget.FilamentState.PRESENT) _item.repaint() elif parameter == "filament_missing": if isinstance(_item, SensorWidget) and hasattr( _item, "change_fil_sensor_state" ): - _item.change_fil_sensor_state( - SensorWidget.FilamentState.MISSING - ) + _item.change_fil_sensor_state(SensorWidget.FilamentState.MISSING) _item.repaint() elif parameter == "enabled": if _item and isinstance(_item, SensorWidget): - self.run_gcode_signal.emit( - _item.toggle_sensor_gcode_command - ) + self.run_gcode_signal.emit(_item.toggle_sensor_gcode_command) @QtCore.pyqtSlot(QtWidgets.QListWidgetItem, name="handle_sensor_clicked") def handle_sensor_clicked(self, sensor: QtWidgets.QListWidgetItem) -> None: + """Handle filament sensor clicked""" _item = self.fs_sensors_list.itemWidget(sensor) # FIXME: This is just not working _item.toggle_button.state = ~_item.toggle_button.state @@ -116,9 +110,7 @@ def create_sensor_widget(self, name: str) -> SensorWidget: return _item_widget - def paintEvent(self, a0: QtGui.QPaintEvent) -> None: ... - - def setupUi(self): + def _setupUi(self): self.setObjectName("filament_sensors_page") sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, @@ -147,9 +139,7 @@ def setupUi(self): font = QtGui.QFont() font.setPointSize(22) palette = QtGui.QPalette() - palette.setColor( - palette.ColorRole.WindowText, QtGui.QColorConstants.White - ) + palette.setColor(palette.ColorRole.WindowText, QtGui.QColorConstants.White) self.fs_page_title.setPalette(palette) self.fs_page_title.setFont(font) self.fs_page_title.setObjectName("fs_page_title") @@ -162,9 +152,7 @@ def setupUi(self): self.fs_back_button.setMinimumSize(QtCore.QSize(60, 60)) self.fs_back_button.setMaximumSize(QtCore.QSize(60, 60)) self.fs_back_button.setFlat(True) - self.fs_back_button.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/back.svg") - ) + self.fs_back_button.setPixmap(QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) self.fs_back_button.setObjectName("fs_back_button") self.fs_header_layout.addWidget( self.fs_back_button, @@ -184,24 +172,17 @@ def setupUi(self): self.fs_sensors_list.setSizePolicy(sizePolicy) self.fs_sensors_list.setMinimumSize(QtCore.QSize(650, 300)) self.fs_sensors_list.setMaximumSize(QtCore.QSize(700, 300)) - self.fs_sensors_list.setLayoutDirection( - QtCore.Qt.LayoutDirection.LeftToRight - ) + self.fs_sensors_list.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self.fs_sensors_list.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) self.fs_sensors_list.setObjectName("fs_sensors_list") - self.fs_sensors_list.setViewMode( - self.fs_sensors_list.ViewMode.ListMode - ) + self.fs_sensors_list.setViewMode(self.fs_sensors_list.ViewMode.ListMode) self.fs_sensors_list.setItemAlignment( - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter ) self.fs_sensors_list.setFlow(self.fs_sensors_list.Flow.TopToBottom) self.fs_sensors_list.setFrameStyle(0) palette = self.fs_sensors_list.palette() - palette.setColor( - palette.ColorRole.Base, QtGui.QColorConstants.Transparent - ) + palette.setColor(palette.ColorRole.Base, QtGui.QColorConstants.Transparent) self.fs_sensors_list.setPalette(palette) self.fs_sensors_list.setDropIndicatorShown(False) self.fs_sensors_list.setAcceptDrops(False) @@ -211,34 +192,31 @@ def setupUi(self): self.content_vertical_layout.addWidget( self.fs_sensors_list, 1, - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) font = QtGui.QFont() font.setPointSize(25) - self.item = QtWidgets.QListWidgetItem() - self.item.setSizeHint(QtCore.QSize(self.fs_sensors_list.width(),self.fs_sensors_list.height())) - + self.item.setSizeHint( + QtCore.QSize(self.fs_sensors_list.width(), self.fs_sensors_list.height()) + ) self.label = QtWidgets.QLabel("No sensors found") self.label.setFont(font) - self.label.setStyleSheet("color: gray;") + self.label.setStyleSheet("color: gray;") self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.label.hide() self.fs_sensors_list.addItem(self.item) - self.fs_sensors_list.setItemWidget(self.item,self.label) - - + self.fs_sensors_list.setItemWidget(self.item, self.label) self.content_vertical_layout.addSpacing(5) self.setLayout(self.content_vertical_layout) - self.retranslateUi() + self._retranslateUi() - def retranslateUi(self): + def _retranslateUi(self): _translate = QtCore.QCoreApplication.translate self.setWindowTitle(_translate("filament_sensors_page", "Form")) self.fs_page_title.setText( diff --git a/BlocksScreen/lib/panels/widgets/slider_selector_page.py b/BlocksScreen/lib/panels/widgets/slider_selector_page.py index 02f9006a..793b7044 100644 --- a/BlocksScreen/lib/panels/widgets/slider_selector_page.py +++ b/BlocksScreen/lib/panels/widgets/slider_selector_page.py @@ -27,26 +27,20 @@ def __init__(self, parent) -> None: self.decrease_button_icon = QtGui.QPixmap( ":/arrow_icons/media/btn_icons/left_arrow.svg" ) - self.background = QtGui.QPixmap( - ":/ui/background/media/1st_background.png" - ) + self.background = QtGui.QPixmap(":/ui/background/media/1st_background.png") self.setStyleSheet( "#SliderPage{background-image: url(:/background/media/1st_background.png);}\n" ) self.setObjectName("SliderPage") - self.setupUI() + self._setupUI() self.back_button.clicked.connect(self.request_back.emit) self.back_button.clicked.connect(self.value_selected.disconnect) self.slider.valueChanged.connect(self.on_slider_value_change) self.increase_button.pressed.connect( - lambda: ( - self.slider.setSliderPosition(self.slider.sliderPosition() + 5) - ) + lambda: (self.slider.setSliderPosition(self.slider.sliderPosition() + 5)) ) self.decrease_button.pressed.connect( - lambda: ( - self.slider.setSliderPosition(self.slider.sliderPosition() - 5) - ) + lambda: (self.slider.setSliderPosition(self.slider.sliderPosition() - 5)) ) self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) @@ -64,9 +58,11 @@ def set_slider_position(self, value: int) -> None: self.slider.setSliderPosition(int(value)) def set_slider_minimum(self, value: int) -> None: + """Set slider minimum value""" self.slider.setMinimum(value) def set_slider_maximum(self, value: int) -> None: + """Set slider maximum value""" self.slider.setMaximum(value) def paintEvent(self, a0: QtGui.QPaintEvent) -> None: @@ -78,21 +74,9 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: painter.drawPixmap(self.rect(), self.background, self.rect()) self.current_value_label.setText(str(self.slider.value()) + " " + "%") self.object_name_label.setText(str(self.name)) - # if "speed" in self.name.lower(): - # # REFACTOR: Change this, so it's not hardcoded to be with objects named "speed. " - # # Range should increase however if a flag is set, if the maximum is above 100 - # # then increase the range of the slider after it is set to the maximum - # if ( - # self.slider.maximum() <= self.max_value - # and self.slider.sliderPosition() + 10 >= self.slider.maximum() - # ): - # self.slider.setMaximum(int(int(self.slider.maximum()) + 100)) - # elif self.slider.maximum() <= 100: - # self.slider.setMaximum(100) - painter.end() - def setupUI(self) -> None: + def _setupUI(self) -> None: """Setup the components for the widget""" self.setMinimumSize(QtCore.QSize(700, 410)) self.setMaximumSize(QtCore.QSize(720, 420)) @@ -131,18 +115,13 @@ def setupUI(self) -> None: self.object_name_label.setFont(font) self.object_name_label.setPalette(palette) self.object_name_label.setMinimumSize(QtCore.QSize(self.width(), 60)) - self.object_name_label.setMaximumSize( - QtCore.QSize(self.width() - 60, 60) - ) + self.object_name_label.setMaximumSize(QtCore.QSize(self.width() - 60, 60)) self.object_name_label.setAlignment( - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter ) self.back_button = IconButton(self) - self.back_button.setPixmap( - QtGui.QPixmap(":ui/media/btn_icons/back.svg") - ) + self.back_button.setPixmap(QtGui.QPixmap(":ui/media/btn_icons/back.svg")) self.back_button.has_text = False self.back_button.setMinimumSize(QtCore.QSize(60, 60)) self.back_button.setMaximumSize(QtCore.QSize(60, 60)) @@ -160,12 +139,9 @@ def setupUI(self) -> None: self.current_value_label.setFont(font) self.current_value_label.setPalette(palette) self.current_value_label.setMinimumSize(QtCore.QSize(self.width(), 80)) - self.current_value_label.setMaximumSize( - QtCore.QSize(self.width(), 300) - ) + self.current_value_label.setMaximumSize(QtCore.QSize(self.width(), 300)) self.current_value_label.setAlignment( - QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter ) self.middle_content_layout.addWidget( self.current_value_label, @@ -189,8 +165,7 @@ def setupUI(self) -> None: self.slider_layout.addWidget( self.slider, 0, - QtCore.Qt.AlignmentFlag.AlignVCenter - | QtCore.Qt.AlignmentFlag.AlignHCenter, + QtCore.Qt.AlignmentFlag.AlignVCenter | QtCore.Qt.AlignmentFlag.AlignHCenter, ) self.increase_button = IconButton(self) self.increase_button.setProperty( diff --git a/BlocksScreen/lib/panels/widgets/troubleshootPage.py b/BlocksScreen/lib/panels/widgets/troubleshootPage.py index 6a9b4886..0c327ac7 100644 --- a/BlocksScreen/lib/panels/widgets/troubleshootPage.py +++ b/BlocksScreen/lib/panels/widgets/troubleshootPage.py @@ -2,9 +2,11 @@ from lib.utils.icon_button import IconButton + class TroubleshootPage(QtWidgets.QDialog): def __init__( - self, parent: QtWidgets.QWidget, + self, + parent: QtWidgets.QWidget, ) -> None: super().__init__(parent) self.setStyleSheet( @@ -16,23 +18,23 @@ def __init__( """ ) self.setWindowFlags( - QtCore.Qt.WindowType.Popup - | QtCore.Qt.WindowType.FramelessWindowHint + QtCore.Qt.WindowType.Popup | QtCore.Qt.WindowType.FramelessWindowHint + ) + self._setupUI() + self.label_4.setText( + "For more information check our website \n www.blockstec.com \n or \nsupport@blockstec.com" ) - self.setupUI() - self.label_4.setText("For more information check our website \n www.blockstec.com \n or \nsupport@blockstec.com") - - self.repaint() - def geometry_calc(self) -> None: + def _geometry_calc(self) -> None: + """Calculate widget position relative to the screen""" app_instance = QtWidgets.QApplication.instance() main_window = app_instance.activeWindow() if app_instance else None if main_window is None and app_instance: for widget in app_instance.allWidgets(): if isinstance(widget, QtWidgets.QMainWindow): main_window = widget - if main_window: + if main_window: x = main_window.geometry().x() y = main_window.geometry().y() width = main_window.width() @@ -40,21 +42,32 @@ def geometry_calc(self) -> None: self.setGeometry(x, y, width, height) def show(self) -> None: - self.geometry_calc() + """Re-implemented method, widget show""" + self._geometry_calc() self.repaint() return super().show() - def setupUI(self) -> None: + def _setupUI(self) -> None: self.setObjectName("troubleshoot_page") self.verticalLayout = QtWidgets.QVBoxLayout(self) self.verticalLayout.setObjectName("verticalLayout") self.leds_slider_header_layout_2 = QtWidgets.QHBoxLayout() self.leds_slider_header_layout_2.setObjectName("leds_slider_header_layout_2") - spacerItem18 = QtWidgets.QSpacerItem(60, 60, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + spacerItem18 = QtWidgets.QSpacerItem( + 60, + 60, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) self.leds_slider_header_layout_2.addItem(spacerItem18) - spacerItem19 = QtWidgets.QSpacerItem(181, 60, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + spacerItem19 = QtWidgets.QSpacerItem( + 181, + 60, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Minimum, + ) self.leds_slider_header_layout_2.addItem(spacerItem19) - self.tb_tittle_label = QtWidgets.QLabel("Troubleshoot",parent=self) + self.tb_tittle_label = QtWidgets.QLabel("Troubleshoot", parent=self) self.tb_tittle_label.setMinimumSize(QtCore.QSize(0, 60)) self.tb_tittle_label.setMaximumSize(QtCore.QSize(16777215, 60)) font = QtGui.QFont() @@ -65,10 +78,18 @@ def setupUI(self) -> None: self.tb_tittle_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.tb_tittle_label.setObjectName("tb_tittle_label") self.leds_slider_header_layout_2.addWidget(self.tb_tittle_label) - spacerItem20 = QtWidgets.QSpacerItem(0, 60, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + spacerItem20 = QtWidgets.QSpacerItem( + 0, + 60, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Minimum, + ) self.leds_slider_header_layout_2.addItem(spacerItem20) self.tb_back_btn = IconButton(parent=self) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.tb_back_btn.sizePolicy().hasHeightForWidth()) @@ -88,7 +109,9 @@ def setupUI(self) -> None: self.tb_back_btn.setStyleSheet("") self.tb_back_btn.setAutoDefault(False) self.tb_back_btn.setFlat(True) - self.tb_back_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) + self.tb_back_btn.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + ) self.tb_back_btn.setObjectName("tb_back_btn") self.leds_slider_header_layout_2.addWidget(self.tb_back_btn) self.verticalLayout.addLayout(self.leds_slider_header_layout_2) @@ -96,8 +119,11 @@ def setupUI(self) -> None: self.horizontalLayout.setObjectName("horizontalLayout") self.verticalLayout_10 = QtWidgets.QVBoxLayout() self.verticalLayout_10.setObjectName("verticalLayout_10") - self.label_4 = QtWidgets.QLabel("idk whar to type this",parent=self) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) + self.label_4 = QtWidgets.QLabel("idk whar to type this", parent=self) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.label_4.sizePolicy().hasHeightForWidth()) @@ -110,11 +136,4 @@ def setupUI(self) -> None: self.label_4.setObjectName("label_4") self.verticalLayout_10.addWidget(self.label_4) self.horizontalLayout.addLayout(self.verticalLayout_10) - # self.widget = QtWidgets.QWidget(parent=self) - # self.widget.setMinimumSize(QtCore.QSize(300, 300)) - # self.widget.setMaximumSize(QtCore.QSize(300, 300)) - # self.widget.setAutoFillBackground(True) - # self.widget.setStyleSheet("color:white") - # self.widget.setObjectName("widget") - # self.horizontalLayout.addWidget(self.widget) - self.verticalLayout.addLayout(self.horizontalLayout) \ No newline at end of file + self.verticalLayout.addLayout(self.horizontalLayout) diff --git a/BlocksScreen/lib/panels/widgets/tunePage.py b/BlocksScreen/lib/panels/widgets/tunePage.py index ece1e770..cb9a00c8 100644 --- a/BlocksScreen/lib/panels/widgets/tunePage.py +++ b/BlocksScreen/lib/panels/widgets/tunePage.py @@ -12,8 +12,8 @@ class TuneWidget(QtWidgets.QWidget): name="request_back_page" ) - request_sensorsPage: typing.ClassVar[QtCore.pyqtSignal] = ( - QtCore.pyqtSignal(name="request_sensorsPage") + request_sensorsPage: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + name="request_sensorsPage" ) request_bbpPage: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="request_bbpPage" @@ -40,14 +40,12 @@ class TuneWidget(QtWidgets.QWidget): def __init__(self, parent) -> None: super().__init__(parent) self.setObjectName("tune_page") - self.setupUI() + self._setupUI() self.sensors_menu_btn.clicked.connect(self.request_sensorsPage.emit) self.tune_babystep_menu_btn.clicked.connect(self.request_bbpPage.emit) self.tune_back_btn.clicked.connect(self.request_back) self.bed_display.clicked.connect( - lambda: self.request_numpad[ - str, int, "PyQt_PyObject", int, int - ].emit( + lambda: self.request_numpad[str, int, "PyQt_PyObject", int, int].emit( "Bed", int(round(self.bed_target)), self.on_numpad_change, @@ -56,9 +54,7 @@ def __init__(self, parent) -> None: ) ) self.extruder_display.clicked.connect( - lambda: self.request_numpad[ - str, int, "PyQt_PyObject", int, int - ].emit( + lambda: self.request_numpad[str, int, "PyQt_PyObject", int, int].emit( "Extruder", int(round(self.extruder_target)), self.on_numpad_change, @@ -67,9 +63,7 @@ def __init__(self, parent) -> None: ) ) self.speed_display.clicked.connect( - lambda: self.request_sliderPage[ - str, int, "PyQt_PyObject", int, int - ].emit( + lambda: self.request_sliderPage[str, int, "PyQt_PyObject", int, int].emit( "Speed", int(self.speed_factor_override * 100), self.on_slider_change, @@ -80,16 +74,16 @@ def __init__(self, parent) -> None: @QtCore.pyqtSlot(str, int, name="on_numpad_change") def on_numpad_change(self, name: str, new_value: int) -> None: + """Handle numpad value inserted""" if "bed" in name.lower(): name = "heater_bed" elif "extruder" in name.lower(): name = "extruder" - self.run_gcode.emit( - f"SET_HEATER_TEMPERATURE HEATER={name} TARGET={new_value}" - ) + self.run_gcode.emit(f"SET_HEATER_TEMPERATURE HEATER={name} TARGET={new_value}") @QtCore.pyqtSlot(str, int, name="on_slider_change") def on_slider_change(self, name: str, new_value: int) -> None: + """Handle slider page value inserted""" if "speed" in name.lower(): self.speed_factor_override = new_value / 100 self.run_gcode.emit(f"M220 S{new_value}") @@ -156,16 +150,12 @@ def on_fan_object_update( ) else: _new_display_button.setDisabled(True) - self.tune_display_vertical_child_layout_2.addWidget( - _new_display_button - ) + self.tune_display_vertical_child_layout_2.addWidget(_new_display_button) _display_button = self.tune_display_buttons.get(name) if not _display_button: return _display_button.update({"speed": int(round(new_value * 100))}) - _display_button.get("display_button").setText( - f"{new_value * 100:.0f}%" - ) + _display_button.get("display_button").setText(f"{new_value * 100:.0f}%") def create_display_button(self, name: str) -> DisplayButton: """Create and return a DisplayButton @@ -187,11 +177,10 @@ def create_display_button(self, name: str) -> DisplayButton: @QtCore.pyqtSlot(str, float, name="on_gcode_move_update") def on_gcode_move_update(self, field: str, value: float) -> None: + """Handle gcode move update""" if "speed_factor" in field: self.speed_factor_override = value - self.speed_display.setText( - str(f"{int(self.speed_factor_override * 100)}%") - ) + self.speed_display.setText(str(f"{int(self.speed_factor_override * 100)}%")) @QtCore.pyqtSlot(str, str, float, name="on_extruder_update") def on_extruder_temperature_change( @@ -226,12 +215,11 @@ def on_heater_bed_temperature_change( self.bed_target = int(new_value) def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" if self.isVisible(): - self.speed_display.setText( - str(f"{int(self.speed_factor_override * 100)}%") - ) + self.speed_display.setText(str(f"{int(self.speed_factor_override * 100)}%")) - def setupUI(self) -> None: + def _setupUI(self) -> None: sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, @@ -259,9 +247,7 @@ def setupUI(self) -> None: self.tune_title_label.setFont(font) self.tune_title_label.setPalette(palette) - self.tune_title_label.setLayoutDirection( - QtCore.Qt.LayoutDirection.RightToLeft - ) + self.tune_title_label.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) self.tune_title_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.tune_title_label.setObjectName("tune_title_label") self.tune_header.addWidget( @@ -273,9 +259,7 @@ def setupUI(self) -> None: self.tune_back_btn.setMinimumSize(QtCore.QSize(60, 60)) self.tune_back_btn.setMaximumSize(QtCore.QSize(60, 60)) self.tune_back_btn.setFlat(True) - self.tune_back_btn.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/back.svg") - ) + self.tune_back_btn.setPixmap(QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) self.tune_header.addWidget( self.tune_back_btn, 0, @@ -332,9 +316,7 @@ def setupUI(self) -> None: self.tune_change_filament_btn.setAutoDefault(False) self.tune_change_filament_btn.setFlat(True) self.tune_change_filament_btn.setPixmap( - QtGui.QPixmap( - ":/filament_related/media/btn_icons/change_filament.svg" - ) + QtGui.QPixmap(":/filament_related/media/btn_icons/change_filament.svg") ) self.tune_change_filament_btn.setObjectName("tune_change_filament_btn") self.tune_menu_buttons.addWidget( @@ -355,46 +337,34 @@ def setupUI(self) -> None: self.sensors_menu_btn.setContextMenuPolicy( QtCore.Qt.ContextMenuPolicy.NoContextMenu ) - self.sensors_menu_btn.setLayoutDirection( - QtCore.Qt.LayoutDirection.LeftToRight - ) + self.sensors_menu_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self.sensors_menu_btn.setAutoDefault(False) self.sensors_menu_btn.setFlat(True) self.sensors_menu_btn.setPixmap( - QtGui.QPixmap( - ":/filament_related/media/btn_icons/filament_sensor.svg" - ) + QtGui.QPixmap(":/filament_related/media/btn_icons/filament_sensor.svg") ) self.sensors_menu_btn.setObjectName("sensors_menu_btn") self.tune_menu_buttons.addWidget(self.sensors_menu_btn) self.tune_content.addLayout(self.tune_menu_buttons, 0) self.tune_display_horizontal_parent_layout = QtWidgets.QHBoxLayout() - self.tune_display_horizontal_parent_layout.setContentsMargins( - 2, 0, 2, 2 - ) + self.tune_display_horizontal_parent_layout.setContentsMargins(2, 0, 2, 2) self.tune_display_horizontal_parent_layout.setObjectName( "tune_display_horizontal_parent_layout" ) self.tune_display_vertical_child_layout_1 = QtWidgets.QVBoxLayout() - self.tune_display_vertical_child_layout_1.setContentsMargins( - 2, 0, 2, 2 - ) + self.tune_display_vertical_child_layout_1.setContentsMargins(2, 0, 2, 2) self.tune_display_vertical_child_layout_1.setSpacing(5) self.tune_display_vertical_child_layout_1.setObjectName( "tune_display_vertical_parent_layout" ) self.tune_display_vertical_child_layout_2 = QtWidgets.QVBoxLayout() - self.tune_display_vertical_child_layout_2.setContentsMargins( - 2, 0, 2, 2 - ) + self.tune_display_vertical_child_layout_2.setContentsMargins(2, 0, 2, 2) self.tune_display_vertical_child_layout_2.setSpacing(5) self.tune_display_vertical_child_layout_2.setObjectName( "tune_display_vertical_parent_layout_2" ) self.tune_display_vertical_child_layout_3 = QtWidgets.QVBoxLayout() - self.tune_display_vertical_child_layout_3.setContentsMargins( - 2, 0, 2, 2 - ) + self.tune_display_vertical_child_layout_3.setContentsMargins(2, 0, 2, 2) self.tune_display_vertical_child_layout_3.setSpacing(5) self.tune_display_vertical_child_layout_3.setObjectName( "tune_display_vertical_parent_layout_3" @@ -409,15 +379,11 @@ def setupUI(self) -> None: self.tune_display_vertical_child_layout_3 ) self.tune_display_horizontal_parent_layout.setSpacing(0) - self.tune_display_horizontal_parent_layout.setContentsMargins( - 0, 0, 0, 0 - ) + self.tune_display_horizontal_parent_layout.setContentsMargins(0, 0, 0, 0) self.bed_display = DisplayButton(parent=self) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth( - self.bed_display.sizePolicy().hasHeightForWidth() - ) + sizePolicy.setHeightForWidth(self.bed_display.sizePolicy().hasHeightForWidth()) self.bed_display.setSizePolicy(sizePolicy) self.bed_display.setMinimumSize(QtCore.QSize(150, 60)) self.bed_display.setMaximumSize(QtCore.QSize(150, 60)) @@ -426,9 +392,7 @@ def setupUI(self) -> None: self.bed_display.setText("") self.bed_display.setFlat(True) self.bed_display.setPixmap( - QtGui.QPixmap( - ":/temperature_related/media/btn_icons/temperature_plate.svg" - ) + QtGui.QPixmap(":/temperature_related/media/btn_icons/temperature_plate.svg") ) self.bed_display.setObjectName("bed_display") self.tune_display_vertical_child_layout_1.addWidget(self.bed_display) @@ -443,14 +407,10 @@ def setupUI(self) -> None: self.extruder_display.setText("") self.extruder_display.setFlat(True) self.extruder_display.setPixmap( - QtGui.QPixmap( - ":/temperature_related/media/btn_icons/temperature.svg" - ) + QtGui.QPixmap(":/temperature_related/media/btn_icons/temperature.svg") ) self.extruder_display.setObjectName("extruder_display") - self.tune_display_vertical_child_layout_1.addWidget( - self.extruder_display - ) + self.tune_display_vertical_child_layout_1.addWidget(self.extruder_display) self.speed_display = DisplayButton(parent=self) self.speed_display.setFont(font) sizePolicy.setHeightForWidth( @@ -466,9 +426,7 @@ def setupUI(self) -> None: ) self.speed_display.setObjectName("speed_display") self.tune_display_vertical_child_layout_3.addWidget(self.speed_display) - self.tune_content.addLayout( - self.tune_display_horizontal_parent_layout, 1 - ) + self.tune_content.addLayout(self.tune_display_horizontal_parent_layout, 1) self.tune_display_horizontal_parent_layout.setStretch(0, 0) self.tune_display_horizontal_parent_layout.setStretch(1, 0) self.tune_display_horizontal_parent_layout.setStretch(2, 1) @@ -478,9 +436,9 @@ def setupUI(self) -> None: self.tune_content.setContentsMargins(2, 0, 2, 0) self.setLayout(self.tune_content) self.setContentsMargins(2, 2, 2, 2) - self.retranslateUI() + self._retranslateUI() - def retranslateUI(self): + def _retranslateUI(self): _translate = QtCore.QCoreApplication.translate self.setWindowTitle(_translate("printStackedWidget", "StackedWidget")) self.tune_title_label.setText(_translate("printStackedWidget", "Tune")) diff --git a/BlocksScreen/lib/panels/widgets/updatePage.py b/BlocksScreen/lib/panels/widgets/updatePage.py index 7b725514..534bbabe 100644 --- a/BlocksScreen/lib/panels/widgets/updatePage.py +++ b/BlocksScreen/lib/panels/widgets/updatePage.py @@ -239,7 +239,7 @@ def handle_update_message(self, message: dict) -> None: elif self.ongoing_update or complete: self.ongoing_update = False self.update_end.emit() - + cli_version_info = message.get("version_info", None) if not cli_version_info: return diff --git a/BlocksScreen/lib/printer.py b/BlocksScreen/lib/printer.py index c3df0d8a..52706fd5 100644 --- a/BlocksScreen/lib/printer.py +++ b/BlocksScreen/lib/printer.py @@ -464,7 +464,7 @@ def _heater_fan_object_updated(self, value: dict, fan_name: str = "") -> None: # Associated with a heater, on when heater is active # Parameters same as a normal fan _names = ["heater_fan", fan_name] - object_name = " ".join(_names) + # object_name = " ".join(_names) def _idle_timeout_object_updated( self, value: dict, name: str = "idle_timeout" diff --git a/BlocksScreen/lib/qrcode_gen.py b/BlocksScreen/lib/qrcode_gen.py index 3720f261..1901cef1 100644 --- a/BlocksScreen/lib/qrcode_gen.py +++ b/BlocksScreen/lib/qrcode_gen.py @@ -4,9 +4,7 @@ BLOCKS_URL = "https://blockstec.com" RF50_MANUAL_PAGE = "https://blockstec.com/RF50" RF50_PRODUCT_PAGE = "https://blockstec.com/rf-50" -RF50_DATASHEET_PAGE = ( - "https://www.blockstec.com/assets/downloads/rf50_datasheet.pdf" -) +RF50_DATASHEET_PAGE = "https://www.blockstec.com/assets/downloads/rf50_datasheet.pdf" RF50_DATASHEET_PAGE = "https://blockstec.com/assets/files/rf50_user_manual.pdf" @@ -28,5 +26,7 @@ def make_qrcode(data) -> ImageQt.ImageQt: def generate_wifi_qrcode( ssid: str, password: str, auth_type: str, hidden: bool = False ) -> ImageQt.ImageQt: - wifi_data = f"WIFI:T:{auth_type};S:{ssid};P:{password};{'H:true;' if hidden else ''};" + wifi_data = ( + f"WIFI:T:{auth_type};S:{ssid};P:{password};{'H:true;' if hidden else ''};" + ) return make_qrcode(wifi_data) diff --git a/BlocksScreen/lib/ui/instructionsWindow.ui b/BlocksScreen/lib/ui/instructionsWindow.ui deleted file mode 100644 index 17c4c46f..00000000 --- a/BlocksScreen/lib/ui/instructionsWindow.ui +++ /dev/null @@ -1,377 +0,0 @@ - - - utilitiesStackedWidget - - - - 0 - 0 - 798 - 417 - - - - StackedWidget - - - 1 - - - - - - 300 - 60 - 241 - 61 - - - - - Momcake - 24 - - - - background: transparent; color: white; - - - Switch Print Cores - - - title_text - - - - - true - - - - 200 - 170 - 411 - 91 - - - - - Montserrat - 14 - - - - background: transparent; color: white; - - - Printer Heating, wait to switch - - - - - - 680 - 40 - 101 - 71 - - - - - 10 - 10 - - - - - Momcake - 20 - false - PreferAntialias - - - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - - - - Cancel - - - false - - - true - - - menu_btn - - - :/button_borders/media/buttons/btn_part1.svg - - - :/button_borders/media/buttons/btn_part2.svg - - - :/button_borders/media/buttons/btn_part3.svg - - - normal - - - - - - - - 270 - 60 - 271 - 61 - - - - - Momcake - 24 - - - - background: transparent; color: white; - - - ROUTINE CHECK - - - title_text - - - - - true - - - - 200 - 180 - 411 - 91 - - - - - Montserrat - 14 - - - - background: transparent; color: white; - - - Do this - - - - - - 680 - 40 - 101 - 71 - - - - - 10 - 10 - - - - - Momcake - 20 - false - PreferAntialias - - - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - - - - Cancel - - - false - - - true - - - menu_btn - - - :/button_borders/media/buttons/btn_part1.svg - - - :/button_borders/media/buttons/btn_part2.svg - - - :/button_borders/media/buttons/btn_part3.svg - - - normal - - - - - - - - 240 - 60 - 271 - 61 - - - - - Momcake - 24 - - - - background: transparent; color: white; - - - AXES MAINTENANCE - - - title_text - - - - - true - - - - 170 - 180 - 411 - 91 - - - - - Montserrat - 14 - - - - background: transparent; color: white; - - - Use oil here - - - - - - 650 - 40 - 101 - 71 - - - - - 10 - 10 - - - - - Momcake - 20 - false - PreferAntialias - - - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - - - - Cancel - - - false - - - true - - - menu_btn - - - :/button_borders/media/buttons/btn_part1.svg - - - :/button_borders/media/buttons/btn_part2.svg - - - :/button_borders/media/buttons/btn_part3.svg - - - normal - - - - - - - BlocksCustomButton - QPushButton -
lib.utils.ui
-
-
- - - - - -
diff --git a/BlocksScreen/lib/ui/instructionsWindow_ui.py b/BlocksScreen/lib/ui/instructionsWindow_ui.py deleted file mode 100644 index 1198cb0c..00000000 --- a/BlocksScreen/lib/ui/instructionsWindow_ui.py +++ /dev/null @@ -1,162 +0,0 @@ -# Form implementation generated from reading ui file '/home/bugo/github/Blocks_Screen/BlocksScreen/lib/ui/instructionsWindow.ui' -# -# Created by: PyQt6 UI code generator 6.4.2 -# -# WARNING: Any manual changes made to this file will be lost when pyuic6 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt6 import QtCore, QtGui, QtWidgets - - -class Ui_utilitiesStackedWidget(object): - def setupUi(self, utilitiesStackedWidget): - utilitiesStackedWidget.setObjectName("utilitiesStackedWidget") - utilitiesStackedWidget.resize(798, 417) - self.switch_pc_page = QtWidgets.QWidget() - self.switch_pc_page.setObjectName("switch_pc_page") - self.switch_pc_title_label = QtWidgets.QLabel(parent=self.switch_pc_page) - self.switch_pc_title_label.setGeometry(QtCore.QRect(300, 60, 241, 61)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(24) - self.switch_pc_title_label.setFont(font) - self.switch_pc_title_label.setStyleSheet("background: transparent; color: white;") - self.switch_pc_title_label.setObjectName("switch_pc_title_label") - self.switch_pc_text_label = QtWidgets.QLabel(parent=self.switch_pc_page) - self.switch_pc_text_label.setEnabled(True) - self.switch_pc_text_label.setGeometry(QtCore.QRect(200, 170, 411, 91)) - font = QtGui.QFont() - font.setFamily("Montserrat") - font.setPointSize(14) - self.switch_pc_text_label.setFont(font) - self.switch_pc_text_label.setStyleSheet("background: transparent; color: white;") - self.switch_pc_text_label.setObjectName("switch_pc_text_label") - self.switch_pc_cancel_btn = BlocksCustomButton(parent=self.switch_pc_page) - self.switch_pc_cancel_btn.setGeometry(QtCore.QRect(680, 40, 101, 71)) - self.switch_pc_cancel_btn.setMinimumSize(QtCore.QSize(10, 10)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(20) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.switch_pc_cancel_btn.setFont(font) - self.switch_pc_cancel_btn.setMouseTracking(False) - self.switch_pc_cancel_btn.setTabletTracking(True) - self.switch_pc_cancel_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.switch_pc_cancel_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.switch_pc_cancel_btn.setStyleSheet("") - self.switch_pc_cancel_btn.setAutoDefault(False) - self.switch_pc_cancel_btn.setFlat(True) - self.switch_pc_cancel_btn.setProperty("borderLeftPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part1.svg")) - self.switch_pc_cancel_btn.setProperty("borderCenterPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part2.svg")) - self.switch_pc_cancel_btn.setProperty("borderRightPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part3.svg")) - self.switch_pc_cancel_btn.setObjectName("switch_pc_cancel_btn") - utilitiesStackedWidget.addWidget(self.switch_pc_page) - self.routine_check_page = QtWidgets.QWidget() - self.routine_check_page.setObjectName("routine_check_page") - self.routine_check_calib_title_label = QtWidgets.QLabel(parent=self.routine_check_page) - self.routine_check_calib_title_label.setGeometry(QtCore.QRect(270, 60, 271, 61)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(24) - self.routine_check_calib_title_label.setFont(font) - self.routine_check_calib_title_label.setStyleSheet("background: transparent; color: white;") - self.routine_check_calib_title_label.setObjectName("routine_check_calib_title_label") - self.routine_check_text_label = QtWidgets.QLabel(parent=self.routine_check_page) - self.routine_check_text_label.setEnabled(True) - self.routine_check_text_label.setGeometry(QtCore.QRect(200, 180, 411, 91)) - font = QtGui.QFont() - font.setFamily("Montserrat") - font.setPointSize(14) - self.routine_check_text_label.setFont(font) - self.routine_check_text_label.setStyleSheet("background: transparent; color: white;") - self.routine_check_text_label.setObjectName("routine_check_text_label") - self.routine_check_cancel_btn = BlocksCustomButton(parent=self.routine_check_page) - self.routine_check_cancel_btn.setGeometry(QtCore.QRect(680, 40, 101, 71)) - self.routine_check_cancel_btn.setMinimumSize(QtCore.QSize(10, 10)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(20) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.routine_check_cancel_btn.setFont(font) - self.routine_check_cancel_btn.setMouseTracking(False) - self.routine_check_cancel_btn.setTabletTracking(True) - self.routine_check_cancel_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.routine_check_cancel_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.routine_check_cancel_btn.setStyleSheet("") - self.routine_check_cancel_btn.setAutoDefault(False) - self.routine_check_cancel_btn.setFlat(True) - self.routine_check_cancel_btn.setProperty("borderLeftPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part1.svg")) - self.routine_check_cancel_btn.setProperty("borderCenterPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part2.svg")) - self.routine_check_cancel_btn.setProperty("borderRightPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part3.svg")) - self.routine_check_cancel_btn.setObjectName("routine_check_cancel_btn") - utilitiesStackedWidget.addWidget(self.routine_check_page) - self.axes_maintenance_page = QtWidgets.QWidget() - self.axes_maintenance_page.setObjectName("axes_maintenance_page") - self.axes_maintenance_calib_title_label = QtWidgets.QLabel(parent=self.axes_maintenance_page) - self.axes_maintenance_calib_title_label.setGeometry(QtCore.QRect(240, 60, 271, 61)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(24) - self.axes_maintenance_calib_title_label.setFont(font) - self.axes_maintenance_calib_title_label.setStyleSheet("background: transparent; color: white;") - self.axes_maintenance_calib_title_label.setObjectName("axes_maintenance_calib_title_label") - self.axes_maintenance_text_label = QtWidgets.QLabel(parent=self.axes_maintenance_page) - self.axes_maintenance_text_label.setEnabled(True) - self.axes_maintenance_text_label.setGeometry(QtCore.QRect(170, 180, 411, 91)) - font = QtGui.QFont() - font.setFamily("Montserrat") - font.setPointSize(14) - self.axes_maintenance_text_label.setFont(font) - self.axes_maintenance_text_label.setStyleSheet("background: transparent; color: white;") - self.axes_maintenance_text_label.setObjectName("axes_maintenance_text_label") - self.axes_maintenance_cancel_btn = BlocksCustomButton(parent=self.axes_maintenance_page) - self.axes_maintenance_cancel_btn.setGeometry(QtCore.QRect(650, 40, 101, 71)) - self.axes_maintenance_cancel_btn.setMinimumSize(QtCore.QSize(10, 10)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(20) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.axes_maintenance_cancel_btn.setFont(font) - self.axes_maintenance_cancel_btn.setMouseTracking(False) - self.axes_maintenance_cancel_btn.setTabletTracking(True) - self.axes_maintenance_cancel_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.axes_maintenance_cancel_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.axes_maintenance_cancel_btn.setStyleSheet("") - self.axes_maintenance_cancel_btn.setAutoDefault(False) - self.axes_maintenance_cancel_btn.setFlat(True) - self.axes_maintenance_cancel_btn.setProperty("borderLeftPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part1.svg")) - self.axes_maintenance_cancel_btn.setProperty("borderCenterPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part2.svg")) - self.axes_maintenance_cancel_btn.setProperty("borderRightPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part3.svg")) - self.axes_maintenance_cancel_btn.setObjectName("axes_maintenance_cancel_btn") - utilitiesStackedWidget.addWidget(self.axes_maintenance_page) - - self.retranslateUi(utilitiesStackedWidget) - utilitiesStackedWidget.setCurrentIndex(1) - QtCore.QMetaObject.connectSlotsByName(utilitiesStackedWidget) - - def retranslateUi(self, utilitiesStackedWidget): - _translate = QtCore.QCoreApplication.translate - utilitiesStackedWidget.setWindowTitle(_translate("utilitiesStackedWidget", "StackedWidget")) - self.switch_pc_title_label.setText(_translate("utilitiesStackedWidget", "Switch Print Cores")) - self.switch_pc_title_label.setProperty("class", _translate("utilitiesStackedWidget", "title_text")) - self.switch_pc_text_label.setText(_translate("utilitiesStackedWidget", "Printer Heating, wait to switch")) - self.switch_pc_cancel_btn.setText(_translate("utilitiesStackedWidget", "Cancel")) - self.switch_pc_cancel_btn.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.switch_pc_cancel_btn.setProperty("button_type", _translate("utilitiesStackedWidget", "normal")) - self.routine_check_calib_title_label.setText(_translate("utilitiesStackedWidget", "ROUTINE CHECK")) - self.routine_check_calib_title_label.setProperty("class", _translate("utilitiesStackedWidget", "title_text")) - self.routine_check_text_label.setText(_translate("utilitiesStackedWidget", "Do this")) - self.routine_check_cancel_btn.setText(_translate("utilitiesStackedWidget", "Cancel")) - self.routine_check_cancel_btn.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.routine_check_cancel_btn.setProperty("button_type", _translate("utilitiesStackedWidget", "normal")) - self.axes_maintenance_calib_title_label.setText(_translate("utilitiesStackedWidget", "AXES MAINTENANCE")) - self.axes_maintenance_calib_title_label.setProperty("class", _translate("utilitiesStackedWidget", "title_text")) - self.axes_maintenance_text_label.setText(_translate("utilitiesStackedWidget", "Use oil here")) - self.axes_maintenance_cancel_btn.setText(_translate("utilitiesStackedWidget", "Cancel")) - self.axes_maintenance_cancel_btn.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.axes_maintenance_cancel_btn.setProperty("button_type", _translate("utilitiesStackedWidget", "normal")) -from lib.utils.ui import BlocksCustomButton diff --git a/BlocksScreen/lib/utils/RepeatedTimer.py b/BlocksScreen/lib/utils/RepeatedTimer.py index ff96a0f8..42b42aa0 100644 --- a/BlocksScreen/lib/utils/RepeatedTimer.py +++ b/BlocksScreen/lib/utils/RepeatedTimer.py @@ -31,6 +31,7 @@ def _run(self): self._function(*self._args, **self._kwargs) def startTimer(self): + """Start timer""" if self.running is False: try: self._timer = threading.Timer(self._timeout, self._run) @@ -47,6 +48,7 @@ def startTimer(self): self.running = True def stopTimer(self): + """Stop timer""" if self._timer is None: return if self.running: @@ -55,15 +57,3 @@ def stopTimer(self): self._timer = None self.stopEvent.clear() self.running = False - - @staticmethod - def pauseTimer(self): - # TODO never tested - self.stopEvent.clear() - self.running = False - - @staticmethod - def resumeTimer(self): - # TODO: never tested - self.stopEvent.set() - self.running = True diff --git a/BlocksScreen/lib/utils/RoutingQueue.py b/BlocksScreen/lib/utils/RoutingQueue.py index c7b2b08f..4efde01a 100644 --- a/BlocksScreen/lib/utils/RoutingQueue.py +++ b/BlocksScreen/lib/utils/RoutingQueue.py @@ -22,6 +22,7 @@ def __init__(self): @property def resend(self): + """Resend queue""" return self._resend @resend.setter @@ -30,10 +31,12 @@ def resend(self, new_resend): self._resend = new_resend def block(self): + """Blocks queue""" # Sets the flag to false self._clear_to_move.clear() def unblock(self): + """Unblock queue""" # Sets the flag to True self._clear_to_move.set() @@ -62,8 +65,8 @@ def add_command( self._read_lines += 1 except Exception as e: raise ValueError( - "Unexpected error while adding a command to queue, and argument " - ) + "Unexpected error while adding a command to queue, and argument %s" + ) from e def get_command(self, block=True, timeout=None, resend=False): """ diff --git a/BlocksScreen/lib/utils/blocks_Scrollbar.py b/BlocksScreen/lib/utils/blocks_Scrollbar.py index 08dc6b68..38e571e0 100644 --- a/BlocksScreen/lib/utils/blocks_Scrollbar.py +++ b/BlocksScreen/lib/utils/blocks_Scrollbar.py @@ -8,6 +8,7 @@ def __init__(self, parent=None): self.setFixedWidth(40) def paintEvent(self, event): + """Re-implemented method, paint widget""" painter = QtGui.QPainter(self) painter.setRenderHint(painter.RenderHint.Antialiasing, True) painter.setRenderHint(painter.RenderHint.SmoothPixmapTransform, True) @@ -43,15 +44,10 @@ def paintEvent(self, event): / (max_val - min_val) ) else: - val = ( - np.interp((handle_percentage), [15, 85], [0, 100]) - / 100 - * max_val - ) + val = np.interp((handle_percentage), [15, 85], [0, 100]) / 100 * max_val base_handle_length = int( - (groove.height() * page_step / (max_val - min_val + page_step)) - + 40 + (groove.height() * page_step / (max_val - min_val + page_step)) + 40 ) handle_pos = int( (groove.height() - base_handle_length) diff --git a/BlocksScreen/lib/utils/blocks_button.py b/BlocksScreen/lib/utils/blocks_button.py index dc8a79f6..141ee1ec 100644 --- a/BlocksScreen/lib/utils/blocks_button.py +++ b/BlocksScreen/lib/utils/blocks_button.py @@ -4,6 +4,8 @@ class ButtonColors(enum.Enum): + """Standard button colors""" + NORMAL_BG = (223, 223, 223) PRESSED_BG = (169, 169, 169) DISABLED_BG = (169, 169, 169) @@ -33,6 +35,7 @@ def __init__( self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) def setShowNotification(self, show: bool) -> None: + """Set notification on button""" if self._show_notification != show: self._show_notification = show self.repaint() @@ -40,6 +43,7 @@ def setShowNotification(self, show: bool) -> None: @property def name(self): + """Button name""" return self._name @name.setter @@ -48,18 +52,22 @@ def name(self, new_name) -> None: self.setObjectName(new_name) def text(self) -> str | None: + """Button text""" return self._text def setText(self, text: str) -> None: + """Set button text""" self._text = text self.update() return def setPixmap(self, pixmap: QtGui.QPixmap) -> None: + """Set button pixmap""" self.icon_pixmap = pixmap self.repaint() def mousePressEvent(self, e: QtGui.QMouseEvent) -> None: + """Handle mouse press events""" if not self.isEnabled(): e.ignore() return @@ -75,9 +83,7 @@ def mousePressEvent(self, e: QtGui.QMouseEvent) -> None: return super().mousePressEvent(e) def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): - opt = QtWidgets.QStyleOptionButton() - # self.initStyleOption(opt) - + """Re-implemented method, paint widget""" painter = QtGui.QPainter(self) painter.setRenderHint(painter.RenderHint.Antialiasing, True) painter.setRenderHint(painter.RenderHint.SmoothPixmapTransform, True) @@ -89,8 +95,6 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): if _style is None or _rect is None: return - - margin = _style.pixelMetric(_style.PixelMetric.PM_ButtonMargin, opt, self) # Determine background and text colors based on state if not self.isEnabled(): bg_color_tuple = ButtonColors.DISABLED_BG.value @@ -160,8 +164,6 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): tinted_icon_pixmap = QtGui.QPixmap(_icon_scaled.size()) tinted_icon_pixmap.fill(QtCore.Qt.GlobalColor.transparent) - margin = _style.pixelMetric(_style.PixelMetric.PM_ButtonMargin, opt, self) - if not self.isEnabled(): tinted_icon_pixmap = QtGui.QPixmap(_icon_scaled.size()) tinted_icon_pixmap.fill(QtCore.Qt.GlobalColor.transparent) @@ -190,24 +192,16 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): destination_point = adjusted_icon_rect.toRect().topLeft() painter.drawPixmap(destination_point, final_pixmap) - if self.text(): font_metrics = self.fontMetrics() self.text_width = font_metrics.horizontalAdvance(self._text) self.label_width = self.contentsRect().width() - margin = _style.pixelMetric( - _style.PixelMetric.PM_ButtonMargin, opt, self - ) - - _start_text_position = int(self.button_ellipse.width()) + # _start_text_position = int(self.button_ellipse.width()) _text_rect = _rect - _text_rect2 = _rect - _text_rect2.setWidth( - self.width() - int(self.button_ellipse.width()) - ) + _text_rect2.setWidth(self.width() - int(self.button_ellipse.width())) _text_rect2.setLeft(int(self.button_ellipse.width())) _text_rect.setWidth(self.width() - int(self.button_ellipse.width())) @@ -218,13 +212,10 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): _pen.setColor(current_text_color) painter.setPen(_pen) - # if self.text_width < _text_rect2.width()*0.6: - _text_rect.setWidth( - self.width() - int(self.button_ellipse.width()*1.4) - ) + _text_rect.setWidth(self.width() - int(self.button_ellipse.width() * 1.4)) _text_rect.setLeft(int(self.button_ellipse.width())) - + painter.drawText( _text_rect, QtCore.Qt.TextFlag.TextShowMnemonic @@ -257,6 +248,7 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): painter.end() def setProperty(self, name: str, value: typing.Any): + """Set widget properties""" if name == "icon_pixmap": self.icon_pixmap = value elif name == "name": @@ -265,14 +257,8 @@ def setProperty(self, name: str, value: typing.Any): self.text_color = QtGui.QColor(value) self.update() - def handleTouchBegin(self, e: QtCore.QEvent): ... - def handleTouchUpdate(self, e: QtCore.QEvent): ... - def handleTouchEnd(self, e: QtCore.QEvent): ... - def handleTouchCancel(self, e: QtCore.QEvent): ... - def setAutoDefault(self, bool): ... - def setFlat(self, bool): ... - def event(self, e: QtCore.QEvent) -> bool: + """Re-implemented method, filter events""" if e.type() == QtCore.QEvent.Type.TouchBegin: self.handleTouchBegin(e) return False diff --git a/BlocksScreen/lib/utils/blocks_frame.py b/BlocksScreen/lib/utils/blocks_frame.py index 05810d8c..6783ad8a 100644 --- a/BlocksScreen/lib/utils/blocks_frame.py +++ b/BlocksScreen/lib/utils/blocks_frame.py @@ -9,13 +9,16 @@ def __init__(self, parent=None): self._radius = 20 def setRadius(self, radius: int): + """Set widget frame radius""" self._radius = radius self.update() def radius(self): + """Get widget frame radius""" return self._radius - def paintEvent(self, event): + def paintEvent(self, event): + """Re-implemented method, paint widget""" painter = QPainter(self) painter.setRenderHint(QPainter.RenderHint.Antialiasing) rect = QRectF(self.rect()) @@ -23,6 +26,4 @@ def paintEvent(self, event): pen.setWidth(2) painter.setPen(pen) painter.setBrush(QBrush(QColor(50, 50, 50, 100))) - painter.drawRoundedRect( - rect.adjusted(1, 1, -1, -1), self._radius, self._radius - ) + painter.drawRoundedRect(rect.adjusted(1, 1, -1, -1), self._radius, self._radius) diff --git a/BlocksScreen/lib/utils/blocks_label.py b/BlocksScreen/lib/utils/blocks_label.py index 4df5a045..a2b05256 100644 --- a/BlocksScreen/lib/utils/blocks_label.py +++ b/BlocksScreen/lib/utils/blocks_label.py @@ -4,7 +4,7 @@ class BlocksLabel(QtWidgets.QLabel): def __init__(self, parent: QtWidgets.QWidget = None, *args, **kwargs): - super(BlocksLabel, self).__init__(parent, *args, **kwargs) + super().__init__(parent, *args, **kwargs) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) self.icon_pixmap: typing.Optional[QtGui.QPixmap] = None @@ -46,10 +46,12 @@ def __init__(self, parent: QtWidgets.QWidget = None, *args, **kwargs): self.first_run = True def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: + """Re-implemented method, handle widget resize event""" self.update_text_metrics() return super().resizeEvent(a0) def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None: + """Re-implemented method, handle mouse press event""" if ( ev.button() == QtCore.Qt.MouseButton.LeftButton and not self.timer.isActive() @@ -58,15 +60,18 @@ def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None: self.start_scroll() def setPixmap(self, a0: QtGui.QPixmap) -> None: + """Set widget pixmap""" self.icon_pixmap = a0 self.update() def setText(self, text: str) -> None: + """Set widget text""" self._text = text self.update_text_metrics() @property def background_color(self) -> typing.Optional[QtGui.QColor]: + """Widget background color""" return self._background_color @background_color.setter @@ -75,6 +80,7 @@ def background_color(self, color: QtGui.QColor) -> None: @property def border_color(self) -> typing.Optional[QtGui.QColor]: + """Widget border color""" return self._border_color @border_color.setter @@ -83,6 +89,7 @@ def border_color(self, color: QtGui.QColor) -> None: @property def rounded(self) -> bool: + """Widget rounded property""" return self._rounded @rounded.setter @@ -91,6 +98,7 @@ def rounded(self, on: bool) -> None: @property def marquee(self) -> bool: + """Widget enable marquee effect""" return self._marquee @marquee.setter @@ -100,6 +108,7 @@ def marquee(self, activate) -> None: @QtCore.pyqtProperty(int) def animation_speed(self) -> int: + """Widget animation speed property""" return self._animation_speed @animation_speed.setter @@ -108,6 +117,7 @@ def animation_speed(self, new_speed: int) -> None: @QtCore.pyqtProperty(QtGui.QColor) def glow_color(self) -> QtGui.QColor: + """Widget glow color property""" return self._glow_color @glow_color.setter @@ -117,6 +127,7 @@ def glow_color(self, color: QtGui.QColor) -> None: @QtCore.pyqtSlot(name="start_glow_animation") def start_glow_animation(self) -> None: + """Start glow animation""" self.glow_animation.setDuration(self.animation_speed) start_color = QtGui.QColor("#00000000") self.glow_animation.setStartValue(start_color) @@ -129,6 +140,7 @@ def start_glow_animation(self) -> None: @QtCore.pyqtSlot(name="change_glow_direction") def change_glow_direction(self) -> None: + """Handle Change glow direction""" current_direction = self.glow_animation.direction() if current_direction == self.glow_animation.Direction.Forward: self.glow_animation.setDirection(self.glow_animation.Direction.Backward) @@ -136,6 +148,7 @@ def change_glow_direction(self) -> None: self.glow_animation.setDirection(self.glow_animation.Direction.Forward) def update_text_metrics(self) -> None: + """Handle widget text metrics""" font_metrics = self.fontMetrics() self.text_width = font_metrics.horizontalAdvance(self._text) self.label_width = self.contentsRect().width() @@ -149,6 +162,7 @@ def update_text_metrics(self) -> None: self.update() def start_scroll(self) -> None: + """Start marquee text scroll effect""" if not self.delay_timer.isActive() and not self.timer.isActive(): self.scroll_pos = 0 self.loop_count = 0 @@ -164,6 +178,7 @@ def _start_marquee(self) -> None: self.timer.start(self.scroll_animation_speed) def stop_scroll(self) -> None: + """Stop marquee text scroll effect""" self.timer.stop() self.delay_timer.stop() @@ -184,6 +199,7 @@ def _scroll_text(self) -> None: self.repaint() def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" qp = QtWidgets.QStylePainter(self) opt = QtWidgets.QStyleOption() opt.initFrom(self) @@ -264,16 +280,8 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: int(self.scroll_pos + self.text_width + self.label_width / 2), 0, ) - # Draw the main text instance - draw_rect = QtCore.QRectF( - self.contentsRect().x() + self.scroll_pos, - self.contentsRect().y(), - self.text_width, - self.contentsRect().height(), - ) qp.drawText(QtCore.QRectF(second_text_rect), self._text, text_option) - - draw_rect2 = QtCore.QRectF( + draw_rect = QtCore.QRectF( self.contentsRect().x() + self.scroll_pos + self.text_width @@ -282,7 +290,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: self.text_width, self.contentsRect().height(), ) - qp.drawText(draw_rect2, self._text, text_option) + qp.drawText(draw_rect, self._text, text_option) else: text_rect = self.contentsRect().toRectF() qp.drawText(text_rect, self._text, text_option) @@ -290,6 +298,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: qp.end() def setProperty(self, name: str, value: typing.Any) -> bool: + """Re-implemented method, set widget properties""" if name == "icon_pixmap": self.setPixmap(value) return super().setProperty(name, value) diff --git a/BlocksScreen/lib/utils/blocks_linedit.py b/BlocksScreen/lib/utils/blocks_linedit.py index c40cabfb..242e4b0d 100644 --- a/BlocksScreen/lib/utils/blocks_linedit.py +++ b/BlocksScreen/lib/utils/blocks_linedit.py @@ -3,7 +3,7 @@ class BlocksCustomLinEdit(QtWidgets.QLineEdit): - clicked = QtCore.pyqtSignal() + clicked = QtCore.pyqtSignal() def __init__( self, @@ -17,11 +17,12 @@ def __init__( self.placeholder_str = "Type here" self._name: str = "" self.text_color: QtGui.QColor = QtGui.QColor(0, 0, 0) - self.secret: bool = False + self.secret: bool = False self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) @property def name(self): + """Widget name""" return self._name @name.setter @@ -30,18 +31,21 @@ def name(self, new_name) -> None: self.setObjectName(new_name) def setText(self, text: str) -> None: + """Set widget text""" super().setText(text) def setHidden(self, hidden: bool) -> None: + """Hide widget text""" self.secret = hidden self.update() def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: - self.clicked.emit() - super().mousePressEvent(event) - + """Re-implemented method, handle mouse press events""" + self.clicked.emit() + super().mousePressEvent(event) def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): + """Re-implemented method, paint widget""" painter = QtGui.QPainter(self) painter.setRenderHint(painter.RenderHint.Antialiasing, True) @@ -51,7 +55,7 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): painter.setPen(QtCore.Qt.PenStyle.NoPen) painter.drawRoundedRect(self.rect(), 8, 8) - margin = 5 + margin = 5 display_text = self.text() if self.secret and display_text: display_text = "*" * len(display_text) diff --git a/BlocksScreen/lib/utils/blocks_progressbar.py b/BlocksScreen/lib/utils/blocks_progressbar.py index 7688bf82..98f7cc52 100644 --- a/BlocksScreen/lib/utils/blocks_progressbar.py +++ b/BlocksScreen/lib/utils/blocks_progressbar.py @@ -1,4 +1,4 @@ -from PyQt6 import QtWidgets ,QtGui ,QtCore +from PyQt6 import QtWidgets, QtGui, QtCore class CustomProgressBar(QtWidgets.QProgressBar): @@ -11,14 +11,17 @@ def __init__(self, parent=None): self.set_pen_width(20) def set_padding(self, value): + """Set widget padding""" self.padding = value self.update() def set_pen_width(self, value): + """Set widget text pen width""" self.pen_width = value self.update() def paintEvent(self, event): + """Re-implemented method, paint widget""" painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) @@ -30,9 +33,8 @@ def _draw_circular_bar(self, painter, width, height): y = (height - size) / 2 arc_rect = QtCore.QRectF(x, y, size, size) - - arc1_start = 236* 16 - arc1_span = -290 * 16 + arc1_start = 236 * 16 + arc1_span = -290 * 16 bg_pen = QtGui.QPen(QtGui.QColor(20, 20, 20)) bg_pen.setWidth(self.pen_width) bg_pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) @@ -40,7 +42,7 @@ def _draw_circular_bar(self, painter, width, height): painter.drawArc(arc_rect, arc1_start, arc1_span) if self.progress_value is not None: - gradient = QtGui.QConicalGradient(arc_rect.center(), -90) + gradient = QtGui.QConicalGradient(arc_rect.center(), -90) gradient.setColorAt(0.0, self.bar_color) gradient.setColorAt(1.0, QtGui.QColor(100, 100, 100)) @@ -51,7 +53,7 @@ def _draw_circular_bar(self, painter, width, height): painter.setPen(progress_pen) # scale only over arc1’s span - progress_span = int(arc1_span * self.progress_value/100) + progress_span = int(arc1_span * self.progress_value / 100) painter.drawArc(arc_rect, arc1_start, progress_span) progress_text = f"{int(self.progress_value)}%" @@ -67,14 +69,14 @@ def _draw_circular_bar(self, painter, width, height): text_y = arc_rect.center().y() # Draw centered text - text_rect = QtCore.QRectF(text_x - 30, text_y + arc_rect.height() / 2 - 25, 60, 40) + text_rect = QtCore.QRectF( + text_x - 30, text_y + arc_rect.height() / 2 - 25, 60, 40 + ) painter.drawText(text_rect, QtCore.Qt.AlignmentFlag.AlignCenter, progress_text) - - - def setValue(self, value): - value*=100 + """Set value""" + value *= 100 if 0 <= value <= 101: self.progress_value = value self.update() @@ -82,6 +84,7 @@ def setValue(self, value): raise ValueError("Progress must be between 0.0 and 1.0.") def set_bar_color(self, red, green, blue): + """Set bar color""" if 0 <= red <= 255 and 0 <= green <= 255 and 0 <= blue <= 255: self.bar_color = QtGui.QColor(red, green, blue) self.update() diff --git a/BlocksScreen/lib/utils/blocks_slider.py b/BlocksScreen/lib/utils/blocks_slider.py index 4d1078c7..ee084a0a 100644 --- a/BlocksScreen/lib/utils/blocks_slider.py +++ b/BlocksScreen/lib/utils/blocks_slider.py @@ -1,5 +1,3 @@ -import sys - from PyQt6 import QtCore, QtGui, QtWidgets @@ -17,11 +15,8 @@ def __init__(self, parent) -> None: self.setMinimum(0) self.setMaximum(100) - def setOrientation(self, a0: QtCore.Qt.Orientation) -> None: - return super().setOrientation(a0) - def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None: - """Handle mouse press events""" + """Re-implemented method, Handle mouse press events""" if (ev.button() == QtCore.Qt.MouseButton.LeftButton) and self.hit_test( ev.position().toPoint().toPointF() ): @@ -69,31 +64,28 @@ def _set_slider_pos(self, pos: QtCore.QPointF): slider_start = self._groove_rect.x() pos_x = pos.x() new_val = ( - min_val - + (max_val - min_val) * (pos_x - slider_start) // slider_length + min_val + (max_val - min_val) * (pos_x - slider_start) // slider_length ) else: slider_length = self._groove_rect.height() slider_start = self._groove_rect.y() pos_y = pos.y() new_val = ( - min_val - + (max_val - min_val) * (pos_y - slider_start) / slider_length + min_val + (max_val - min_val) * (pos_y - slider_start) / slider_length ) self.setSliderPosition(int(round(new_val))) self.setValue(int(round(new_val))) self.update() def paintEvent(self, ev: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" opt = QtWidgets.QStyleOptionSlider() self.initStyleOption(opt) _style = self.style() # Clip the opt rect inside, so the handle and # groove doesn't exceed the limits - opt.rect = opt.rect.adjusted( - 12, 10, -18, 20 - ) # This is a bit hardcoded + opt.rect = opt.rect.adjusted(12, 10, -18, 20) # This is a bit hardcoded self._groove_rect = _style.subControlRect( QtWidgets.QStyle.ComplexControl.CC_Slider, @@ -162,9 +154,7 @@ def paintEvent(self, ev: QtGui.QPaintEvent) -> None: painter.setRenderHint(painter.RenderHint.TextAntialiasing, True) _color = QtGui.QColor(164, 164, 164) _color.setAlphaF(0.5) - painter.fillPath( - _groove_path, _color - ) # Primary groove background color + painter.fillPath(_groove_path, _color) # Primary groove background color _color = QtGui.QColor(self.highlight_color) _color_1 = QtGui.QColor(self.highlight_color) @@ -187,7 +177,6 @@ def paintEvent(self, ev: QtGui.QPaintEvent) -> None: QtWidgets.QStyle.SubControl.SC_SliderTickmarks, self, ) - tick_interval = self.tickInterval() or self.singleStep() min_v, max_v = self.minimum(), self.maximum() painter.setPen(QtGui.QColor("#888888")) fm = QtGui.QFontMetrics(painter.font()) diff --git a/BlocksScreen/lib/utils/blocks_tabwidget.py b/BlocksScreen/lib/utils/blocks_tabwidget.py index 274607a2..4696d967 100644 --- a/BlocksScreen/lib/utils/blocks_tabwidget.py +++ b/BlocksScreen/lib/utils/blocks_tabwidget.py @@ -2,17 +2,21 @@ class NotificationTabBar(QtWidgets.QTabBar): + """Re-implemented QTabBar so that the widget can have notifications""" + def __init__(self, parent=None): super().__init__(parent) self._notifications = {} # {tab_index: bool} def setNotification(self, index: int, show: bool): + """Set notification""" if index < 0 or index >= self.count(): return self._notifications[index] = show self.update(self.tabRect(index)) # repaint only that tab def paintEvent(self, event): + """Re-implemented method, paint widget""" super().paintEvent(event) painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) @@ -31,10 +35,13 @@ def paintEvent(self, event): class NotificationQTabWidget(QtWidgets.QTabWidget): + """Re-implemented QTabWidget so that we can have notifications""" + def __init__(self, parent=None): super().__init__(parent) self._custom_tabbar = NotificationTabBar() self.setTabBar(self._custom_tabbar) def setNotification(self, index: int, show: bool): + """Set tab notification""" self._custom_tabbar.setNotification(index, show) diff --git a/BlocksScreen/lib/utils/blocks_togglebutton.py b/BlocksScreen/lib/utils/blocks_togglebutton.py index 4363b655..c97e8f1f 100644 --- a/BlocksScreen/lib/utils/blocks_togglebutton.py +++ b/BlocksScreen/lib/utils/blocks_togglebutton.py @@ -12,39 +12,41 @@ def __init__(self, parent): self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self._icon_label = None self._text_label = None - self._text: str = ("la test") + self._text: str = "la test" self.icon_pixmap_fp: QtGui.QPixmap = QtGui.QPixmap( ":/filament_related/media/btn_icons/filament_sensor_turn_on.svg" ) - - self.setupUI() + + self._setupUI() self.tb = self.toggle_button def text(self) -> str: + """Button text""" return self._text def setText(self, new_text) -> None: + """Set widget text""" if self._text_label is not None: self._text_label.setText(f"{new_text}") self._text = new_text - - def setPixmap(self,pixmap: QtGui.QPixmap): + def setPixmap(self, pixmap: QtGui.QPixmap): + """Set widget pixmap""" self.icon_pixmap_fp = pixmap def mousePressEvent(self, event: QtGui.QMouseEvent): + """Re-implemented method, handle mouse press events""" if self.toggle_button.geometry().contains(event.pos()): event.ignore() return if event.button() == QtCore.Qt.MouseButton.LeftButton: self.clicked.emit() - event.accept() + event.accept() def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" style_painter = QtWidgets.QStylePainter(self) - style_painter.setRenderHint( - style_painter.RenderHint.Antialiasing, True - ) + style_painter.setRenderHint(style_painter.RenderHint.Antialiasing, True) style_painter.setRenderHint( style_painter.RenderHint.SmoothPixmapTransform, True ) @@ -76,12 +78,13 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: style_painter.end() def setDisabled(self, a0: bool) -> None: + """Re-implemented method, disable widget""" self.toggle_button.setDisabled(a0) self.repaint() self.toggle_button.repaint() return super().setDisabled(a0) - def setupUI(self): + def _setupUI(self): _policy = QtWidgets.QSizePolicy.Policy.MinimumExpanding size_policy = QtWidgets.QSizePolicy(_policy, _policy) size_policy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) @@ -90,29 +93,21 @@ def setupUI(self): self.sensor_horizontal_layout.setGeometry(self.rect()) self.sensor_horizontal_layout.setObjectName("sensorHorizontalLayout") self._icon_label = BlocksLabel(self) - size_policy.setHeightForWidth( - self._icon_label.sizePolicy().hasHeightForWidth() - ) + size_policy.setHeightForWidth(self._icon_label.sizePolicy().hasHeightForWidth()) self._icon_label.setSizePolicy(size_policy) self._icon_label.setMinimumSize(60, 60) self._icon_label.setMaximumSize(60, 60) - self._icon_label.setPixmap( - self.icon_pixmap_fp - ) + self._icon_label.setPixmap(self.icon_pixmap_fp) self.sensor_horizontal_layout.addWidget(self._icon_label) self._text_label = QtWidgets.QLabel(parent=self) - size_policy.setHeightForWidth( - self._text_label.sizePolicy().hasHeightForWidth() - ) + size_policy.setHeightForWidth(self._text_label.sizePolicy().hasHeightForWidth()) self._text_label.setMinimumSize(100, 60) self._text_label.setMaximumSize(500, 60) _font = QtGui.QFont() _font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) _font.setPointSize(18) palette = self._text_label.palette() - palette.setColor( - palette.ColorRole.WindowText, QtGui.QColorConstants.White - ) + palette.setColor(palette.ColorRole.WindowText, QtGui.QColorConstants.White) self._text_label.setPalette(palette) self._text_label.setFont(_font) self._text_label.setText(str(self._text)) diff --git a/BlocksScreen/lib/utils/display_button.py b/BlocksScreen/lib/utils/display_button.py index a0e5a886..1f798dfe 100644 --- a/BlocksScreen/lib/utils/display_button.py +++ b/BlocksScreen/lib/utils/display_button.py @@ -4,9 +4,7 @@ class DisplayButton(QtWidgets.QPushButton): - def __init__( - self, parent: typing.Optional["QtWidgets.QWidget"] = None - ) -> None: + def __init__(self, parent: typing.Optional["QtWidgets.QWidget"] = None) -> None: if parent: super().__init__(parent=parent) else: @@ -19,22 +17,23 @@ def __init__( self._text: str = "" self._secondary_text: str = "" self._name: str = "" - self.display_format: typing.Literal["normal", "upper_downer"] = ( - "normal" - ) + self.display_format: typing.Literal["normal", "upper_downer"] = "normal" self.text_color: QtGui.QColor = QtGui.QColor(0, 0, 0) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) @property def name(self): + """Widget name""" return self._name def setPixmap(self, pixmap: QtGui.QPixmap) -> None: + """Set widget pixmap""" self.icon_pixmap = pixmap self.repaint() @property def button_type(self) -> str: + """Widget button type""" return self._button_type @button_type.setter @@ -44,29 +43,28 @@ def button_type(self, type) -> None: self._button_type = type def text(self) -> str: + """Widget text""" return self._text def setText(self, text: str) -> None: + """Set widget text""" self._text = text self.update() super().setText(text) @property def secondary_text(self) -> str: + """Widget secondary text""" return self._secondary_text @secondary_text.setter def secondary_text(self, text: str) -> None: + """Set secondary text""" self._secondary_text = text self.update() - def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: - return super().resizeEvent(a0) - - def mousePressEvent(self, e: QtGui.QMouseEvent) -> None: - return super().mousePressEvent(e) - def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" opt = QtWidgets.QStyleOptionButton() self.initStyleOption(opt) painter = QtWidgets.QStylePainter(self) @@ -78,9 +76,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: if not _style or _rect is None: return - margin = _style.pixelMetric( - _style.PixelMetric.PM_ButtonMargin, opt, self - ) + margin = _style.pixelMetric(_style.PixelMetric.PM_ButtonMargin, opt, self) # Rounded background edges path = QtGui.QPainterPath() path.addRoundedRect( @@ -119,13 +115,11 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: _pen.setBrush(_gradient) painter.fillPath(path, _pen.brush()) - _icon_rect = ( - QtCore.QRectF( # x,y, width * size reduction factor, height - 0.0, - 0.0, - (_rect.width() * 0.3) - 5.0, - _rect.height() - 5, - ) + _icon_rect = QtCore.QRectF( # x,y, width * size reduction factor, height + 0.0, + 0.0, + (_rect.width() * 0.3) - 5.0, + _rect.height() - 5, ) _icon_scaled = self.icon_pixmap.scaled( @@ -192,9 +186,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: QtCore.Qt.TextFlag.TextShowMnemonic | QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - str(self.secondary_text) - if self.secondary_text - else str("?"), + str(self.secondary_text) if self.secondary_text else str("?"), ) painter.drawText( _mtl_rect, @@ -205,9 +197,9 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: ) elif self.display_format == "upper_downer": _mtl = QtCore.QRectF( - int(_icon_rect.width()) + margin , + int(_icon_rect.width()) + margin, 0.0, - int(_rect.width() - _icon_rect.width() - margin ), + int(_rect.width() - _icon_rect.width() - margin), _rect.height(), ) _upper_rect = QtCore.QRectF( @@ -226,7 +218,9 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: font.setPointSize(20) font.setFamily("Momcake-bold") painter.setFont(font) - painter.setCompositionMode(painter.CompositionMode.CompositionMode_SourceAtop) + painter.setCompositionMode( + painter.CompositionMode.CompositionMode_SourceAtop + ) painter.drawText( _upper_rect, # QtCore.Qt.AlignmentFlag.AlignCenter, @@ -237,7 +231,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: font.setPointSize(15) painter.setPen(QtGui.QColor("#b6b0b0")) painter.setFont(font) - + painter.drawText( _downer_rect, QtCore.Qt.AlignmentFlag.AlignRight @@ -258,6 +252,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: return def setProperty(self, name: str, value: typing.Any) -> bool: + """Re-implemented method, set widget properties""" if name == "icon_pixmap": self.icon_pixmap = value elif name == "button_type": diff --git a/BlocksScreen/lib/utils/group_button.py b/BlocksScreen/lib/utils/group_button.py index b9af752b..79f4251e 100644 --- a/BlocksScreen/lib/utils/group_button.py +++ b/BlocksScreen/lib/utils/group_button.py @@ -27,6 +27,7 @@ def __init__( @property def name(self): + """Widget name""" return self._name @name.setter @@ -35,18 +36,22 @@ def name(self, new_name) -> None: self.setObjectName(new_name) def text(self) -> str | None: + """Widget text""" return self._text def setText(self, text: str) -> None: + """Set widget text""" self._text = text self.update() # Force button update return def setPixmap(self, pixmap: QtGui.QPixmap) -> None: + """Set widget pixmap""" self.icon_pixmap = pixmap self.repaint() def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): + """Re-implemented method, paint widget""" opt = QtWidgets.QStyleOptionButton() self.initStyleOption(opt) @@ -117,28 +122,15 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): painter.setPen(QtCore.Qt.PenStyle.NoPen) def setProperty(self, name: str, value: typing.Any): + """Re-implemented method, set widget properties""" if name == "name": self._name = name elif name == "text_color": self.text_color = QtGui.QColor(value) # return super().setProperty(name, value) - def handleTouchBegin(self, e: QtCore.QEvent): - ... - # if not self.button_background: - # if self.button_background.contains(e.pos()): # type: ignore - # # super().mousePressEvent(e) - # self.mousePressEvent(e) # type: ignore - # return - # else: - # e.ignore() - # return - - def handleTouchUpdate(self, e: QtCore.QEvent): ... - def handleTouchEnd(self, e: QtCore.QEvent): ... - def handleTouchCancel(self, e: QtCore.QEvent): ... - def event(self, e: QtCore.QEvent) -> bool: + """Re-implemented method, filter events""" if e.type() == QtCore.QEvent.Type.TouchBegin: self.handleTouchBegin(e) return False diff --git a/BlocksScreen/lib/utils/icon_button.py b/BlocksScreen/lib/utils/icon_button.py index 019471b4..a60a7f1d 100644 --- a/BlocksScreen/lib/utils/icon_button.py +++ b/BlocksScreen/lib/utils/icon_button.py @@ -16,21 +16,25 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: @property def name(self): + """Widget name""" return self._name def text(self) -> str: + """Widget text""" return self._text def setPixmap(self, pixmap: QtGui.QPixmap) -> None: + """Set widget pixmap""" self.icon_pixmap = pixmap self.repaint() def setText(self, text: str) -> None: + """Set widget text""" self._text = text self.update() - # super().setText(text) def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" opt = QtWidgets.QStyleOptionButton() self.initStyleOption(opt) painter = QtWidgets.QStylePainter(self) @@ -53,9 +57,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: # * Build icon x = y = 15.0 if self.text_formatting else 5.0 - _icon_rect = QtCore.QRectF( - 0.0, 0.0, (self.width() - x), (self.height() - y) - ) + _icon_rect = QtCore.QRectF(0.0, 0.0, (self.width() - x), (self.height() - y)) _icon_scaled = self.icon_pixmap.scaled( _icon_rect.size().toSize(), @@ -120,6 +122,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: painter.end() def setProperty(self, name: str, value: typing.Any) -> bool: + """Re-implemented method, set widget properties""" if name == "icon_pixmap": self.icon_pixmap = value elif name == "text_formatting": diff --git a/BlocksScreen/lib/utils/list_button.py b/BlocksScreen/lib/utils/list_button.py index 0b7eab6c..deb01bf1 100644 --- a/BlocksScreen/lib/utils/list_button.py +++ b/BlocksScreen/lib/utils/list_button.py @@ -29,56 +29,66 @@ def __init__(self, parent=None) -> None: self.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) def setText(self, text: str) -> None: + """Set widget text""" self._text = text self.update() def text(self) -> str: + """Widget text""" return self._text def setRightText(self, text: str) -> None: + """Set widget right text""" self._right_text = text self.update() def rightText(self) -> str: + """Widget right text""" return self._right_text def setLeftFontSize(self, size: int) -> None: + """Set widget left text font size""" self._lfontsize = size self.update() def setRightFontSize(self, size: int) -> None: + """Set widget right text font size""" self._rfontsize = size self.update() def setPixmap(self, pixmap: QtGui.QPixmap) -> None: + """Set widget pixmap""" self.icon_pixmap = pixmap self.update() def setSecondPixmap(self, pixmap: QtGui.QPixmap) -> None: + """Set widget secondary pixmap""" self.second_icon_pixmap = pixmap self.update() def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: + """Re-implemented method, handle mouse press event""" self._is_pressed = True self.update() super().mousePressEvent(event) def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None: + """Re-implemented method, handle mouse release event""" self._is_pressed = False self.update() super().mouseReleaseEvent(event) def leaveEvent(self, event: QtCore.QEvent) -> None: + """Re-implemented method, handle leave event""" self._is_hovered = False self.update() super().leaveEvent(event) def paintEvent(self, e: QtGui.QPaintEvent | None) -> None: + """Re-implemented method, paint widget""" painter = QtGui.QPainter(self) painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) - painter.setRenderHint( - QtGui.QPainter.RenderHint.SmoothPixmapTransform, True - ) + painter.setRenderHint(QtGui.QPainter.RenderHint.SmoothPixmapTransform, True) rect = self.rect() radius = rect.height() / 5.0 @@ -140,12 +150,9 @@ def paintEvent(self, e: QtGui.QPaintEvent | None) -> None: QtCore.Qt.TransformationMode.SmoothTransformation, ) # Center the icon in the ellipse - adjusted_x = ( - icon_rect.x() + (icon_rect.width() - icon_scaled.width()) / 2.0 - ) + adjusted_x = icon_rect.x() + (icon_rect.width() - icon_scaled.width()) / 2.0 adjusted_y = ( - icon_rect.y() - + (icon_rect.height() - icon_scaled.height()) / 2.0 + icon_rect.y() + (icon_rect.height() - icon_scaled.height()) / 2.0 ) adjusted_icon_rect = QtCore.QRectF( adjusted_x, @@ -185,9 +192,7 @@ def paintEvent(self, e: QtGui.QPaintEvent | None) -> None: left_icon_scaled, left_icon_scaled.rect().toRectF(), ) - left_margin = ( - left_icon_margin + left_icon_size + 8 - ) # 8px gap after icon + left_margin = left_icon_margin + left_icon_size + 8 # 8px gap after icon # Draw text, area before the ellipse (adjusted for left icon) text_margin = int( @@ -209,11 +214,7 @@ def paintEvent(self, e: QtGui.QPaintEvent | None) -> None: main_text_height = metrics.height() # Vertically center text - text_y = ( - rect.top() - + (rect.height() + main_text_height) / 2 - - metrics.descent() - ) + text_y = rect.top() + (rect.height() + main_text_height) / 2 - metrics.descent() # Calculate where to start the right text: just left of the right icon ellipse gap = 10 # gap between right text and icon ellipse diff --git a/BlocksScreen/lib/utils/list_model.py b/BlocksScreen/lib/utils/list_model.py index a143f90b..24c9467f 100644 --- a/BlocksScreen/lib/utils/list_model.py +++ b/BlocksScreen/lib/utils/list_model.py @@ -6,7 +6,7 @@ @dataclass class ListItem: - """Data for a list item""" + """List item data""" text: str right_text: str = "" diff --git a/BlocksScreen/lib/utils/loadAnimatedLabel.py b/BlocksScreen/lib/utils/loadAnimatedLabel.py deleted file mode 100644 index 4e091b48..00000000 --- a/BlocksScreen/lib/utils/loadAnimatedLabel.py +++ /dev/null @@ -1,6 +0,0 @@ -from PyQt6 import QtGui, QtWidgets, QtCore - -class LoadAnimatedLabel(QtWidgets.QLabel): - def __init__(self, parent) -> None: - super().__init__(parent) - \ No newline at end of file diff --git a/BlocksScreen/lib/utils/numpad_button.py b/BlocksScreen/lib/utils/numpad_button.py index 5f0ebafe..feaf3c61 100644 --- a/BlocksScreen/lib/utils/numpad_button.py +++ b/BlocksScreen/lib/utils/numpad_button.py @@ -9,12 +9,15 @@ def __init__(self, parent=None): self._position: str = "" def get_position(self): + """Get numpad button position""" return self._position def set_position(self, value): + """Set position""" self._position = str(value).lower() def paintEvent(self, e: QtGui.QPaintEvent | None): + """Re-implemented method, paint widget""" opt = QtWidgets.QStyleOptionButton() self.initStyleOption(opt) @@ -28,9 +31,7 @@ def paintEvent(self, e: QtGui.QPaintEvent | None): if _style is None or _rect is None: return - margin = _style.pixelMetric( - _style.PixelMetric.PM_ButtonMargin, opt, self - ) + margin = _style.pixelMetric(_style.PixelMetric.PM_ButtonMargin, opt, self) bg_color = ( QtGui.QColor(164, 164, 164) if self.isDown() @@ -143,6 +144,7 @@ def paintEvent(self, e: QtGui.QPaintEvent | None): painter.setPen(QtCore.Qt.PenStyle.NoPen) def setProperty(self, name: str, value: typing.Any): + """Re-implemented method, set widget properties""" if name == "position": self.set_position(value) diff --git a/BlocksScreen/lib/utils/others.py b/BlocksScreen/lib/utils/others.py deleted file mode 100644 index f2b1e600..00000000 --- a/BlocksScreen/lib/utils/others.py +++ /dev/null @@ -1,48 +0,0 @@ -#!/usr/bin/python -import logging -import queue -import threading -import typing -from functools import partial - -from PyQt6 import QtCore, QtGui, QtWidgets -from PyQt6.QtCore import Qt, pyqtSignal, pyqtSlot -from PyQt6.QtWidgets import QPushButton, QStackedWidget, QStyle, QWidget - -# from qt_ui.customNumpad_ui import Ui_customNumpad - -_logger = logging.getLogger(__name__) - - - - -# PYTHON 'is' checks if the object points to the same object, which is different than == - - - - - - -# TODO: Create a method that checks if the application requirements -# TODO: Create a method that validates the working directory of the GUI - - -def validate_requirements(): ... -def validate_working_dir(): ... -def scan_dir(dir: str) -> typing.Dict: ... -def scan_file(filename: str, dir: str) -> bool: ... - - -def validate_directory() -> bool: ... - - -def scan_directory(dir: str, ext: str = None): - ... - # """Scan a directory for files and nested directories""" - # if not isinstance(dir, str): - # raise ValueError("dir expected str type") - - # if os.access(dir, os.X_OK and os.W_OK and os.R_OK): - # for root, dirs, files in os.walk(dir): - - # for diff --git a/BlocksScreen/lib/utils/toggleAnimatedButton.py b/BlocksScreen/lib/utils/toggleAnimatedButton.py index 557c01a4..8cf90f10 100644 --- a/BlocksScreen/lib/utils/toggleAnimatedButton.py +++ b/BlocksScreen/lib/utils/toggleAnimatedButton.py @@ -16,9 +16,7 @@ def __init__(self, parent) -> None: super().__init__(parent) self.setMinimumSize(QtCore.QSize(80, 40)) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) - self.setAttribute( - QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True - ) + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) self.setMaximumHeight(80) self.setMouseTracking(True) @@ -49,17 +47,14 @@ def __init__(self, parent) -> None: else self._handle_OFFPosition ) - self.slide_animation = QtCore.QPropertyAnimation( - self, b"handle_position" - ) + self.slide_animation = QtCore.QPropertyAnimation(self, b"handle_position") self.slide_animation.setDuration(self._animation_speed) - self.slide_animation.setEasingCurve( - QtCore.QEasingCurve().Type.InOutQuart - ) + self.slide_animation.setEasingCurve(QtCore.QEasingCurve().Type.InOutQuart) self.pressed.connect(self.setup_animation) def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: + """Re-implemented method, handle widget resize event""" self.handle_radius = ( self.contentsRect().toRectF().normalized().height() * 0.80 ) // 2 @@ -74,10 +69,12 @@ def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: return super().resizeEvent(a0) def sizeHint(self) -> QtCore.QSize: + """Re-implemented method, widget size hint""" return QtCore.QSize(80, 40) @QtCore.pyqtProperty(int) def animation_speed(self) -> int: + """Widget property animation speed""" return self._animation_speed @animation_speed.setter @@ -87,12 +84,13 @@ def animation_speed(self, new_speed: int) -> None: @property def state(self) -> State: + """Widget property, toggle state""" return self._state @state.setter def state(self, new_state: State) -> None: - if self._state == new_state: - return + if self._state == new_state: + return self._state = new_state if self.isVisible(): self.stateChange.emit(self._state) @@ -101,6 +99,7 @@ def state(self, new_state: State) -> None: @QtCore.pyqtProperty(float) def handle_position(self) -> float: + """Widget property handle position""" return self._handle_position @handle_position.setter @@ -110,6 +109,7 @@ def handle_position(self, new_pos: float) -> None: @QtCore.pyqtProperty(QtGui.QColor) def backgroundColor(self) -> QtGui.QColor: + """Widget property background color""" return self._backgroundColor @backgroundColor.setter @@ -119,6 +119,7 @@ def backgroundColor(self, new_color: QtGui.QColor) -> None: @QtCore.pyqtProperty(QtGui.QColor) def handleColor(self) -> QtGui.QColor: + """Widget property handle color""" return self._handleColor @handleColor.setter @@ -127,6 +128,7 @@ def handleColor(self, new_color: QtGui.QColor) -> None: self.update() def showEvent(self, a0: QtGui.QShowEvent) -> None: + """Re-implemented method, widget show""" _rect = self.contentsRect() self.trailPath: QtGui.QPainterPath = QtGui.QPainterPath() self.handlePath: QtGui.QPainterPath = QtGui.QPainterPath() @@ -155,16 +157,15 @@ def showEvent(self, a0: QtGui.QShowEvent) -> None: return super().showEvent(a0) def setPixmap(self, pixmap: QtGui.QPixmap) -> None: + """Set widget pixmap""" self.icon_pixmap = pixmap # self.repaint() self.update() @QtCore.pyqtSlot(name="clicked") def setup_animation(self) -> None: - if ( - not self.slide_animation.state - == self.slide_animation.State.Running - ): + """Setup widget animation""" + if not self.slide_animation.state == self.slide_animation.State.Running: self.slide_animation.setEndValue( self._handle_ONPosition if self.state == ToggleAnimatedButton.State.OFF @@ -173,23 +174,17 @@ def setup_animation(self) -> None: self.slide_animation.start() def mousePressEvent(self, e: QtGui.QMouseEvent) -> None: + """Re-implemented method, handle mouse press events""" if self.trailPath: - if ( - self.trailPath.contains(e.pos().toPointF()) - and self.underMouse() - ): - if ( - not self.slide_animation.state - == self.slide_animation.State.Running - ): - self._state = ToggleAnimatedButton.State( - not self._state.value - ) + if self.trailPath.contains(e.pos().toPointF()) and self.underMouse(): + if not self.slide_animation.state == self.slide_animation.State.Running: + self._state = ToggleAnimatedButton.State(not self._state.value) self.stateChange.emit(self._state) super().mousePressEvent(e) e.ignore() def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" option = QtWidgets.QStyleOptionButton() option.initFrom(self) option.state |= QtWidgets.QStyle.StateFlag.State_Off @@ -197,7 +192,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: option.state |= QtWidgets.QStyle.StateFlag.State_Active _rect = self.contentsRect() - bg_color = (self.backgroundColor) + bg_color = self.backgroundColor self.handlePath: QtGui.QPainterPath = QtGui.QPainterPath() self.handle_ellipseRect = QtCore.QRectF( self._handle_position, @@ -211,8 +206,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: painter.setRenderHint(painter.RenderHint.SmoothPixmapTransform) painter.setBackgroundMode(QtCore.Qt.BGMode.TransparentMode) painter.setRenderHint(painter.RenderHint.LosslessImageRendering) - - + painter.fillPath( self.trailPath, bg_color if self.isEnabled() else self.disable_bg_color, diff --git a/BlocksScreen/lib/utils/ui.py b/BlocksScreen/lib/utils/ui.py deleted file mode 100644 index f74bd5df..00000000 --- a/BlocksScreen/lib/utils/ui.py +++ /dev/null @@ -1,2 +0,0 @@ -import typing - diff --git a/BlocksScreen/lib/utils/url.py b/BlocksScreen/lib/utils/url.py deleted file mode 100644 index 727cbb49..00000000 --- a/BlocksScreen/lib/utils/url.py +++ /dev/null @@ -1,76 +0,0 @@ - -class URLTYPE(object): - _prefix_type = ["ws://", "wss://", "http://", "https://"] - link_type = ["rest", "websocket"] - - def __init__(self, host: str, port=None, type: str = "rest"): - # self._prefix:str = - if isinstance(port, int) is False and port is not None: - raise AttributeError("If port is specified it can only be an integer") - - if type not in self.link_type: - raise AttributeError(f"Url type can only be of: {self.link_type}") - - self._websocket_suffix: str = "/websocket" - - self._host: str = host - self._port = port - self._type = type.lower() - self._build_url - # self._url = self._prefix_type[self._type] + self._host + ":" + str(self._port) + self._websocket_suffix - - def _build_url(self) -> None: - if self._type == "rest": - self._url = ( - self.link_type[2] + self._host - if self._host.endswith(".com") - else self.link_type[2] + self._host + ".com" - ) - - if self._type == "websocket": - self._url = ( - self.link_type[0] - + self._host - + ":" - + str(self._port) - + self._websocket_suffix - ) - - def type(self) -> str: - return self.__class__.__name__ - - @property - def url_link(self): - return self._url - - @url_link.setter - def url_link(self, host, port, type): - if self._type == "rest": - if port is None: - self._url = ( - self.link_type[2] + host - if host.endswith(".com") - else self.link_type[2] + host + ".com" - ) - else: - self._url = ( - self.link_type[2] + host + ":" + port - if host.endswith(".com") - else self.link_type[2] + host + ":" + port + ".com" - ) - - if self._type == "websocket": - self._url = ( - self.link_type[0] - + self._host - + ":" - + str(self._port) - + self._websocket_suffix - ) - - def __repr__(self) -> str: - cls = self.__class__.__name__ - return f"{cls}(host = {self._host}, port= {self._port}, type= {self._type})" - - def __str__(self) -> str: - return self._url \ No newline at end of file diff --git a/BlocksScreen/logger.py b/BlocksScreen/logger.py index 22de8402..e2aa90ca 100644 --- a/BlocksScreen/logger.py +++ b/BlocksScreen/logger.py @@ -21,6 +21,7 @@ def __init__( self.setLevel(level) def emit(self, record): + """Emit logging record""" try: msg = self.format(record) record = copy.copy(record) @@ -31,11 +32,8 @@ def emit(self, record): except Exception: self.handleError(record) - def flush(self): ... - - # TODO: Implement this - def setFormatter(self, fmt: logging.Formatter | None) -> None: + """Set logging formatter""" return super().setFormatter(fmt) @@ -44,12 +42,17 @@ class QueueListener(logging.handlers.TimedRotatingFileHandler): def __init__(self, filename, encoding="utf-8"): super(QueueListener, self).__init__( - filename=filename, when="MIDNIGHT", backupCount=10, encoding=encoding, delay=True + filename=filename, + when="MIDNIGHT", + backupCount=10, + encoding=encoding, + delay=True, ) self.queue = queue.Queue() - self._thread = threading.Thread(name=f"log.{filename}",target=self._run, daemon=True) + self._thread = threading.Thread( + name=f"log.{filename}", target=self._run, daemon=True + ) self._thread.start() - def _run(self): while True: @@ -62,26 +65,23 @@ def _run(self): break def close(self): + """Close logger listener""" if self._thread is None: return self.queue.put_nowait(None) self._thread.join() self._thread = None - def doRollover(self) -> None: ... - - # TODO: Implement this - def getFilesToDelete(self) -> list[str]: ... +global MainLoggingHandler - # TODO: Delete files that one month old -global MainLoggingHandler def create_logger( name: str = "log", level=logging.INFO, format: str = "'[%(levelname)s] | %(asctime)s | %(name)s | %(relativeCreated)6d | %(threadName)s : %(message)s", ): + """Create amd return logger""" global MainLoggingHandler logger = logging.getLogger(name) logger.setLevel(level) @@ -89,13 +89,3 @@ def create_logger( MainLoggingHandler = QueueHandler(ql.queue, format, level) logger.addHandler(MainLoggingHandler) return ql - - -def destroy_logger(name): ... # TODO: Implement this - - - - - - - diff --git a/BlocksScreen/screensaver.py b/BlocksScreen/screensaver.py index 8c733f0a..de02ba02 100644 --- a/BlocksScreen/screensaver.py +++ b/BlocksScreen/screensaver.py @@ -4,15 +4,9 @@ class ScreenSaver(QtCore.QObject): timer = QtCore.QTimer() - dpms_off_timeout = helper_methods.get_dpms_timeouts().get("off_timeout") - dpms_suspend_timeout = helper_methods.get_dpms_timeouts().get( - "suspend_timeout" - ) - dpms_standby_timeout = helper_methods.get_dpms_timeouts().get( - "standby_timeout" - ) - + dpms_suspend_timeout = helper_methods.get_dpms_timeouts().get("suspend_timeout") + dpms_standby_timeout = helper_methods.get_dpms_timeouts().get("standby_timeout") touch_blocked: bool = False def __init__(self, parent) -> None: @@ -23,9 +17,7 @@ def __init__(self, parent) -> None: ) if not self.screensaver_config: self.blank_timeout = ( - self.dpms_standby_timeout - if self.dpms_standby_timeout - else 900000 + self.dpms_standby_timeout if self.dpms_standby_timeout else 900000 ) else: self.blank_timeout = self.screensaver_config.getint( @@ -66,9 +58,6 @@ def eventFilter(self, object, event) -> bool: self.timer.start() return False - def timerEvent(self, a0: QtCore.QTimerEvent) -> None: - return super().timerEvent(a0) - def check_dpms(self) -> None: """Checks the X11 extension dpms for the status of the screen""" self.touch_blocked = True From 8e48a21606a916a5f28302667edda5d945d26624 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Thu, 11 Dec 2025 10:36:01 +0000 Subject: [PATCH 08/70] ADD: added overriedCursor to blank cursor (#118) * ADD: added overriedCursor to blank cursor * Refactor: ran ruff formater --------- Co-authored-by: Roberto --- BlocksScreen/lib/panels/mainWindow.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index b7921e55..18549a39 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -81,6 +81,8 @@ def __init__(self): self.printPanel = PrintTab( self.ui.printTab, self.file_data, self.ws, self.printer ) + QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.CursorShape.BlankCursor) + self.filamentPanel = FilamentTab(self.ui.filamentTab, self.printer, self.ws) self.controlPanel = ControlTab(self.ui.controlTab, self.ws, self.printer) self.utilitiesPanel = UtilitiesTab(self.ui.utilitiesTab, self.ws, self.printer) From e8523468113236bd0a533c6b884e740cee910a47 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Thu, 11 Dec 2025 12:42:11 +0000 Subject: [PATCH 09/70] ADD: color degrade when ON/OFF (#120) --- .../lib/utils/toggleAnimatedButton.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/BlocksScreen/lib/utils/toggleAnimatedButton.py b/BlocksScreen/lib/utils/toggleAnimatedButton.py index 8cf90f10..b9555876 100644 --- a/BlocksScreen/lib/utils/toggleAnimatedButton.py +++ b/BlocksScreen/lib/utils/toggleAnimatedButton.py @@ -36,6 +36,10 @@ def __init__(self, parent) -> None: self.icon_pixmap: QtGui.QPixmap = QtGui.QPixmap() self._backgroundColor: QtGui.QColor = QtGui.QColor(223, 223, 223) self._handleColor: QtGui.QColor = QtGui.QColor(255, 100, 10) + + self._handleONcolor: QtGui.QColor = QtGui.QColor(0, 200, 0) + self._handleOFFcolor: QtGui.QColor = QtGui.QColor(200, 0, 0) + self.disable_bg_color: QtGui.QColor = QtGui.QColor("#A9A9A9") self.disable_handle_color: QtGui.QColor = QtGui.QColor("#666666") self._state = ToggleAnimatedButton.State.OFF @@ -207,6 +211,32 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: painter.setBackgroundMode(QtCore.Qt.BGMode.TransparentMode) painter.setRenderHint(painter.RenderHint.LosslessImageRendering) + rect_norm = _rect.toRectF().normalized() + min_x = rect_norm.x() + max_x = rect_norm.x() + rect_norm.width() - rect_norm.height() * 0.80 + progress = (self._handle_position - min_x) / (max_x - min_x) + progress = max(0.0, min(1.0, progress)) + + # Inline color interpolation (no separate functions) + r = ( + self._handleOFFcolor.red() + + (self._handleONcolor.red() - self._handleOFFcolor.red()) * progress + ) + g = ( + self._handleOFFcolor.green() + + (self._handleONcolor.green() - self._handleOFFcolor.green()) * progress + ) + b = ( + self._handleOFFcolor.blue() + + (self._handleONcolor.blue() - self._handleOFFcolor.blue()) * progress + ) + a = ( + self._handleOFFcolor.alpha() + + (self._handleONcolor.alpha() - self._handleOFFcolor.alpha()) * progress + ) + + self.handleColor = QtGui.QColor(int(r), int(g), int(b), int(a)) + painter.fillPath( self.trailPath, bg_color if self.isEnabled() else self.disable_bg_color, From 868d0fd823c9f01fbf756a3c5e4ea94dd259cbe4 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Thu, 11 Dec 2025 15:59:43 +0000 Subject: [PATCH 10/70] Bugfix label overlap (#121) * bugfix: label overlapping when stoped * Upd: rollback some parts * ADD: added delay to marquee effect * bugfix: text when marquee wasnt needed and glow effect * Refactor: Ran ruff formatter * Remove unecessary lambda expression * Add docstring to paintEvent method --------- Co-authored-by: Roberto Co-authored-by: Hugo Costa --- BlocksScreen/lib/utils/blocks_label.py | 175 +++++++++---------------- 1 file changed, 63 insertions(+), 112 deletions(-) diff --git a/BlocksScreen/lib/utils/blocks_label.py b/BlocksScreen/lib/utils/blocks_label.py index a2b05256..7e64d805 100644 --- a/BlocksScreen/lib/utils/blocks_label.py +++ b/BlocksScreen/lib/utils/blocks_label.py @@ -15,15 +15,13 @@ def __init__(self, parent: QtWidgets.QWidget = None, *args, **kwargs): self._marquee: bool = True self.timer = QtCore.QTimer() self.timer.timeout.connect(self._scroll_text) - self.delay_timer = QtCore.QTimer() - self.delay_timer.setSingleShot(True) - self.delay_timer.timeout.connect(self._start_marquee) - self.scroll_pos = 0.0 - self.marquee_spacing = 20 + self.marquee_spacing = 40 + self.scroll_speed = 40 + self.scroll_animation_speed = 30 + self.max_loops = 2 + self.loop_count = 0 self.paused = False - self.scroll_speed = 20 - self.scroll_animation_speed = 50 self.setMouseTracking(True) self.setTabletTracking(True) self.setSizePolicy( @@ -41,8 +39,8 @@ def __init__(self, parent: QtWidgets.QWidget = None, *args, **kwargs): self.glow_animation.finished.connect(self.repaint) self.total_scroll_width: float = 0.0 - self.marquee_delay = 5000 - self.loop_count = 0 + self.text_width: float = 0.0 + self.label_width: float = 0.0 self.first_run = True def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: @@ -67,6 +65,7 @@ def setPixmap(self, a0: QtGui.QPixmap) -> None: def setText(self, text: str) -> None: """Set widget text""" self._text = text + self.scroll_pos = 0.0 self.update_text_metrics() @property @@ -102,7 +101,7 @@ def marquee(self) -> bool: return self._marquee @marquee.setter - def marquee(self, activate) -> None: + def marquee(self, activate: bool) -> None: self._marquee = activate self.update_text_metrics() @@ -130,10 +129,9 @@ def start_glow_animation(self) -> None: """Start glow animation""" self.glow_animation.setDuration(self.animation_speed) start_color = QtGui.QColor("#00000000") + end_color = QtGui.QColor("#E95757") self.glow_animation.setStartValue(start_color) - base_end_color = QtGui.QColor("#E95757") - self.glow_animation.setEndValue(base_end_color) - + self.glow_animation.setEndValue(end_color) self.glow_animation.setDirection(QtCore.QPropertyAnimation.Direction.Forward) self.glow_animation.setLoopCount(-1) self.glow_animation.start() @@ -155,145 +153,98 @@ def update_text_metrics(self) -> None: self.total_scroll_width = float(self.text_width + self.marquee_spacing) if self._marquee and self.text_width > self.label_width: - self.start_scroll() + self.scroll_pos = 0.0 + QtCore.QTimer.singleShot(2000, self.start_scroll) else: self.stop_scroll() self.scroll_pos = 0.0 self.update() def start_scroll(self) -> None: - """Start marquee text scroll effect""" - if not self.delay_timer.isActive() and not self.timer.isActive(): + """Start or restart the scrolling.""" + if not self.timer.isActive(): self.scroll_pos = 0 self.loop_count = 0 - if self.first_run: - self.delay_timer.start(self.marquee_delay) - self.first_run = False - else: - self._start_marquee() - - def _start_marquee(self) -> None: - """Starts the actual marquee animation after the delay or immediately.""" - if not self.timer.isActive(): self.timer.start(self.scroll_animation_speed) def stop_scroll(self) -> None: """Stop marquee text scroll effect""" self.timer.stop() - self.delay_timer.stop() + self.repaint() def _scroll_text(self) -> None: - if self.paused: + """Smoothly scroll the text leftwards.""" + if not self._marquee or self.paused: return - p_to_m = self.scroll_speed * (self.scroll_animation_speed / 1000.0) self.scroll_pos -= p_to_m - if self.scroll_pos <= -self.total_scroll_width: self.loop_count += 1 - if self.loop_count >= 2: + if self.loop_count >= self.max_loops: self.stop_scroll() - else: - self.scroll_pos = 0 - - self.repaint() + self.scroll_pos = 0.0 + self.update() def paintEvent(self, a0: QtGui.QPaintEvent) -> None: """Re-implemented method, paint widget""" - qp = QtWidgets.QStylePainter(self) - opt = QtWidgets.QStyleOption() - opt.initFrom(self) - + qp = QtGui.QPainter(self) qp.setRenderHint(qp.RenderHint.Antialiasing, True) qp.setRenderHint(qp.RenderHint.SmoothPixmapTransform, True) qp.setRenderHint(qp.RenderHint.LosslessImageRendering, True) - _rect = self.rect() - _style = self.style() - - icon_margin = _style.pixelMetric(_style.PixelMetric.PM_HeaderMargin, opt, self) - if not _style or _rect.isNull(): - return + rect = self.contentsRect() + if self._background_color: + qp.setBrush(self._background_color) + qp.setPen(QtCore.Qt.PenStyle.NoPen) + if self._rounded: + path = QtGui.QPainterPath() + path.addRoundedRect(QtCore.QRectF(rect), 10, 10) + qp.fillPath(path, self._background_color) + else: + qp.fillRect(rect, self._background_color) if self.icon_pixmap: - qp.setCompositionMode(qp.CompositionMode.CompositionMode_SourceOver) - _icon_rect = QtCore.QRectF( - 0.0 + icon_margin, - 0.0 + icon_margin, - self.width() - icon_margin, - self.height() - icon_margin, - ) - _icon_scaled = self.icon_pixmap.scaled( - _icon_rect.size().toSize(), + icon_rect = QtCore.QRectF(0, 0, self.height(), self.height()) + scaled = self.icon_pixmap.scaled( + icon_rect.size().toSize(), QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.TransformationMode.SmoothTransformation, ) - scaled_width = _icon_scaled.width() - scaled_height = _icon_scaled.height() - adjusted_x = (_icon_rect.width() - scaled_width) / 2.0 - adjusted_y = (_icon_rect.height() - scaled_height) / 2.0 - adjusted_icon_rect = QtCore.QRectF( - _icon_rect.x() + adjusted_x, - _icon_rect.y() + adjusted_y, - scaled_width, - scaled_height, - ) - qp.drawPixmap( - adjusted_icon_rect, _icon_scaled, _icon_scaled.rect().toRectF() - ) - - big_rect = QtGui.QPainterPath() - rect = self.contentsRect().toRectF() - big_rect.addRoundedRect(rect, 10.0, 10.0, QtCore.Qt.SizeMode.AbsoluteSize) - mini_rect = QtCore.QRectF( - (rect.width() - rect.width() * 0.99) / 2, - (rect.height() - rect.height() * 0.85) / 2, - rect.width() * 0.99, - rect.height() * 0.85, - ) - mini_path = QtGui.QPainterPath() - mini_path.addRoundedRect(mini_rect, 10.0, 10.0, QtCore.Qt.SizeMode.AbsoluteSize) - subtracted = big_rect.subtracted(mini_path) - + qp.drawPixmap(icon_rect.toRect(), scaled) if self.glow_animation.state() == self.glow_animation.State.Running: - qp.setCompositionMode(qp.CompositionMode.CompositionMode_SourceAtop) - subtracted.setFillRule(QtCore.Qt.FillRule.OddEvenFill) - qp.fillPath(subtracted, self.glow_color) + path = QtGui.QPainterPath() + path.addRoundedRect(QtCore.QRectF(rect), 10, 10) + qp.fillPath(path, self.glow_color) if self._text: - qp.setCompositionMode(qp.CompositionMode.CompositionMode_SourceOver) - - text_rect = self.contentsRect() - text_rect.translate(int(self.scroll_pos), 0) - text_path = QtGui.QPainterPath() - text_path.addRect(self.contentsRect().toRectF()) - qp.setClipPath(text_path) - text_option = QtGui.QTextOption(self.alignment()) text_option.setWrapMode(QtGui.QTextOption.WrapMode.NoWrap) - qp.drawText( - QtCore.QRectF(text_rect), - self._text, - text_option, + qp.save() + qp.setClipRect(rect) + baseline_y = ( + rect.y() + + ( + rect.height() + + self.fontMetrics().ascent() + - self.fontMetrics().descent() + ) + / 2 ) - if self._marquee and self.text_width > self.label_width: - second_text_rect = self.rect() - second_text_rect.translate( - int(self.scroll_pos + self.text_width + self.label_width / 2), - 0, + + if self.text_width > self.label_width: + qp.drawText( + QtCore.QPointF(rect.x() + self.scroll_pos, baseline_y), self._text ) - qp.drawText(QtCore.QRectF(second_text_rect), self._text, text_option) - draw_rect = QtCore.QRectF( - self.contentsRect().x() - + self.scroll_pos - + self.text_width - + self.marquee_spacing, - self.contentsRect().y(), - self.text_width, - self.contentsRect().height(), + # Draw scrolling repeater text + qp.drawText( + QtCore.QPointF( + rect.x() + self.scroll_pos + self.total_scroll_width, baseline_y + ), + self._text, ) - qp.drawText(draw_rect, self._text, text_option) else: - text_rect = self.contentsRect().toRectF() - qp.drawText(text_rect, self._text, text_option) + center_x = rect.x() + (rect.width() - self.text_width) / 2 + + qp.drawText(QtCore.QPointF(center_x, baseline_y), self._text) + qp.restore() qp.end() From eb1805525338cfd5c47c0e385a4b5aa216bb2309 Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Fri, 12 Dec 2025 11:13:53 +0000 Subject: [PATCH 11/70] Bugfix: Delete file handling and QDialog class refactoring (#128) * Refactor: Ran ruff formatter * Remove unecessary lambda expression * Add docstring to paintEvent method * Refactor back signal from reject to request_back * Change delete file button from reject to delete_file_button * Make setupUI method private * Change request back signal name * Make setupUI the last method on file * Separate logic, created get mainwindow widget method * Set dialog modal, add class vars, x and y dialog offsets * Use accept and reject signals for dialog result * implement open method, calculate dialog position relative to window * Refactor cancel print dialog * Simplify print cancel signal emition * Refactor delete file logic * Change delete file signal name * Fix empty directory * Fix directory handling for file deletion operationsFix empty directory * Dev debt --------- Co-authored-by: Roberto Martins Co-authored-by: Roberto --- BlocksScreen/lib/files.py | 14 ++ BlocksScreen/lib/moonrakerComm.py | 1 + BlocksScreen/lib/panels/printTab.py | 38 ++--- .../lib/panels/widgets/confirmPage.py | 44 +++--- BlocksScreen/lib/panels/widgets/dialogPage.py | 138 +++++++++--------- BlocksScreen/lib/panels/widgets/filesPage.py | 5 - .../lib/panels/widgets/jobStatusPage.py | 17 +-- 7 files changed, 122 insertions(+), 135 deletions(-) diff --git a/BlocksScreen/lib/files.py b/BlocksScreen/lib/files.py index 68a399c2..f080e6d7 100644 --- a/BlocksScreen/lib/files.py +++ b/BlocksScreen/lib/files.py @@ -54,6 +54,7 @@ def __init__( @property def file_list(self): + """Get the current list of files""" return self.files def handle_message_received(self, method: str, data, params: dict) -> None: @@ -80,6 +81,19 @@ def handle_message_received(self, method: str, data, params: dict) -> None: self.on_file_list[list].emit(self.files) self.on_dirs[list].emit(self.directories) + @QtCore.pyqtSlot(str, str, name="on_request_delete_file") + def on_request_delete_file(self, filename: str, directory: str = "gcodes") -> None: + """Requests deletion of a file + + Args: + filename (str): file to delete + directory (str): root directory where the file is located + """ + if not directory: + self.ws.api.delete_file(filename) + return + self.ws.api.delete_file(filename, directory) # Use the root directory 'gcodes' + @QtCore.pyqtSlot(str, name="on_request_fileinfo") def on_request_fileinfo(self, filename: str) -> None: """Requests metadata for a file diff --git a/BlocksScreen/lib/moonrakerComm.py b/BlocksScreen/lib/moonrakerComm.py index b2f41446..78fba08e 100644 --- a/BlocksScreen/lib/moonrakerComm.py +++ b/BlocksScreen/lib/moonrakerComm.py @@ -544,6 +544,7 @@ def get_gcode_thumbnail(self, filename_dir: str): def delete_file(self, filename: str, root_dir: str = "gcodes"): """Request file deletion""" filepath = f"{root_dir}/{filename}" + filepath = f"gcodes/{root_dir}/{filename}" if root_dir != "gcodes" else filepath return self._ws.send_request( method="server.files.delete_file", params={"path": filepath}, diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index 98301283..65f0030d 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -126,7 +126,6 @@ def __init__( self.file_data.on_file_list.connect(self.filesPage_widget.on_file_list) self.jobStatusPage_widget = JobStatusWidget(self) self.addWidget(self.jobStatusPage_widget) - self.confirmPage_widget.on_accept.connect( self.jobStatusPage_widget.on_print_start ) @@ -167,19 +166,15 @@ def __init__( self.printer.print_stats_update[str, float].connect( self.jobStatusPage_widget.on_print_stats_update ) - self.printer.print_stats_update[str, str].connect(self.on_print_stats_update) self.printer.print_stats_update[str, dict].connect(self.on_print_stats_update) self.printer.print_stats_update[str, float].connect(self.on_print_stats_update) - self.printer.gcode_move_update[str, list].connect( self.jobStatusPage_widget.on_gcode_move_update ) - self.babystepPage = BabystepPage(self) self.babystepPage.request_back.connect(self.back_button) self.addWidget(self.babystepPage) - self.tune_page = TuneWidget(self) self.addWidget(self.tune_page) self.jobStatusPage_widget.tune_clicked.connect( @@ -225,10 +220,8 @@ def __init__( self.tune_page.request_sensorsPage.connect( lambda: self.change_page(self.indexOf(self.sensorsPanel)) ) - self.sensorsPanel = SensorsWindow(self) self.addWidget(self.sensorsPanel) - self.printer.request_object_subscription_signal.connect( self.sensorsPanel.handle_available_fil_sensors ) @@ -245,11 +238,8 @@ def __init__( partial(self.change_page, self.indexOf(self.filesPage_widget)) ) self.babystepPage.run_gcode.connect(self.ws.api.run_gcode) - self.run_gcode_signal.connect(self.ws.api.run_gcode) - self.confirmPage_widget.on_delete.connect(self.delete_file) - self.change_page(self.indexOf(self.print_page)) # force set the initial page @QtCore.pyqtSlot(str, dict, name="on_print_stats_update") @@ -301,21 +291,22 @@ def on_slidePage_request( self.sliderPage.set_slider_maximum(max_value) self.change_page(self.indexOf(self.sliderPage)) - def delete_file(self, direcotry: str, name: str): - """Handle Delete file button clicked""" - self.directory: str = direcotry - self.filename: str = name + @QtCore.pyqtSlot(str, str, name="delete_file") + @QtCore.pyqtSlot(str, name="delete_file") + def delete_file(self, filename: str, directory: str = "gcodes") -> None: + """Handle Delete file signal, shows confirmation dialog""" self.dialogPage.set_message("Are you sure you want to delete this file?") - self.dialogPage.button_clicked.connect(self.on_dialog_button_clicked) - self.dialogPage.show() + self.dialogPage.accepted.connect( + lambda: self._on_delete_file_confirmed(filename, directory) + ) + self.dialogPage.open() - def on_dialog_button_clicked(self, button_name: str) -> None: - """Handle dialog button clicks""" - if button_name == "Confirm": - self.ws.api.delete_file(self.filename, self.directory) - self.dialogPage.hide() - else: - self.dialogPage.hide() + def _on_delete_file_confirmed(self, filename: str, directory: str) -> None: + """Handle confirmed file deletion after user accepted the dialog""" + self.file_data.on_request_delete_file(filename, directory) + self.request_back.emit() + self.filesPage_widget.reset_dir() + self.dialogPage.disconnect() def paintEvent(self, a0: QtGui.QPaintEvent) -> None: """Widget painting""" @@ -349,6 +340,7 @@ def handle_cancel_print(self) -> None: self.ws.api.cancel_print() self.on_cancel_print.emit() self.loadscreen.show() + self.loadscreen.setModal(True) self.loadscreen.set_status_message("Cancelling print...\nPlease wait") def change_page(self, index: int) -> None: diff --git a/BlocksScreen/lib/panels/widgets/confirmPage.py b/BlocksScreen/lib/panels/widgets/confirmPage.py index 95a12579..c09f31b5 100644 --- a/BlocksScreen/lib/panels/widgets/confirmPage.py +++ b/BlocksScreen/lib/panels/widgets/confirmPage.py @@ -1,44 +1,42 @@ +import os import typing +import helper_methods from lib.utils.blocks_button import BlocksCustomButton +from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.blocks_label import BlocksLabel from lib.utils.icon_button import IconButton -from lib.utils.blocks_frame import BlocksCustomFrame from PyQt6 import QtCore, QtGui, QtWidgets -import helper_methods - - -import os - class ConfirmWidget(QtWidgets.QWidget): on_accept: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( str, list, name="on_accept" ) - on_reject: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal(name="on_reject") - + request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + name="request-back" + ) on_delete: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, str, name="on_delete" + str, str, name="delete_file" ) def __init__(self, parent) -> None: super().__init__(parent) - self.setupUI() + self._setupUI() self.setMouseTracking(True) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) self.thumbnail: QtGui.QImage = QtGui.QImage() self._thumbnails: typing.List = [] - self.directory = "" + self.directory = "gcodes" self.filename = "" self.confirm_button.clicked.connect( lambda: self.on_accept.emit( str(os.path.join(self.directory, self.filename)), self._thumbnails ) ) - self.back_btn.clicked.connect(self.on_reject.emit) - self.reject_button.clicked.connect( - lambda: self.on_delete.emit(self.directory, self.filename) + self.back_btn.clicked.connect(self.request_back.emit) + self.delete_file_button.clicked.connect( + lambda: self.on_delete.emit(self.filename, self.directory) ) @QtCore.pyqtSlot(str, dict, name="on_show_widget") @@ -152,7 +150,7 @@ def showEvent(self, a0: QtGui.QShowEvent) -> None: self.cf_thumbnail.close() return super().showEvent(a0) - def setupUI(self) -> None: + def _setupUI(self) -> None: """Setup widget ui""" sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, @@ -263,18 +261,18 @@ def setupUI(self) -> None: self.confirm_button, 0, QtCore.Qt.AlignmentFlag.AlignCenter ) - self.reject_button = BlocksCustomButton(parent=self.info_frame) - self.reject_button.setMinimumSize(QtCore.QSize(250, 70)) - self.reject_button.setMaximumSize(QtCore.QSize(250, 70)) - self.reject_button.setFont(font) - self.reject_button.setFlat(True) - self.reject_button.setProperty( + self.delete_file_button = BlocksCustomButton(parent=self.info_frame) + self.delete_file_button.setMinimumSize(QtCore.QSize(250, 70)) + self.delete_file_button.setMaximumSize(QtCore.QSize(250, 70)) + self.delete_file_button.setFont(font) + self.delete_file_button.setFlat(True) + self.delete_file_button.setProperty( "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/garbage-icon.svg") ) - self.reject_button.setText("Delete") + self.delete_file_button.setText("Delete") # 2. Align buttons to the right self.cf_confirm_layout.addWidget( - self.reject_button, 0, QtCore.Qt.AlignmentFlag.AlignCenter + self.delete_file_button, 0, QtCore.Qt.AlignmentFlag.AlignCenter ) self.info_layout.addLayout(self.cf_confirm_layout) diff --git a/BlocksScreen/lib/panels/widgets/dialogPage.py b/BlocksScreen/lib/panels/widgets/dialogPage.py index 65f7c727..45d067a4 100644 --- a/BlocksScreen/lib/panels/widgets/dialogPage.py +++ b/BlocksScreen/lib/panels/widgets/dialogPage.py @@ -1,79 +1,74 @@ +import typing + from PyQt6 import QtCore, QtGui, QtWidgets class DialogPage(QtWidgets.QDialog): - button_clicked = QtCore.pyqtSignal(str) # Signal to emit which button was clicked + """Simple confirmation dialog with custom message and Confirm/Back buttons + + To assert if the user accepted or rejected the dialog connect to the **accepted()** or **rejected()** signals. + + The `finished()` signal can also be used to get the result of the dialog. This is emitted after + the accepted and rejected signals. + + + """ + + x_offset: float = 0.7 + y_offset: float = 0.7 + border_radius: int = 20 + border_margin: int = 5 def __init__( self, parent: QtWidgets.QWidget, ) -> None: super().__init__(parent) + self._setupUI() self.setWindowFlags( QtCore.Qt.WindowType.Popup | QtCore.Qt.WindowType.FramelessWindowHint ) self.setAttribute( QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True ) # Make background transparent - self._setupUI() - self.repaint() + self.setWindowModality( # Force window modality to block input to other windows + QtCore.Qt.WindowModality.WindowModal + ) + self.confirm_button.clicked.connect(self.accept) + self.cancel_button.clicked.connect(self.reject) + self.setModal(True) + self.update() def set_message(self, message: str) -> None: """Set dialog text message""" self.label.setText(message) - def _geometry_calc(self) -> None: - """Calculate dialog widget position relative to the window""" + def _get_mainWindow_widget(self) -> typing.Optional[QtWidgets.QMainWindow]: + """Get the main application window""" app_instance = QtWidgets.QApplication.instance() - main_window = app_instance.activeWindow() if app_instance else None - if main_window is None and app_instance: + if not app_instance: + return None + main_window = app_instance.activeWindow() + if main_window is None: for widget in app_instance.allWidgets(): if isinstance(widget, QtWidgets.QMainWindow): main_window = widget + break + return main_window if isinstance(main_window, QtWidgets.QMainWindow) else None - x_offset = 0.7 - y_offset = 0.7 - - width = int(main_window.width() * x_offset) - height = int(main_window.height() * y_offset) - self.testwidth = width - self.testheight = height + def _geometry_calc(self) -> None: + """Calculate dialog widget position relative to the window""" + main_window = self._get_mainWindow_widget() + width = int(main_window.width() * self.x_offset) + height = int(main_window.height() * self.y_offset) x = int(main_window.geometry().x() + (main_window.width() - width) / 2) y = int(main_window.geometry().y() + (main_window.height() - height) / 2) - self.setGeometry(x, y, width, height) - def paintEvent(self, event: QtGui.QPaintEvent) -> None: - """Re-implemented method, paint widget""" - self._geometry_calc() - painter = QtGui.QPainter(self) - painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) - - rect = self.rect() - radius = 20 # Adjust the radius for rounded corners - - # Set background color - painter.setBrush( - QtGui.QBrush(QtGui.QColor(63, 63, 63)) - ) # Semi-transparent dark gray - - # Set border color and width - border_color = QtGui.QColor(128, 128, 128) # Gray color - border_width = 5 # Reduced border thickness - - pen = QtGui.QPen() - pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) - painter.setPen(QtGui.QPen(border_color, border_width)) - - painter.drawRoundedRect(rect, radius, radius) - - painter.end() - def sizeHint(self) -> QtCore.QSize: """Re-implemented method, widget size hint""" popup_width = int(self.geometry().width()) popup_height = int(self.geometry().height()) - # Centering logic popup_x = self.x() popup_y = self.y() + (self.height() - popup_height) // 2 self.move(popup_x, popup_y) @@ -84,17 +79,14 @@ def sizeHint(self) -> QtCore.QSize: def resizeEvent(self, event: QtGui.QResizeEvent) -> None: """Re-implemented method, handle resize event""" super().resizeEvent(event) - - label_width = self.testwidth - label_height = self.testheight - label_x = (self.width() - label_width) // 2 - label_y = ( - int(label_height / 4) - 20 - ) # Move the label to the top (adjust as needed) - - self.label.setGeometry(label_x, -label_y, label_width, label_height) - - # Adjust button positions on resize + main_window = self._get_mainWindow_widget() + if main_window is None: + return + width = int(main_window.width() * self.x_offset) + height = int(main_window.height() * self.y_offset) + label_x = (self.width() - width) // 2 + label_y = int(height / 4) - 20 # Move the label to the top (adjust as needed) + self.label.setGeometry(label_x, -label_y, width, height) self.confirm_button.setGeometry( int(0), self.height() - 70, int(self.width() / 2), 70 ) @@ -105,11 +97,32 @@ def resizeEvent(self, event: QtGui.QResizeEvent) -> None: 70, ) + def open(self): + """Re-implemented method, open widget""" + self._geometry_calc() + return super().open() + def show(self) -> None: """Re-implemented method, show widget""" self._geometry_calc() return super().show() + def paintEvent(self, event: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" + self._geometry_calc() + painter = QtGui.QPainter(self) + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) + rect = self.rect() + painter.setBrush( + QtGui.QBrush(QtGui.QColor(63, 63, 63)) + ) # Semi-transparent dark gray + border_color = QtGui.QColor(128, 128, 128) # Gray color + pen = QtGui.QPen() + pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) + painter.setPen(QtGui.QPen(border_color, self.border_margin)) + painter.drawRoundedRect(rect, self.border_radius, self.border_radius) + painter.end() + def _setupUI(self) -> None: self.label = QtWidgets.QLabel("Test", self) font = QtGui.QFont() @@ -118,18 +131,12 @@ def _setupUI(self) -> None: self.label.setStyleSheet("color: #ffffff; background: transparent;") self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.label.setWordWrap(True) - - # Create Confirm and Cancel buttons self.confirm_button = QtWidgets.QPushButton("Confirm", self) self.cancel_button = QtWidgets.QPushButton("Back", self) - - # Set button styles button_font = QtGui.QFont() button_font.setPointSize(14) self.confirm_button.setFont(button_font) self.cancel_button.setFont(button_font) - - # Apply styles for rounded corners self.confirm_button.setStyleSheet( """ background-color: #4CAF50; @@ -148,7 +155,6 @@ def _setupUI(self) -> None: padding: 10px; """ ) - # Position buttons self.confirm_button.setGeometry( int(0), self.height() - 70, int(self.width() / 2), 70 @@ -159,15 +165,3 @@ def _setupUI(self) -> None: int(self.width() / 2), 70, ) - - # Connect button signals - self.confirm_button.clicked.connect(lambda: self.on_button_clicked("Confirm")) - self.cancel_button.clicked.connect(lambda: self.on_button_clicked("Cancel")) - - def on_button_clicked(self, button_name: str) -> None: - """Handle dialog buttons clicked""" - self.button_clicked.emit(button_name) # Emit the signal with the button name - if button_name == "Confirm": - self.accept() # Close the dialog with an accepted state - elif button_name == "Cancel": - self.reject() # Close the dialog with a rejected state diff --git a/BlocksScreen/lib/panels/widgets/filesPage.py b/BlocksScreen/lib/panels/widgets/filesPage.py index 8de160c0..fbfb3377 100644 --- a/BlocksScreen/lib/panels/widgets/filesPage.py +++ b/BlocksScreen/lib/panels/widgets/filesPage.py @@ -74,11 +74,6 @@ def on_directories(self, directories_data: list) -> None: if self.isVisible(): self._build_file_list() - @QtCore.pyqtSlot(str, name="on-delete-file") - def on_delete_file(self, filename: str) -> None: - """Handle file deleted""" - ... - @QtCore.pyqtSlot(dict, name="on-fileinfo") def on_fileinfo(self, filedata: dict) -> None: """Handle receive file information/metadata""" diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index bb9754ea..a09bdcd7 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -62,7 +62,7 @@ class JobStatusWidget(QtWidgets.QWidget): def __init__(self, parent) -> None: super().__init__(parent) - self.canceldialog = dialogPage.DialogPage(self) + self.cancel_print_dialog = dialogPage.DialogPage(self) self._setupUI() self.tune_menu_btn.clicked.connect(self.tune_clicked.emit) self.pause_printing_btn.clicked.connect(self.pause_resume_print) @@ -121,18 +121,11 @@ def hidethumbnail(self): @QtCore.pyqtSlot(name="handle-cancel") def handleCancel(self) -> None: """Handle cancel print job dialog""" - self.canceldialog.set_message( - "Are you sure you \n want to cancel \n this print job?" + self.cancel_print_dialog.set_message( + "Are you sure you \n want to cancel \n the current print job?" ) - self.canceldialog.button_clicked.connect(self.on_dialog_button_clicked) - self.canceldialog.show() - - def on_dialog_button_clicked(self, button_name: str) -> None: - """Handle dialog button clicks""" - if button_name == "Confirm": - self.print_cancel.emit() # Emit the print_cancel signal - elif button_name == "Cancel": - pass + self.cancel_print_dialog.accepted.connect(self.print_cancel) + self.cancel_print_dialog.open() @QtCore.pyqtSlot(str, list, name="on_print_start") def on_print_start(self, file: str, thumbnails: list) -> None: From 260b126538c0efc9265371593ca3a31b274b985f Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Fri, 12 Dec 2025 14:41:39 +0000 Subject: [PATCH 12/70] Work fan page (#119) * ADD: added fans page * add: added fans into fans page * ADD: added line to fans page * Refacotr: changed layout type * UPD: updated options card to have 2 text . added cards into controltab fans * Rev: removed line from fans page * Refactor: refactor on_fan_object_update * Refactor: Ran ruff formatter * Fix incorrect method name * Fix incorrect method name * bugfix: option card clicklable area * Ran: ruff formatter * Bugfix: names being wrong and slider spaming gcodes on change * ADD: color degrade when ON/OFF (#120) * Del: deleted some prints * Bugfix: fan not updating when slider updates * Bugfix: label size * Bugfix: Delete file handling and QDialog class refactoring (#128) * Refactor: Ran ruff formatter * Remove unecessary lambda expression * Add docstring to paintEvent method * Refactor back signal from reject to request_back * Change delete file button from reject to delete_file_button * Make setupUI method private * Change request back signal name * Make setupUI the last method on file * Separate logic, created get mainwindow widget method * Set dialog modal, add class vars, x and y dialog offsets * Use accept and reject signals for dialog result * implement open method, calculate dialog position relative to window * Refactor cancel print dialog * Simplify print cancel signal emition * Refactor delete file logic * Change delete file signal name * Fix empty directory * Fix directory handling for file deletion operationsFix empty directory * Dev debt --------- Co-authored-by: Roberto Martins Co-authored-by: Roberto * bugfix: option card centering --------- Co-authored-by: Roberto Co-authored-by: Hugo Costa --- BlocksScreen/lib/panels/controlTab.py | 145 +++++++++- .../lib/panels/widgets/optionCardWidget.py | 106 +++++-- .../panels/widgets/slider_selector_page.py | 23 +- BlocksScreen/lib/ui/controlStackedWidget.ui | 262 +++++++++++++++--- .../lib/ui/controlStackedWidget_ui.py | 212 +++++++++----- BlocksScreen/lib/utils/blocks_slider.py | 1 + 6 files changed, 620 insertions(+), 129 deletions(-) diff --git a/BlocksScreen/lib/panels/controlTab.py b/BlocksScreen/lib/panels/controlTab.py index bef67b3f..99f8a3c5 100644 --- a/BlocksScreen/lib/panels/controlTab.py +++ b/BlocksScreen/lib/panels/controlTab.py @@ -13,6 +13,11 @@ from PyQt6 import QtCore, QtGui, QtWidgets from lib.panels.widgets.popupDialogWidget import Popup +from lib.utils.display_button import DisplayButton +from lib.panels.widgets.slider_selector_page import SliderPage + +from lib.panels.widgets.optionCardWidget import OptionCard +from helper_methods import normalize class ControlTab(QtWidgets.QStackedWidget): @@ -42,6 +47,8 @@ class ControlTab(QtWidgets.QStackedWidget): request_file_info: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( str, name="request-file-info" ) + tune_display_buttons: dict = {} + card_options: dict = {} def __init__( self, @@ -74,6 +81,11 @@ def __init__( self.addWidget(self.printcores_page) self.loadpage = LoadScreen(self, LoadScreen.AnimationGIF.DEFAULT) self.addWidget(self.loadpage) + + self.sliderPage = SliderPage(self) + self.addWidget(self.sliderPage) + self.sliderPage.request_back.connect(self.back_button) + self.probe_helper_page.request_page_view.connect( partial(self.change_page, self.indexOf(self.probe_helper_page)) ) @@ -108,6 +120,10 @@ def __init__( self.panel.cp_temperature_btn.clicked.connect( partial(self.change_page, self.indexOf(self.panel.temperature_page)) ) + self.panel.cp_fans_btn.clicked.connect( + partial(self.change_page, self.indexOf(self.panel.fans_page)) + ) + self.panel.fans_back_btn.clicked.connect(self.back_button) self.panel.cp_switch_print_core_btn.clicked.connect(self.show_swapcore) self.panel.cp_nozzles_calibration_btn.clicked.connect( partial(self.change_page, self.indexOf(self.probe_helper_page)) @@ -267,6 +283,133 @@ def __init__( self.panel.cooldown_btn.hide() self.panel.cp_switch_print_core_btn.hide() + self.printer.fan_update[str, str, float].connect(self.on_fan_object_update) + self.printer.fan_update[str, str, int].connect(self.on_fan_object_update) + + @QtCore.pyqtSlot(str, str, float, name="on_fan_update") + @QtCore.pyqtSlot(str, str, int, name="on_fan_update") + def on_fan_object_update( + self, name: str, field: str, new_value: int | float + ) -> None: + """Slot that receives updates from fan objects. + + Args: + name (str): Fan object name + field (str): Field name + new_value (int | float): New value for the field + """ + if "speed" not in field: + return + + if name == "fan_generic Auxiliary_Cooling_Fans": + name = "Auxiliary\ncooling fans" + elif name == "fan_generic CHAMBER_EXHAUST": + name = "Exhaust Fan" + elif name == "fan_generic Part_Cooling_Fan": + name = "Cooling fan" + else: + name = name.removeprefix("fan_generic") + fan_card = self.tune_display_buttons.get(name) + + if fan_card is None: + icon_path = ( + ":/temperature_related/media/btn_icons/blower.svg" + if "blower" in name.lower() + else ":/temperature_related/media/btn_icons/fan.svg" + ) + icon = QtGui.QPixmap(icon_path) + + card = OptionCard(self, name, str(name), icon) # type: ignore + card.setObjectName(str(name)) + + # Add card to layout and record reference + self.card_options[name] = card + self.panel.fans_content_layout.addWidget(card) + + # If the card doesn't have expected UI properties, discard it + if not hasattr(card, "continue_clicked"): + del card + self.card_options.pop(name, None) + return + + card.setMode(True) + card.secondtext.setText(f"{new_value}%") + card.continue_clicked.connect( + lambda: self.on_slidePage_request( + str(name), + card.secondtext.text().replace("%", ""), + self.on_slider_change, + 0, + 100, + ) + ) + + self.tune_display_buttons[name] = card + self.update() + fan_card = card + + if fan_card: + value_percent = new_value * 100 if new_value <= 1 else new_value + fan_card.secondtext.setText(f"{value_percent:.0f}%") + + @QtCore.pyqtSlot(str, int, "PyQt_PyObject", name="on_slidePage_request") + @QtCore.pyqtSlot(str, int, "PyQt_PyObject", int, int, name="on_slidePage_request") + def on_slidePage_request( + self, + name: str, + current_value: int, + callback, + min_value: int = 0, + max_value: int = 100, + ) -> None: + self.sliderPage.value_selected.connect(callback) + self.sliderPage.set_name(name) + self.sliderPage.set_slider_position(int(current_value)) + self.sliderPage.set_slider_minimum(min_value) + self.sliderPage.set_slider_maximum(max_value) + self.change_page(self.indexOf(self.sliderPage)) + + @QtCore.pyqtSlot(str, int, name="on_slider_change") + def on_slider_change(self, name: str, new_value: int) -> None: + if "speed" in name.lower(): + self.speed_factor_override = new_value / 100 + self.run_gcode_signal.emit(f"M220 S{new_value}") + + if name == "Auxiliary\ncooling fans": + name = "Auxiliary_Cooling_Fans" + elif name == "Exhaust Fan": + name = "CHAMBER_EXHAUST" + elif name == "Cooling fan": + name = "Part_Cooling_Fan" + else: + ... + if name.lower() == "fan": + self.run_gcode_signal.emit( + f"M106 S{int(round((normalize(float(new_value / 100), 0.0, 1.0, 0, 255))))}" + ) # [0, 255] Range + else: + self.run_gcode_signal.emit( + f'SET_FAN_SPEED FAN="{name}" SPEED={float(new_value / 100.00)}' + ) # [0.0, 1.0] Range + + def create_display_button(self, name: str) -> DisplayButton: + """Create and return a DisplayButton + + Args: + name (str): Name for the display button + + Returns: + DisplayButton: The created DisplayButton object + """ + display_button = DisplayButton() + display_button.setObjectName(str(name + "_display")) + display_button.setMinimumSize(QtCore.QSize(150, 50)) + display_button.setMaximumSize(QtCore.QSize(150, 80)) + font = QtGui.QFont() + font.setPointSize(16) + display_button.setFont(font) + return display_button + def handle_printcoreupdate(self, value: dict): if value["swapping"] == "idle": return @@ -297,9 +440,7 @@ def _handle_gcode_response(self, messages: list): and "range:" in msg_list and "tolerance:" in msg_list ): - print("Match candidate:", msg_list) match = re.search(pattern, msg_list) - print("Regex match:", match) if match: retries_done = int(match.group(1)) diff --git a/BlocksScreen/lib/panels/widgets/optionCardWidget.py b/BlocksScreen/lib/panels/widgets/optionCardWidget.py index 711dcd02..789a9885 100644 --- a/BlocksScreen/lib/panels/widgets/optionCardWidget.py +++ b/BlocksScreen/lib/panels/widgets/optionCardWidget.py @@ -1,12 +1,11 @@ import typing from PyQt6 import QtCore, QtGui, QtWidgets -from lib.utils.blocks_label import BlocksLabel from lib.utils.icon_button import IconButton -class OptionCard(QtWidgets.QFrame): - continue_clicked: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( +class OptionCard(QtWidgets.QAbstractButton): + clicked: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( "PyQt_PyObject", name="continue_clicked" ) @@ -25,8 +24,22 @@ def __init__( self.icon_background_color = QtGui.QColor(150, 150, 130, 80) self.name = name self.card_text = text + self.doubleT: bool = False self._setupUi(self) - self.continue_button.clicked.connect(lambda: self.continue_clicked.emit(self)) + self.option_icon.setAttribute( + QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents + ) + self.option_text.setAttribute( + QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents + ) + self.secondtext.setAttribute( + QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents + ) + self.line_separator.setAttribute( + QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents + ) + + self.setMode(False) self.set_card_icon(icon) self.set_card_text(text) @@ -42,7 +55,13 @@ def enable_button(self) -> None: def set_card_icon(self, pixmap: QtGui.QPixmap) -> None: """Set widget icon""" - self.option_icon.setPixmap(pixmap) + scaled = pixmap.scaled( + 300, + 300, + QtCore.Qt.AspectRatioMode.IgnoreAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, + ) + self.option_icon.setPixmap(scaled) self.repaint() def set_card_text(self, text: str) -> None: @@ -79,9 +98,49 @@ def leaveEvent(self, a0: QtCore.QEvent) -> None: def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: """Re-implemented method, handle mouse press event""" + self.clicked.emit(self) self.update() return super().mousePressEvent(a0) + def setMode(self, double_mode: bool = False): + """Set the mode of the layout: single or double text.""" + self.doubleT = double_mode + + # Clear existing widgets from layout before adding new ones + while self.verticalLayout.count(): + item = self.verticalLayout.takeAt(0) + widget = item.widget() + if widget is not None: + widget.setParent(None) + + if self.doubleT: + self.verticalLayout.addWidget( + self.option_icon, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter + | QtCore.Qt.AlignmentFlag.AlignBottom, + ) + self.verticalLayout.addWidget( + self.secondtext, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) + self.verticalLayout.addWidget( + self.line_separator, 0, QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.verticalLayout.addWidget(self.option_text) + self.verticalLayout.addItem(self.spacer) + self.secondtext.show() + else: + self.verticalLayout.addWidget( + self.option_icon, 0, QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.verticalLayout.addWidget( + self.line_separator, 0, QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.verticalLayout.addWidget(self.option_text) + self.verticalLayout.addWidget(self.continue_button) + + self.update() + def paintEvent(self, a0: QtGui.QPaintEvent) -> None: """Re-implemented method, paint widget""" # Rounded background edges @@ -154,12 +213,20 @@ def _setupUi(self, option_card): self.verticalLayout = QtWidgets.QVBoxLayout(option_card) self.verticalLayout.setContentsMargins(0, 0, -1, -1) self.verticalLayout.setObjectName("verticalLayout") - self.option_icon = BlocksLabel(parent=option_card) + self.option_icon = IconButton(parent=option_card) self.option_icon.setMinimumSize(QtCore.QSize(200, 150)) self.option_icon.setObjectName("option_icon") - self.verticalLayout.addWidget( - self.option_icon, 0, QtCore.Qt.AlignmentFlag.AlignHCenter - ) + _button_font = QtGui.QFont() + _button_font.setBold(True) + _button_font.setPointSize(20) + self.secondtext = QtWidgets.QLabel(parent=option_card) + self.secondtext.setText("%") + self.secondtext.setStyleSheet("color:white") + self.secondtext.setFont(_button_font) + self.secondtext.setObjectName("option_text") + self.secondtext.setWordWrap(True) + self.secondtext.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.secondtext.hide() self.line_separator = QtWidgets.QFrame(parent=option_card) self.line_separator.setFrameShape(QtWidgets.QFrame.Shape.HLine) self.line_separator.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) @@ -174,21 +241,15 @@ def _setupUi(self, option_card): self.option_text = QtWidgets.QLabel(parent=option_card) self.option_text.setMinimumSize(QtCore.QSize(200, 50)) self.option_text.setObjectName("option_text") - self.verticalLayout.addWidget( - self.option_text, - ) - self.continue_button = IconButton(parent=option_card) - self.option_text.setAlignment( - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter - ) self.option_text.setWordWrap(True) - _button_font = QtGui.QFont() - _button_font.setBold(True) + self.option_text.setStyleSheet("color:white") + self.option_text.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) _palette = self.option_text.palette() _palette.setColor(QtGui.QPalette.ColorRole.WindowText, self.text_color) self.option_text.setPalette(_palette) - _button_font.setPointSize(15) + self.option_text.setFont(_button_font) + self.continue_button = IconButton(parent=option_card) sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, @@ -207,8 +268,13 @@ def _setupUi(self, option_card): QtGui.QPixmap(":/arrow_icons/media/btn_icons/right_arrow.svg"), ) self.continue_button.setObjectName("continue_button") - self.verticalLayout.addWidget(self.continue_button) + self.spacer = QtWidgets.QSpacerItem( + 20, + 40, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Expanding, + ) self._retranslateUi(option_card) QtCore.QMetaObject.connectSlotsByName(option_card) diff --git a/BlocksScreen/lib/panels/widgets/slider_selector_page.py b/BlocksScreen/lib/panels/widgets/slider_selector_page.py index 793b7044..2f6c89f7 100644 --- a/BlocksScreen/lib/panels/widgets/slider_selector_page.py +++ b/BlocksScreen/lib/panels/widgets/slider_selector_page.py @@ -35,19 +35,26 @@ def __init__(self, parent) -> None: self._setupUI() self.back_button.clicked.connect(self.request_back.emit) self.back_button.clicked.connect(self.value_selected.disconnect) - self.slider.valueChanged.connect(self.on_slider_value_change) + self.slider.sliderReleased.connect(self.on_slider_value_change) self.increase_button.pressed.connect( - lambda: (self.slider.setSliderPosition(self.slider.sliderPosition() + 5)) + lambda: { + (self.slider.setSliderPosition(self.slider.sliderPosition() + 5)), + self.on_slider_value_change(), + } ) self.decrease_button.pressed.connect( - lambda: (self.slider.setSliderPosition(self.slider.sliderPosition() - 5)) + lambda: { + ( + self.slider.setSliderPosition(self.slider.sliderPosition() - 5), + self.on_slider_value_change(), + ) + } ) self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - @QtCore.pyqtSlot(int, name="valueChanged") - def on_slider_value_change(self, value) -> None: + def on_slider_value_change(self) -> None: """Handles slider position changes""" - self.value_selected.emit(self.name, value) + self.value_selected.emit(self.name, self.slider.value()) def set_name(self, name: str) -> None: """Sets the header name for the page""" @@ -114,8 +121,8 @@ def _setupUI(self) -> None: self.object_name_label = QtWidgets.QLabel(self) self.object_name_label.setFont(font) self.object_name_label.setPalette(palette) - self.object_name_label.setMinimumSize(QtCore.QSize(self.width(), 60)) - self.object_name_label.setMaximumSize(QtCore.QSize(self.width() - 60, 60)) + self.object_name_label.setMinimumSize(QtCore.QSize(self.width(), 80)) + self.object_name_label.setMaximumSize(QtCore.QSize(self.width() - 60, 80)) self.object_name_label.setAlignment( QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter ) diff --git a/BlocksScreen/lib/ui/controlStackedWidget.ui b/BlocksScreen/lib/ui/controlStackedWidget.ui index 949bf57e..92475345 100644 --- a/BlocksScreen/lib/ui/controlStackedWidget.ui +++ b/BlocksScreen/lib/ui/controlStackedWidget.ui @@ -32,7 +32,7 @@ StackedWidget - 0 + 7 @@ -102,8 +102,8 @@ - - + + 0 @@ -146,8 +146,7 @@ - Motion -Control + Z-Tilt false @@ -159,12 +158,12 @@ Control menu_btn - :/motion/media/btn_icons/axis_maintenance.svg + :/z_levelling/media/btn_icons/bed_levelling.svg - - + + 0 @@ -207,8 +206,8 @@ Control - Nozzle -Calibration + Temp. +Control false @@ -220,12 +219,12 @@ Calibration menu_btn - :/z_levelling/media/btn_icons/bed_levelling.svg + :/temperature_related/media/btn_icons/temperature.svg - - + + 0 @@ -268,8 +267,8 @@ Calibration - Temp. -Control + Nozzle +Calibration false @@ -281,12 +280,12 @@ Control menu_btn - :/temperature_related/media/btn_icons/temperature.svg + :/z_levelling/media/btn_icons/bed_levelling.svg - - + + 0 @@ -329,7 +328,8 @@ Control - Z-Tilt + Motion +Control false @@ -341,12 +341,12 @@ Control menu_btn - :/z_levelling/media/btn_icons/bed_levelling.svg + :/motion/media/btn_icons/axis_maintenance.svg - + 0 @@ -389,8 +389,7 @@ Control - Swap -Print Core + Fans false @@ -402,12 +401,12 @@ Print Core menu_btn - :/extruder_related/media/btn_icons/switch_print_core.svg + :/temperature_related/media/btn_icons/fan.svg - + 0 @@ -426,6 +425,45 @@ Print Core 80 + + + Momcake + 19 + false + PreferAntialias + + + + false + + + true + + + Qt::NoContextMenu + + + Qt::LeftToRight + + + + + + Swap +Print Core + + + false + + + true + + + menu_btn + + + :/extruder_related/media/btn_icons/switch_print_core.svg + @@ -5545,6 +5583,166 @@ Home + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 24 + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 60 + 20 + + + + + + + + + 0 + 0 + + + + + Momcake + 24 + + + + background: transparent; color: white; + + + Fans + + + Qt::AlignCenter + + + title_text + + + + + + + + 0 + 0 + + + + + 60 + 60 + + + + + 60 + 60 + + + + + Momcake + 20 + false + PreferAntialias + + + + false + + + true + + + Qt::NoContextMenu + + + Qt::LeftToRight + + + + + + Back + + + false + + + true + + + menu_btn + + + icon + + + :/ui/media/btn_icons/back.svg + + + + + + + + + Qt::Vertical + + + + 20 + 111 + + + + + + + + + + + Qt::Vertical + + + + 20 + 111 + + + + + + @@ -5557,6 +5755,11 @@ Home QPushButton
lib.utils.icon_button
+ + GroupButton + QPushButton +
lib.utils.group_button
+
DisplayButton QPushButton @@ -5567,11 +5770,6 @@ Home QLabel
lib.utils.blocks_label
- - GroupButton - QPushButton -
lib.utils.group_button
-
@@ -5583,9 +5781,9 @@ Home + + - - diff --git a/BlocksScreen/lib/ui/controlStackedWidget_ui.py b/BlocksScreen/lib/ui/controlStackedWidget_ui.py index c4d67b04..88817b36 100644 --- a/BlocksScreen/lib/ui/controlStackedWidget_ui.py +++ b/BlocksScreen/lib/ui/controlStackedWidget_ui.py @@ -1,4 +1,4 @@ -# Form implementation generated from reading ui file '/home/levi/main/BlocksScreen/BlocksScreen/lib/ui/controlStackedWidget.ui' +# Form implementation generated from reading ui file '/home/levi/BlocksScreen/BlocksScreen/lib/ui/controlStackedWidget.ui' # # Created by: PyQt6 UI code generator 6.7.1 # @@ -54,30 +54,54 @@ def setupUi(self, controlStackedWidget): self.verticalLayout.addLayout(self.cp_header_layout) self.cp_content_layout = QtWidgets.QGridLayout() self.cp_content_layout.setObjectName("cp_content_layout") - self.cp_motion_btn = BlocksCustomButton(parent=self.control_page) + self.cp_z_tilt_btn = BlocksCustomButton(parent=self.control_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cp_motion_btn.sizePolicy().hasHeightForWidth()) - self.cp_motion_btn.setSizePolicy(sizePolicy) - self.cp_motion_btn.setMinimumSize(QtCore.QSize(10, 80)) - self.cp_motion_btn.setMaximumSize(QtCore.QSize(250, 80)) + sizePolicy.setHeightForWidth(self.cp_z_tilt_btn.sizePolicy().hasHeightForWidth()) + self.cp_z_tilt_btn.setSizePolicy(sizePolicy) + self.cp_z_tilt_btn.setMinimumSize(QtCore.QSize(10, 80)) + self.cp_z_tilt_btn.setMaximumSize(QtCore.QSize(250, 80)) font = QtGui.QFont() font.setFamily("Momcake") font.setPointSize(19) font.setItalic(False) font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.cp_motion_btn.setFont(font) - self.cp_motion_btn.setMouseTracking(False) - self.cp_motion_btn.setTabletTracking(True) - self.cp_motion_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.cp_motion_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.cp_motion_btn.setStyleSheet("") - self.cp_motion_btn.setAutoDefault(False) - self.cp_motion_btn.setFlat(True) - self.cp_motion_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/motion/media/btn_icons/axis_maintenance.svg")) - self.cp_motion_btn.setObjectName("cp_motion_btn") - self.cp_content_layout.addWidget(self.cp_motion_btn, 0, 0, 1, 1) + self.cp_z_tilt_btn.setFont(font) + self.cp_z_tilt_btn.setMouseTracking(False) + self.cp_z_tilt_btn.setTabletTracking(True) + self.cp_z_tilt_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.cp_z_tilt_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.cp_z_tilt_btn.setStyleSheet("") + self.cp_z_tilt_btn.setAutoDefault(False) + self.cp_z_tilt_btn.setFlat(True) + self.cp_z_tilt_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/z_levelling/media/btn_icons/bed_levelling.svg")) + self.cp_z_tilt_btn.setObjectName("cp_z_tilt_btn") + self.cp_content_layout.addWidget(self.cp_z_tilt_btn, 1, 1, 1, 1) + self.cp_temperature_btn = BlocksCustomButton(parent=self.control_page) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.cp_temperature_btn.sizePolicy().hasHeightForWidth()) + self.cp_temperature_btn.setSizePolicy(sizePolicy) + self.cp_temperature_btn.setMinimumSize(QtCore.QSize(10, 80)) + self.cp_temperature_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setFamily("Momcake") + font.setPointSize(19) + font.setItalic(False) + font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) + self.cp_temperature_btn.setFont(font) + self.cp_temperature_btn.setMouseTracking(False) + self.cp_temperature_btn.setTabletTracking(True) + self.cp_temperature_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.cp_temperature_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.cp_temperature_btn.setStyleSheet("") + self.cp_temperature_btn.setAutoDefault(False) + self.cp_temperature_btn.setFlat(True) + self.cp_temperature_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/temperature_related/media/btn_icons/temperature.svg")) + self.cp_temperature_btn.setObjectName("cp_temperature_btn") + self.cp_content_layout.addWidget(self.cp_temperature_btn, 0, 1, 1, 1) self.cp_nozzles_calibration_btn = BlocksCustomButton(parent=self.control_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -102,54 +126,54 @@ def setupUi(self, controlStackedWidget): self.cp_nozzles_calibration_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/z_levelling/media/btn_icons/bed_levelling.svg")) self.cp_nozzles_calibration_btn.setObjectName("cp_nozzles_calibration_btn") self.cp_content_layout.addWidget(self.cp_nozzles_calibration_btn, 1, 0, 1, 1) - self.cp_temperature_btn = BlocksCustomButton(parent=self.control_page) + self.cp_motion_btn = BlocksCustomButton(parent=self.control_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cp_temperature_btn.sizePolicy().hasHeightForWidth()) - self.cp_temperature_btn.setSizePolicy(sizePolicy) - self.cp_temperature_btn.setMinimumSize(QtCore.QSize(10, 80)) - self.cp_temperature_btn.setMaximumSize(QtCore.QSize(250, 80)) + sizePolicy.setHeightForWidth(self.cp_motion_btn.sizePolicy().hasHeightForWidth()) + self.cp_motion_btn.setSizePolicy(sizePolicy) + self.cp_motion_btn.setMinimumSize(QtCore.QSize(10, 80)) + self.cp_motion_btn.setMaximumSize(QtCore.QSize(250, 80)) font = QtGui.QFont() font.setFamily("Momcake") font.setPointSize(19) font.setItalic(False) font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.cp_temperature_btn.setFont(font) - self.cp_temperature_btn.setMouseTracking(False) - self.cp_temperature_btn.setTabletTracking(True) - self.cp_temperature_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.cp_temperature_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.cp_temperature_btn.setStyleSheet("") - self.cp_temperature_btn.setAutoDefault(False) - self.cp_temperature_btn.setFlat(True) - self.cp_temperature_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/temperature_related/media/btn_icons/temperature.svg")) - self.cp_temperature_btn.setObjectName("cp_temperature_btn") - self.cp_content_layout.addWidget(self.cp_temperature_btn, 0, 1, 1, 1) - self.cp_z_tilt_btn = BlocksCustomButton(parent=self.control_page) + self.cp_motion_btn.setFont(font) + self.cp_motion_btn.setMouseTracking(False) + self.cp_motion_btn.setTabletTracking(True) + self.cp_motion_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.cp_motion_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.cp_motion_btn.setStyleSheet("") + self.cp_motion_btn.setAutoDefault(False) + self.cp_motion_btn.setFlat(True) + self.cp_motion_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/motion/media/btn_icons/axis_maintenance.svg")) + self.cp_motion_btn.setObjectName("cp_motion_btn") + self.cp_content_layout.addWidget(self.cp_motion_btn, 0, 0, 1, 1) + self.cp_fans_btn = BlocksCustomButton(parent=self.control_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cp_z_tilt_btn.sizePolicy().hasHeightForWidth()) - self.cp_z_tilt_btn.setSizePolicy(sizePolicy) - self.cp_z_tilt_btn.setMinimumSize(QtCore.QSize(10, 80)) - self.cp_z_tilt_btn.setMaximumSize(QtCore.QSize(250, 80)) + sizePolicy.setHeightForWidth(self.cp_fans_btn.sizePolicy().hasHeightForWidth()) + self.cp_fans_btn.setSizePolicy(sizePolicy) + self.cp_fans_btn.setMinimumSize(QtCore.QSize(10, 80)) + self.cp_fans_btn.setMaximumSize(QtCore.QSize(250, 80)) font = QtGui.QFont() font.setFamily("Momcake") font.setPointSize(19) font.setItalic(False) font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.cp_z_tilt_btn.setFont(font) - self.cp_z_tilt_btn.setMouseTracking(False) - self.cp_z_tilt_btn.setTabletTracking(True) - self.cp_z_tilt_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.cp_z_tilt_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.cp_z_tilt_btn.setStyleSheet("") - self.cp_z_tilt_btn.setAutoDefault(False) - self.cp_z_tilt_btn.setFlat(True) - self.cp_z_tilt_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/z_levelling/media/btn_icons/bed_levelling.svg")) - self.cp_z_tilt_btn.setObjectName("cp_z_tilt_btn") - self.cp_content_layout.addWidget(self.cp_z_tilt_btn, 1, 1, 1, 1) + self.cp_fans_btn.setFont(font) + self.cp_fans_btn.setMouseTracking(False) + self.cp_fans_btn.setTabletTracking(True) + self.cp_fans_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.cp_fans_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.cp_fans_btn.setStyleSheet("") + self.cp_fans_btn.setAutoDefault(False) + self.cp_fans_btn.setFlat(True) + self.cp_fans_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/temperature_related/media/btn_icons/fan.svg")) + self.cp_fans_btn.setObjectName("cp_fans_btn") + self.cp_content_layout.addWidget(self.cp_fans_btn, 2, 0, 1, 1) self.cp_switch_print_core_btn = BlocksCustomButton(parent=self.control_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -173,17 +197,7 @@ def setupUi(self, controlStackedWidget): self.cp_switch_print_core_btn.setFlat(True) self.cp_switch_print_core_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/extruder_related/media/btn_icons/switch_print_core.svg")) self.cp_switch_print_core_btn.setObjectName("cp_switch_print_core_btn") - self.cp_content_layout.addWidget(self.cp_switch_print_core_btn, 2, 0, 1, 1) - self.blank_2 = QtWidgets.QWidget(parent=self.control_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.blank_2.sizePolicy().hasHeightForWidth()) - self.blank_2.setSizePolicy(sizePolicy) - self.blank_2.setMinimumSize(QtCore.QSize(10, 80)) - self.blank_2.setMaximumSize(QtCore.QSize(250, 80)) - self.blank_2.setObjectName("blank_2") - self.cp_content_layout.addWidget(self.blank_2, 2, 1, 1, 1) + self.cp_content_layout.addWidget(self.cp_switch_print_core_btn, 2, 1, 1, 1) self.verticalLayout.addLayout(self.cp_content_layout) controlStackedWidget.addWidget(self.control_page) self.motion_page = QtWidgets.QWidget() @@ -1981,9 +1995,66 @@ def setupUi(self, controlStackedWidget): self.printer_setting_content_layout.setContentsMargins(0, 0, 0, 0) self.printer_setting_content_layout.setObjectName("printer_setting_content_layout") controlStackedWidget.addWidget(self.printer_settings_page) + self.fans_page = QtWidgets.QWidget() + self.fans_page.setObjectName("fans_page") + self.verticalLayout_5 = QtWidgets.QVBoxLayout(self.fans_page) + self.verticalLayout_5.setObjectName("verticalLayout_5") + spacerItem9 = QtWidgets.QSpacerItem(20, 24, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.verticalLayout_5.addItem(spacerItem9) + self.fans_header_layout = QtWidgets.QHBoxLayout() + self.fans_header_layout.setObjectName("fans_header_layout") + spacerItem10 = QtWidgets.QSpacerItem(60, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.fans_header_layout.addItem(spacerItem10) + self.fans_title_label = QtWidgets.QLabel(parent=self.fans_page) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.fans_title_label.sizePolicy().hasHeightForWidth()) + self.fans_title_label.setSizePolicy(sizePolicy) + font = QtGui.QFont() + font.setFamily("Momcake") + font.setPointSize(24) + self.fans_title_label.setFont(font) + self.fans_title_label.setStyleSheet("background: transparent; color: white;") + self.fans_title_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.fans_title_label.setObjectName("fans_title_label") + self.fans_header_layout.addWidget(self.fans_title_label) + self.fans_back_btn = IconButton(parent=self.fans_page) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.fans_back_btn.sizePolicy().hasHeightForWidth()) + self.fans_back_btn.setSizePolicy(sizePolicy) + self.fans_back_btn.setMinimumSize(QtCore.QSize(60, 60)) + self.fans_back_btn.setMaximumSize(QtCore.QSize(60, 60)) + font = QtGui.QFont() + font.setFamily("Momcake") + font.setPointSize(20) + font.setItalic(False) + font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) + self.fans_back_btn.setFont(font) + self.fans_back_btn.setMouseTracking(False) + self.fans_back_btn.setTabletTracking(True) + self.fans_back_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.fans_back_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.fans_back_btn.setStyleSheet("") + self.fans_back_btn.setAutoDefault(False) + self.fans_back_btn.setFlat(True) + self.fans_back_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) + self.fans_back_btn.setObjectName("fans_back_btn") + self.fans_header_layout.addWidget(self.fans_back_btn) + self.verticalLayout_5.addLayout(self.fans_header_layout) + spacerItem11 = QtWidgets.QSpacerItem(20, 111, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) + self.verticalLayout_5.addItem(spacerItem11) + self.fans_content_layout = QtWidgets.QHBoxLayout() + self.fans_content_layout.setObjectName("fans_content_layout") + self.verticalLayout_5.addLayout(self.fans_content_layout) + spacerItem12 = QtWidgets.QSpacerItem(20, 111, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) + self.verticalLayout_5.addItem(spacerItem12) + controlStackedWidget.addWidget(self.fans_page) self.retranslateUi(controlStackedWidget) - controlStackedWidget.setCurrentIndex(0) + controlStackedWidget.setCurrentIndex(7) QtCore.QMetaObject.connectSlotsByName(controlStackedWidget) def retranslateUi(self, controlStackedWidget): @@ -1991,17 +2062,19 @@ def retranslateUi(self, controlStackedWidget): controlStackedWidget.setWindowTitle(_translate("controlStackedWidget", "StackedWidget")) self.cp_header_title.setText(_translate("controlStackedWidget", "Control")) self.cp_header_title.setProperty("class", _translate("controlStackedWidget", "title_text")) - self.cp_motion_btn.setText(_translate("controlStackedWidget", "Motion\n" + self.cp_z_tilt_btn.setText(_translate("controlStackedWidget", "Z-Tilt")) + self.cp_z_tilt_btn.setProperty("class", _translate("controlStackedWidget", "menu_btn")) + self.cp_temperature_btn.setText(_translate("controlStackedWidget", "Temp.\n" "Control")) - self.cp_motion_btn.setProperty("class", _translate("controlStackedWidget", "menu_btn")) + self.cp_temperature_btn.setProperty("class", _translate("controlStackedWidget", "menu_btn")) self.cp_nozzles_calibration_btn.setText(_translate("controlStackedWidget", "Nozzle\n" "Calibration")) self.cp_nozzles_calibration_btn.setProperty("class", _translate("controlStackedWidget", "menu_btn")) - self.cp_temperature_btn.setText(_translate("controlStackedWidget", "Temp.\n" + self.cp_motion_btn.setText(_translate("controlStackedWidget", "Motion\n" "Control")) - self.cp_temperature_btn.setProperty("class", _translate("controlStackedWidget", "menu_btn")) - self.cp_z_tilt_btn.setText(_translate("controlStackedWidget", "Z-Tilt")) - self.cp_z_tilt_btn.setProperty("class", _translate("controlStackedWidget", "menu_btn")) + self.cp_motion_btn.setProperty("class", _translate("controlStackedWidget", "menu_btn")) + self.cp_fans_btn.setText(_translate("controlStackedWidget", "Fans")) + self.cp_fans_btn.setProperty("class", _translate("controlStackedWidget", "menu_btn")) self.cp_switch_print_core_btn.setText(_translate("controlStackedWidget", "Swap\n" "Print Core")) self.cp_switch_print_core_btn.setProperty("class", _translate("controlStackedWidget", "menu_btn")) @@ -2109,6 +2182,11 @@ def retranslateUi(self, controlStackedWidget): self.printer_settings_back_btn.setText(_translate("controlStackedWidget", "Back")) self.printer_settings_back_btn.setProperty("class", _translate("controlStackedWidget", "menu_btn")) self.printer_settings_back_btn.setProperty("button_type", _translate("controlStackedWidget", "icon")) + self.fans_title_label.setText(_translate("controlStackedWidget", "Fans")) + self.fans_title_label.setProperty("class", _translate("controlStackedWidget", "title_text")) + self.fans_back_btn.setText(_translate("controlStackedWidget", "Back")) + self.fans_back_btn.setProperty("class", _translate("controlStackedWidget", "menu_btn")) + self.fans_back_btn.setProperty("button_type", _translate("controlStackedWidget", "icon")) from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_label import BlocksLabel from lib.utils.display_button import DisplayButton diff --git a/BlocksScreen/lib/utils/blocks_slider.py b/BlocksScreen/lib/utils/blocks_slider.py index ee084a0a..f122589a 100644 --- a/BlocksScreen/lib/utils/blocks_slider.py +++ b/BlocksScreen/lib/utils/blocks_slider.py @@ -14,6 +14,7 @@ def __init__(self, parent) -> None: self.setTickInterval(20) self.setMinimum(0) self.setMaximum(100) + self.setPageStep(0) def mousePressEvent(self, ev: QtGui.QMouseEvent) -> None: """Re-implemented method, Handle mouse press events""" From 70e2c43a1d6cea14a642cd28ac8528e0968907d0 Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Fri, 12 Dec 2025 14:43:04 +0000 Subject: [PATCH 13/70] Fix issues intruduced in Bugfix label overlap #121 (#129) * ADD: added fans page * add: added fans into fans page * ADD: added line to fans page * Refacotr: changed layout type * UPD: updated options card to have 2 text . added cards into controltab fans * Rev: removed line from fans page * Refactor: refactor on_fan_object_update * Refactor: Ran ruff formatter * Fix incorrect method name * Fix incorrect method name * bugfix: option card clicklable area * Ran: ruff formatter * Bugfix: names being wrong and slider spaming gcodes on change * ADD: color degrade when ON/OFF (#120) * Del: deleted some prints * Bugfix: fan not updating when slider updates * Bugfix: label size * Fix integration problems #121 fix * Refactor variable into local method variable --------- Signed-off-by: Hugo Costa Co-authored-by: Roberto Martins Co-authored-by: Roberto --- .../lib/panels/widgets/optionCardWidget.py | 9 ++-- BlocksScreen/lib/utils/blocks_label.py | 45 +++++++++++++++---- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/BlocksScreen/lib/panels/widgets/optionCardWidget.py b/BlocksScreen/lib/panels/widgets/optionCardWidget.py index 789a9885..259179c2 100644 --- a/BlocksScreen/lib/panels/widgets/optionCardWidget.py +++ b/BlocksScreen/lib/panels/widgets/optionCardWidget.py @@ -144,8 +144,8 @@ def setMode(self, double_mode: bool = False): def paintEvent(self, a0: QtGui.QPaintEvent) -> None: """Re-implemented method, paint widget""" # Rounded background edges - self.background_path = QtGui.QPainterPath() - self.background_path.addRoundedRect( + background_path = QtGui.QPainterPath() + background_path.addRoundedRect( self.rect().toRectF(), 20.0, 20.0, QtCore.Qt.SizeMode.AbsoluteSize ) @@ -167,7 +167,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: painter.setRenderHint(painter.RenderHint.Antialiasing) painter.setRenderHint(painter.RenderHint.SmoothPixmapTransform) painter.setRenderHint(painter.RenderHint.LosslessImageRendering) - painter.fillPath(self.background_path, bg_color) + painter.fillPath(background_path, bg_color) if self.underMouse(): _pen = QtGui.QPen() _pen.setStyle(QtCore.Qt.PenStyle.SolidLine) @@ -193,7 +193,7 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: _gradient.setColorAt(1, _color3) painter.setBrush(_gradient) painter.setPen(QtCore.Qt.PenStyle.NoPen) - painter.fillPath(self.background_path, painter.brush()) + painter.fillPath(background_path, painter.brush()) painter.end() @@ -216,6 +216,7 @@ def _setupUi(self, option_card): self.option_icon = IconButton(parent=option_card) self.option_icon.setMinimumSize(QtCore.QSize(200, 150)) self.option_icon.setObjectName("option_icon") + self.option_icon.setScaledContents(True) _button_font = QtGui.QFont() _button_font.setBold(True) _button_font.setPointSize(20) diff --git a/BlocksScreen/lib/utils/blocks_label.py b/BlocksScreen/lib/utils/blocks_label.py index 7e64d805..1aaa173e 100644 --- a/BlocksScreen/lib/utils/blocks_label.py +++ b/BlocksScreen/lib/utils/blocks_label.py @@ -28,19 +28,17 @@ def __init__(self, parent: QtWidgets.QWidget = None, *args, **kwargs): QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, ) - self._glow_color: QtGui.QColor = QtGui.QColor("#E95757") self._animation_speed: int = 300 self.glow_animation = QtCore.QPropertyAnimation(self, b"glow_color") self.glow_animation.setEasingCurve(QtCore.QEasingCurve().Type.InOutQuart) self.glow_animation.setDuration(self.animation_speed) - self.glow_animation.finished.connect(self.change_glow_direction) self.glow_animation.finished.connect(self.repaint) - self.total_scroll_width: float = 0.0 self.text_width: float = 0.0 self.label_width: float = 0.0 + self.icon_margin: int = 5 self.first_run = True def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: @@ -203,17 +201,46 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: qp.fillRect(rect, self._background_color) if self.icon_pixmap: - icon_rect = QtCore.QRectF(0, 0, self.height(), self.height()) - scaled = self.icon_pixmap.scaled( + icon_rect = QtCore.QRectF( + 0.0 + self.icon_margin, + 0.0 + self.icon_margin, + self.width() - self.icon_margin, + self.height() - self.icon_margin, + ) + _icon_scaled = self.icon_pixmap.scaled( icon_rect.size().toSize(), QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.TransformationMode.SmoothTransformation, ) - qp.drawPixmap(icon_rect.toRect(), scaled) + scaled_width = _icon_scaled.width() + scaled_height = _icon_scaled.height() + adjusted_x = (icon_rect.width() - scaled_width) // 2.0 + adjusted_y = (icon_rect.height() - scaled_height) // 2.0 + adjusted_icon = QtCore.QRectF( + icon_rect.x() + adjusted_x, + icon_rect.y() + adjusted_y, + scaled_width, + scaled_height, + ) + qp.drawPixmap(adjusted_icon, _icon_scaled, _icon_scaled.rect().toRectF()) if self.glow_animation.state() == self.glow_animation.State.Running: - path = QtGui.QPainterPath() - path.addRoundedRect(QtCore.QRectF(rect), 10, 10) - qp.fillPath(path, self.glow_color) + big_rect = QtGui.QPainterPath() + rect = self.contentsRect().toRectF() + big_rect.addRoundedRect(rect, 10.0, 10.0, QtCore.Qt.SizeMode.AbsoluteSize) + sub_rect = QtCore.QRectF( + (rect.width() - rect.width() * 0.99) / 2, + (rect.height() - rect.height() * 0.85) / 2, + rect.width() * 0.99, + rect.height() * 0.85, + ) + sub_path = QtGui.QPainterPath() + sub_path.addRoundedRect( + sub_rect, 10.0, 10.0, QtCore.Qt.SizeMode.AbsoluteSize + ) + subtracted = big_rect.subtracted(sub_path) + qp.setCompositionMode(qp.CompositionMode.CompositionMode_SourceOver) + subtracted.setFillRule(QtCore.Qt.FillRule.OddEvenFill) + qp.fillPath(subtracted, self.glow_color) if self._text: text_option = QtGui.QTextOption(self.alignment()) text_option.setWrapMode(QtGui.QTextOption.WrapMode.NoWrap) From f703fc68d86f333df4d8ade88930e4bfd1931b9f Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Mon, 15 Dec 2025 11:21:50 +0000 Subject: [PATCH 14/70] Fix Merge problems introduced on the previous pull requests (#131) * Add 'flat' visual property, seperate methods to their own methods * Delete attribute set, BlocksLabel does not have that attribute * Refactor: reorder methods * Fix missing slot for klippy signal, now reacts to klippy connected signal * Delete: touch handlers, will be implemented in the future --- .../lib/panels/widgets/connectionPage.py | 13 +- .../lib/panels/widgets/optionCardWidget.py | 1 - BlocksScreen/lib/utils/blocks_button.py | 188 ++++++++---------- 3 files changed, 98 insertions(+), 104 deletions(-) diff --git a/BlocksScreen/lib/panels/widgets/connectionPage.py b/BlocksScreen/lib/panels/widgets/connectionPage.py index 33bd0576..0b7e054d 100644 --- a/BlocksScreen/lib/panels/widgets/connectionPage.py +++ b/BlocksScreen/lib/panels/widgets/connectionPage.py @@ -43,7 +43,7 @@ def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket, /): self.restart_klipper_clicked.emit ) self.ws.connection_lost.connect(slot=self.show) - self.ws.klippy_connected_signal.connect(self.on_klippy_connection) + self.ws.klippy_connected_signal.connect(self.on_klippy_connected) self.ws.klippy_state_signal.connect(self.on_klippy_state) def show_panel(self, reason: str | None = None): @@ -55,6 +55,17 @@ def show_panel(self, reason: str | None = None): self.text_update() return False + @QtCore.pyqtSlot(bool, name="on_klippy_connected") + def on_klippy_connection(self, connected: bool): + """Handle klippy connection state""" + self._klippy_connection = connected + if not connected: + self.panel.connectionTextBox.setText("Klipper Disconnected") + if not self.isVisible(): + self.show() + else: + self.panel.connectionTextBox.setText("Klipper Connected") + @QtCore.pyqtSlot(str, name="on_klippy_state") def on_klippy_state(self, state: str): """Handle klippy state changes""" diff --git a/BlocksScreen/lib/panels/widgets/optionCardWidget.py b/BlocksScreen/lib/panels/widgets/optionCardWidget.py index 259179c2..8c79ee68 100644 --- a/BlocksScreen/lib/panels/widgets/optionCardWidget.py +++ b/BlocksScreen/lib/panels/widgets/optionCardWidget.py @@ -216,7 +216,6 @@ def _setupUi(self, option_card): self.option_icon = IconButton(parent=option_card) self.option_icon.setMinimumSize(QtCore.QSize(200, 150)) self.option_icon.setObjectName("option_icon") - self.option_icon.setScaledContents(True) _button_font = QtGui.QFont() _button_font.setBold(True) _button_font.setPointSize(20) diff --git a/BlocksScreen/lib/utils/blocks_button.py b/BlocksScreen/lib/utils/blocks_button.py index 141ee1ec..292b5125 100644 --- a/BlocksScreen/lib/utils/blocks_button.py +++ b/BlocksScreen/lib/utils/blocks_button.py @@ -17,15 +17,15 @@ class ButtonColors(enum.Enum): class BlocksCustomButton(QtWidgets.QAbstractButton): def __init__( self, - parent: QtWidgets.QWidget = None, + parent: QtWidgets.QWidget | None = None, ) -> None: if parent: - super(BlocksCustomButton, self).__init__(parent) + super().__init__(parent) else: - super(BlocksCustomButton, self).__init__() - + super().__init__() self.icon_pixmap: QtGui.QPixmap = QtGui.QPixmap() self._icon_rect: QtCore.QRectF = QtCore.QRectF() + self._is_flat: bool = False self.button_background = None self.button_ellipse = None self._text: str = "" @@ -38,12 +38,11 @@ def setShowNotification(self, show: bool) -> None: """Set notification on button""" if self._show_notification != show: self._show_notification = show - self.repaint() self.update() @property def name(self): - """Button name""" + """Widget name""" return self._name @name.setter @@ -59,28 +58,53 @@ def setText(self, text: str) -> None: """Set button text""" self._text = text self.update() - return def setPixmap(self, pixmap: QtGui.QPixmap) -> None: """Set button pixmap""" self.icon_pixmap = pixmap - self.repaint() + self.update() def mousePressEvent(self, e: QtGui.QMouseEvent) -> None: """Handle mouse press events""" if not self.isEnabled(): e.ignore() return - - if self.button_background is not None: + if self.button_background: pos_f = QtCore.QPointF(e.pos()) if self.button_background.contains(pos_f): super().mousePressEvent(e) return - else: - e.ignore() - return - return super().mousePressEvent(e) + e.ignore() + return + super().mousePressEvent(e) + + def setFlat(self, flat) -> None: + """Enable 'flat' appearance to the button""" + if self._is_flat != flat: + self._is_flat = flat + self.update() # Schedule repaint + + def isFlat(self) -> bool: + """Get flat property + + Returns: + bool: Button has 'flat' appearance enabled + """ + return self._is_flat + + def setAutoDefault(self, _): + """Disable auto default behavior""" + return + + def setProperty(self, name: str, value: typing.Any): + """Set widget properties""" + if name == "icon_pixmap": + self.icon_pixmap = value + if name == "name": + self._name = name + if name == "text_color": + self.text_color = QtGui.QColor(value) + self.update() def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): """Re-implemented method, paint widget""" @@ -88,13 +112,25 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): painter.setRenderHint(painter.RenderHint.Antialiasing, True) painter.setRenderHint(painter.RenderHint.SmoothPixmapTransform, True) painter.setRenderHint(painter.RenderHint.LosslessImageRendering, True) - _rect = self.rect() _style = self.style() - - if _style is None or _rect is None: + if not _style or not _rect: return - + # Flat button control + opt = QtWidgets.QStyleOptionButton() + draw_frame = ( + not self._is_flat + or self.underMouse() + or opt.state & QtWidgets.QStyle.StateFlag.State_Sunken + ) + if draw_frame: + _style.drawControl( + QtWidgets.QStyle.ControlElement.CE_PushButtonLabel, opt, painter, self + ) + _style.drawControl( + QtWidgets.QStyle.ControlElement.CE_PushButtonLabel, opt, painter, self + ) + self.setStyle(_style) # Determine background and text colors based on state if not self.isEnabled(): bg_color_tuple = ButtonColors.DISABLED_BG.value @@ -135,7 +171,6 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): painter.setPen(QtCore.Qt.PenStyle.NoPen) painter.setBrush(bg_color) painter.fillPath(self.button_background, bg_color) - _parent_rect = self.button_ellipse.toRect() _icon_rect = QtCore.QRectF( _parent_rect.left() * 2.8, @@ -149,7 +184,6 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): QtCore.Qt.AspectRatioMode.KeepAspectRatio, QtCore.Qt.TransformationMode.SmoothTransformation, ) - scaled_width = _icon_scaled.width() scaled_height = _icon_scaled.height() adjusted_x = (_icon_rect.width() - scaled_width) / 2.0 @@ -160,22 +194,17 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): scaled_width, scaled_height, ) - tinted_icon_pixmap = QtGui.QPixmap(_icon_scaled.size()) tinted_icon_pixmap.fill(QtCore.Qt.GlobalColor.transparent) - if not self.isEnabled(): tinted_icon_pixmap = QtGui.QPixmap(_icon_scaled.size()) tinted_icon_pixmap.fill(QtCore.Qt.GlobalColor.transparent) - icon_painter = QtGui.QPainter(tinted_icon_pixmap) icon_painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) icon_painter.setRenderHint( QtGui.QPainter.RenderHint.SmoothPixmapTransform ) - icon_painter.drawPixmap(0, 0, _icon_scaled) - icon_painter.setCompositionMode( QtGui.QPainter.CompositionMode.CompositionMode_SourceAtop ) @@ -184,91 +213,46 @@ def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): ) icon_painter.fillRect(tinted_icon_pixmap.rect(), tint) icon_painter.end() - final_pixmap = tinted_icon_pixmap else: final_pixmap = _icon_scaled - destination_point = adjusted_icon_rect.toRect().topLeft() painter.drawPixmap(destination_point, final_pixmap) - if self.text(): - font_metrics = self.fontMetrics() - self.text_width = font_metrics.horizontalAdvance(self._text) - self.label_width = self.contentsRect().width() - - # _start_text_position = int(self.button_ellipse.width()) - _text_rect = _rect - - _text_rect2 = _rect - _text_rect2.setWidth(self.width() - int(self.button_ellipse.width())) - _text_rect2.setLeft(int(self.button_ellipse.width())) - - _text_rect.setWidth(self.width() - int(self.button_ellipse.width())) - _text_rect.setLeft(int(self.button_ellipse.width())) - _pen = painter.pen() - _pen.setStyle(QtCore.Qt.PenStyle.SolidLine) - _pen.setWidth(1) - _pen.setColor(current_text_color) - painter.setPen(_pen) - - # if self.text_width < _text_rect2.width()*0.6: - _text_rect.setWidth(self.width() - int(self.button_ellipse.width() * 1.4)) - _text_rect.setLeft(int(self.button_ellipse.width())) - - painter.drawText( - _text_rect, - QtCore.Qt.TextFlag.TextShowMnemonic - | QtCore.Qt.AlignmentFlag.AlignCenter, - str(self.text()), - ) - # else: - # _text_rect.setLeft(_start_text_position + margin) - - # _text_rect.setWidth(self.width() - int(self.button_ellipse.width())) - - # painter.drawText( - # _text_rect, - # QtCore.Qt.TextFlag.TextShowMnemonic - # | QtCore.Qt.AlignmentFlag.AlignLeft - # | QtCore.Qt.AlignmentFlag.AlignVCenter, - # str(self.text()), - # ) - painter.setPen(QtCore.Qt.PenStyle.NoPen) - + self._paint_text(painter, _rect, current_text_color) if self._show_notification: - dot_diameter = self.height() * 0.4 - dot_x = self.width() - dot_diameter - notification_color = QtGui.QColor(*ButtonColors.NOTIFICATION_DOT.value) - painter.setBrush(notification_color) - painter.setPen(QtCore.Qt.PenStyle.NoPen) - dot_rect = QtCore.QRectF(dot_x, 0, dot_diameter, dot_diameter) - painter.drawEllipse(dot_rect) + self._paint_notification(painter) painter.end() - def setProperty(self, name: str, value: typing.Any): - """Set widget properties""" - if name == "icon_pixmap": - self.icon_pixmap = value - elif name == "name": - self._name = name - elif name == "text_color": - self.text_color = QtGui.QColor(value) - self.update() + def _paint_text( + self, painter: QtGui.QPainter, rect: QtCore.QRect, text_color: QtGui.QColor + ) -> None: + _text_rect = rect + _text_rect2 = rect + _text_rect2.setWidth(self.width() - int(self.button_ellipse.width())) + _text_rect2.setLeft(int(self.button_ellipse.width())) + _text_rect.setWidth(self.width() - int(self.button_ellipse.width())) + _text_rect.setLeft(int(self.button_ellipse.width())) + _pen = painter.pen() + _pen.setStyle(QtCore.Qt.PenStyle.SolidLine) + _pen.setWidth(1) + _pen.setColor(text_color) + painter.setPen(_pen) + _text_rect.setWidth(self.width() - int(self.button_ellipse.width() * 1.4)) + _text_rect.setLeft(int(self.button_ellipse.width())) + painter.drawText( + _text_rect, + QtCore.Qt.TextFlag.TextShowMnemonic | QtCore.Qt.AlignmentFlag.AlignCenter, + str(self.text()), + ) + painter.setPen(QtCore.Qt.PenStyle.NoPen) - def event(self, e: QtCore.QEvent) -> bool: - """Re-implemented method, filter events""" - if e.type() == QtCore.QEvent.Type.TouchBegin: - self.handleTouchBegin(e) - return False - elif e.type() == QtCore.QEvent.Type.TouchUpdate: - self.handleTouchUpdate(e) - return False - elif e.type() == QtCore.QEvent.Type.TouchEnd: - self.handleTouchEnd(e) - return False - elif e.type() == QtCore.QEvent.Type.TouchCancel: - self.handleTouchCancel(e) - return False - return super().event(e) + def _paint_notification(self, painter: QtGui.QPainter) -> None: + dot_diameter = self.height() * 0.4 + dot_x = self.width() - dot_diameter + notification_color = QtGui.QColor(*ButtonColors.NOTIFICATION_DOT.value) + painter.setBrush(notification_color) + painter.setPen(QtCore.Qt.PenStyle.NoPen) + dot_rect = QtCore.QRectF(dot_x, 0, dot_diameter, dot_diameter) + painter.drawEllipse(dot_rect) From ca9b7f0a9833a644ee4c8efa8b6a9252e59ad0de Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Mon, 15 Dec 2025 15:55:30 +0000 Subject: [PATCH 15/70] Added standard pull request template (#133) --- .../pull_request_template.md | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE/pull_request_template.md diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md new file mode 100644 index 00000000..51c968a0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md @@ -0,0 +1,67 @@ +--- +name: Pull Request +about: Propose code changes +title: '' +labels: '' +assignees: '' + +--- + +# PR Checklist (Delete the section after checking everything) + +- [ ] Make sure you are requesting to PR **feature/bugfix/refactor branch**. +- [ ] Make sure you are making the pull request against the **dev** branch. +- [ ] Make sure you include labels to the PR +- [ ] Request a reviewer for the current PR +- [ ] After the PR is published, make sure you view the tests conducted by github actions, download artifacts and inspect them. Failure to pass the tests will result in a request for changes. + + + +# Description + +Select the type: +- [ ] Feature +- [ ] Bug fix +- [ ] Code refactor +- [ ] Documentation + +Include a summary of the changes made in this PR and which issue is fixed. + +If the current PR is related to a bug introduced in another PR please insert the reference of the previous PR with **#**. + + +Add a concise checklist of the implemented changes, as exemplified below. + +- Change 1. +- Change 2. +- Change 3. +- ... + + +**Depending on the type of change the current PR relates to, delete sections that are not applicable.** + + +# Motivation + +Include a detailed explanation for the current PR. Delete section if not applicable. + +# Tests +Please describe the conducted tests, and include logs, reports on the tests. + +Also include test configuration. + + +Delete section if not applicable. +# Screenshots + +Include screenshots of the changes. Mostly used for UI changes. + +Delete section if not applicable. + + +# Future work + +Emphasize future tasks and improvements tied directly to this pull request. +Include only non-breaking changes to this pull request + +Delete section if not applicable. \ No newline at end of file From 3b91e46ed43f1b49c23e481e8c23d1ae1d6a00ee Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Mon, 15 Dec 2025 16:09:06 +0000 Subject: [PATCH 16/70] Bugfix: fixed white dot on list_model.py (#130) Fix: white dot on widget lists built using **EntryDelegate** by deleting unused code --- BlocksScreen/lib/utils/list_model.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/BlocksScreen/lib/utils/list_model.py b/BlocksScreen/lib/utils/list_model.py index 24c9467f..2f4cbc3a 100644 --- a/BlocksScreen/lib/utils/list_model.py +++ b/BlocksScreen/lib/utils/list_model.py @@ -142,12 +142,6 @@ def paint( rect = option.rect rect.setHeight(item.height) button = QtWidgets.QStyleOptionButton() - style = QtWidgets.QApplication.style() - if not style: - return - style.drawControl( - QtWidgets.QStyle.ControlElement.CE_PushButton, button, painter - ) button.rect = rect painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) painter.setRenderHint(QtGui.QPainter.RenderHint.SmoothPixmapTransform, True) From 6f4c3e38563bd950a104b4f94b0d8a2899ad6d33 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Thu, 18 Dec 2025 18:59:20 +0000 Subject: [PATCH 17/70] Bugfix thumbnail not working (#123) * Refactor: reafactor how thumbnail are painted * Refactor: Ran ruff formatter * Refactor: resolved issues on the code * Adhere to snake casing * Adhere to snake casing * Return event handled * Requests files on websocket open Instead of only requesting the available files when Klippy is ready request them when websocket opens. Moonraker handles the files, so there is no need to wait until klippy is connected. This also ensures that when the screen connects with moonraker and klipper the information about all available print files are ready to be handled by the GUI. This stems from a problem loading the thumbnails. There were many instances of initializing the application mid print, when this happened a request for the available thumbnails is made, because the files are loaded after receiving the klippy ready event, the moment when asking about the current job thumbnail there are not files nor metadata loaded onto the GUI, so no thumbnail is fetched. Now with this change the same situation doesn't occur. * Change on_accept signal types. There is no need to add list type to the signal. The thumbnails were passed there before, we will load the print thumbnails when we get the metadata from the file. Passing the thumbnails here was unnecessary. * Add inner progress bar icon The progress bar fills a circumference until the print is complete, the inner part of the widget is blank and is suposed to have a thumbnail of the current print, before a QGraphicsView overlaped with the progress bar widget. Now we add the functionality to add a pixmap on the progress bar itself. It calculates the scaling for the pixmap to fill the inner part of the progress bar widget without overlaping anything. There are some bugs still, such as what happens when resizeEvent is emitted, the automatic scaling is not working properly yet. * Refactor Thumnail painting and widget building The solution provided by @Robert0Mart showed us that the thumbnail building needed some work, this commit intends to extend that effort to rewrite this feature. We will simplify thumbnail building, less widgets for this feature to work. We will also let the progress bar widget handle the small widget painting, and propagate the click to expand to a bigger thumbnail painting that fills the entire screen. * Final progress bar refactor, send signal on thumbnail click Refactored class method positions to add some structure to the class. Privatised some class variables, should add @property decorator with getter and setter for these variables since they are actually properties. Captured MousePressEvent of the widget and filtered the events only to those inside the inner rect of the widget, when triggered, send an event `thumbnail_clicked` to signal that the thumbnail was actually clicked. * Refactor `setValue`method Added type for argument, refactored method body readability * Added thumbnail expansion Added initial thumbnail expansion, it works, but it needs to be worked on. We need to hide all widgets so that the thumbnail doesn't have anything on the background while its expanded. this requires us to hide all widgets used one by one. it's not incorrect per-se but it's not the best. The thumbnail is loaded once when the thumbnail is received, so it just paints once and stays in memory until `.show()` and `.hide()` methods are called. Once the print stops for whatever reason. Since the widget is built when the thumbnail comes, the QGraphicsView and its childs are deleted, freeing memory. It'll be built again when a new print job is selected. * Added type for arguments, refactored `setValue` and `set_bar_color` methods. * Reduced conditional branching, added docstring to class Reduced branching on printer object handlers, Added simple docstring to the `jobStatusPage` class. * Conditional logic when thumbnail pixmaps are Null When the provided thumbnails are Null Pixmaps, the should not build the QGraphicsView Widget. Before , even when pixmaps were null, it whould be built, the user could click the progress widget and the scene whould expand, only without anything to show. Now the widget simply does not build anything when all pixmaps are Null. The click signal connections are now done inside the `_load_thumbnails` method, the progress widget pixmap is also set inside that method. Everything related to thumbnails (except the eventFilter method) is now handled inside the `_load_thumbnails` method. The next step, filtering the provided Null Pixmaps. Only load not Null pixmaps * Refactor filter null pixmaps during thumbnail loading Simplified thumbnail filtering. Now i can have any number of Null pixmaps, if no pixmap is usable, cancels thumbnail loading, if there is at least one usable pixmap. It'll load the thumbnails on the progress widget and on the QGraphicsView. This means that if only the smallest resolution is available (48x48) it'll paint with low resolution --------- Co-authored-by: Roberto Co-authored-by: Hugo Costa --- BlocksScreen/lib/files.py | 22 +- .../lib/panels/widgets/confirmPage.py | 4 +- .../lib/panels/widgets/jobStatusPage.py | 415 ++++++------------ BlocksScreen/lib/utils/blocks_progressbar.py | 200 ++++++--- 4 files changed, 303 insertions(+), 338 deletions(-) diff --git a/BlocksScreen/lib/files.py b/BlocksScreen/lib/files.py index f080e6d7..0eda561d 100644 --- a/BlocksScreen/lib/files.py +++ b/BlocksScreen/lib/files.py @@ -54,14 +54,12 @@ def __init__( @property def file_list(self): - """Get the current list of files""" + """Available files list""" return self.files def handle_message_received(self, method: str, data, params: dict) -> None: """Handle file related messages received by moonraker""" if "server.files.list" in method: - # Get all files in root and its subdirectories and - # request their metadata self.files.clear() self.files = data [self.request_file_metadata.emit(item["path"]) for item in self.files] @@ -73,8 +71,6 @@ def handle_message_received(self, method: str, data, params: dict) -> None: else: self.files_metadata[data["filename"]] = data elif "server.files.get_directory" in method: - # Emit here the files for each directory so the - # ui can build the files list self.directories = data.get("dirs", {}) self.files.clear() self.files = data.get("files", []) @@ -99,7 +95,7 @@ def on_request_fileinfo(self, filename: str) -> None: """Requests metadata for a file Args: - filename (str): file + filename (str): file to get metadata from """ _data: dict = { "thumbnail_images": list, @@ -134,7 +130,6 @@ def on_request_fileinfo(self, filename: str) -> None: ) _thumbnail_images = list(map(lambda path: QtGui.QImage(path), _thumbnail_paths)) _data.update({"thumbnail_images": _thumbnail_images}) - _data.update({"filament_total": _file_metadata.get("filament_total", "?")}) _data.update({"estimated_time": _file_metadata.get("estimated_time", 0)}) _data.update({"layer_count": _file_metadata.get("layer_count", -1.0)}) @@ -165,18 +160,15 @@ def on_request_fileinfo(self, filename: str) -> None: self.fileinfo.emit(_data) def eventFilter(self, a0: QtCore.QObject, a1: QtCore.QEvent) -> bool: - """Filter Klippy related events""" + """Handle Websocket and Klippy events""" + if a1.type() == events.WebSocketOpen.type(): + self.request_file_list.emit() + self.request_dir_info[str, bool].emit("", False) + return False if a1.type() == events.KlippyDisconnected.type(): self.files_metadata.clear() self.files.clear() return False - if a1.type() == events.KlippyReady.type(): - # Request all files including in subdirectories - # in order to get all metadata - self.request_file_list.emit() - # List and directory build is depended only on this signal - self.request_dir_info[str, bool].emit("", False) - return False return super().eventFilter(a0, a1) def event(self, a0: QtCore.QEvent) -> bool: diff --git a/BlocksScreen/lib/panels/widgets/confirmPage.py b/BlocksScreen/lib/panels/widgets/confirmPage.py index c09f31b5..12432449 100644 --- a/BlocksScreen/lib/panels/widgets/confirmPage.py +++ b/BlocksScreen/lib/panels/widgets/confirmPage.py @@ -11,7 +11,7 @@ class ConfirmWidget(QtWidgets.QWidget): on_accept: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, list, name="on_accept" + str, name="on_accept" ) request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="request-back" @@ -31,7 +31,7 @@ def __init__(self, parent) -> None: self.filename = "" self.confirm_button.clicked.connect( lambda: self.on_accept.emit( - str(os.path.join(self.directory, self.filename)), self._thumbnails + str(os.path.join(self.directory, self.filename)) ) ) self.back_btn.clicked.connect(self.request_back.emit) diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index a09bdcd7..0d7c4e29 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -1,6 +1,5 @@ import logging import typing - import events from helper_methods import calculate_current_layer, estimate_print_time from lib.panels.widgets import dialogPage @@ -10,22 +9,19 @@ from lib.utils.display_button import DisplayButton from PyQt6 import QtCore, QtGui, QtWidgets +logger = logging.getLogger("logs/BlocksScreen.log") -class ClickableGraphicsView(QtWidgets.QGraphicsView): - """Re-implementation of QGraphicsView that adds clicked signal""" - clicked = QtCore.pyqtSignal() +class JobStatusWidget(QtWidgets.QWidget): + """Job status widget page, page shown when there is a active print job. + Enables mid print printer tuning and inspection of print progress. -def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: - """Filter mouse press events""" - if event.button() == QtCore.Qt.MouseButton.LeftButton: - self.clicked.emit() - return True # Issue event handled - super(ClickableGraphicsView, self).mousePressEvent(event) + Args: + QtWidgets (QtWidgets.QWidget): Parent widget + """ -class JobStatusWidget(QtWidgets.QWidget): print_start: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( str, name="print_start" ) @@ -61,62 +57,90 @@ class JobStatusWidget(QtWidgets.QWidget): def __init__(self, parent) -> None: super().__init__(parent) - - self.cancel_print_dialog = dialogPage.DialogPage(self) + self.thumbnail_graphics = [] self._setupUI() + self.cancel_print_dialog = dialogPage.DialogPage(self) self.tune_menu_btn.clicked.connect(self.tune_clicked.emit) self.pause_printing_btn.clicked.connect(self.pause_resume_print) self.stop_printing_btn.clicked.connect(self.handleCancel) - self.CBVSmallThumbnail.clicked.connect(self.showthumbnail) - self.CBVBigThumbnail.clicked.connect(self.hidethumbnail) - - self.smalthumbnail = QtGui.QImage( - "BlocksScreen/lib/ui/resources/media/smalltest.png" - ) - self.bigthumbnail = QtGui.QImage( - "BlocksScreen/lib/ui/resources/media/thumbnailmissing.png" - ) - self.CBVSmallThumbnail.installEventFilter(self) - self.CBVBigThumbnail.installEventFilter(self) + @QtCore.pyqtSlot(name="toggle-thumbnail-expansion") + def toggle_thumbnail_expansion(self) -> None: + """Toggle thumbnail expansion""" + if not self.thumbnail_view.scene(): + return + if not self.thumbnail_view.isVisible(): + self.thumbnail_view.show() + self.progressWidget.hide() + self.contentWidget.hide() + self.printing_progress_bar.hide() + self.btnWidget.hide() + self.headerWidget.hide() + return + self.thumbnail_view.hide() + self.progressWidget.show() + self.contentWidget.show() + self.printing_progress_bar.show() + self.btnWidget.show() + self.headerWidget.show() + self.show() - def eventFilter(self, source, event): - """Re-implemented method, filter events""" - if ( - source == self.CBVSmallThumbnail - and event.type() == QtCore.QEvent.Type.MouseButtonPress - ): - if event.button() == QtCore.Qt.MouseButton.LeftButton: - self.showthumbnail() + def eventFilter(self, sender_obj: QtCore.QObject, event: events.QEvent) -> bool: + """Filter events, + currently only filters events from `self.thumbnail_view` QGraphicsView widget + """ if ( - source == self.CBVBigThumbnail + sender_obj == self.thumbnail_view and event.type() == QtCore.QEvent.Type.MouseButtonPress ): - if event.button() == QtCore.Qt.MouseButton.LeftButton: - self.hidethumbnail() - - return super().eventFilter(source, event) - - @QtCore.pyqtSlot(name="show-thumbnail") - def showthumbnail(self): - """Show print job fullscreen thumbnail""" - self.contentWidget.hide() - self.progressWidget.hide() - self.headerWidget.hide() - self.btnWidget.hide() - self.smallthumb_widget.hide() - self.bigthumb_widget.show() - - @QtCore.pyqtSlot(name="hide-thumbnail") - def hidethumbnail(self): - """Hide print job fullscreen thumbnail""" - self.contentWidget.show() - self.progressWidget.show() - self.headerWidget.show() - self.btnWidget.show() - self.smallthumb_widget.show() - self.bigthumb_widget.hide() + self.toggle_thumbnail_expansion() + return True + return super().eventFilter(sender_obj, event) + + def _load_thumbnails(self, *thumbnails) -> None: + """Pre-load available thumbnails for the current print object""" + self.thumbnail_graphics = list( + filter( + lambda thumb: not thumb.isNull(), + [QtGui.QPixmap(thumb) for thumb in thumbnails], + ) + ) + if not self.thumbnail_graphics: + logger.debug("Unable to load thumbnails, no thumbnails provided") + return + self.create_thumbnail_widget() + self.thumbnail_view.installEventFilter( + self + ) # Filter events on this widget, for clicks + scene = QtWidgets.QGraphicsScene() + _biggest_thumb = self.thumbnail_graphics[-1] + self.thumbnail_view.setSceneRect( + QtCore.QRectF( + self.rect().x(), + self.rect().y(), + _biggest_thumb.width(), + _biggest_thumb.height(), + ) + ) + scaled = QtGui.QPixmap(_biggest_thumb).scaled( + _biggest_thumb.width(), + _biggest_thumb.height(), + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, + ) + item = QtWidgets.QGraphicsPixmapItem(scaled) + scene.addItem(item) + self.thumbnail_view.setFrameRect( + QtCore.QRect( + 0, 0, self.contentsRect().width(), self.contentsRect().height() + ) + ) + self.thumbnail_view.setScene(scene) + self.printing_progress_bar.set_inner_pixmap(self.thumbnail_graphics[-1]) + self.printing_progress_bar.thumbnail_clicked.connect( + self.toggle_thumbnail_expansion + ) @QtCore.pyqtSlot(name="handle-cancel") def handleCancel(self) -> None: @@ -127,23 +151,16 @@ def handleCancel(self) -> None: self.cancel_print_dialog.accepted.connect(self.print_cancel) self.cancel_print_dialog.open() - @QtCore.pyqtSlot(str, list, name="on_print_start") - def on_print_start(self, file: str, thumbnails: list) -> None: + @QtCore.pyqtSlot(str, name="on_print_start") + def on_print_start(self, file: str) -> None: """Start a print job, show job status page""" self._current_file_name = file self.js_file_name_label.setText(self._current_file_name) self.layer_display_button.setText("?") self.print_time_display_button.setText("?") - if thumbnails: - self.smalthumbnail = thumbnails[0] - self.bigthumbnail = thumbnails[1] - self.printing_progress_bar.reset() self._internal_print_status = "printing" - self.request_file_info.emit( - file - ) # Request file metadata (or file info whatever) - + self.request_file_info.emit(file) self.print_start.emit(file) print_start_event = events.PrintStart( self._current_file_name, self.file_metadata @@ -155,24 +172,16 @@ def on_print_start(self, file: str, thumbnails: list) -> None: else: raise TypeError("QApplication.instance expected non None value") except Exception as e: - logging.debug(f"Unexpected error while posting print job start event: {e}") + logger.debug(f"Unexpected error while posting print job start event: {e}") @QtCore.pyqtSlot(dict, name="on_fileinfo") def on_fileinfo(self, fileinfo: dict) -> None: """Handle received file information/metadata""" self.total_layers = str(fileinfo.get("layer_count", "?")) self.layer_display_button.setText("?") - if ( - fileinfo.get("thumbnail_images", []) - and len(fileinfo.get("thumbnail_images", [])) > 0 - ): - self.smalthumbnail = fileinfo["thumbnail_images"][1] - self.bigthumbnail = fileinfo["thumbnail_images"][ - -1 - ] # Last 'biggest' element - self.layer_display_button.secondary_text = str(self.total_layers) self.file_metadata = fileinfo + self._load_thumbnails(*fileinfo.get("thumbnail_images", [])) @QtCore.pyqtSlot(name="pause_resume_print") def pause_resume_print(self) -> None: @@ -234,6 +243,8 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: self.total_layers = "?" self.file_metadata.clear() self.hide_request.emit() + self.thumbnail_view.deleteLater() + self.thumbnail_view_layout.deleteLater() if hasattr(events, str("Print" + value.capitalize())): event_obj = getattr(events, str("Print" + value.capitalize())) @@ -247,7 +258,7 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: "QApplication.instance expected non None value" ) except Exception as e: - logging.info( + logger.info( f"Unexpected error while posting print job start event: {e}" ) @@ -280,24 +291,19 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: @QtCore.pyqtSlot(str, list, name="on_gcode_move_update") def on_gcode_move_update(self, field: str, value: list) -> None: """Handle gcode move""" - if isinstance(value, list): - if "gcode_position" in field: # Without offsets - if self._internal_print_status == "printing": - _current_layer = calculate_current_layer( - z_position=value[2], - object_height=float( - self.file_metadata.get("object_height", -1.0) - ), - layer_height=float( - self.file_metadata.get("layer_height", -1.0) - ), - first_layer_height=float( - self.file_metadata.get("first_layer_height", -1.0) - ), - ) - self.layer_display_button.setText( - f"{int(_current_layer)}" if _current_layer != -1 else "?" - ) + if "gcode_position" in field: # Without offsets + if self._internal_print_status == "printing": + _current_layer = calculate_current_layer( + z_position=value[2], + object_height=float(self.file_metadata.get("object_height", -1.0)), + layer_height=float(self.file_metadata.get("layer_height", -1.0)), + first_layer_height=float( + self.file_metadata.get("first_layer_height", -1.0) + ), + ) + self.layer_display_button.setText( + f"{int(_current_layer)}" if _current_layer != -1 else "?" + ) @QtCore.pyqtSlot(str, float, name="virtual_sdcard_update") @QtCore.pyqtSlot(str, bool, name="virtual_sdcard_update") @@ -309,72 +315,11 @@ def virtual_sdcard_update(self, field: str, value: float | bool) -> None: value (float | bool): The updated information for the corresponding field """ if isinstance(value, bool): - self.sdcard_read = value - elif isinstance(value, float): - if "progress" == field: - self.print_progress = value - self.printing_progress_bar.setValue(self.print_progress) - - def paintEvent(self, a0: QtGui.QPaintEvent) -> None: - """Re-implemented method, paint widget""" - _scene = QtWidgets.QGraphicsScene() - if not self.smalthumbnail.isNull(): - _graphics_rect = self.CBVSmallThumbnail.rect().toRectF() - _image_rect = self.smalthumbnail.rect() - - scaled_width = _image_rect.width() - scaled_height = _image_rect.height() - adjusted_x = (_graphics_rect.width() - scaled_width) // 2.0 - adjusted_y = (_graphics_rect.height() - scaled_height) // 2.0 - - adjusted_rect = QtCore.QRectF( - _image_rect.x() + adjusted_x, - _image_rect.y() + adjusted_y, - scaled_width, - scaled_height, - ) - _scene.setSceneRect(adjusted_rect) - _item_scaled = QtWidgets.QGraphicsPixmapItem( - QtGui.QPixmap.fromImage(self.smalthumbnail).scaled( - int(scaled_width), - int(scaled_height), - QtCore.Qt.AspectRatioMode.KeepAspectRatio, - QtCore.Qt.TransformationMode.SmoothTransformation, - ) - ) - _scene.addItem(_item_scaled) - self.CBVSmallThumbnail.setScene(_scene) - - else: - self.request_file_info.emit(self.js_file_name_label.text()) - _scene = QtWidgets.QGraphicsScene() - - if not self.bigthumbnail.isNull(): - _graphics_rect = self.CBVBigThumbnail.rect().toRectF() - _image_rect = self.bigthumbnail.rect() - - scaled_width = _image_rect.width() - scaled_height = _image_rect.height() - adjusted_x = (_graphics_rect.width() - scaled_width) // 2.0 - adjusted_y = (_graphics_rect.height() - scaled_height) // 2.0 - - adjusted_rect = QtCore.QRectF( - _image_rect.x() + adjusted_x, - _image_rect.y() + adjusted_y, - scaled_width, - scaled_height, - ) - _scene.setSceneRect(adjusted_rect) - _item_scaled = QtWidgets.QGraphicsPixmapItem( - QtGui.QPixmap.fromImage(self.bigthumbnail).scaled( - int(scaled_width), - int(scaled_height), - QtCore.Qt.AspectRatioMode.KeepAspectRatio, - QtCore.Qt.TransformationMode.SmoothTransformation, - ) - ) - _scene.addItem(_item_scaled) - self.CBVBigThumbnail.setScene(_scene) + ... + if "progress" == field: + self.printing_progress_bar.setValue(value) + if "file_position" == field: + ... def _setupUI(self) -> None: """Setup widget ui""" @@ -391,73 +336,41 @@ def _setupUI(self) -> None: self.setMinimumSize(QtCore.QSize(710, 420)) self.setMaximumSize(QtCore.QSize(720, 420)) self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - - # ---------------------------------Widgets - self.bigthumb_widget = QtWidgets.QWidget(self) - self.bigthumb_widget.setGeometry( - QtCore.QRect(0, 0, self.width(), self.height()) - ) - self.bigthumb_widget.setObjectName("bigthumb_widget") - self.headerWidget = QtWidgets.QWidget(self) self.headerWidget.setGeometry(QtCore.QRect(11, 11, 691, 62)) self.headerWidget.setObjectName("headerWidget") - self.btnWidget = QtWidgets.QWidget(self) self.btnWidget.setGeometry(QtCore.QRect(10, 80, 691, 90)) self.btnWidget.setObjectName("btnWidget") - self.progressWidget = QtWidgets.QWidget(self) self.progressWidget.setGeometry(QtCore.QRect(10, 170, 471, 241)) self.progressWidget.setObjectName("progressWidget") - self.contentWidget = QtWidgets.QWidget(self) self.contentWidget.setGeometry(QtCore.QRect(480, 170, 221, 241)) self.contentWidget.setObjectName("contentWidget") - - self.smallthumb_widget = QtWidgets.QLabel(self) - self.smallthumb_widget.setGeometry(QtCore.QRect(10, 170, 471, 241)) - self.smallthumb_widget.setObjectName("smallthumb_widget") - - # ---------------------------------layout - - self.smalllayout = QtWidgets.QHBoxLayout(self.smallthumb_widget) - - self.biglayout = QtWidgets.QHBoxLayout(self.bigthumb_widget) - self.job_status_header_layout = QtWidgets.QHBoxLayout(self.headerWidget) self.job_status_header_layout.setSpacing(20) self.job_status_header_layout.setObjectName("job_status_header_layout") - self.job_status_progress_layout = QtWidgets.QVBoxLayout(self.progressWidget) self.job_status_progress_layout.setSizeConstraint( QtWidgets.QLayout.SizeConstraint.SetMinimumSize ) - self.job_status_btn_layout = QtWidgets.QHBoxLayout(self.btnWidget) self.job_status_btn_layout.setSizeConstraint( QtWidgets.QLayout.SizeConstraint.SetMinimumSize ) - self.job_content_layout = QtWidgets.QVBoxLayout(self.contentWidget) self.job_content_layout.setObjectName("job_content_layout") - self.job_status_btn_layout.setContentsMargins(5, 5, 5, 5) self.job_status_btn_layout.setSpacing(5) self.job_status_btn_layout.setObjectName("job_status_btn_layout") - self.job_stats_display_layout = QtWidgets.QVBoxLayout() self.job_stats_display_layout.setObjectName("job_stats_display_layout") - - # -----------------------------Fonts font = QtGui.QFont() font.setFamily("Montserrat") font.setPointSize(14) - # ------------------------------Header - self.js_file_name_icon = BlocksLabel(parent=self) - self.js_file_name_icon.setSizePolicy(sizePolicy) self.js_file_name_icon.setMinimumSize(QtCore.QSize(60, 60)) self.js_file_name_icon.setMaximumSize(QtCore.QSize(60, 60)) @@ -476,19 +389,14 @@ def _setupUI(self) -> None: self.js_file_name_label.setSizePolicy(sizePolicy) self.js_file_name_label.setMinimumSize(QtCore.QSize(200, 80)) self.js_file_name_label.setMaximumSize(QtCore.QSize(16777215, 60)) - self.js_file_name_label.setFont(font) self.js_file_name_label.setStyleSheet("background: transparent; color: white;") self.js_file_name_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.js_file_name_label.setObjectName("js_file_name_label") - self.job_status_header_layout.addWidget(self.js_file_name_icon) self.job_status_header_layout.addWidget(self.js_file_name_label) - # -----------------------------buttons - font.setPointSize(18) - self.pause_printing_btn = BlocksCustomButton(self) self.pause_printing_btn.setSizePolicy(sizePolicy) self.pause_printing_btn.setMinimumSize(QtCore.QSize(200, 80)) @@ -498,108 +406,37 @@ def _setupUI(self) -> None: "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/pause.svg") ) self.pause_printing_btn.setObjectName("pause_printing_btn") - self.stop_printing_btn = BlocksCustomButton(self) self.stop_printing_btn.setSizePolicy(sizePolicy) self.stop_printing_btn.setMinimumSize(QtCore.QSize(200, 80)) self.stop_printing_btn.setMaximumSize(QtCore.QSize(200, 80)) - self.stop_printing_btn.setFont(font) self.stop_printing_btn.setProperty( "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/stop.svg") ) self.stop_printing_btn.setObjectName("stop_printing_btn") - self.tune_menu_btn = BlocksCustomButton(self) self.tune_menu_btn.setSizePolicy(sizePolicy) - self.tune_menu_btn.setMinimumSize(QtCore.QSize(200, 60)) self.tune_menu_btn.setMaximumSize(QtCore.QSize(200, 80)) - self.tune_menu_btn.setFont(font) self.tune_menu_btn.setProperty( "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/tune.svg") ) self.tune_menu_btn.setObjectName("tune_menu_btn") - self.job_status_btn_layout.addWidget(self.pause_printing_btn) self.job_status_btn_layout.addWidget(self.stop_printing_btn) self.job_status_btn_layout.addWidget(self.tune_menu_btn) - self.tune_menu_btn.setText("Tune") self.stop_printing_btn.setText("Cancel") self.pause_printing_btn.setText("Pause") - # -----------------------------Progress bar - - self.printing_progress_bar = CustomProgressBar() + self.printing_progress_bar = CustomProgressBar(self) self.printing_progress_bar.setMinimumHeight(150) - self.printing_progress_bar.setObjectName("printing_progress_bar") self.printing_progress_bar.setSizePolicy(sizePolicy) - self.job_status_progress_layout.addWidget(self.printing_progress_bar) - # -----------------------------SMALL-THUMBNAIL - - self.CBVSmallThumbnail = ClickableGraphicsView(self.smallthumb_widget) - self.CBVSmallThumbnail.setSizePolicy(sizePolicy) - self.CBVSmallThumbnail.setMaximumSize(QtCore.QSize(48, 48)) - self.CBVSmallThumbnail.setStyleSheet( - "QGraphicsView{\nbackground-color:transparent;\n}" - ) - self.CBVSmallThumbnail.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.CBVSmallThumbnail.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) - self.CBVSmallThumbnail.setSizeAdjustPolicy( - QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) - self.CBVSmallThumbnail.setBackgroundBrush(brush) - self.CBVSmallThumbnail.setRenderHints( - QtGui.QPainter.RenderHint.Antialiasing - | QtGui.QPainter.RenderHint.SmoothPixmapTransform - | QtGui.QPainter.RenderHint.TextAntialiasing - ) - self.CBVSmallThumbnail.setObjectName("CBVSmallThumbnail") - - self.smalllayout.addWidget(self.CBVSmallThumbnail) - - # -----------------------------Big-Thumbnail - self.CBVBigThumbnail = ClickableGraphicsView() - self.CBVBigThumbnail.setSizePolicy(sizePolicy) - self.CBVBigThumbnail.setMaximumSize(QtCore.QSize(300, 300)) - self.CBVBigThumbnail.setStyleSheet( - "QGraphicsView{\nbackground-color:transparent;\n}" - ) - # "QGraphicsView{\nbackground-color:grey;border-radius:10px;\n}" grey background - self.CBVBigThumbnail.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.CBVBigThumbnail.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) - self.CBVBigThumbnail.setVerticalScrollBarPolicy( - QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) - self.CBVBigThumbnail.setHorizontalScrollBarPolicy( - QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) - self.CBVBigThumbnail.setSizeAdjustPolicy( - QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) - self.CBVBigThumbnail.setBackgroundBrush(brush) - self.CBVBigThumbnail.setRenderHints( - QtGui.QPainter.RenderHint.Antialiasing - | QtGui.QPainter.RenderHint.SmoothPixmapTransform - | QtGui.QPainter.RenderHint.TextAntialiasing - ) - self.CBVBigThumbnail.setViewportUpdateMode( - QtWidgets.QGraphicsView.ViewportUpdateMode.SmartViewportUpdate - ) - - self.CBVBigThumbnail.setObjectName("CBVBigThumbnail") - self.biglayout.addWidget(self.CBVBigThumbnail) - self.bigthumb_widget.hide() - # -----------------------------display buttons self.layer_display_button = DisplayButton(self) @@ -638,3 +475,39 @@ def _setupUI(self) -> None: QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) self.job_content_layout.addLayout(self.job_stats_display_layout) + + def create_thumbnail_widget(self) -> None: + """Create thumbnail graphics view widget""" + self.thumbnail_view = QtWidgets.QGraphicsView() + self.thumbnail_view.setMinimumSize(QtCore.QSize(48, 48)) + # self.thumbnail_view.setMaximumSize(QtCore.QSize(300, 300)) + self.thumbnail_view.setAttribute( + QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True + ) + self.thumbnail_view.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.thumbnail_view.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) + self.thumbnail_view.setWindowFlags(QtCore.Qt.WindowType.FramelessWindowHint) + self.thumbnail_view.setObjectName("thumbnail_scene") + _thumbnail_palette = QtGui.QPalette() + _thumbnail_palette.setColor( + QtGui.QPalette.ColorRole.Window, QtGui.QColor(0, 0, 0, 0) + ) + _thumbnail_palette.setColor( + QtGui.QPalette.ColorRole.Base, QtGui.QColor(0, 0, 0, 0) + ) + self.thumbnail_view.setPalette(_thumbnail_palette) + _thumbnail_brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + _thumbnail_brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) + self.thumbnail_view.setBackgroundBrush(_thumbnail_brush) + self.thumbnail_view.setRenderHints( + QtGui.QPainter.RenderHint.Antialiasing + | QtGui.QPainter.RenderHint.SmoothPixmapTransform + | QtGui.QPainter.RenderHint.LosslessImageRendering + ) + self.thumbnail_view.setViewportUpdateMode( + QtWidgets.QGraphicsView.ViewportUpdateMode.SmartViewportUpdate + ) + self.thumbnail_view.setObjectName("thumbnail_scene") + self.thumbnail_view_layout = QtWidgets.QHBoxLayout(self) + self.thumbnail_view_layout.addWidget(self.thumbnail_view) + self.thumbnail_view.hide() diff --git a/BlocksScreen/lib/utils/blocks_progressbar.py b/BlocksScreen/lib/utils/blocks_progressbar.py index 98f7cc52..a05aafa5 100644 --- a/BlocksScreen/lib/utils/blocks_progressbar.py +++ b/BlocksScreen/lib/utils/blocks_progressbar.py @@ -1,92 +1,192 @@ +import typing from PyQt6 import QtWidgets, QtGui, QtCore class CustomProgressBar(QtWidgets.QProgressBar): + """Custom circular progress bar for tracking print jobs + + Args: + QtWidgets (QtWidget): Parent widget + + Raises: + ValueError: Thrown when setting progress is not between 0.0 and 1.0 + ValueError: Thrown when setting bar color is not between 0 and 255. + + """ + + thumbnail_clicked: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + name="thumbnail-clicked" + ) + def __init__(self, parent=None): super().__init__(parent) self.progress_value = 0 - self.bar_color = QtGui.QColor(223, 223, 223) + self._pen_width = 20 + self._padding = 50 + self._pixmap: QtGui.QPixmap = QtGui.QPixmap() + self._pixmap_cached: QtGui.QPixmap = QtGui.QPixmap() + self._pixmap_dirty: bool = True + self._bar_color = QtGui.QColor(223, 223, 223) self.setMinimumSize(100, 100) - self.set_padding(50) - self.set_pen_width(20) + self._inner_rect: QtCore.QRectF = QtCore.QRectF() - def set_padding(self, value): + def set_padding(self, value) -> None: """Set widget padding""" - self.padding = value + self._padding = value self.update() - def set_pen_width(self, value): + def set_pen_width(self, value) -> None: """Set widget text pen width""" - self.pen_width = value + self._pen_width = value self.update() - def paintEvent(self, event): - """Re-implemented method, paint widget""" - painter = QtGui.QPainter(self) - painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) + def set_inner_pixmap(self, pixmap: QtGui.QPixmap) -> None: + """Set the inner icon pixmap on the progress bar + circumference. + """ + self._pixmap = pixmap + self.update() - self._draw_circular_bar(painter, self.width(), self.height()) + def resizeEvent(self, a0) -> None: + """Re-implemented method, handle widget resize Events + + Currently rescales the set pixmap so it has the optimal + size. + """ + self._inner_rect = self._calculate_inner_geometry() + self._pixmap_cached = self._pixmap.scaled( + self._inner_rect.size().toSize(), + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, + ) + self.update() + return super().resizeEvent(a0) + + def sizeHint(self) -> QtCore.QSize: + """Re-implemented method, preferable widget size""" + self._inner_rect = self._calculate_inner_geometry() + return QtCore.QSize(100, 100) + + def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: + """Re-implemented method, check if thumbnail was clicked, + filter clicks inside inner section of the widget, + if a mouse event happens there we know that the thumbnail + was pressed. + """ + if self._inner_rect.contains(a0.pos().x(), a0.pos().y()): + self.thumbnail_clicked.emit() + return super().mousePressEvent(a0) + + def minimumSizeHint(self) -> QtCore.QSize: + """Re-implemented method, minimum widget size""" + self._inner_rect = self._calculate_inner_geometry() + return QtCore.QSize(100, 100) + + def setValue(self, value: float) -> None: + """Set progress value + + Args: + value (float): Progress value between 0.0 and 1.0 + + Raises: + ValueError: If provided value in not between 0.0 and 1.0 + """ + if not (0 <= value <= 100): + raise ValueError("Argument `value` expected value between 0.0 and 1.0 ") + value *= 100 + self.progress_value = value + self.update() - def _draw_circular_bar(self, painter, width, height): - size = min(width, height) - (self.padding * 1.3) - x = (width - size) / 2 - y = (height - size) / 2 - arc_rect = QtCore.QRectF(x, y, size, size) + def set_bar_color(self, red: int, green: int, blue: int) -> None: + """Set widget progress bar color + + Args: + red (int): red component value between 0 and 255 + green (int): green component value between 0 and 255 + blue (int): blue component value between 0 and 255 + + Raises: + ValueError: Raised if any provided argument value is not between 0 and 255 + """ + if not (0 <= red <= 255 and 0 <= green <= 255 and 0 <= blue <= 255): + raise ValueError("Color values must be between 0 and 255.") + self._bar_color = QtGui.QColor(red, green, blue) + self.update() + + def _calculate_inner_geometry(self) -> QtCore.QRectF: + size = min(self.width(), self.height()) - (self._padding * 1.3) + x = (self.width() - size) // 2 + y = (self.height() - size) // 2 + return QtCore.QRectF( + x + self._pen_width // 2, + y + self._pen_width // 2, + size - self._pen_width, + size - self._pen_width, + ) - arc1_start = 236 * 16 - arc1_span = -290 * 16 + def _draw_cached_pixmap( + self, painter: QtGui.QPainter, pixmap: QtGui.QPixmap, inner_rect: QtCore.QRectF + ) -> None: + """Internal method draw already scaled pixmap on the widget inner section""" + if pixmap.isNull(): + return + scaled_width = pixmap.width() + scaled_height = pixmap.height() + adjusted_x = (inner_rect.width() - scaled_width) // 2.0 + adjusted_y = (inner_rect.height() - scaled_height) // 2.0 + adjusted_icon = QtCore.QRectF( + inner_rect.x() + adjusted_x, + inner_rect.y() + adjusted_y, + scaled_width, + scaled_height, + ) + painter.drawPixmap(adjusted_icon, pixmap, pixmap.rect().toRectF()) + + def _draw_circular_bar( + self, + painter: QtGui.QPainter, + ) -> None: + size = min(self.width(), self.height()) - (self._padding * 1.3) + x = (self.width() - size) / 2 + y = (self.height() - size) / 2 + arc_rect = QtCore.QRectF(x, y, size, size) + arc_start = 236 * 16 + arc_span = -290 * 16 bg_pen = QtGui.QPen(QtGui.QColor(20, 20, 20)) - bg_pen.setWidth(self.pen_width) + bg_pen.setWidth(self._pen_width) bg_pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) painter.setPen(bg_pen) - painter.drawArc(arc_rect, arc1_start, arc1_span) - + painter.drawArc(arc_rect, arc_start, arc_span) if self.progress_value is not None: gradient = QtGui.QConicalGradient(arc_rect.center(), -90) - gradient.setColorAt(0.0, self.bar_color) + gradient.setColorAt(0.0, self._bar_color) gradient.setColorAt(1.0, QtGui.QColor(100, 100, 100)) - progress_pen = QtGui.QPen() - progress_pen.setWidth(self.pen_width) + progress_pen.setWidth(self._pen_width) progress_pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) progress_pen.setBrush(QtGui.QBrush(gradient)) painter.setPen(progress_pen) - - # scale only over arc1’s span - progress_span = int(arc1_span * self.progress_value / 100) - painter.drawArc(arc_rect, arc1_start, progress_span) - + # scale only over arc span + progress_span = int(arc_span * self.progress_value / 100) + painter.drawArc(arc_rect, arc_start, progress_span) progress_text = f"{int(self.progress_value)}%" painter.setPen(QtGui.QPen(QtGui.QColor(0, 0, 0))) font = painter.font() font.setPointSize(16) - bg_pen = QtGui.QPen(QtGui.QColor(255, 255, 255)) painter.setPen(bg_pen) painter.setFont(font) - text_x = arc_rect.center().x() text_y = arc_rect.center().y() - - # Draw centered text text_rect = QtCore.QRectF( text_x - 30, text_y + arc_rect.height() / 2 - 25, 60, 40 ) painter.drawText(text_rect, QtCore.Qt.AlignmentFlag.AlignCenter, progress_text) - def setValue(self, value): - """Set value""" - value *= 100 - if 0 <= value <= 101: - self.progress_value = value - self.update() - else: - raise ValueError("Progress must be between 0.0 and 1.0.") - - def set_bar_color(self, red, green, blue): - """Set bar color""" - if 0 <= red <= 255 and 0 <= green <= 255 and 0 <= blue <= 255: - self.bar_color = QtGui.QColor(red, green, blue) - self.update() - else: - raise ValueError("Color values must be between 0 and 255.") + def paintEvent(self, _) -> None: + """Re-implemented method, paint widget""" + painter = QtGui.QPainter(self) + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) + self._draw_circular_bar(painter) + self._draw_cached_pixmap(painter, self._pixmap_cached, self._inner_rect) + painter.end() From 6fbc375a5b18274fd71c63545e2dce58badff42a Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Fri, 2 Jan 2026 11:50:44 +0000 Subject: [PATCH 18/70] Bugfix uninitilized variable access introduced on #123 (#141) * Del: reference to uninitialized QGraphicsView variable When there is no thumbnail for the current print, we shouldn't referece `self.thumbnail_view` since it hasn't been initialized. When the print stops for whatever reason we want to delete the object, but if it hasn't been initilialized there is nothing to delete. So we must only delete when the print stops if the class has been attributed. * Added clear thumbnail object on print stop * Small refactor, exit method when page is not visible * Calculate and scale thumbnail pixmap on set When setting the pixmap on the progress bar, the image was not scaled and the inner rect was not calculated. This resulted in the pixmap not showing. Now when setting the progressbar thumbnail this is calculated so that the pixmap can be shown in the middle of the progress bar circumference. * Del forgotten print() * Refactor and handle show event Refactored some methods, including accessing values in dicts, by using .get(). Now the slot `on_fileinfo` only runs when the `jobStatusPage` is visible this is because the request for file information is done on `file.py` and the confirmation page. This whould result in the slot triggering multiple times before it was actually necessary and on asking for imformation for all files, while we only want information on one file. Now the request for file information is done when the `jobStatusPage` is actually visible. The `showEvent` method requests the file information when that event is triggered on the class. * Split print state logic into seperate method Split the state logic in a seperate method (`_handle_print_state(state: str))` just so it's more readable than handling all `print_status` object updates in a single method. It was getting to big of a method. * Change print state event dispatch logic --- .../lib/panels/widgets/jobStatusPage.py | 175 ++++++++---------- BlocksScreen/lib/utils/blocks_progressbar.py | 20 +- 2 files changed, 89 insertions(+), 106 deletions(-) diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index 0d7c4e29..9f74ba6e 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -85,6 +85,11 @@ def toggle_thumbnail_expansion(self) -> None: self.headerWidget.show() self.show() + def showEvent(self, a0) -> None: + """Reimplemented method, handle `show` Event""" + if self._current_file_name: + self.request_file_info.emit(self._current_file_name) + def eventFilter(self, sender_obj: QtCore.QObject, event: events.QEvent) -> bool: """Filter events, @@ -110,9 +115,7 @@ def _load_thumbnails(self, *thumbnails) -> None: logger.debug("Unable to load thumbnails, no thumbnails provided") return self.create_thumbnail_widget() - self.thumbnail_view.installEventFilter( - self - ) # Filter events on this widget, for clicks + self.thumbnail_view.installEventFilter(self) scene = QtWidgets.QGraphicsScene() _biggest_thumb = self.thumbnail_graphics[-1] self.thumbnail_view.setSceneRect( @@ -172,11 +175,13 @@ def on_print_start(self, file: str) -> None: else: raise TypeError("QApplication.instance expected non None value") except Exception as e: - logger.debug(f"Unexpected error while posting print job start event: {e}") + logger.debug("Unexpected error while posting print job start event: %s", e) @QtCore.pyqtSlot(dict, name="on_fileinfo") def on_fileinfo(self, fileinfo: dict) -> None: """Handle received file information/metadata""" + if not self.isVisible(): + return self.total_layers = str(fileinfo.get("layer_count", "?")) self.layer_display_button.setText("?") self.layer_display_button.secondary_text = str(self.total_layers) @@ -185,24 +190,59 @@ def on_fileinfo(self, fileinfo: dict) -> None: @QtCore.pyqtSlot(name="pause_resume_print") def pause_resume_print(self) -> None: - """Handle pause/resume print job""" - if not getattr(self, "_pause_locked", False): - self._pause_locked = True - self.pause_printing_btn.setEnabled(False) - - if self._internal_print_status == "printing": - self.print_pause.emit() - self._internal_print_status = "paused" - - elif self._internal_print_status == "paused": - self.print_resume.emit() - self._internal_print_status = "printing" - - QtCore.QTimer.singleShot(5000, self._unlock_pause_button) - - def _unlock_pause_button(self): - self._pause_locked = False - self.pause_printing_btn.setEnabled(True) + """Handle pause/resume print job button clicked""" + self.pause_printing_btn.setEnabled(False) + if self._internal_print_status == "printing": + self._internal_print_status = "paused" + self.print_pause.emit() + elif self._internal_print_status == "paused": + self._internal_print_status = "printing" + self.print_resume.emit() + + def _handle_print_state(self, state: str) -> None: + """Handle print state change received from + printer_status object updated + """ + valid_states = {"printing", "paused"} + invalid_states = {"cancelled", "complete", "error", "standby"} + lstate = state.lower() + if lstate in valid_states: + self._internal_print_status = lstate + if lstate == "paused": + self.pause_printing_btn.setText(" Resume") + self.pause_printing_btn.setPixmap( + QtGui.QPixmap(":/ui/media/btn_icons/play.svg") + ) + elif lstate == "printing": + self.pause_printing_btn.setText("Pause") + self.pause_printing_btn.setPixmap( + QtGui.QPixmap(":/ui/media/btn_icons/pause.svg") + ) + self.pause_printing_btn.setEnabled(True) + self.request_query_print_stats.emit({"print_stats": ["filename"]}) + self.show_request.emit() + lstate = "start" + elif lstate in invalid_states: + self._current_file_name = "" + self._internal_print_status = "" + self.total_layers = "?" + self.file_metadata.clear() + self.hide_request.emit() + if hasattr(self, "thumbnail_view"): + getattr(self, "thumbnail_view").deleteLater() + # Send Event on Print state + if hasattr(events, str("Print" + lstate.capitalize())): + event_obj = getattr(events, str("Print" + lstate.capitalize())) + event = event_obj(self._current_file_name, self.file_metadata) + instance = QtWidgets.QApplication.instance() + if instance: + instance.postEvent(self.window(), event) + return + logger.error( + "QApplication.instance expected non None value,\ + Unable to post event %s", + str("Print" + lstate.capitalize()), + ) @QtCore.pyqtSlot(str, dict, name="on_print_stats_update") @QtCore.pyqtSlot(str, float, name="on_print_stats_update") @@ -216,82 +256,42 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: value (dict | float | str): The value for the field. """ if isinstance(value, str): + if "state" in field: + self._handle_print_state(value) if "filename" in field: self._current_file_name = value if self.js_file_name_label.text().lower() != value.lower(): self.js_file_name_label.setText(self._current_file_name) - self.request_file_info.emit(value) # Request file metadata - if "state" in field: - if value.lower() == "printing" or value == "paused": - self._internal_print_status = value - if value == "paused": - self.pause_printing_btn.setText(" Resume") - self.pause_printing_btn.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/play.svg") - ) - elif value == "printing": - self.pause_printing_btn.setText("Pause") - self.pause_printing_btn.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/pause.svg") - ) - self.request_query_print_stats.emit({"print_stats": ["filename"]}) - self.show_request.emit() - value = "start" # This is for event compatibility - elif value in ("cancelled", "complete", "error", "standby"): - self._current_file_name = "" - self._internal_print_status = "" - self.total_layers = "?" - self.file_metadata.clear() - self.hide_request.emit() - self.thumbnail_view.deleteLater() - self.thumbnail_view_layout.deleteLater() - - if hasattr(events, str("Print" + value.capitalize())): - event_obj = getattr(events, str("Print" + value.capitalize())) - event = event_obj(self._current_file_name, self.file_metadata) - try: - instance = QtWidgets.QApplication.instance() - if instance: - instance.postEvent(self.window(), event) - else: - raise TypeError( - "QApplication.instance expected non None value" - ) - except Exception as e: - logger.info( - f"Unexpected error while posting print job start event: {e}" - ) - + if self.isVisible(): + self.request_file_info.emit(value) if not self.file_metadata: return + if not self.isVisible(): + return if isinstance(value, dict): if "total_layer" in value.keys(): - self.total_layers = value["total_layer"] + self.total_layers = value.get("total_layer", "?") self.layer_display_button.secondary_text = str(self.total_layers) if "current_layer" in value.keys(): - if value["current_layer"] is not None: - _current_layer = value["current_layer"] - if _current_layer is not None: - self.layer_display_button.setText(f"{int(_current_layer)}") + _current_layer = value.get("current_layer", None) + if _current_layer: + self.layer_display_button.setText(f"{int(_current_layer)}") elif isinstance(value, float): if "total_duration" in field: - self.print_total_duration = value - _time = estimate_print_time(int(self.print_total_duration)) + _time = estimate_print_time(int(value)) _print_time_string = ( f"{_time[0]}Day {_time[1]}H {_time[2]}min {_time[3]} s" if _time[0] != 0 else f"{_time[1]}H {_time[2]}min {_time[3]}s" ) self.print_time_display_button.setText(_print_time_string) - elif "print_duration" in field: - self.current_print_duration_seconds = value - elif "filament_used" in field: - self.filament_used_mm = value @QtCore.pyqtSlot(str, list, name="on_gcode_move_update") def on_gcode_move_update(self, field: str, value: list) -> None: """Handle gcode move""" - if "gcode_position" in field: # Without offsets + if not self.isVisible(): + return + if "gcode_position" in field: if self._internal_print_status == "printing": _current_layer = calculate_current_layer( z_position=value[2], @@ -314,12 +314,10 @@ def virtual_sdcard_update(self, field: str, value: float | bool) -> None: field (str): Name of the updated field on the virtual_sdcard object value (float | bool): The updated information for the corresponding field """ - if isinstance(value, bool): - ... + if not self.isVisible(): + return if "progress" == field: self.printing_progress_bar.setValue(value) - if "file_position" == field: - ... def _setupUI(self) -> None: """Setup widget ui""" @@ -330,8 +328,6 @@ def _setupUI(self) -> None: sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(1) sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) - # ----------------------------------size policy - self.setSizePolicy(sizePolicy) self.setMinimumSize(QtCore.QSize(710, 420)) self.setMaximumSize(QtCore.QSize(720, 420)) @@ -369,7 +365,6 @@ def _setupUI(self) -> None: font = QtGui.QFont() font.setFamily("Montserrat") font.setPointSize(14) - # ------------------------------Header self.js_file_name_icon = BlocksLabel(parent=self) self.js_file_name_icon.setSizePolicy(sizePolicy) self.js_file_name_icon.setMinimumSize(QtCore.QSize(60, 60)) @@ -383,7 +378,6 @@ def _setupUI(self) -> None: QtGui.QPixmap(":/files/media/btn_icons/file_icon.svg"), ) self.js_file_name_icon.setObjectName("js_file_name_icon") - self.js_file_name_label = BlocksLabel(parent=self) self.js_file_name_label.setEnabled(True) self.js_file_name_label.setSizePolicy(sizePolicy) @@ -395,7 +389,6 @@ def _setupUI(self) -> None: self.js_file_name_label.setObjectName("js_file_name_label") self.job_status_header_layout.addWidget(self.js_file_name_icon) self.job_status_header_layout.addWidget(self.js_file_name_label) - # -----------------------------buttons font.setPointSize(18) self.pause_printing_btn = BlocksCustomButton(self) self.pause_printing_btn.setSizePolicy(sizePolicy) @@ -430,45 +423,34 @@ def _setupUI(self) -> None: self.tune_menu_btn.setText("Tune") self.stop_printing_btn.setText("Cancel") self.pause_printing_btn.setText("Pause") - # -----------------------------Progress bar self.printing_progress_bar = CustomProgressBar(self) self.printing_progress_bar.setMinimumHeight(150) self.printing_progress_bar.setObjectName("printing_progress_bar") self.printing_progress_bar.setSizePolicy(sizePolicy) self.job_status_progress_layout.addWidget(self.printing_progress_bar) - - # -----------------------------display buttons - self.layer_display_button = DisplayButton(self) self.layer_display_button.button_type = "display_secondary" self.layer_display_button.setEnabled(False) self.layer_display_button.setSizePolicy(sizePolicy) - self.layer_display_button.setMinimumSize(QtCore.QSize(200, 80)) - self.layer_display_button.setProperty( "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/layers.svg") ) self.layer_display_button.setObjectName("layer_display_button") - self.print_time_display_button = DisplayButton(self) self.print_time_display_button.button_type = "normal" self.print_time_display_button.setEnabled(False) self.print_time_display_button.setSizePolicy(sizePolicy) - self.print_time_display_button.setMinimumSize(QtCore.QSize(200, 80)) - self.print_time_display_button.setProperty( "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/time.svg") ) self.print_time_display_button.setObjectName("print_time_display_button") - self.job_stats_display_layout.addWidget( self.layer_display_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) - self.job_stats_display_layout.addWidget( self.print_time_display_button, 0, @@ -480,7 +462,6 @@ def create_thumbnail_widget(self) -> None: """Create thumbnail graphics view widget""" self.thumbnail_view = QtWidgets.QGraphicsView() self.thumbnail_view.setMinimumSize(QtCore.QSize(48, 48)) - # self.thumbnail_view.setMaximumSize(QtCore.QSize(300, 300)) self.thumbnail_view.setAttribute( QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True ) diff --git a/BlocksScreen/lib/utils/blocks_progressbar.py b/BlocksScreen/lib/utils/blocks_progressbar.py index a05aafa5..414097bb 100644 --- a/BlocksScreen/lib/utils/blocks_progressbar.py +++ b/BlocksScreen/lib/utils/blocks_progressbar.py @@ -40,27 +40,29 @@ def set_pen_width(self, value) -> None: self._pen_width = value self.update() + def _scale_pixmap(self) -> None: + self._inner_rect = self._calculate_inner_geometry() + self._pixmap_cached = self._pixmap.scaled( + self._inner_rect.size().toSize(), + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, + ) + def set_inner_pixmap(self, pixmap: QtGui.QPixmap) -> None: """Set the inner icon pixmap on the progress bar circumference. """ self._pixmap = pixmap - self.update() + self._scale_pixmap() def resizeEvent(self, a0) -> None: - """Re-implemented method, handle widget resize Events + """Reimplemented method, handle widget resize Events Currently rescales the set pixmap so it has the optimal size. """ - self._inner_rect = self._calculate_inner_geometry() - self._pixmap_cached = self._pixmap.scaled( - self._inner_rect.size().toSize(), - QtCore.Qt.AspectRatioMode.KeepAspectRatio, - QtCore.Qt.TransformationMode.SmoothTransformation, - ) + self._scale_pixmap() self.update() - return super().resizeEvent(a0) def sizeHint(self) -> QtCore.QSize: """Re-implemented method, preferable widget size""" From de9fe96183bb0418fb72a47b80cb6ce519c7d56b Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Fri, 2 Jan 2026 11:57:27 +0000 Subject: [PATCH 19/70] Refactor `SensorPanel`: replace `QListWidgetItem` with `EntryListModel` (#125) * refactor: migrated sensorsPanel.py sensor list from a QtWidgets.QListWidgetItem to a ListModelView Arq with some bugfixes Signed-off-by: Guilherme Costa * sensors: resolve bugs and some cleanup Signed-off-by: Guilherme Costa * sensors: add cutter sensor handling and visual update for list item Signed-off-by: Guilherme Costa * code cleanup and formatting Signed-off-by: Guilherme Costa * sensorwidget bugfix Signed-off-by: Guilherme Costa * sensorsPanel.py: ensure first item is checked on startup sensorWidget.py: lock toggle_button until action succeeds and replace repaint() with update() for proper refresh Signed-off-by: Guilherme Costa * sensorsPanel.py: reformat code to be complient with ruff guidelines Signed-off-by: Guilherme Costa --------- Signed-off-by: Guilherme Costa Co-authored-by: Guilherme Costa Co-authored-by: HugoCLSC --- .../lib/panels/widgets/sensorWidget.py | 213 ++++++--- .../lib/panels/widgets/sensorsPanel.py | 426 ++++++++++++------ 2 files changed, 428 insertions(+), 211 deletions(-) diff --git a/BlocksScreen/lib/panels/widgets/sensorWidget.py b/BlocksScreen/lib/panels/widgets/sensorWidget.py index 5223ac83..e0ed9955 100644 --- a/BlocksScreen/lib/panels/widgets/sensorWidget.py +++ b/BlocksScreen/lib/panels/widgets/sensorWidget.py @@ -1,4 +1,5 @@ import enum +import typing from lib.utils.blocks_label import BlocksLabel from lib.utils.toggleAnimatedButton import ToggleAnimatedButton @@ -30,8 +31,12 @@ class SensorState(enum.IntEnum): OFF = False ON = True + run_gcode_signal: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="run_gcode" + ) + def __init__(self, parent, sensor_name: str): - super(SensorWidget, self).__init__(parent) + super().__init__(parent) self.name = str(sensor_name).split(" ")[1] self.sensor_type: SensorWidget.SensorType = ( self.SensorType.SWITCH @@ -40,18 +45,18 @@ def __init__(self, parent, sensor_name: str): ) self.setObjectName(self.name) - self.setMinimumSize(parent.contentsRect().width(), 60) + self.setMinimumSize(250, 250) self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self._sensor_type: SensorWidget.SensorType = self.SensorType.SWITCH self._flags: SensorWidget.SensorFlags = self.SensorFlags.CLICKABLE self.filament_state: SensorWidget.FilamentState = ( - SensorWidget.FilamentState.MISSING + SensorWidget.FilamentState.PRESENT ) - self.sensor_state: SensorWidget.SensorState = SensorWidget.SensorState.OFF + self.sensor_state: SensorWidget.SensorState = SensorWidget.SensorState.ON self._icon_label = None self._text_label = None - self._text: str = str(self.sensor_type.name) + " Sensor: " + str(self.name) + self._text = self.name self._item_rect: QtCore.QRect = QtCore.QRect() self.icon_pixmap_fp: QtGui.QPixmap = QtGui.QPixmap( ":/filament_related/media/btn_icons/filament_sensor_turn_on.svg" @@ -60,6 +65,7 @@ def __init__(self, parent, sensor_name: str): ":/filament_related/media/btn_icons/filament_sensor_off.svg" ) self._setupUI() + self.toggle_button.stateChange.connect(self.toggle_sensor_state) @property def type(self) -> SensorType: @@ -90,24 +96,40 @@ def text(self, new_text) -> None: self._text_label.setText(f"{new_text}") self._text = new_text - @QtCore.pyqtSlot(bool, name="change_fil_sensor_state") + @QtCore.pyqtSlot(FilamentState, name="change_fil_sensor_state") def change_fil_sensor_state(self, state: FilamentState): - """Change filament sensor state""" - if isinstance(state, SensorWidget.FilamentState): - self.filament_state = state + """Invert the filament state in response to a Klipper update""" + if not isinstance(state, SensorWidget.FilamentState): + return + self.filament_state = SensorWidget.FilamentState(not state.value) + self.update() + + def toggle_button_state(self, state: ToggleAnimatedButton.State) -> None: + """Called when the Klipper firmware reports an update to the filament sensor state""" + self.toggle_button.setDisabled(False) + if state.value != self.sensor_state.value: + self.sensor_state = self.SensorState(state.value) + self.toggle_button.state = ToggleAnimatedButton.State( + self.sensor_state.value + ) + self.update() + + @QtCore.pyqtSlot(ToggleAnimatedButton.State, name="state-change") + def toggle_sensor_state(self, state: ToggleAnimatedButton.State) -> None: + """Emit the appropriate G-Code command to change the filament sensor state.""" + if state.value != self.sensor_state.value: + self.toggle_button.setDisabled(True) + self.run_gcode_signal.emit( + f"SET_FILAMENT_SENSOR SENSOR={self.text} ENABLE={int(state.value)}" + ) + self.update() + + def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: + """Handle widget resize events.""" + return super().resizeEvent(a0) def paintEvent(self, a0: QtGui.QPaintEvent) -> None: """Re-implemented method, paint widget""" - # if ( - # self._scaled_select_on_pixmap is not None - # and self._scaled_select_off_pixmap is not None - # ): # Update the toggle button pixmap which indicates the sensor state - # self._button_icon_label.setPixmap( - # self._scaled_select_on_pixmap - # if self.sensor_state == SensorWidget.SensorState.ON - # else self._scaled_select_off_pixmap - # ) - style_painter = QtWidgets.QStylePainter(self) style_painter.setRenderHint(style_painter.RenderHint.Antialiasing, True) style_painter.setRenderHint( @@ -116,79 +138,128 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: style_painter.setRenderHint( style_painter.RenderHint.LosslessImageRendering, True ) - - if self.filament_state == SensorWidget.FilamentState.PRESENT: - _color = QtGui.QColor(2, 204, 59, 100) - else: - _color = QtGui.QColor(204, 50, 50, 100) - _brush = QtGui.QBrush() - _brush.setColor(_color) - - _brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - pen = style_painter.pen() - pen.setStyle(QtCore.Qt.PenStyle.NoPen) if self._icon_label: self._icon_label.setPixmap( self.icon_pixmap_fp if self.filament_state == self.FilamentState.PRESENT else self.icon_pixmap_fnp ) - background_rect = QtGui.QPainterPath() - background_rect.addRoundedRect( - self.contentsRect().toRectF(), - 15, - 15, - QtCore.Qt.SizeMode.AbsoluteSize, - ) - style_painter.setBrush(_brush) - style_painter.fillPath(background_rect, _brush) - style_painter.end() + _font = QtGui.QFont() + _font.setPointSize(20) + style_painter.setFont(_font) - @property - def toggle_sensor_gcode_command(self) -> str: - """Toggle filament sensor""" - self.sensor_state = ( - SensorWidget.SensorState.ON - if self.sensor_state == SensorWidget.SensorState.OFF - else SensorWidget.SensorState.OFF + label_name = self._text_label_name_ + label_detected = self._text_label_detected + label_state = self._text_label_state + + palette = label_name.palette() + palette.setColor(palette.ColorRole.WindowText, QtGui.QColorConstants.White) + style_painter.drawItemText( + label_name.geometry(), + label_name.alignment(), + palette, + True, + label_name.text(), + QtGui.QPalette.ColorRole.WindowText, + ) + + _font.setPointSize(16) + style_painter.setFont(_font) + filament_text = self.filament_state.name.capitalize() + tab_spacer = 12 * "\t" + style_painter.drawItemText( + label_state.geometry(), + label_state.alignment(), + palette, + True, + f"Filament: {tab_spacer}{filament_text}", + QtGui.QPalette.ColorRole.WindowText, ) - return str( - f"SET_FILAMENT_SENSOR SENSOR={self.text} ENABLE={not self.sensor_state.value}" + + sensor_state_text = self.sensor_state.name.capitalize() + tab_spacer += 3 * "\t" + style_painter.drawItemText( + label_detected.geometry(), + label_detected.alignment(), + palette, + True, + f"Enable: {tab_spacer}{sensor_state_text}", + QtGui.QPalette.ColorRole.WindowText, ) + style_painter.end() def _setupUI(self): _policy = QtWidgets.QSizePolicy.Policy.MinimumExpanding size_policy = QtWidgets.QSizePolicy(_policy, _policy) size_policy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.setSizePolicy(size_policy) - self.sensor_horizontal_layout = QtWidgets.QHBoxLayout() - self.sensor_horizontal_layout.setGeometry(QtCore.QRect(0, 0, 640, 60)) - self.sensor_horizontal_layout.setObjectName("sensorHorizontalLayout") + self.sensor_vertical_layout = QtWidgets.QVBoxLayout() + self.sensor_vertical_layout.setObjectName("sensorVerticalLayout") self._icon_label = BlocksLabel(self) size_policy.setHeightForWidth(self._icon_label.sizePolicy().hasHeightForWidth()) + parent_width = self.parentWidget().width() self._icon_label.setSizePolicy(size_policy) - self._icon_label.setMinimumSize(60, 60) - self._icon_label.setMaximumSize(60, 60) + self._icon_label.setMinimumSize(120, 100) + self._icon_label.setPixmap( self.icon_pixmap_fp if self.filament_state == self.FilamentState.PRESENT else self.icon_pixmap_fnp ) - self.sensor_horizontal_layout.addWidget(self._icon_label) - self._text_label = QtWidgets.QLabel(parent=self) - size_policy.setHeightForWidth(self._text_label.sizePolicy().hasHeightForWidth()) - self._text_label.setMinimumSize(100, 60) - self._text_label.setMaximumSize(500, 60) - _font = QtGui.QFont() - _font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - _font.setPointSize(18) - palette = self._text_label.palette() - palette.setColor(palette.ColorRole.WindowText, QtGui.QColorConstants.White) - self._text_label.setPalette(palette) - self._text_label.setFont(_font) - self._text_label.setText(str(self._text)) - self.sensor_horizontal_layout.addWidget(self._text_label) + self._text_label_name_ = QtWidgets.QLabel(parent=self) + size_policy.setHeightForWidth( + self._text_label_name_.sizePolicy().hasHeightForWidth() + ) + self._text_label_name_.setMinimumSize(self.rect().width(), 40) + self._text_label_name_.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + palette = self._text_label_name_.palette() + palette.setColor( + palette.ColorRole.WindowText, QtGui.QColorConstants.Transparent + ) + self._text_label_name_.setPalette(palette) + self._text_label_name_.setText(str(self._text)) + self._icon_label.setSizePolicy(size_policy) + + self._text_label_detected = QtWidgets.QLabel(parent=self) + size_policy.setHeightForWidth( + self._text_label_detected.sizePolicy().hasHeightForWidth() + ) + self._text_label_detected.setMinimumSize(parent_width, 20) + + self._text_label_detected.setPalette(palette) + self._text_label_detected.setText(f"Filament: {self.filament_state}") + + self._text_label_state = QtWidgets.QLabel(parent=self) + size_policy.setHeightForWidth( + self._text_label_state.sizePolicy().hasHeightForWidth() + ) + self._text_label_state.setMinimumSize(parent_width, 20) + + self._text_label_state.setPalette(palette) + self._text_label_state.setText(f"Enable: {self.sensor_state.name}") + + self._icon_label.setSizePolicy(size_policy) self.toggle_button = ToggleAnimatedButton(self) - self.toggle_button.setMaximumWidth(100) - self.sensor_horizontal_layout.addWidget(self.toggle_button) - self.setLayout(self.sensor_horizontal_layout) + self.toggle_button.setMinimumSize(100, 50) + self.toggle_button.state = ToggleAnimatedButton.State.ON + + self.sensor_vertical_layout.addWidget( + self._icon_label, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.sensor_vertical_layout.addWidget( + self._text_label_name_, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.sensor_vertical_layout.addStretch() + self.sensor_vertical_layout.addWidget( + self._text_label_state, alignment=QtCore.Qt.AlignmentFlag.AlignLeft + ) + self.sensor_vertical_layout.addStretch() + self.sensor_vertical_layout.addWidget( + self._text_label_detected, alignment=QtCore.Qt.AlignmentFlag.AlignLeft + ) + self.sensor_vertical_layout.addStretch() + self.sensor_vertical_layout.addWidget( + self.toggle_button, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + ) + + self.setLayout(self.sensor_vertical_layout) diff --git a/BlocksScreen/lib/panels/widgets/sensorsPanel.py b/BlocksScreen/lib/panels/widgets/sensorsPanel.py index 51a5b2c1..29eea5e7 100644 --- a/BlocksScreen/lib/panels/widgets/sensorsPanel.py +++ b/BlocksScreen/lib/panels/widgets/sensorsPanel.py @@ -1,7 +1,9 @@ import typing -from lib.panels.widgets.sensorWidget import SensorWidget +from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.icon_button import IconButton +from lib.panels.widgets.sensorWidget import SensorWidget +from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem from PyQt6 import QtCore, QtGui, QtWidgets @@ -15,80 +17,85 @@ class SensorsWindow(QtWidgets.QWidget): request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="request_back" ) - sensor_list: list[SensorWidget] = [] def __init__(self, parent): super(SensorsWindow, self).__init__(parent) + self.model = EntryListModel() + self.entry_delegate = EntryDelegate() + self.sensor_tracking_widget = {} + self.current_widget = None + self.sensor_list: list[SensorWidget] = [] self._setupUi() self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) self.setTabletTracking(True) - self.fs_sensors_list.itemClicked.connect(self.handle_sensor_clicked) - self.fs_sensors_list.itemClicked self.fs_back_button.clicked.connect(self.request_back) + def reset_view_model(self) -> None: + """Clears items from ListView + (Resets `QAbstractListModel` by clearing entries) + """ + self.model.clear() + self.entry_delegate.clear() + @QtCore.pyqtSlot(dict, name="handle_available_fil_sensors") def handle_available_fil_sensors(self, sensors: dict) -> None: - """Handle available filament sensors, create `SensorWidget` for each detected - sensor - """ + """Handle available filament sensors, create `SensorWidget` for each detected sensor""" if not isinstance(sensors, dict): return + self.reset_view_model() filtered_sensors = list( filter( lambda printer_obj: str(printer_obj).startswith( "filament_switch_sensor" ) - or str(printer_obj).startswith("filament_motion_sensor"), + or str(printer_obj).startswith("filament_motion_sensor") + or str(printer_obj).startswith("cutter_sensor"), sensors.keys(), ) ) if filtered_sensors: - self.fs_sensors_list.setRowHidden(self.fs_sensors_list.row(self.item), True) self.sensor_list = [ self.create_sensor_widget(name=sensor) for sensor in filtered_sensors ] + self.model.setData(self.model.index(0), True, EntryListModel.EnableRole) else: - self.fs_sensors_list.setRowHidden( - self.fs_sensors_list.row(self.item), False - ) + self.no_update_placeholder.show() @QtCore.pyqtSlot(str, str, bool, name="handle_fil_state_change") def handle_fil_state_change( self, sensor_name: str, parameter: str, value: bool ) -> None: - """Handle filament state chage""" - if sensor_name in self.sensor_list: - _split = sensor_name.split(" ") - _item = self.fs_sensors_list.findChild( - SensorWidget, - name=_split[1], - options=QtCore.Qt.FindChildOption.FindChildrenRecursively, - ) + """Handle Klipper signals for filament sensor changes""" + _item = self.sensor_tracking_widget.get(sensor_name) + if _item: if parameter == "filament_detected": - if isinstance(_item, SensorWidget) and hasattr( - _item, "change_fil_sensor_state" - ): - _item.change_fil_sensor_state(SensorWidget.FilamentState.PRESENT) - _item.repaint() - elif parameter == "filament_missing": - if isinstance(_item, SensorWidget) and hasattr( - _item, "change_fil_sensor_state" - ): - _item.change_fil_sensor_state(SensorWidget.FilamentState.MISSING) - _item.repaint() + state = SensorWidget.FilamentState(not value) + _item.change_fil_sensor_state(state) elif parameter == "enabled": - if _item and isinstance(_item, SensorWidget): - self.run_gcode_signal.emit(_item.toggle_sensor_gcode_command) - - @QtCore.pyqtSlot(QtWidgets.QListWidgetItem, name="handle_sensor_clicked") - def handle_sensor_clicked(self, sensor: QtWidgets.QListWidgetItem) -> None: - """Handle filament sensor clicked""" - _item = self.fs_sensors_list.itemWidget(sensor) - # FIXME: This is just not working - _item.toggle_button.state = ~_item.toggle_button.state - if _item and isinstance(_item, SensorWidget): - self.run_gcode_signal.emit(_item.toggle_sensor_gcode_command) + _item.toggle_button_state(SensorWidget.SensorState(value)) + + def showEvent(self, event: QtGui.QShowEvent | None) -> None: + """Re-add clients to update list""" + return super().showEvent(event) + + @QtCore.pyqtSlot(ListItem, name="on-item-clicked") + def on_item_clicked(self, item: ListItem) -> None: + """Setup information for the currently clicked list item on the info box. + Keeps track of the list item + """ + if not item: + return + + if self.current_widget: + self.current_widget.hide() + + name_id = item.text + current_widget = self.sensor_tracking_widget.get(name_id) + if current_widget is None: + return + self.current_widget = current_widget + self.current_widget.show() def create_sensor_widget(self, name: str) -> SensorWidget: """Creates a sensor row to be added to the QListWidget @@ -96,132 +103,271 @@ def create_sensor_widget(self, name: str) -> SensorWidget: Args: name (str): The name of the filament sensor object """ - _item_widget = SensorWidget(self.fs_sensors_list, name) - _list_item = QtWidgets.QListWidgetItem() - _list_item.setFlags(~QtCore.Qt.ItemFlag.ItemIsEditable) - _list_item.setSizeHint( - QtCore.QSize(self.fs_sensors_list.contentsRect().width(), 80) - ) - _item_widget.toggle_button.stateChange.connect( - lambda: self.fs_sensors_list.itemClicked.emit(_item_widget) - ) + _item_widget = SensorWidget(self.infobox_frame, name) + self.info_box_layout.addWidget(_item_widget) - self.fs_sensors_list.setItemWidget(_list_item, _item_widget) + if self.current_widget: + _item_widget.hide() + else: + _item_widget.show() + self.current_widget = _item_widget + name_id = str(name).split(" ")[1] + item = ListItem( + text=name_id, + right_text="", + right_icon=self.pixmap, + left_icon=None, + callback=None, + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=80, + notificate=False, + ) + _item_widget.run_gcode_signal.connect(self.run_gcode_signal) + self.sensor_tracking_widget[name_id] = _item_widget + self.model.add_item(item) return _item_widget - def _setupUi(self): - self.setObjectName("filament_sensors_page") + def _setupUi(self) -> None: + """Setup UI for updatePage""" + font_id = QtGui.QFontDatabase.addApplicationFont( + ":/font/media/fonts for text/Momcake-Bold.ttf" + ) + font_family = QtGui.QFontDatabase.applicationFontFamilies(font_id)[0] sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, ) - self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) + sizePolicy.setHorizontalStretch(1) + sizePolicy.setVerticalStretch(1) self.setSizePolicy(sizePolicy) - self.setMinimumSize(QtCore.QSize(710, 410)) + self.setMinimumSize(QtCore.QSize(710, 400)) self.setMaximumSize(QtCore.QSize(720, 420)) - self.content_vertical_layout = QtWidgets.QVBoxLayout() - self.content_vertical_layout.setObjectName("contentVerticalLayout") - self.fs_header_layout = QtWidgets.QHBoxLayout() - self.fs_header_layout.setContentsMargins(0, 0, 0, 0) - self.fs_header_layout.setObjectName("fs_header_layout") - self.fs_header_layout.setGeometry(QtCore.QRect(10, 10, 691, 71)) - self.fs_page_title = QtWidgets.QLabel(parent=self) - sizePolicy.setHeightForWidth( - self.fs_page_title.sizePolicy().hasHeightForWidth() - ) - self.fs_page_title.setSizePolicy(sizePolicy) - self.fs_page_title.setMinimumSize(QtCore.QSize(300, 71)) - self.fs_page_title.setMaximumSize(QtCore.QSize(16777215, 71)) + self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.update_page_content_layout = QtWidgets.QVBoxLayout() + self.update_page_content_layout.setContentsMargins(15, 15, 2, 2) + + self.header_content_layout = QtWidgets.QHBoxLayout() + self.header_content_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + self.fs_page_title = QtWidgets.QLabel(self) + self.fs_page_title.setMinimumSize(QtCore.QSize(100, 60)) + self.fs_page_title.setMaximumSize(QtCore.QSize(16777215, 60)) font = QtGui.QFont() - font.setPointSize(22) - palette = QtGui.QPalette() - palette.setColor(palette.ColorRole.WindowText, QtGui.QColorConstants.White) - self.fs_page_title.setPalette(palette) + font.setFamily(font_family) + font.setPointSize(24) + palette = self.fs_page_title.palette() + palette.setColor(palette.ColorRole.WindowText, QtGui.QColor("#FFFFFF")) self.fs_page_title.setFont(font) + self.fs_page_title.setPalette(palette) + self.fs_page_title.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) + self.fs_page_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.fs_page_title.setObjectName("fs_page_title") - self.fs_header_layout.addWidget(self.fs_page_title, 0) + self.fs_page_title.setText("Filament Sensors") + self.header_content_layout.addWidget(self.fs_page_title, 0) self.fs_back_button = IconButton(self) - sizePolicy.setHeightForWidth( - self.fs_back_button.sizePolicy().hasHeightForWidth() - ) - self.fs_back_button.setSizePolicy(sizePolicy) self.fs_back_button.setMinimumSize(QtCore.QSize(60, 60)) self.fs_back_button.setMaximumSize(QtCore.QSize(60, 60)) self.fs_back_button.setFlat(True) self.fs_back_button.setPixmap(QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) - self.fs_back_button.setObjectName("fs_back_button") - self.fs_header_layout.addWidget( - self.fs_back_button, - 0, + self.header_content_layout.addWidget(self.fs_back_button, 0) + self.update_page_content_layout.addLayout(self.header_content_layout, 0) + + self.main_content_layout = QtWidgets.QHBoxLayout() + self.main_content_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.sensor_buttons_frame = BlocksCustomFrame(self) + + self.sensor_buttons_frame.setMinimumSize(QtCore.QSize(320, 300)) + self.sensor_buttons_frame.setMaximumSize(QtCore.QSize(450, 500)) + + palette = QtGui.QPalette() + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorRole.Button, + brush, ) - self.content_vertical_layout.addLayout(self.fs_header_layout) - self.fs_sensors_list = QtWidgets.QListWidget(self) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.MinimumExpanding, - QtWidgets.QSizePolicy.Policy.MinimumExpanding, + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) + palette.setBrush( + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorRole.Base, + brush, ) - sizePolicy.setHorizontalStretch(1) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth( - self.fs_sensors_list.sizePolicy().hasHeightForWidth() + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorRole.Window, + brush, ) - self.fs_sensors_list.setSizePolicy(sizePolicy) - self.fs_sensors_list.setMinimumSize(QtCore.QSize(650, 300)) - self.fs_sensors_list.setMaximumSize(QtCore.QSize(700, 300)) - self.fs_sensors_list.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.fs_sensors_list.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.fs_sensors_list.setObjectName("fs_sensors_list") - self.fs_sensors_list.setViewMode(self.fs_sensors_list.ViewMode.ListMode) - self.fs_sensors_list.setItemAlignment( - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter - ) - self.fs_sensors_list.setFlow(self.fs_sensors_list.Flow.TopToBottom) - self.fs_sensors_list.setFrameStyle(0) - palette = self.fs_sensors_list.palette() - palette.setColor(palette.ColorRole.Base, QtGui.QColorConstants.Transparent) + brush = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorRole.Highlight, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorRole.Link, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Inactive, + QtGui.QPalette.ColorRole.Button, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) + palette.setBrush( + QtGui.QPalette.ColorGroup.Inactive, + QtGui.QPalette.ColorRole.Base, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Inactive, + QtGui.QPalette.ColorRole.Window, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Inactive, + QtGui.QPalette.ColorRole.Highlight, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Inactive, + QtGui.QPalette.ColorRole.Link, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.Button, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.Base, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.Window, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.Highlight, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.Link, + brush, + ) + self.fs_sensors_list = QtWidgets.QListView(self.sensor_buttons_frame) + self.fs_sensors_list.setModel(self.model) + self.fs_sensors_list.setItemDelegate(self.entry_delegate) + self.entry_delegate.item_selected.connect(self.on_item_clicked) + self.fs_sensors_list.setMouseTracking(True) + self.fs_sensors_list.setTabletTracking(True) + self.fs_sensors_list.setSpacing(7) self.fs_sensors_list.setPalette(palette) - self.fs_sensors_list.setDropIndicatorShown(False) - self.fs_sensors_list.setAcceptDrops(False) + self.fs_sensors_list.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.fs_sensors_list.setStyleSheet("background-color:transparent") + self.fs_sensors_list.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + self.fs_sensors_list.setMinimumSize(self.sensor_buttons_frame.size()) + self.fs_sensors_list.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.fs_sensors_list.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.fs_sensors_list.setHorizontalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.fs_sensors_list.setSizeAdjustPolicy( + QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents + ) + self.fs_sensors_list.setAutoScroll(False) self.fs_sensors_list.setProperty("showDropIndicator", False) - self.content_vertical_layout.setStretch(0, 0) - self.content_vertical_layout.setStretch(1, 1) - self.content_vertical_layout.addWidget( + self.fs_sensors_list.setDefaultDropAction(QtCore.Qt.DropAction.IgnoreAction) + self.fs_sensors_list.setAlternatingRowColors(False) + self.fs_sensors_list.setSelectionMode( + QtWidgets.QAbstractItemView.SelectionMode.NoSelection + ) + self.fs_sensors_list.setSelectionBehavior( + QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems + ) + self.fs_sensors_list.setVerticalScrollMode( + QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel + ) + self.fs_sensors_list.setHorizontalScrollMode( + QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel + ) + QtWidgets.QScroller.grabGesture( self.fs_sensors_list, - 1, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtWidgets.QScroller.ScrollerGestureType.TouchGesture, ) - - font = QtGui.QFont() - font.setPointSize(25) - - self.item = QtWidgets.QListWidgetItem() - self.item.setSizeHint( - QtCore.QSize(self.fs_sensors_list.width(), self.fs_sensors_list.height()) + QtWidgets.QScroller.grabGesture( + self.fs_sensors_list, + QtWidgets.QScroller.ScrollerGestureType.LeftMouseButtonGesture, ) + self.sensor_buttons_layout = QtWidgets.QVBoxLayout() + self.sensor_buttons_layout.setContentsMargins(15, 20, 20, 5) + self.sensor_buttons_layout.addWidget(self.fs_sensors_list, 0) + self.sensor_buttons_frame.setLayout(self.sensor_buttons_layout) - self.label = QtWidgets.QLabel("No sensors found") - self.label.setFont(font) - self.label.setStyleSheet("color: gray;") - self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.label.hide() + self.main_content_layout.addWidget(self.sensor_buttons_frame, 0) - self.fs_sensors_list.addItem(self.item) - self.fs_sensors_list.setItemWidget(self.item, self.label) + self.infobox_frame = BlocksCustomFrame() + self.infobox_frame.setMinimumSize(QtCore.QSize(250, 300)) + self.infobox_frame.setMaximumSize(QtCore.QSize(450, 500)) - self.content_vertical_layout.addSpacing(5) - self.setLayout(self.content_vertical_layout) - self._retranslateUi() + self.info_box_layout = QtWidgets.QVBoxLayout() + self.info_box_layout.setContentsMargins(0, 0, 0, 0) - def _retranslateUi(self): - _translate = QtCore.QCoreApplication.translate - self.setWindowTitle(_translate("filament_sensors_page", "Form")) - self.fs_page_title.setText( - _translate("filament_sensors_page", "Filament Sensors") - ) - self.fs_back_button.setProperty( - "button_type", _translate("filament_sensors_page", "icon") + font = QtGui.QFont() + font.setFamily(font_family) + font.setPointSize(20) + self.version_box = QtWidgets.QHBoxLayout() + self.no_update_placeholder = QtWidgets.QLabel(self) + self.no_update_placeholder.setMinimumSize(QtCore.QSize(200, 60)) + self.no_update_placeholder.setMaximumSize(QtCore.QSize(300, 60)) + self.no_update_placeholder.setFont(font) + self.no_update_placeholder.setPalette(palette) + self.no_update_placeholder.setSizePolicy(sizePolicy) + self.no_update_placeholder.setText("No Sensors Available") + self.no_update_placeholder.setWordWrap(True) + self.no_update_placeholder.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.info_box_layout.addWidget( + self.no_update_placeholder, 0, QtCore.Qt.AlignmentFlag.AlignBottom ) + self.pixmap = QtGui.QPixmap(":/ui/media/btn_icons/info.svg") + self.no_update_placeholder.hide() + self.infobox_frame.setLayout(self.info_box_layout) + self.main_content_layout.addWidget(self.infobox_frame, 1) + self.update_page_content_layout.addLayout(self.main_content_layout, 1) + self.setLayout(self.update_page_content_layout) From 4d973b6819a5fae2d4f97d6747cca8402e7edee9 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Fri, 2 Jan 2026 12:02:10 +0000 Subject: [PATCH 20/70] Refactor `filesPage.py`: Changed Files List `QtWidgets.QListWidgetItem` to our custom `EntryListModel` (#126) * FilesPage: Refactor almost complete missing passing the directory by itemclick helper_methods: updated the get_file_loc method to return always the filename instead of the full path Signed-off-by: Guilherme Costa * filesPage.py: refactor concluded, scrollbar bugfix Signed-off-by: Guilherme Costa * helper_method.py: Change naming of a method from get_file_loc to get_file_name Signed-off-by: Guilherme Costa * filesPage.py: code cleanup, small docstring generation and add missing commented lines Signed-off-by: Guilherme Costa * filesPage.py: remove unused lines of code Signed-off-by: Guilherme Costa * filesPage.py - fix rebase conflits Signed-off-by: Guilherme Costa * formatting fix Signed-off-by: Guilherme Costa * filesPage.py: Add sync timeout and change click behavior in files page Set file list to refresh after 1.5s to improve UI responsiveness. Clicks are now handled in the _on_item_selected slot instead of inline callbacks to separate logic and unify behavior. --------- Signed-off-by: Guilherme Costa Signed-off-by: gmmcosta15 Co-authored-by: Guilherme Costa Co-authored-by: HugoCLSC --- BlocksScreen/helper_methods.py | 29 +++ BlocksScreen/lib/panels/widgets/filesPage.py | 250 ++++++++++--------- 2 files changed, 165 insertions(+), 114 deletions(-) diff --git a/BlocksScreen/helper_methods.py b/BlocksScreen/helper_methods.py index 0dd2b2db..8de5fc13 100644 --- a/BlocksScreen/helper_methods.py +++ b/BlocksScreen/helper_methods.py @@ -324,3 +324,32 @@ def check_file_on_path( """Check if file exists on path. Returns true if file exists on that specified directory""" _filepath = os.path.join(path, filename) return os.path.exists(_filepath) + + +def get_file_loc(filename) -> pathlib.Path: ... + + +def get_file_name(filename: typing.Optional[str]) -> str: + # If filename is None or empty, return empty string instead of None + if not filename: + return "" + # Remove trailing slashes or backslashes + filename = filename.rstrip("/\\") + + # Normalize Windows backslashes to forward slashes + filename = filename.replace("\\", "/") + + parts = filename.split("/") + + # Split and return the last path component + return parts[-1] if filename else "" + + +# def get_hash(data) -> hashlib._Hash: +# hash = hashlib.sha256() +# hash.update(data.encode()) +# hash.digest() +# return hash + + +def digest_hash() -> None: ... diff --git a/BlocksScreen/lib/panels/widgets/filesPage.py b/BlocksScreen/lib/panels/widgets/filesPage.py index fbfb3377..f8fa490f 100644 --- a/BlocksScreen/lib/panels/widgets/filesPage.py +++ b/BlocksScreen/lib/panels/widgets/filesPage.py @@ -5,9 +5,10 @@ import helper_methods from lib.utils.blocks_Scrollbar import CustomScrollBar from lib.utils.icon_button import IconButton -from lib.utils.list_button import ListCustomButton from PyQt6 import QtCore, QtGui, QtWidgets +from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem + logger = logging.getLogger("logs/BlocksScreen.log") @@ -35,7 +36,9 @@ class FilesPage(QtWidgets.QWidget): directories: list = [] def __init__(self, parent) -> None: - super().__init__(parent) + super().__init__() + self.model = EntryListModel() + self.entry_delegate = EntryDelegate() self._setupUI() self.setMouseTracking(True) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) @@ -50,6 +53,33 @@ def __init__(self, parent) -> None: ) self.back_btn.clicked.connect(self.reset_dir) + self.entry_delegate.item_selected.connect(self._on_item_selected) + self._refresh_one_and_half_sec_timer = QtCore.QTimer() + self._refresh_one_and_half_sec_timer.timeout.connect( + lambda: self.request_dir_info[str].emit(self.curr_dir) + ) + self._refresh_one_and_half_sec_timer.start(1500) + + @QtCore.pyqtSlot(ListItem, name="on-item-selected") + def _on_item_selected(self, item: ListItem) -> None: + """Slot called when a list item is selected in the UI. + This method is connected to the `item_selected` signal of the entry delegate. + It handles the selection of a `ListItem` and process it accoding it with its type + Args: + item : ListItem The item that was selected by the user. + """ + if not item.left_icon: + filename = self.curr_dir + "/" + item.text + ".gcode" + self._fileItemClicked(filename) + else: + if item.text == "Go Back": + go_back_path = os.path.dirname(self.curr_dir) + if go_back_path == "/": + go_back_path = "" + self._on_goback_dir(go_back_path) + else: + self._dirItemClicked("/" + item.text) + @QtCore.pyqtSlot(name="reset-dir") def reset_dir(self) -> None: """Reset current directory""" @@ -76,7 +106,7 @@ def on_directories(self, directories_data: list) -> None: @QtCore.pyqtSlot(dict, name="on-fileinfo") def on_fileinfo(self, filedata: dict) -> None: - """Handle receive file information/metadata""" + """Method called per file to contruct file entry to the list""" if not filedata or not self.isVisible(): return filename = filedata.get("filename", "") @@ -104,53 +134,53 @@ def on_fileinfo(self, filedata: dict) -> None: else: time_str = f"{minutes}m" - list_items = [self.listWidget.item(i) for i in range(self.listWidget.count())] - if not list_items: - return - for list_item in list_items: - item_widget = self.listWidget.itemWidget(list_item) - if item_widget.text() in filename: - item_widget.setRightText(f"{filament_type} - {time_str}") + name = helper_methods.get_file_name(filename) + item = ListItem( + text=name[:-6], + right_text=f"{filament_type} - {time_str}", + right_icon=self.path.get("right_arrow"), + left_icon=None, + callback=None, + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=80, + notificate=False, + ) + + self.model.add_item(item) - @QtCore.pyqtSlot(QtWidgets.QListWidgetItem, name="file-item-clicked") - def _fileItemClicked(self, item: QtWidgets.QListWidgetItem) -> None: + @QtCore.pyqtSlot(str, name="file-item-clicked") + def _fileItemClicked(self, filename: str) -> None: """Slot for List Item clicked Args: - item (QListWidgetItem): Clicked item + filename (str): Clicked item path """ - if item: - widget = self.listWidget.itemWidget(item) - for file in self.file_list: - path = ( - file.get("path") if "path" in file.keys() else file.get("filename") - ) - if not path: - return - if widget.text() in path: - file_path = ( - path if not self.curr_dir else str(self.curr_dir + "/" + path) - ) - self.file_selected.emit( - str(file_path.removeprefix("/")), - self.files_data.get( - file_path.removeprefix("/") - ), # Defaults to Nothing - ) - - @QtCore.pyqtSlot(QtWidgets.QListWidgetItem, str, name="dir-item-clicked") - def _dirItemClicked(self, item: QtWidgets.QListWidgetItem, directory: str) -> None: + self.file_selected.emit( + str(filename.removeprefix("/")), + self.files_data.get(filename.removeprefix("/")), + ) + + def _dirItemClicked(self, directory: str) -> None: + """Method that changes the current view in the list""" self.curr_dir = self.curr_dir + directory self.request_dir_info[str].emit(self.curr_dir) def _build_file_list(self) -> None: """Inserts the currently available gcode files on the QListWidget""" self.listWidget.blockSignals(True) - self.listWidget.clear() - if not self.file_list and not self.directories: + self.model.clear() + self.entry_delegate.clear() + if ( + not self.file_list + and not self.directories + and os.path.islink(self.curr_dir) + ): self._add_placeholder() return - self.listWidget.setSpacing(35) + if self.directories or self.curr_dir != "": if self.curr_dir != "" and self.curr_dir != "/": self._add_back_folder_entry() @@ -161,51 +191,58 @@ def _build_file_list(self) -> None: sorted_list = sorted(self.file_list, key=lambda x: x["modified"], reverse=True) for item in sorted_list: self._add_file_list_item(item) - self._add_spacer() + self._setup_scrollbar() self.listWidget.blockSignals(False) - self.repaint() + self.listWidget.update() def _add_directory_list_item(self, dir_data: dict) -> None: + """Method that adds directories to the list""" dir_name = dir_data.get("dirname", "") if not dir_name: return - button = ListCustomButton() - button.setText(str(dir_data.get("dirname"))) - button.setSecondPixmap(QtGui.QPixmap(":/ui/media/btn_icons/folderIcon.svg")) - button.setMinimumSize(600, 80) - button.setMaximumSize(700, 80) - button.setLeftFontSize(17) - button.setRightFontSize(12) - list_item = QtWidgets.QListWidgetItem() - list_item.setSizeHint(button.sizeHint()) - self.listWidget.addItem(list_item) - self.listWidget.setItemWidget(list_item, button) - button.clicked.connect(lambda: self._dirItemClicked(list_item, "/" + dir_name)) + item = ListItem( + text=str(dir_name), + left_icon=self.path.get("folderIcon"), + right_text="", + selected=False, + callback=None, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=80, + ) + self.model.add_item(item) def _add_back_folder_entry(self) -> None: - button = ListCustomButton() - button.setText("Go Back") - button.setSecondPixmap(QtGui.QPixmap(":/ui/media/btn_icons/back_folder.svg")) - button.setMinimumSize(600, 80) - button.setMaximumSize(700, 80) - button.setLeftFontSize(17) - button.setRightFontSize(12) - list_item = QtWidgets.QListWidgetItem() - list_item.setSizeHint(button.sizeHint()) - self.listWidget.addItem(list_item) - self.listWidget.setItemWidget(list_item, button) + """Method to insert in the list the "Go back" item""" go_back_path = os.path.dirname(self.curr_dir) if go_back_path == "/": go_back_path = "" - button.clicked.connect(lambda: (self._on_goback_dir(go_back_path))) + + item = ListItem( + text="Go Back", + right_text="", + right_icon=None, + left_icon=self.path.get("back_folder"), + callback=None, + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=80, + notificate=False, + ) + self.model.add_item(item) @QtCore.pyqtSlot(str, str, name="on-goback-dir") def _on_goback_dir(self, directory) -> None: + """Go back behaviour""" self.request_dir_info[str].emit(directory) self.curr_dir = directory def _add_file_list_item(self, file_data_item) -> None: + """Request file information and metadata to create filelist""" if not file_data_item: return @@ -215,61 +252,28 @@ def _add_file_list_item(self, file_data_item) -> None: else file_data_item["filename"] ) if not name.endswith(".gcode"): - # Only list .gcode files, all else ignore return - button = ListCustomButton() - button.setText(name[:-6]) - button.setPixmap(QtGui.QPixmap(":/arrow_icons/media/btn_icons/right_arrow.svg")) - button.setMinimumSize(600, 80) - button.setMaximumSize(700, 80) - button.setLeftFontSize(17) - button.setRightFontSize(12) - list_item = QtWidgets.QListWidgetItem() - list_item.setSizeHint(button.sizeHint()) - self.listWidget.addItem(list_item) - self.listWidget.setItemWidget(list_item, button) - button.clicked.connect(lambda: self._fileItemClicked(list_item)) file_path = ( name if not self.curr_dir else str(self.curr_dir + "/" + name) ).removeprefix("/") + self.request_file_metadata.emit(file_path.removeprefix("/")) self.request_file_info.emit(file_path.removeprefix("/")) - def _add_spacer(self) -> None: - spacer_item = QtWidgets.QListWidgetItem() - spacer_widget = QtWidgets.QWidget() - spacer_widget.setFixedHeight(10) - spacer_item.setSizeHint(spacer_widget.sizeHint()) - self.listWidget.addItem(spacer_item) - def _add_placeholder(self) -> None: - self.listWidget.setSpacing(-1) - placeholder_label = QtWidgets.QLabel("No Files found") - font = QtGui.QFont() - font.setPointSize(25) - placeholder_label.setFont(font) - placeholder_label.setStyleSheet("color: gray;") - placeholder_label.setAlignment( - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter - ) - placeholder_label.setMinimumSize( - QtCore.QSize(self.listWidget.width(), self.listWidget.height()) - ) + """Shows placeholder when no items exist""" self.scrollbar.hide() - placeholder_item = QtWidgets.QListWidgetItem() - placeholder_item.setSizeHint( - QtCore.QSize(self.listWidget.width(), self.listWidget.height()) - ) - self.listWidget.addItem(placeholder_item) - self.listWidget.setItemWidget(placeholder_item, placeholder_label) + self.listWidget.hide() + self.label.show() def _handle_scrollbar(self, value): - # Block signals to avoid recursion + """Updates scrollbar value""" self.scrollbar.blockSignals(True) self.scrollbar.setValue(value) self.scrollbar.blockSignals(False) def _setup_scrollbar(self) -> None: + """Syncs the scrollbar with the list size""" self.scrollbar.setMinimum(self.listWidget.verticalScrollBar().minimum()) self.scrollbar.setMaximum(self.listWidget.verticalScrollBar().maximum()) self.scrollbar.setPageStep(self.listWidget.verticalScrollBar().pageStep()) @@ -327,7 +331,10 @@ def _setupUI(self): self.fp_content_layout = QtWidgets.QHBoxLayout() self.fp_content_layout.setContentsMargins(0, 0, 0, 0) self.fp_content_layout.setObjectName("fp_content_layout") - self.listWidget = QtWidgets.QListWidget(parent=self) + self.listWidget = QtWidgets.QListView(parent=self) + self.listWidget.setModel(self.model) + self.listWidget.setItemDelegate(self.entry_delegate) + self.listWidget.setSpacing(5) self.listWidget.setProperty("showDropIndicator", False) self.listWidget.setProperty("selectionMode", "NoSelection") self.listWidget.setStyleSheet("background: transparent;") @@ -335,11 +342,10 @@ def _setupUI(self): self.listWidget.setUniformItemSizes(True) self.listWidget.setObjectName("listWidget") self.listWidget.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.listWidget.setDefaultDropAction(QtCore.Qt.DropAction.IgnoreAction) self.listWidget.setSelectionBehavior( QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems ) - self.listWidget.setHorizontalScrollBarPolicy( # No horizontal scroll + self.listWidget.setHorizontalScrollBarPolicy( QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff ) self.listWidget.setVerticalScrollMode( @@ -364,26 +370,42 @@ def _setupUI(self): scroller_props = scroller_instance.scrollerProperties() scroller_props.setScrollMetric( QtWidgets.QScrollerProperties.ScrollMetric.DragVelocitySmoothingFactor, - 0.05, # Lower = more responsive + 0.05, ) scroller_props.setScrollMetric( QtWidgets.QScrollerProperties.ScrollMetric.DecelerationFactor, - 0.4, # higher = less inertia + 0.4, ) QtWidgets.QScroller.scroller(self.listWidget).setScrollerProperties( scroller_props ) + font = QtGui.QFont() font.setPointSize(25) - placeholder_item = QtWidgets.QListWidgetItem() - placeholder_item.setSizeHint( + self.label = QtWidgets.QLabel("No Files found") + self.label.setFont(font) + self.label.setStyleSheet("color: gray;") + self.label.setMinimumSize( QtCore.QSize(self.listWidget.width(), self.listWidget.height()) ) - self.fp_content_layout.addWidget(self.listWidget) + self.scrollbar = CustomScrollBar() + + self.fp_content_layout.addWidget( + self.label, + alignment=QtCore.Qt.AlignmentFlag.AlignHCenter + | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + self.fp_content_layout.addWidget(self.listWidget) self.fp_content_layout.addWidget(self.scrollbar) self.verticalLayout_5.addLayout(self.fp_content_layout) - self.scrollbar.setAttribute( - QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents, True - ) - self.scroller = QtWidgets.QScroller.scroller(self.listWidget) + self.scrollbar.show() + self.label.hide() + + self.path = { + "back_folder": QtGui.QPixmap(":/ui/media/btn_icons/back_folder.svg"), + "folderIcon": QtGui.QPixmap(":/ui/media/btn_icons/folderIcon.svg"), + "right_arrow": QtGui.QPixmap( + ":/arrow_icons/media/btn_icons/right_arrow.svg" + ), + } From d013f128761f04017e93debb8de711caafc1c1a2 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Fri, 2 Jan 2026 12:08:42 +0000 Subject: [PATCH 21/70] Work group button refactor (#137) * Refactor: optimized group button and renamed to check button * Refactor: updated button import to the newer file name and class * Refactor: ran ruff formatter --------- Co-authored-by: Roberto --- .../lib/panels/widgets/babystepPage.py | 10 +- .../lib/panels/widgets/probeHelperPage.py | 22 ++- .../lib/ui/controlStackedWidget_ui.py | 26 ++-- .../lib/ui/utilitiesStackedWidget_ui.py | 24 +-- BlocksScreen/lib/utils/check_button.py | 87 +++++++++++ BlocksScreen/lib/utils/group_button.py | 146 ------------------ 6 files changed, 133 insertions(+), 182 deletions(-) create mode 100644 BlocksScreen/lib/utils/check_button.py delete mode 100644 BlocksScreen/lib/utils/group_button.py diff --git a/BlocksScreen/lib/panels/widgets/babystepPage.py b/BlocksScreen/lib/panels/widgets/babystepPage.py index b0fdfdca..6505c0aa 100644 --- a/BlocksScreen/lib/panels/widgets/babystepPage.py +++ b/BlocksScreen/lib/panels/widgets/babystepPage.py @@ -3,7 +3,7 @@ from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_label import BlocksLabel from lib.utils.icon_button import IconButton -from lib.utils.group_button import GroupButton +from lib.utils.check_button import BlocksCustomCheckButton from PyQt6 import QtCore, QtGui, QtWidgets @@ -212,7 +212,7 @@ def setupUI(self): self.bbp_offset_steps_buttons.setObjectName("bbp_offset_steps_buttons") # 0.1mm button - self.bbp_nozzle_offset_1 = GroupButton( + self.bbp_nozzle_offset_1 = BlocksCustomCheckButton( parent=self.bbp_offset_steps_buttons_group_box ) self.bbp_nozzle_offset_1.setMinimumSize(QtCore.QSize(100, 70)) @@ -237,7 +237,7 @@ def setupUI(self): # Line separator for 0.1mm - set size policy to expanding horizontally # 0.01mm button - self.bbp_nozzle_offset_01 = GroupButton( + self.bbp_nozzle_offset_01 = BlocksCustomCheckButton( parent=self.bbp_offset_steps_buttons_group_box ) self.bbp_nozzle_offset_01.setMinimumSize(QtCore.QSize(100, 70)) @@ -261,7 +261,7 @@ def setupUI(self): ) # 0.05mm button - self.bbp_nozzle_offset_05 = GroupButton( + self.bbp_nozzle_offset_05 = BlocksCustomCheckButton( parent=self.bbp_offset_steps_buttons_group_box ) self.bbp_nozzle_offset_05.setMinimumSize(QtCore.QSize(100, 70)) @@ -285,7 +285,7 @@ def setupUI(self): ) # 0.025mm button - self.bbp_nozzle_offset_025 = GroupButton( + self.bbp_nozzle_offset_025 = BlocksCustomCheckButton( parent=self.bbp_offset_steps_buttons_group_box ) self.bbp_nozzle_offset_025.setMinimumSize(QtCore.QSize(100, 70)) diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index 5eeb3a3d..cc0f1be8 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -4,7 +4,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets from lib.utils.blocks_label import BlocksLabel from lib.utils.icon_button import IconButton -from lib.utils.group_button import GroupButton +from lib.utils.check_button import BlocksCustomCheckButton from lib.utils.blocks_button import BlocksCustomButton from lib.panels.widgets.loadPage import LoadScreen @@ -709,7 +709,9 @@ def _setupUi(self) -> None: self.bbp_offset_steps_buttons.setObjectName("bbp_offset_steps_buttons") # 0.1mm button - self.move_option_1 = GroupButton(parent=self.bbp_offset_steps_buttons_group_box) + self.move_option_1 = BlocksCustomCheckButton( + parent=self.bbp_offset_steps_buttons_group_box + ) self.move_option_1.setMinimumSize(QtCore.QSize(100, 60)) self.move_option_1.setMaximumSize(QtCore.QSize(100, 60)) self.move_option_1.setText("0.01 mm") @@ -730,7 +732,9 @@ def _setupUi(self) -> None: ) # 0.01mm button - self.move_option_2 = GroupButton(parent=self.bbp_offset_steps_buttons_group_box) + self.move_option_2 = BlocksCustomCheckButton( + parent=self.bbp_offset_steps_buttons_group_box + ) self.move_option_2.setMinimumSize(QtCore.QSize(100, 60)) self.move_option_2.setMaximumSize( QtCore.QSize(100, 60) @@ -752,7 +756,9 @@ def _setupUi(self) -> None: ) # 0.05mm button - self.move_option_3 = GroupButton(parent=self.bbp_offset_steps_buttons_group_box) + self.move_option_3 = BlocksCustomCheckButton( + parent=self.bbp_offset_steps_buttons_group_box + ) self.move_option_3.setMinimumSize(QtCore.QSize(100, 60)) self.move_option_3.setMaximumSize( QtCore.QSize(100, 60) @@ -774,7 +780,9 @@ def _setupUi(self) -> None: ) # 0.025mm button - self.move_option_4 = GroupButton(parent=self.bbp_offset_steps_buttons_group_box) + self.move_option_4 = BlocksCustomCheckButton( + parent=self.bbp_offset_steps_buttons_group_box + ) self.move_option_4.setMinimumSize(QtCore.QSize(100, 60)) self.move_option_4.setMaximumSize( QtCore.QSize(100, 60) @@ -796,7 +804,9 @@ def _setupUi(self) -> None: ) # 0.01mm button - self.move_option_5 = GroupButton(parent=self.bbp_offset_steps_buttons_group_box) + self.move_option_5 = BlocksCustomCheckButton( + parent=self.bbp_offset_steps_buttons_group_box + ) self.move_option_5.setMinimumSize(QtCore.QSize(100, 60)) self.move_option_5.setMaximumSize( QtCore.QSize(100, 60) diff --git a/BlocksScreen/lib/ui/controlStackedWidget_ui.py b/BlocksScreen/lib/ui/controlStackedWidget_ui.py index 88817b36..d76bb627 100644 --- a/BlocksScreen/lib/ui/controlStackedWidget_ui.py +++ b/BlocksScreen/lib/ui/controlStackedWidget_ui.py @@ -453,7 +453,7 @@ def setupUi(self, controlStackedWidget): self.exp_length_content_layout.setContentsMargins(5, 5, 5, 5) self.exp_length_content_layout.setSpacing(5) self.exp_length_content_layout.setObjectName("exp_length_content_layout") - self.extrude_select_length_10_btn = GroupButton(parent=self.layoutWidget) + self.extrude_select_length_10_btn = BlocksCustomCheckButton(parent=self.layoutWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -521,7 +521,7 @@ def setupUi(self, controlStackedWidget): self.extrude_select_length_group.setObjectName("extrude_select_length_group") self.extrude_select_length_group.addButton(self.extrude_select_length_10_btn) self.exp_length_content_layout.addWidget(self.extrude_select_length_10_btn) - self.extrude_select_length_50_btn = GroupButton(parent=self.layoutWidget) + self.extrude_select_length_50_btn = BlocksCustomCheckButton(parent=self.layoutWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -586,7 +586,7 @@ def setupUi(self, controlStackedWidget): self.extrude_select_length_50_btn.setObjectName("extrude_select_length_50_btn") self.extrude_select_length_group.addButton(self.extrude_select_length_50_btn) self.exp_length_content_layout.addWidget(self.extrude_select_length_50_btn) - self.extrude_select_length_100_btn = GroupButton(parent=self.layoutWidget) + self.extrude_select_length_100_btn = BlocksCustomCheckButton(parent=self.layoutWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -677,7 +677,7 @@ def setupUi(self, controlStackedWidget): self.exp_feedrate_content_layout.setContentsMargins(5, 5, 5, 5) self.exp_feedrate_content_layout.setSpacing(5) self.exp_feedrate_content_layout.setObjectName("exp_feedrate_content_layout") - self.extrude_select_feedrate_2_btn = GroupButton(parent=self.layoutWidget1) + self.extrude_select_feedrate_2_btn = BlocksCustomCheckButton(parent=self.layoutWidget1) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -745,7 +745,7 @@ def setupUi(self, controlStackedWidget): self.extrude_select_feedrate_group.setObjectName("extrude_select_feedrate_group") self.extrude_select_feedrate_group.addButton(self.extrude_select_feedrate_2_btn) self.exp_feedrate_content_layout.addWidget(self.extrude_select_feedrate_2_btn) - self.extrude_select_feedrate_5_btn = GroupButton(parent=self.layoutWidget1) + self.extrude_select_feedrate_5_btn = BlocksCustomCheckButton(parent=self.layoutWidget1) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -810,7 +810,7 @@ def setupUi(self, controlStackedWidget): self.extrude_select_feedrate_5_btn.setObjectName("extrude_select_feedrate_5_btn") self.extrude_select_feedrate_group.addButton(self.extrude_select_feedrate_5_btn) self.exp_feedrate_content_layout.addWidget(self.extrude_select_feedrate_5_btn) - self.extrude_select_feedrate_10_btn = GroupButton(parent=self.layoutWidget1) + self.extrude_select_feedrate_10_btn = BlocksCustomCheckButton(parent=self.layoutWidget1) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -1196,7 +1196,7 @@ def setupUi(self, controlStackedWidget): self.mva_home_all_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/motion/media/btn_icons/home_all.svg")) self.mva_home_all_btn.setObjectName("mva_home_all_btn") self.mva_home_axis_layout.addWidget(self.mva_home_all_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.mva_select_speed_25_btn = GroupButton(parent=self.move_axis_page) + self.mva_select_speed_25_btn = BlocksCustomCheckButton(parent=self.move_axis_page) self.mva_select_speed_25_btn.setGeometry(QtCore.QRect(96, 240, 100, 100)) self.mva_select_speed_25_btn.setMinimumSize(QtCore.QSize(60, 60)) self.mva_select_speed_25_btn.setMaximumSize(QtCore.QSize(100, 100)) @@ -1239,7 +1239,7 @@ def setupUi(self, controlStackedWidget): self.axis_select_speed_group = QtWidgets.QButtonGroup(controlStackedWidget) self.axis_select_speed_group.setObjectName("axis_select_speed_group") self.axis_select_speed_group.addButton(self.mva_select_speed_25_btn) - self.mva_select_speed_50_btn = GroupButton(parent=self.move_axis_page) + self.mva_select_speed_50_btn = BlocksCustomCheckButton(parent=self.move_axis_page) self.mva_select_speed_50_btn.setGeometry(QtCore.QRect(205, 240, 100, 100)) self.mva_select_speed_50_btn.setMinimumSize(QtCore.QSize(60, 60)) self.mva_select_speed_50_btn.setMaximumSize(QtCore.QSize(100, 100)) @@ -1279,7 +1279,7 @@ def setupUi(self, controlStackedWidget): self.mva_select_speed_50_btn.setFlat(False) self.mva_select_speed_50_btn.setObjectName("mva_select_speed_50_btn") self.axis_select_speed_group.addButton(self.mva_select_speed_50_btn) - self.mva_select_speed_100_btn = GroupButton(parent=self.move_axis_page) + self.mva_select_speed_100_btn = BlocksCustomCheckButton(parent=self.move_axis_page) self.mva_select_speed_100_btn.setGeometry(QtCore.QRect(315, 240, 100, 100)) self.mva_select_speed_100_btn.setMinimumSize(QtCore.QSize(60, 60)) self.mva_select_speed_100_btn.setMaximumSize(QtCore.QSize(100, 100)) @@ -1326,7 +1326,7 @@ def setupUi(self, controlStackedWidget): self.label.setFont(font) self.label.setStyleSheet("color:white") self.label.setObjectName("label") - self.mva_select_length_1_btn = GroupButton(parent=self.move_axis_page) + self.mva_select_length_1_btn = BlocksCustomCheckButton(parent=self.move_axis_page) self.mva_select_length_1_btn.setGeometry(QtCore.QRect(96, 110, 100, 100)) self.mva_select_length_1_btn.setMinimumSize(QtCore.QSize(60, 60)) self.mva_select_length_1_btn.setMaximumSize(QtCore.QSize(100, 100)) @@ -1369,7 +1369,7 @@ def setupUi(self, controlStackedWidget): self.axis_select_length_group = QtWidgets.QButtonGroup(controlStackedWidget) self.axis_select_length_group.setObjectName("axis_select_length_group") self.axis_select_length_group.addButton(self.mva_select_length_1_btn) - self.mva_select_length_10_btn = GroupButton(parent=self.move_axis_page) + self.mva_select_length_10_btn = BlocksCustomCheckButton(parent=self.move_axis_page) self.mva_select_length_10_btn.setGeometry(QtCore.QRect(204, 110, 100, 100)) self.mva_select_length_10_btn.setMinimumSize(QtCore.QSize(60, 60)) self.mva_select_length_10_btn.setMaximumSize(QtCore.QSize(100, 100)) @@ -1409,7 +1409,7 @@ def setupUi(self, controlStackedWidget): self.mva_select_length_10_btn.setFlat(False) self.mva_select_length_10_btn.setObjectName("mva_select_length_10_btn") self.axis_select_length_group.addButton(self.mva_select_length_10_btn) - self.mva_select_length_100_btn = GroupButton(parent=self.move_axis_page) + self.mva_select_length_100_btn = BlocksCustomCheckButton(parent=self.move_axis_page) self.mva_select_length_100_btn.setGeometry(QtCore.QRect(315, 110, 100, 100)) self.mva_select_length_100_btn.setMinimumSize(QtCore.QSize(60, 60)) self.mva_select_length_100_btn.setMaximumSize(QtCore.QSize(100, 100)) @@ -2190,5 +2190,5 @@ def retranslateUi(self, controlStackedWidget): from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_label import BlocksLabel from lib.utils.display_button import DisplayButton -from lib.utils.group_button import GroupButton +from lib.utils.check_button import BlocksCustomCheckButton from lib.utils.icon_button import IconButton diff --git a/BlocksScreen/lib/ui/utilitiesStackedWidget_ui.py b/BlocksScreen/lib/ui/utilitiesStackedWidget_ui.py index 987ed2f8..bb2ca93e 100644 --- a/BlocksScreen/lib/ui/utilitiesStackedWidget_ui.py +++ b/BlocksScreen/lib/ui/utilitiesStackedWidget_ui.py @@ -821,7 +821,7 @@ def setupUi(self, utilitiesStackedWidget): self.is_xy_layout.addWidget(self.is_X_startis_btn, 0, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) self.gridLayout = QtWidgets.QGridLayout() self.gridLayout.setObjectName("gridLayout") - self.btn2 = GroupButton(parent=self.input_shaper_page) + self.btn2 = BlocksCustomCheckButton(parent=self.input_shaper_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -851,7 +851,7 @@ def setupUi(self, utilitiesStackedWidget): self.isc_btn_group.setObjectName("isc_btn_group") self.isc_btn_group.addButton(self.btn2) self.gridLayout.addWidget(self.btn2, 1, 1, 1, 1) - self.btn3 = GroupButton(parent=self.input_shaper_page) + self.btn3 = BlocksCustomCheckButton(parent=self.input_shaper_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -879,7 +879,7 @@ def setupUi(self, utilitiesStackedWidget): self.btn3.setObjectName("btn3") self.isc_btn_group.addButton(self.btn3) self.gridLayout.addWidget(self.btn3, 2, 0, 1, 1) - self.btn5 = GroupButton(parent=self.input_shaper_page) + self.btn5 = BlocksCustomCheckButton(parent=self.input_shaper_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -907,7 +907,7 @@ def setupUi(self, utilitiesStackedWidget): self.btn5.setObjectName("btn5") self.isc_btn_group.addButton(self.btn5) self.gridLayout.addWidget(self.btn5, 3, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.btn1 = GroupButton(parent=self.input_shaper_page) + self.btn1 = BlocksCustomCheckButton(parent=self.input_shaper_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -936,7 +936,7 @@ def setupUi(self, utilitiesStackedWidget): self.btn1.setObjectName("btn1") self.isc_btn_group.addButton(self.btn1) self.gridLayout.addWidget(self.btn1, 1, 0, 1, 1) - self.btn4 = GroupButton(parent=self.input_shaper_page) + self.btn4 = BlocksCustomCheckButton(parent=self.input_shaper_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -1000,7 +1000,7 @@ def setupUi(self, utilitiesStackedWidget): self.gridLayout_4 = QtWidgets.QGridLayout() self.gridLayout_4.setSpacing(0) self.gridLayout_4.setObjectName("gridLayout_4") - self.am_zv = GroupButton(parent=self.is_page) + self.am_zv = BlocksCustomCheckButton(parent=self.is_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -1027,7 +1027,7 @@ def setupUi(self, utilitiesStackedWidget): self.is_btn_group.setObjectName("is_btn_group") self.is_btn_group.addButton(self.am_zv) self.gridLayout_4.addWidget(self.am_zv, 0, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.am_ei = GroupButton(parent=self.is_page) + self.am_ei = BlocksCustomCheckButton(parent=self.is_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -1052,7 +1052,7 @@ def setupUi(self, utilitiesStackedWidget): self.am_ei.setObjectName("am_ei") self.is_btn_group.addButton(self.am_ei) self.gridLayout_4.addWidget(self.am_ei, 0, 2, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.am_mzv = GroupButton(parent=self.is_page) + self.am_mzv = BlocksCustomCheckButton(parent=self.is_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -1077,7 +1077,7 @@ def setupUi(self, utilitiesStackedWidget): self.am_mzv.setObjectName("am_mzv") self.is_btn_group.addButton(self.am_mzv) self.gridLayout_4.addWidget(self.am_mzv, 1, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.am_3hump_ei = GroupButton(parent=self.is_page) + self.am_3hump_ei = BlocksCustomCheckButton(parent=self.is_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -1102,7 +1102,7 @@ def setupUi(self, utilitiesStackedWidget): self.am_3hump_ei.setObjectName("am_3hump_ei") self.is_btn_group.addButton(self.am_3hump_ei) self.gridLayout_4.addWidget(self.am_3hump_ei, 2, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.am_user_input = GroupButton(parent=self.is_page) + self.am_user_input = BlocksCustomCheckButton(parent=self.is_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -1128,7 +1128,7 @@ def setupUi(self, utilitiesStackedWidget): self.am_user_input.setObjectName("am_user_input") self.is_btn_group.addButton(self.am_user_input) self.gridLayout_4.addWidget(self.am_user_input, 2, 2, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.am_2hump_ei = GroupButton(parent=self.is_page) + self.am_2hump_ei = BlocksCustomCheckButton(parent=self.is_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -1553,6 +1553,6 @@ def retranslateUi(self, utilitiesStackedWidget): from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.blocks_slider import BlocksSlider -from lib.utils.group_button import GroupButton +from lib.utils.check_button import BlocksCustomCheckButton from lib.utils.icon_button import IconButton from lib.utils.toggleAnimatedButton import ToggleAnimatedButton diff --git a/BlocksScreen/lib/utils/check_button.py b/BlocksScreen/lib/utils/check_button.py new file mode 100644 index 00000000..e5b184d5 --- /dev/null +++ b/BlocksScreen/lib/utils/check_button.py @@ -0,0 +1,87 @@ +import typing +from PyQt6 import QtCore, QtGui, QtWidgets + + +class BlocksCustomCheckButton(QtWidgets.QAbstractButton): + """Custom Blocks QPushButton + Rounded button with a hole on the left side where an icon can be inserted + + Args: + parent (QWidget): Parent of the button + """ + + def __init__( + self, + parent: QtWidgets.QWidget, + ) -> None: + super().__init__(parent) + self.button_ellipse = None + self._text: str = "" + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) + + def setFlat(self, flat) -> None: + """Disable setFlat behavior""" + return + + def setAutoDefault(self, _): + """Disable auto default behavior""" + return + + def text(self) -> str: + """returns Widget text""" + return self._text + + def setText(self, text: str | None) -> None: + """Set widget text""" + if text is None: + return + self._text = text + self.update() + return + + def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): + """Re-implemented method, paint widget, optimized for performance.""" + + painter = QtGui.QPainter(self) + rect_f = self.rect().toRectF().normalized() + painter.setRenderHint(painter.RenderHint.Antialiasing, True) + height = rect_f.height() + + radius = height / 5.0 + self.button_ellipse = QtCore.QRectF( + rect_f.left() + height * 0.05, + rect_f.top() + height * 0.05, + (height * 0.40), + (height * 0.40), + ) + + if self.isChecked(): + bg_color = QtGui.QColor(223, 223, 223) + text_color = QtGui.QColor(0, 0, 0) + elif self.isDown(): + bg_color = QtGui.QColor(164, 164, 164, 90) + text_color = QtGui.QColor(255, 255, 255) + else: + bg_color = QtGui.QColor(0, 0, 0, 90) + text_color = QtGui.QColor(255, 255, 255) + + path = QtGui.QPainterPath() + path.addRoundedRect( + rect_f, + radius, + radius, + QtCore.Qt.SizeMode.AbsoluteSize, + ) + + painter.setPen(QtCore.Qt.PenStyle.NoPen) + painter.setBrush(bg_color) + painter.fillPath(path, bg_color) + + if self.text(): + painter.setPen(text_color) + painter.setFont(QtGui.QFont("Momcake", 14)) + painter.drawText( + rect_f, + QtCore.Qt.AlignmentFlag.AlignCenter, + str(self.text()), + ) diff --git a/BlocksScreen/lib/utils/group_button.py b/BlocksScreen/lib/utils/group_button.py deleted file mode 100644 index 79f4251e..00000000 --- a/BlocksScreen/lib/utils/group_button.py +++ /dev/null @@ -1,146 +0,0 @@ -import typing -from PyQt6 import QtCore, QtGui, QtWidgets - - -class GroupButton(QtWidgets.QPushButton): - """Custom Blocks QPushButton - Rounded button with a hole on the left side where an icon can be inserted - - Args: - parent (QWidget): Parent of the button - """ - - def __init__( - self, - parent: QtWidgets.QWidget, - ) -> None: - super(GroupButton, self).__init__(parent) - - self.icon_pixmap: QtGui.QPixmap = QtGui.QPixmap() - self._icon_rect: QtCore.QRectF = QtCore.QRectF() - self.button_background = None - self.button_ellipse = None - self._text: str = "" - self._name: str = "" - self.text_color: QtGui.QColor = QtGui.QColor(0, 0, 0) - self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) - - @property - def name(self): - """Widget name""" - return self._name - - @name.setter - def name(self, new_name) -> None: - self._name = new_name - self.setObjectName(new_name) - - def text(self) -> str | None: - """Widget text""" - return self._text - - def setText(self, text: str) -> None: - """Set widget text""" - self._text = text - self.update() # Force button update - return - - def setPixmap(self, pixmap: QtGui.QPixmap) -> None: - """Set widget pixmap""" - self.icon_pixmap = pixmap - self.repaint() - - def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): - """Re-implemented method, paint widget""" - opt = QtWidgets.QStyleOptionButton() - self.initStyleOption(opt) - - painter = QtGui.QPainter(self) - painter.setRenderHint(painter.RenderHint.Antialiasing, True) - painter.setRenderHint(painter.RenderHint.SmoothPixmapTransform, True) - painter.setRenderHint(painter.RenderHint.LosslessImageRendering, True) - - _rect = self.rect() - _style = self.style() - - if _style is None or _rect is None: - return - - bg_color = ( - QtGui.QColor(223, 223, 223) - if self.isChecked() - else QtGui.QColor(164, 164, 164, 90) - if self.isDown() - else QtGui.QColor(0, 0, 0, 90) - ) - - path = QtGui.QPainterPath() - xRadius = self.rect().toRectF().normalized().height() / 5.0 - yRadius = self.rect().toRectF().normalized().height() / 5.0 - painter.setBackgroundMode(QtCore.Qt.BGMode.TransparentMode) - path.addRoundedRect( - 0, - 0, - self.rect().toRectF().normalized().width(), - self.rect().toRectF().normalized().height(), - xRadius, - yRadius, - QtCore.Qt.SizeMode.AbsoluteSize, - ) - - self.button_ellipse = QtCore.QRectF( - self.rect().toRectF().normalized().left() - + self.rect().toRectF().normalized().height() * 0.05, - self.rect().toRectF().normalized().top() - + self.rect().toRectF().normalized().height() * 0.05, - (self.rect().toRectF().normalized().height() * 0.40), - (self.rect().toRectF().normalized().height() * 0.40), - ) - - painter.setPen(QtCore.Qt.PenStyle.NoPen) - painter.setBrush(bg_color) - painter.fillPath(path, bg_color) - - if self.text(): - if self.isChecked(): - painter.setPen(QtGui.QColor(0, 0, 0)) - else: - painter.setPen(QtGui.QColor(255, 255, 255)) - _start_text_position = int(self.button_ellipse.width() / 2) - _text_rect = _rect - _pen = painter.pen() - _pen.setStyle(QtCore.Qt.PenStyle.SolidLine) - _pen.setWidth(1) - painter.setPen(_pen) - painter.setFont(QtGui.QFont("Momcake-Thin", 14)) - - painter.drawText( - _text_rect, - QtCore.Qt.AlignmentFlag.AlignCenter, - str(self.text()), - ) - painter.setPen(QtCore.Qt.PenStyle.NoPen) - - def setProperty(self, name: str, value: typing.Any): - """Re-implemented method, set widget properties""" - if name == "name": - self._name = name - elif name == "text_color": - self.text_color = QtGui.QColor(value) - # return super().setProperty(name, value) - - def event(self, e: QtCore.QEvent) -> bool: - """Re-implemented method, filter events""" - if e.type() == QtCore.QEvent.Type.TouchBegin: - self.handleTouchBegin(e) - return False - elif e.type() == QtCore.QEvent.Type.TouchUpdate: - self.handleTouchUpdate(e) - return False - elif e.type() == QtCore.QEvent.Type.TouchEnd: - self.handleTouchEnd(e) - return False - elif e.type() == QtCore.QEvent.Type.TouchCancel: - self.handleTouchCancel(e) - return False - return super().event(e) From db29de9c488b96f23ef40e81937f96e2548eeaab Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Fri, 2 Jan 2026 12:10:27 +0000 Subject: [PATCH 22/70] Bugfix `tunePage`: Add clickability and distinct icons to controllable fans (#138) * update fan icons and show only user-controllable fans Signed-off-by: Guilherme Costa * tunePage.py: improve icon management and add regex to display the correct icon for each fan type * networkWindow.py: reorganize imports and refactor icons management condition * Test signed commit --------- Signed-off-by: Guilherme Costa Co-authored-by: Guilherme Costa Co-authored-by: Hugo Costa --- BlocksScreen/lib/panels/widgets/tunePage.py | 64 +- .../lib/ui/resources/icon_resources.qrc | 1 + .../lib/ui/resources/icon_resources_rc.py | 963 ++++++++++-------- 3 files changed, 599 insertions(+), 429 deletions(-) diff --git a/BlocksScreen/lib/panels/widgets/tunePage.py b/BlocksScreen/lib/panels/widgets/tunePage.py index cb9a00c8..0fc8faf7 100644 --- a/BlocksScreen/lib/panels/widgets/tunePage.py +++ b/BlocksScreen/lib/panels/widgets/tunePage.py @@ -1,10 +1,11 @@ +import re import typing +from helper_methods import normalize from lib.utils.blocks_button import BlocksCustomButton from lib.utils.display_button import DisplayButton from lib.utils.icon_button import IconButton from PyQt6 import QtCore, QtGui, QtWidgets -from helper_methods import normalize class TuneWidget(QtWidgets.QWidget): @@ -110,18 +111,26 @@ def on_fan_object_update( field (str): field name new_value (int | float): New value for field name """ + fields = name.split() + first_field = fields[0] + second_field = fields[1].lower() if len(fields) > 1 else None if "speed" in field: - if not self.tune_display_buttons.get(name, None): + if not self.tune_display_buttons.get(name, None) and first_field in ( + "fan", + "fan_generic", + ): + pattern_blower = r"(?:^|_)(?:blower|auxiliary)(?:_|$)" + pattern_exhaust = r"(?:^|_)exhaust(?:_|$)" + _new_display_button = self.create_display_button(name) _new_display_button.setParent(self) - if "blower" in name: - _new_display_button.icon_pixmap = QtGui.QPixmap( - ":/temperature_related/media/btn_icons/blower.svg" - ) - else: - _new_display_button.icon_pixmap = QtGui.QPixmap( - ":/temperature_related/media/btn_icons/fan.svg" - ) + _new_display_button.icon_pixmap = self.path.get("fan") + if second_field: + if re.search(pattern_blower, second_field): + _new_display_button.icon_pixmap = self.path.get("blower") + elif re.search(pattern_exhaust, second_field): + _new_display_button.icon_pixmap = self.path.get("fan_cage") + self.tune_display_buttons.update( { name: { @@ -130,26 +139,17 @@ def on_fan_object_update( } } ) - if name in ("fan", "fan_generic"): - _new_display_button.clicked.connect( - lambda: self.request_sliderPage[ - str, int, "PyQt_PyObject", int, int - ].emit( - str(name), - int( - round( - self.tune_display_buttons.get(name).get( # type:ignore - "speed", 0 - ) - ) - ), - self.on_slider_change, - 0, - 100, - ) + _new_display_button.clicked.connect( + lambda: self.request_sliderPage[ + str, int, "PyQt_PyObject", int, int + ].emit( + str(name), + int(round(self.tune_display_buttons.get(name).get("speed", 0))), + self.on_slider_change, + 0, + 100, ) - else: - _new_display_button.setDisabled(True) + ) self.tune_display_vertical_child_layout_2.addWidget(_new_display_button) _display_button = self.tune_display_buttons.get(name) if not _display_button: @@ -436,6 +436,12 @@ def _setupUI(self) -> None: self.tune_content.setContentsMargins(2, 0, 2, 0) self.setLayout(self.tune_content) self.setContentsMargins(2, 2, 2, 2) + + self.path = { + "fan_cage": QtGui.QPixmap(":/fan_related/media/btn_icons/fan_cage.svg"), + "blower": QtGui.QPixmap(":/fan_related/media/btn_icons/blower.svg"), + "fan": QtGui.QPixmap(":/fan_related/media/btn_icons/fan.svg"), + } self._retranslateUI() def _retranslateUI(self): diff --git a/BlocksScreen/lib/ui/resources/icon_resources.qrc b/BlocksScreen/lib/ui/resources/icon_resources.qrc index 3022fd4d..a3bf6d57 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources.qrc +++ b/BlocksScreen/lib/ui/resources/icon_resources.qrc @@ -36,6 +36,7 @@ media/btn_icons/fan.svg media/btn_icons/fan_cage.svg + media/btn_icons/blower.svg media/btn_icons/standart_temperature.svg diff --git a/BlocksScreen/lib/ui/resources/icon_resources_rc.py b/BlocksScreen/lib/ui/resources/icon_resources_rc.py index 14285978..1346a1f1 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/icon_resources_rc.py @@ -11621,6 +11621,166 @@ \x36\x36\x2c\x33\x31\x37\x2e\x33\x2c\x32\x39\x39\x2e\x37\x36\x2c\ \x33\x31\x37\x2e\x34\x32\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ \ +\x00\x00\x09\xd1\ +\x3c\ +\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ +\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ +\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ +\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ +\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ +\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ +\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x65\x30\x65\x30\x64\x66\x3b\x7d\ +\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\ +\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ +\x31\x22\x20\x64\x3d\x22\x4d\x34\x35\x36\x2e\x31\x37\x2c\x33\x31\ +\x37\x2e\x38\x35\x63\x2e\x34\x36\x2d\x31\x31\x2e\x38\x39\x2c\x35\ +\x2e\x31\x38\x2d\x31\x37\x2e\x34\x32\x2c\x31\x34\x2e\x38\x36\x2d\ +\x31\x36\x2e\x37\x39\x2c\x31\x30\x2e\x37\x35\x2e\x36\x39\x2c\x31\ +\x33\x2e\x32\x34\x2c\x37\x2e\x37\x37\x2c\x31\x33\x2e\x32\x32\x2c\ +\x31\x37\x2e\x32\x2d\x2e\x31\x31\x2c\x36\x36\x2c\x30\x2c\x31\x33\ +\x32\x2d\x2e\x31\x2c\x31\x39\x38\x2c\x30\x2c\x31\x31\x2d\x35\x2c\ +\x31\x36\x2e\x35\x32\x2d\x31\x34\x2e\x31\x38\x2c\x31\x36\x2e\x32\ +\x2d\x31\x30\x2d\x2e\x33\x34\x2d\x31\x34\x2e\x34\x34\x2d\x36\x2e\ +\x35\x31\x2d\x31\x33\x2e\x35\x39\x2d\x31\x35\x2e\x36\x2c\x31\x2e\ +\x30\x35\x2d\x31\x31\x2e\x32\x32\x2d\x33\x2e\x33\x38\x2d\x31\x33\ +\x2e\x36\x32\x2d\x31\x34\x2d\x31\x33\x2e\x35\x34\x2d\x36\x34\x2e\ +\x34\x39\x2e\x34\x37\x2d\x31\x32\x39\x2c\x2e\x33\x38\x2d\x31\x39\ +\x33\x2e\x34\x39\x2e\x31\x32\x2d\x34\x33\x2e\x36\x33\x2d\x2e\x31\ +\x37\x2d\x38\x31\x2e\x35\x2d\x31\x35\x2e\x36\x2d\x31\x31\x34\x2e\ +\x32\x33\x2d\x34\x34\x2e\x33\x32\x61\x31\x39\x32\x2e\x31\x38\x2c\ +\x31\x39\x32\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x30\x2d\x32\x32\x2e\ +\x34\x34\x2d\x31\x36\x2e\x38\x39\x41\x32\x30\x34\x2e\x38\x32\x2c\ +\x32\x30\x34\x2e\x38\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x32\x2e\ +\x33\x32\x2c\x32\x33\x33\x2e\x38\x36\x43\x34\x34\x2e\x31\x31\x2c\ +\x31\x31\x35\x2e\x36\x39\x2c\x31\x36\x35\x2c\x34\x31\x2e\x37\x2c\ +\x32\x38\x30\x2e\x34\x34\x2c\x37\x35\x2e\x38\x37\x2c\x33\x36\x32\ +\x2e\x32\x35\x2c\x31\x30\x30\x2e\x30\x38\x2c\x34\x32\x30\x2e\x32\ +\x38\x2c\x31\x37\x31\x2e\x37\x37\x2c\x34\x32\x35\x2c\x32\x35\x37\ +\x63\x31\x2e\x33\x32\x2c\x32\x33\x2e\x36\x35\x2d\x32\x2e\x32\x2c\ +\x34\x37\x2e\x35\x36\x2d\x33\x2e\x35\x33\x2c\x37\x31\x2e\x38\x38\ +\x68\x33\x34\x2e\x30\x38\x43\x34\x35\x35\x2e\x38\x32\x2c\x33\x32\ +\x34\x2e\x34\x38\x2c\x34\x35\x36\x2c\x33\x32\x31\x2e\x31\x36\x2c\ +\x34\x35\x36\x2e\x31\x37\x2c\x33\x31\x37\x2e\x38\x35\x5a\x4d\x32\ +\x32\x33\x2e\x37\x38\x2c\x39\x36\x43\x31\x32\x37\x2c\x39\x35\x2e\ +\x35\x36\x2c\x34\x36\x2e\x36\x34\x2c\x31\x37\x36\x2c\x34\x37\x2e\ +\x35\x39\x2c\x32\x37\x32\x2e\x32\x32\x63\x2e\x39\x34\x2c\x39\x35\ +\x2e\x38\x34\x2c\x37\x39\x2e\x34\x31\x2c\x31\x37\x33\x2e\x39\x34\ +\x2c\x31\x37\x34\x2e\x37\x38\x2c\x31\x37\x33\x2e\x39\x34\x61\x31\ +\x37\x35\x2e\x32\x38\x2c\x31\x37\x35\x2e\x32\x38\x2c\x30\x2c\x30\ +\x2c\x30\x2c\x31\x37\x35\x2e\x34\x34\x2d\x31\x37\x35\x2e\x34\x43\ +\x33\x39\x37\x2e\x38\x31\x2c\x31\x37\x35\x2e\x37\x32\x2c\x33\x31\ +\x38\x2e\x38\x36\x2c\x39\x36\x2e\x34\x34\x2c\x32\x32\x33\x2e\x37\ +\x38\x2c\x39\x36\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ +\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\ +\x33\x33\x32\x2e\x32\x35\x2c\x31\x37\x39\x2e\x39\x34\x63\x2d\x32\ +\x30\x2e\x34\x39\x2d\x32\x2e\x30\x37\x2d\x33\x39\x2e\x33\x35\x2e\ +\x37\x39\x2d\x35\x37\x2e\x32\x36\x2c\x39\x2e\x31\x38\x61\x38\x30\ +\x2c\x38\x30\x2c\x30\x2c\x30\x2c\x30\x2d\x33\x36\x2c\x33\x32\x2e\ +\x34\x38\x63\x2d\x32\x2e\x37\x36\x2c\x34\x2e\x37\x31\x2d\x32\x2e\ +\x37\x39\x2c\x37\x2e\x39\x33\x2c\x31\x2e\x35\x34\x2c\x31\x32\x2c\ +\x39\x2e\x32\x34\x2c\x38\x2e\x35\x39\x2c\x38\x2e\x39\x31\x2c\x38\ +\x2e\x36\x33\x2c\x32\x30\x2e\x32\x33\x2c\x34\x2e\x31\x38\x2c\x33\ +\x34\x2e\x36\x39\x2d\x31\x33\x2e\x36\x33\x2c\x36\x34\x2e\x38\x32\ +\x2d\x34\x2c\x39\x33\x2e\x33\x32\x2c\x31\x37\x2e\x34\x2c\x31\x30\ +\x2e\x33\x34\x2c\x37\x2e\x37\x34\x2c\x39\x2e\x37\x31\x2c\x31\x36\ +\x2e\x36\x31\x2c\x38\x2e\x31\x38\x2c\x32\x37\x2e\x30\x36\x71\x2d\ +\x37\x2e\x34\x34\x2c\x35\x30\x2e\x39\x33\x2d\x34\x36\x2e\x36\x35\ +\x2c\x38\x34\x2e\x30\x35\x61\x37\x2e\x36\x31\x2c\x37\x2e\x36\x31\ +\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x38\x36\x2c\x31\x63\x2d\x2e\ +\x31\x37\x2e\x30\x38\x2d\x2e\x34\x36\x2d\x2e\x31\x32\x2d\x31\x2e\ +\x30\x36\x2d\x2e\x33\x31\x2c\x32\x2e\x30\x36\x2d\x31\x39\x2e\x36\ +\x34\x2e\x31\x31\x2d\x33\x38\x2e\x38\x38\x2d\x38\x2e\x31\x33\x2d\ +\x35\x37\x2e\x31\x33\x2d\x37\x2e\x32\x36\x2d\x31\x36\x2e\x30\x38\ +\x2d\x31\x38\x2e\x33\x34\x2d\x32\x38\x2e\x38\x32\x2d\x33\x33\x2e\ +\x38\x2d\x33\x37\x2e\x36\x35\x2d\x33\x2e\x34\x37\x2d\x32\x2d\x35\ +\x2e\x34\x39\x2d\x31\x2e\x35\x33\x2d\x38\x2e\x38\x32\x2c\x31\x2e\ +\x31\x36\x2d\x38\x2e\x37\x2c\x37\x2d\x39\x2e\x36\x33\x2c\x31\x33\ +\x2e\x31\x31\x2d\x35\x2e\x31\x33\x2c\x32\x34\x2e\x33\x32\x2c\x31\ +\x32\x2e\x37\x39\x2c\x33\x31\x2e\x37\x37\x2c\x32\x2e\x31\x32\x2c\ +\x36\x30\x2e\x32\x32\x2d\x31\x37\x2e\x30\x38\x2c\x38\x36\x2e\x35\ +\x31\x2d\x37\x2e\x33\x31\x2c\x31\x30\x2d\x31\x35\x2e\x34\x33\x2c\ +\x31\x34\x2e\x30\x37\x2d\x32\x38\x2e\x35\x37\x2c\x31\x31\x2e\x37\ +\x31\x2d\x33\x32\x2e\x31\x39\x2d\x35\x2e\x37\x38\x2d\x35\x39\x2d\ +\x32\x30\x2e\x32\x33\x2d\x38\x30\x2e\x36\x39\x2d\x34\x34\x2e\x35\ +\x31\x61\x31\x37\x2e\x32\x2c\x31\x37\x2e\x32\x2c\x30\x2c\x30\x2c\ +\x31\x2d\x31\x2e\x34\x33\x2d\x32\x2e\x37\x2c\x31\x31\x31\x2e\x38\ +\x2c\x31\x31\x31\x2e\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x31\x2e\ +\x34\x34\x2d\x33\x2e\x34\x36\x63\x32\x32\x2e\x31\x32\x2d\x36\x2e\ +\x31\x34\x2c\x33\x39\x2e\x38\x33\x2d\x31\x38\x2e\x32\x33\x2c\x35\ +\x31\x2e\x38\x38\x2d\x33\x38\x2e\x32\x35\x2c\x33\x2d\x35\x2c\x32\ +\x2e\x35\x34\x2d\x38\x2e\x31\x33\x2d\x31\x2e\x34\x37\x2d\x31\x32\ +\x2e\x30\x36\x2d\x39\x2e\x31\x31\x2d\x38\x2e\x39\x34\x2d\x38\x2e\ +\x39\x2d\x39\x2d\x32\x30\x2e\x39\x32\x2d\x34\x2e\x31\x35\x2d\x33\ +\x33\x2c\x31\x33\x2e\x31\x38\x2d\x36\x31\x2e\x38\x38\x2c\x34\x2d\ +\x38\x39\x2e\x37\x35\x2d\x31\x35\x2e\x31\x34\x2d\x31\x32\x2e\x38\ +\x31\x2d\x38\x2e\x37\x39\x2d\x31\x32\x2e\x39\x31\x2d\x31\x39\x2e\ +\x34\x34\x2d\x31\x30\x2e\x36\x34\x2d\x33\x32\x2e\x35\x31\x2c\x35\ +\x2e\x36\x34\x2d\x33\x32\x2e\x34\x36\x2c\x32\x30\x2e\x38\x2d\x35\ +\x39\x2e\x32\x38\x2c\x34\x35\x2e\x37\x39\x2d\x38\x30\x2e\x36\x34\ +\x2e\x36\x36\x2d\x2e\x35\x36\x2c\x31\x2e\x34\x32\x2d\x31\x2c\x32\ +\x2e\x38\x37\x2d\x32\x2c\x2e\x37\x37\x2c\x31\x32\x2e\x33\x36\x2d\ +\x2e\x37\x2c\x32\x33\x2e\x38\x34\x2c\x31\x2e\x35\x36\x2c\x33\x35\ +\x2e\x31\x39\x2c\x35\x2e\x30\x38\x2c\x32\x35\x2e\x34\x36\x2c\x31\ +\x37\x2e\x31\x32\x2c\x34\x36\x2e\x30\x39\x2c\x33\x39\x2e\x37\x31\ +\x2c\x35\x39\x2e\x39\x35\x2c\x34\x2e\x31\x38\x2c\x32\x2e\x35\x36\ +\x2c\x36\x2e\x38\x38\x2c\x32\x2e\x38\x31\x2c\x31\x30\x2e\x36\x31\ +\x2d\x31\x2e\x30\x39\x2c\x39\x2e\x32\x38\x2d\x39\x2e\x37\x34\x2c\ +\x39\x2e\x32\x2d\x39\x2e\x34\x2c\x34\x2e\x35\x35\x2d\x32\x32\x2e\ +\x31\x36\x2d\x31\x31\x2e\x36\x39\x2d\x33\x32\x2e\x30\x37\x2d\x33\ +\x2d\x36\x30\x2e\x34\x32\x2c\x31\x36\x2e\x31\x33\x2d\x38\x37\x2c\ +\x37\x2e\x36\x36\x2d\x31\x30\x2e\x36\x33\x2c\x31\x36\x2d\x31\x35\ +\x2e\x35\x35\x2c\x33\x30\x2e\x32\x34\x2d\x31\x32\x2e\x38\x32\x2c\ +\x33\x31\x2e\x38\x32\x2c\x36\x2e\x30\x39\x2c\x35\x38\x2e\x33\x38\ +\x2c\x32\x30\x2e\x34\x34\x2c\x38\x30\x2c\x34\x34\x2e\x33\x33\x43\ +\x33\x33\x31\x2e\x34\x38\x2c\x31\x37\x37\x2e\x33\x39\x2c\x33\x33\ +\x31\x2e\x35\x37\x2c\x31\x37\x38\x2e\x31\x38\x2c\x33\x33\x32\x2e\ +\x32\x35\x2c\x31\x37\x39\x2e\x39\x34\x5a\x4d\x32\x33\x30\x2e\x35\ +\x39\x2c\x32\x38\x30\x2e\x37\x32\x63\x33\x2e\x35\x39\x2e\x31\x2c\ +\x31\x36\x2e\x33\x37\x2d\x31\x32\x2e\x34\x39\x2c\x31\x36\x2e\x35\ +\x37\x2d\x31\x36\x2e\x33\x34\x53\x32\x33\x35\x2e\x31\x36\x2c\x32\ +\x34\x38\x2c\x32\x33\x31\x2c\x32\x34\x37\x2e\x37\x35\x63\x2d\x33\ +\x2e\x35\x32\x2d\x2e\x31\x39\x2d\x31\x36\x2e\x35\x34\x2c\x31\x32\ +\x2e\x35\x2d\x31\x36\x2e\x36\x35\x2c\x31\x36\x2e\x32\x33\x53\x32\ +\x32\x36\x2e\x36\x39\x2c\x32\x38\x30\x2e\x36\x31\x2c\x32\x33\x30\ +\x2e\x35\x39\x2c\x32\x38\x30\x2e\x37\x32\x5a\x22\x2f\x3e\x3c\x70\ +\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\ +\x22\x20\x64\x3d\x22\x4d\x35\x38\x31\x2c\x33\x33\x32\x2e\x38\x32\ +\x63\x2d\x33\x2e\x36\x31\x2c\x32\x2e\x31\x33\x2d\x37\x2e\x32\x33\ +\x2c\x34\x2e\x32\x35\x2d\x31\x30\x2e\x38\x33\x2c\x36\x2e\x33\x39\ +\x6c\x2d\x34\x35\x2e\x34\x31\x2c\x32\x37\x63\x2d\x2e\x31\x36\x2e\ +\x31\x2d\x2e\x33\x33\x2e\x31\x39\x2d\x2e\x36\x36\x2e\x33\x37\x2c\ +\x30\x2d\x32\x2e\x33\x31\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x34\ +\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x34\x68\x2d\x32\x33\x56\x33\ +\x31\x39\x2e\x34\x68\x32\x33\x73\x30\x2d\x32\x30\x2e\x33\x36\x2c\ +\x30\x2d\x32\x30\x2e\x35\x31\x63\x2e\x30\x37\x2c\x30\x2c\x31\x30\ +\x2e\x31\x38\x2c\x35\x2e\x39\x31\x2c\x31\x34\x2e\x38\x34\x2c\x38\ +\x2e\x36\x38\x4c\x35\x38\x31\x2c\x33\x33\x32\x2e\x36\x34\x5a\x22\ +\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ +\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x35\x38\x31\x2c\x34\x39\ +\x34\x63\x2d\x33\x2e\x36\x31\x2c\x32\x2e\x31\x33\x2d\x37\x2e\x32\ +\x33\x2c\x34\x2e\x32\x35\x2d\x31\x30\x2e\x38\x33\x2c\x36\x2e\x33\ +\x39\x6c\x2d\x34\x35\x2e\x34\x31\x2c\x32\x37\x63\x2d\x2e\x31\x36\ +\x2e\x31\x2d\x2e\x33\x33\x2e\x31\x39\x2d\x2e\x36\x36\x2e\x33\x37\ +\x2c\x30\x2d\x32\x2e\x33\x31\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\ +\x34\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x34\x68\x2d\x32\x33\x56\ +\x34\x38\x30\x2e\x36\x31\x68\x32\x33\x73\x30\x2d\x32\x30\x2e\x33\ +\x36\x2c\x30\x2d\x32\x30\x2e\x35\x31\x2c\x31\x30\x2e\x31\x38\x2c\ +\x35\x2e\x39\x31\x2c\x31\x34\x2e\x38\x34\x2c\x38\x2e\x36\x39\x4c\ +\x35\x38\x31\x2c\x34\x39\x33\x2e\x38\x35\x5a\x22\x2f\x3e\x3c\x70\ +\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\ +\x22\x20\x64\x3d\x22\x4d\x35\x38\x31\x2c\x34\x31\x33\x2e\x34\x33\ +\x63\x2d\x33\x2e\x36\x31\x2c\x32\x2e\x31\x33\x2d\x37\x2e\x32\x33\ +\x2c\x34\x2e\x32\x34\x2d\x31\x30\x2e\x38\x33\x2c\x36\x2e\x33\x38\ +\x6c\x2d\x34\x35\x2e\x34\x31\x2c\x32\x37\x63\x2d\x2e\x31\x36\x2e\ +\x31\x2d\x2e\x33\x33\x2e\x31\x38\x2d\x2e\x36\x36\x2e\x33\x37\x2c\ +\x30\x2d\x32\x2e\x33\x32\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x35\ +\x2d\x2e\x30\x36\x2d\x31\x39\x2e\x33\x35\x68\x2d\x32\x33\x56\x34\ +\x30\x30\x68\x32\x33\x73\x30\x2d\x32\x30\x2e\x33\x36\x2c\x30\x2d\ +\x32\x30\x2e\x35\x31\x2c\x31\x30\x2e\x31\x38\x2c\x35\x2e\x39\x31\ +\x2c\x31\x34\x2e\x38\x34\x2c\x38\x2e\x36\x38\x4c\x35\x38\x31\x2c\ +\x34\x31\x33\x2e\x32\x34\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\ \x00\x00\x04\xf7\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -25442,6 +25602,10 @@ \x0c\x81\x5a\x07\ \x00\x66\ \x00\x61\x00\x6e\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0a\ +\x0d\xc8\x7c\x07\ +\x00\x62\ +\x00\x6c\x00\x6f\x00\x77\x00\x65\x00\x72\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x11\ \x00\xdf\x07\x87\ \x00\x63\ @@ -25691,10 +25855,6 @@ \x0b\xad\x9f\xa7\ \x00\x63\ \x00\x6f\x00\x6f\x00\x6c\x00\x64\x00\x6f\x00\x77\x00\x6e\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0a\ -\x0d\xc8\x7c\x07\ -\x00\x62\ -\x00\x6c\x00\x6f\x00\x77\x00\x65\x00\x72\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x11\ \x0f\x7b\x93\xc7\ \x00\x68\ @@ -25876,15 +26036,15 @@ qt_resource_struct_v1 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x0f\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ -\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x91\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x85\ -\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7a\ -\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x76\ -\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x70\ -\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x62\ -\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x58\ -\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x49\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x95\ +\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ +\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x86\ +\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7b\ +\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x77\ +\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x71\ +\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x63\ +\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x59\ +\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4a\ \x00\x00\x00\xc0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x45\ \x00\x00\x00\xdc\x00\x02\x00\x00\x00\x01\x00\x00\x00\x35\ \x00\x00\x01\x02\x00\x02\x00\x00\x00\x01\x00\x00\x00\x31\ @@ -25945,148 +26105,149 @@ \x00\x00\x08\x2c\x00\x00\x00\x00\x00\x01\x00\x02\xb8\xa4\ \x00\x00\x08\x58\x00\x00\x00\x00\x00\x01\x00\x02\xbd\xed\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x46\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x47\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x03\x00\x00\x00\x47\ \x00\x00\x08\x8c\x00\x00\x00\x00\x00\x01\x00\x02\xc1\x65\ \x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x02\xca\x47\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4a\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x4b\ \x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x02\xcf\x7c\ -\x00\x00\x08\xe6\x00\x00\x00\x00\x00\x01\x00\x02\xd4\x77\ -\x00\x00\x09\x1e\x00\x00\x00\x00\x00\x01\x00\x02\xdd\x4b\ -\x00\x00\x09\x56\x00\x00\x00\x00\x00\x01\x00\x02\xe5\xef\ -\x00\x00\x09\x86\x00\x00\x00\x00\x00\x01\x00\x02\xed\x8e\ -\x00\x00\x09\xaa\x00\x00\x00\x00\x00\x01\x00\x02\xf5\x6a\ -\x00\x00\x09\xce\x00\x00\x00\x00\x00\x01\x00\x02\xfd\x20\ -\x00\x00\x0a\x02\x00\x00\x00\x00\x00\x01\x00\x03\x05\x2e\ -\x00\x00\x0a\x36\x00\x00\x00\x00\x00\x01\x00\x03\x0d\x10\ -\x00\x00\x0a\x6a\x00\x00\x00\x00\x00\x01\x00\x03\x14\xda\ -\x00\x00\x0a\xa4\x00\x00\x00\x00\x00\x01\x00\x03\x1c\x41\ -\x00\x00\x0a\xc8\x00\x00\x00\x00\x00\x01\x00\x03\x1f\xfe\ -\x00\x00\x0a\xf6\x00\x00\x00\x00\x00\x01\x00\x03\x23\xd7\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x59\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x08\x00\x00\x00\x5a\ -\x00\x00\x0b\x1c\x00\x00\x00\x00\x00\x01\x00\x03\x29\xe0\ -\x00\x00\x0b\x48\x00\x00\x00\x00\x00\x01\x00\x03\x4c\x64\ -\x00\x00\x0b\x76\x00\x00\x00\x00\x00\x01\x00\x03\x52\x51\ -\x00\x00\x0b\xa0\x00\x00\x00\x00\x00\x01\x00\x03\x54\x71\ -\x00\x00\x0b\xc8\x00\x00\x00\x00\x00\x01\x00\x03\x5d\x09\ -\x00\x00\x0b\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x6c\x52\ -\x00\x00\x0c\x0e\x00\x00\x00\x00\x00\x01\x00\x03\x72\xd0\ -\x00\x00\x0c\x36\x00\x00\x00\x00\x00\x01\x00\x03\x7d\x4a\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x63\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x64\ -\x00\x00\x0c\x6c\x00\x00\x00\x00\x00\x01\x00\x03\x86\x91\ -\x00\x00\x0c\x9a\x00\x00\x00\x00\x00\x01\x00\x03\x89\x38\ -\x00\x00\x0c\xc8\x00\x01\x00\x00\x00\x01\x00\x03\x95\xb5\ -\x00\x00\x0c\xf4\x00\x00\x00\x00\x00\x01\x00\x03\xc3\x34\ -\x00\x00\x0d\x14\x00\x00\x00\x00\x00\x01\x00\x03\xc7\xeb\ -\x00\x00\x0d\x46\x00\x01\x00\x00\x00\x01\x00\x04\x20\xe4\ -\x00\x00\x0d\x78\x00\x00\x00\x00\x00\x01\x00\x04\x55\x7e\ -\x00\x00\x0d\x92\x00\x00\x00\x00\x00\x01\x00\x04\x5a\xc8\ -\x00\x00\x0d\xac\x00\x00\x00\x00\x00\x01\x00\x04\x60\x57\ -\x00\x00\x0d\xc6\x00\x00\x00\x00\x00\x01\x00\x04\x65\xc0\ -\x00\x00\x0d\xde\x00\x00\x00\x00\x00\x01\x00\x04\x71\x9e\ -\x00\x00\x0d\xfc\x00\x00\x00\x00\x00\x01\x00\x04\x77\xa2\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x71\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x72\ -\x00\x00\x0e\x24\x00\x00\x00\x00\x00\x01\x00\x04\x7c\xd7\ -\x00\x00\x0e\x38\x00\x00\x00\x00\x00\x01\x00\x04\x82\xd4\ -\x00\x00\x0e\x4a\x00\x00\x00\x00\x00\x01\x00\x04\x84\x5a\ -\x00\x00\x0e\x5c\x00\x00\x00\x00\x00\x01\x00\x04\x8a\x54\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x77\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x78\ -\x00\x00\x0e\x70\x00\x00\x00\x00\x00\x01\x00\x04\x8c\xaa\ -\x00\x00\x0e\x9c\x00\x00\x00\x00\x00\x01\x00\x04\x93\x91\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7b\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x09\x00\x00\x00\x7c\ -\x00\x00\x0e\xc0\x00\x00\x00\x00\x00\x01\x00\x04\x9c\xf5\ -\x00\x00\x0e\xe0\x00\x01\x00\x00\x00\x01\x00\x04\x9f\xa1\ -\x00\x00\x0f\x04\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xf2\ -\x00\x00\x0f\x26\x00\x00\x00\x00\x00\x01\x00\x04\xb2\x97\ -\x00\x00\x0f\x42\x00\x00\x00\x00\x00\x01\x00\x04\xc3\x97\ -\x00\x00\x0f\x66\x00\x00\x00\x00\x00\x01\x00\x04\xcd\x18\ -\x00\x00\x0f\x86\x00\x00\x00\x00\x00\x01\x00\x04\xd2\xb5\ -\x00\x00\x0f\xae\x00\x00\x00\x00\x00\x01\x00\x04\xdc\x7e\ -\x00\x00\x0f\xce\x00\x00\x00\x00\x00\x01\x00\x04\xe0\x61\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x86\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x87\ -\x00\x00\x0f\xea\x00\x00\x00\x00\x00\x01\x00\x04\xe6\x9c\ -\x00\x00\x10\x1a\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x32\ -\x00\x00\x10\x4a\x00\x00\x00\x00\x00\x01\x00\x04\xfc\x0e\ -\x00\x00\x10\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x02\x52\ -\x00\x00\x10\x98\x00\x00\x00\x00\x00\x01\x00\x05\x09\xdb\ -\x00\x00\x10\xc4\x00\x00\x00\x00\x00\x01\x00\x05\x10\x39\ -\x00\x00\x10\xfa\x00\x00\x00\x00\x00\x01\x00\x05\x18\x28\ -\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x25\xcb\ -\x00\x00\x11\x18\x00\x00\x00\x00\x00\x01\x00\x05\x2b\x00\ -\x00\x00\x11\x32\x00\x00\x00\x00\x00\x01\x00\x05\x34\xd5\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ -\x00\x00\x11\x5a\x00\x00\x00\x00\x00\x01\x00\x05\x3c\x9f\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x95\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x28\x00\x00\x00\x96\ -\x00\x00\x11\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x41\x65\ -\x00\x00\x11\x90\x00\x00\x00\x00\x00\x01\x00\x05\x49\x19\ -\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x4b\x3e\ -\x00\x00\x11\xe0\x00\x00\x00\x00\x00\x01\x00\x05\x4c\xbe\ -\x00\x00\x11\xfc\x00\x00\x00\x00\x00\x01\x00\x05\x54\x6b\ -\x00\x00\x12\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x59\x40\ -\x00\x00\x12\x32\x00\x00\x00\x00\x00\x01\x00\x05\x5a\x30\ -\x00\x00\x12\x48\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x14\ -\x00\x00\x12\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x63\x4b\ -\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x05\x78\x58\ -\x00\x00\x12\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x7d\x48\ -\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x80\x47\ -\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x86\x51\ -\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x05\x89\xa0\ -\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x93\x21\ -\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\x99\x18\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9b\x29\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\xec\ -\x00\x00\x13\x88\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xa0\ -\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xab\xee\ -\x00\x00\x13\xd4\x00\x00\x00\x00\x00\x01\x00\x05\xaf\x7c\ -\x00\x00\x13\xfa\x00\x00\x00\x00\x00\x01\x00\x05\xb4\x1d\ -\x00\x00\x14\x0e\x00\x00\x00\x00\x00\x01\x00\x05\xbd\xef\ -\x00\x00\x14\x3a\x00\x00\x00\x00\x00\x01\x00\x05\xc3\x39\ -\x00\x00\x14\x62\x00\x00\x00\x00\x00\x01\x00\x05\xc9\x40\ -\x00\x00\x14\x78\x00\x00\x00\x00\x00\x01\x00\x05\xca\x24\ -\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x05\xcc\x6d\ -\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x1d\ -\x00\x00\x14\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x61\ -\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xd7\x8d\ -\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x05\xdd\x4e\ -\x00\x00\x15\x2a\x00\x00\x00\x00\x00\x01\x00\x05\xde\x70\ -\x00\x00\x15\x48\x00\x00\x00\x00\x00\x01\x00\x05\xe4\x63\ -\x00\x00\x15\x68\x00\x00\x00\x00\x00\x01\x00\x05\xe7\x67\ -\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x88\ -\x00\x00\x15\xaa\x00\x00\x00\x00\x00\x01\x00\x05\xeb\x5c\ -\x00\x00\x15\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xf3\xc8\ -\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x88\ -\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x06\x00\xa3\ -\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x01\xf6\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4b\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x4c\ +\x00\x00\x08\xd8\x00\x00\x00\x00\x00\x01\x00\x02\xd9\x51\ +\x00\x00\x09\x00\x00\x00\x00\x00\x00\x01\x00\x02\xde\x4c\ +\x00\x00\x09\x38\x00\x00\x00\x00\x00\x01\x00\x02\xe7\x20\ +\x00\x00\x09\x70\x00\x00\x00\x00\x00\x01\x00\x02\xef\xc4\ +\x00\x00\x09\xa0\x00\x00\x00\x00\x00\x01\x00\x02\xf7\x63\ +\x00\x00\x09\xc4\x00\x00\x00\x00\x00\x01\x00\x02\xff\x3f\ +\x00\x00\x09\xe8\x00\x00\x00\x00\x00\x01\x00\x03\x06\xf5\ +\x00\x00\x0a\x1c\x00\x00\x00\x00\x00\x01\x00\x03\x0f\x03\ +\x00\x00\x0a\x50\x00\x00\x00\x00\x00\x01\x00\x03\x16\xe5\ +\x00\x00\x0a\x84\x00\x00\x00\x00\x00\x01\x00\x03\x1e\xaf\ +\x00\x00\x0a\xbe\x00\x00\x00\x00\x00\x01\x00\x03\x26\x16\ +\x00\x00\x0a\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x29\xd3\ +\x00\x00\x0b\x10\x00\x00\x00\x00\x00\x01\x00\x03\x2d\xac\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5a\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x08\x00\x00\x00\x5b\ +\x00\x00\x0b\x36\x00\x00\x00\x00\x00\x01\x00\x03\x33\xb5\ +\x00\x00\x0b\x62\x00\x00\x00\x00\x00\x01\x00\x03\x56\x39\ +\x00\x00\x0b\x90\x00\x00\x00\x00\x00\x01\x00\x03\x5c\x26\ +\x00\x00\x0b\xba\x00\x00\x00\x00\x00\x01\x00\x03\x5e\x46\ +\x00\x00\x0b\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x66\xde\ +\x00\x00\x0b\xfc\x00\x00\x00\x00\x00\x01\x00\x03\x76\x27\ +\x00\x00\x0c\x28\x00\x00\x00\x00\x00\x01\x00\x03\x7c\xa5\ +\x00\x00\x0c\x50\x00\x00\x00\x00\x00\x01\x00\x03\x87\x1f\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x64\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x65\ +\x00\x00\x0c\x86\x00\x00\x00\x00\x00\x01\x00\x03\x90\x66\ +\x00\x00\x0c\xb4\x00\x00\x00\x00\x00\x01\x00\x03\x93\x0d\ +\x00\x00\x0c\xe2\x00\x01\x00\x00\x00\x01\x00\x03\x9f\x8a\ +\x00\x00\x0d\x0e\x00\x00\x00\x00\x00\x01\x00\x03\xcd\x09\ +\x00\x00\x0d\x2e\x00\x00\x00\x00\x00\x01\x00\x03\xd1\xc0\ +\x00\x00\x0d\x60\x00\x01\x00\x00\x00\x01\x00\x04\x2a\xb9\ +\x00\x00\x0d\x92\x00\x00\x00\x00\x00\x01\x00\x04\x5f\x53\ +\x00\x00\x0d\xac\x00\x00\x00\x00\x00\x01\x00\x04\x64\x9d\ +\x00\x00\x0d\xc6\x00\x00\x00\x00\x00\x01\x00\x04\x6a\x2c\ +\x00\x00\x0d\xe0\x00\x00\x00\x00\x00\x01\x00\x04\x6f\x95\ +\x00\x00\x0d\xf8\x00\x00\x00\x00\x00\x01\x00\x04\x7b\x73\ +\x00\x00\x0e\x16\x00\x00\x00\x00\x00\x01\x00\x04\x81\x77\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x72\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x73\ +\x00\x00\x0e\x3e\x00\x00\x00\x00\x00\x01\x00\x04\x86\xac\ +\x00\x00\x0e\x52\x00\x00\x00\x00\x00\x01\x00\x04\x8c\xa9\ +\x00\x00\x0e\x64\x00\x00\x00\x00\x00\x01\x00\x04\x8e\x2f\ +\x00\x00\x0e\x76\x00\x00\x00\x00\x00\x01\x00\x04\x94\x29\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x78\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x79\ +\x00\x00\x0e\x8a\x00\x00\x00\x00\x00\x01\x00\x04\x96\x7f\ +\x00\x00\x0e\xb6\x00\x00\x00\x00\x00\x01\x00\x04\x9d\x66\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7c\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x09\x00\x00\x00\x7d\ +\x00\x00\x0e\xda\x00\x00\x00\x00\x00\x01\x00\x04\xa6\xca\ +\x00\x00\x0e\xfa\x00\x01\x00\x00\x00\x01\x00\x04\xa9\x76\ +\x00\x00\x0f\x1e\x00\x00\x00\x00\x00\x01\x00\x04\xb4\xc7\ +\x00\x00\x0f\x40\x00\x00\x00\x00\x00\x01\x00\x04\xbc\x6c\ +\x00\x00\x0f\x5c\x00\x00\x00\x00\x00\x01\x00\x04\xcd\x6c\ +\x00\x00\x0f\x80\x00\x00\x00\x00\x00\x01\x00\x04\xd6\xed\ +\x00\x00\x0f\xa0\x00\x00\x00\x00\x00\x01\x00\x04\xdc\x8a\ +\x00\x00\x0f\xc8\x00\x00\x00\x00\x00\x01\x00\x04\xe6\x53\ +\x00\x00\x0f\xe8\x00\x00\x00\x00\x00\x01\x00\x04\xea\x36\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x87\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x88\ +\x00\x00\x10\x04\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x71\ +\x00\x00\x10\x34\x00\x00\x00\x00\x00\x01\x00\x04\xfa\x07\ +\x00\x00\x10\x64\x00\x00\x00\x00\x00\x01\x00\x05\x05\xe3\ +\x00\x00\x10\x88\x00\x00\x00\x00\x00\x01\x00\x05\x0c\x27\ +\x00\x00\x10\xb2\x00\x00\x00\x00\x00\x01\x00\x05\x13\xb0\ +\x00\x00\x10\xde\x00\x00\x00\x00\x00\x01\x00\x05\x1a\x0e\ +\x00\x00\x11\x14\x00\x00\x00\x00\x00\x01\x00\x05\x21\xfd\ +\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x2f\xa0\ +\x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x34\xd5\ +\x00\x00\x11\x32\x00\x00\x00\x00\x00\x01\x00\x05\x3e\xaa\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ +\x00\x00\x11\x5a\x00\x00\x00\x00\x00\x01\x00\x05\x46\x74\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x96\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x28\x00\x00\x00\x97\ +\x00\x00\x11\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x4b\x3a\ +\x00\x00\x11\x90\x00\x00\x00\x00\x00\x01\x00\x05\x52\xee\ +\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x55\x13\ +\x00\x00\x11\xe0\x00\x00\x00\x00\x00\x01\x00\x05\x56\x93\ +\x00\x00\x11\xfc\x00\x00\x00\x00\x00\x01\x00\x05\x5e\x40\ +\x00\x00\x12\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x63\x15\ +\x00\x00\x12\x32\x00\x00\x00\x00\x00\x01\x00\x05\x64\x05\ +\x00\x00\x12\x48\x00\x00\x00\x00\x00\x01\x00\x05\x66\xe9\ +\x00\x00\x12\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x6d\x20\ +\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x05\x82\x2d\ +\x00\x00\x12\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x87\x1d\ +\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x8a\x1c\ +\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x90\x26\ +\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x05\x93\x75\ +\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x9c\xf6\ +\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\xa2\xed\ +\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\xa4\xfe\ +\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xc1\ +\x00\x00\x13\x88\x00\x00\x00\x00\x00\x01\x00\x05\xb2\x75\ +\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xb5\xc3\ +\x00\x00\x13\xd4\x00\x00\x00\x00\x00\x01\x00\x05\xb9\x51\ +\x00\x00\x13\xfa\x00\x00\x00\x00\x00\x01\x00\x05\xbd\xf2\ +\x00\x00\x14\x0e\x00\x00\x00\x00\x00\x01\x00\x05\xc7\xc4\ +\x00\x00\x14\x3a\x00\x00\x00\x00\x00\x01\x00\x05\xcd\x0e\ +\x00\x00\x14\x62\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x15\ +\x00\x00\x14\x78\x00\x00\x00\x00\x00\x01\x00\x05\xd3\xf9\ +\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x42\ +\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xdc\xf2\ +\x00\x00\x14\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xe0\x36\ +\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xe1\x62\ +\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x05\xe7\x23\ +\x00\x00\x15\x2a\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x45\ +\x00\x00\x15\x48\x00\x00\x00\x00\x00\x01\x00\x05\xee\x38\ +\x00\x00\x15\x68\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x3c\ +\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xf2\x5d\ +\x00\x00\x15\xaa\x00\x00\x00\x00\x00\x01\x00\x05\xf5\x31\ +\x00\x00\x15\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xfd\x9d\ +\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x06\x05\x5d\ +\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x06\x0a\x78\ +\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x0b\xcb\ " qt_resource_struct_v2 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x0f\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x95\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x91\ +\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x85\ +\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x86\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7a\ +\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7b\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x76\ +\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x77\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x70\ +\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x71\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x62\ +\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x63\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x58\ +\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x59\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x49\ +\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4a\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\xc0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x45\ \x00\x00\x00\x00\x00\x00\x00\x00\ @@ -26105,75 +26266,75 @@ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x12\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x01\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x08\x66\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x09\x52\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x02\x20\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x35\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x02\x48\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x18\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x02\x70\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x06\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x02\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x14\x42\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x02\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf7\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ \x00\x00\x02\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x22\x41\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ \x00\x00\x03\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x26\x90\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x03\x36\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x6e\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x03\x52\x00\x00\x00\x00\x00\x01\x00\x00\x30\xca\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x03\x7a\x00\x00\x00\x00\x00\x01\x00\x00\x32\xb1\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x21\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x03\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x3a\x55\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ \x00\x00\x03\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x3c\xcf\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x03\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x3f\x43\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ \x00\x00\x03\xfe\x00\x00\x00\x00\x00\x01\x00\x00\x41\xbf\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x26\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x27\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x04\x20\x00\x00\x00\x00\x00\x01\x00\x00\x44\x3b\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x04\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x45\x3c\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x04\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x4a\x54\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ \x00\x00\x04\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x56\x16\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x04\xc4\x00\x00\x00\x00\x00\x01\x00\x00\x58\x86\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ \x00\x00\x04\xea\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x1e\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ \x00\x00\x05\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x62\x7a\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x05\x48\x00\x00\x00\x00\x00\x01\x00\x00\x69\x07\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x05\x64\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x03\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x05\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x6d\x01\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x32\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x33\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x05\xbe\x00\x01\x00\x00\x00\x01\x00\x00\x77\x6b\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ \x00\x00\x05\xd0\x00\x00\x00\x00\x00\x01\x00\x02\x3a\xc0\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x02\x00\x00\x00\x36\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x06\x00\x00\x00\x3f\ @@ -26181,273 +26342,275 @@ \x00\x00\x05\xe4\x00\x02\x00\x00\x00\x07\x00\x00\x00\x38\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x05\xf6\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ -\x00\x00\x01\x98\xe1\xb8\x63\x96\ +\x00\x00\x01\x9a\x72\xe1\x95\x8f\ \x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x49\xaa\ -\x00\x00\x01\x98\xe1\xb8\x63\x96\ +\x00\x00\x01\x9a\x72\xe1\x95\x93\ \x00\x00\x06\x5e\x00\x00\x00\x00\x00\x01\x00\x02\x53\xd3\ -\x00\x00\x01\x98\xe1\xb8\x63\x96\ +\x00\x00\x01\x9a\x72\xe1\x95\x8f\ \x00\x00\x06\x98\x00\x00\x00\x00\x00\x01\x00\x02\x5e\x2b\ -\x00\x00\x01\x98\xe1\xb8\x63\x9a\ +\x00\x00\x01\x9a\x72\xe1\x95\x93\ \x00\x00\x06\xcc\x00\x00\x00\x00\x00\x01\x00\x02\x68\x43\ -\x00\x00\x01\x98\xe1\xb8\x63\x96\ +\x00\x00\x01\x9a\x72\xe1\x95\x8f\ \x00\x00\x07\x02\x00\x00\x00\x00\x00\x01\x00\x02\x72\x6c\ -\x00\x00\x01\x98\xe1\xb8\x63\x96\ +\x00\x00\x01\x9a\x72\xe1\x95\x93\ \x00\x00\x07\x3a\x00\x00\x00\x00\x00\x01\x00\x02\x7c\x82\ -\x00\x00\x01\x98\xe1\xb8\x63\x96\ +\x00\x00\x01\x9a\x72\xe1\x95\x93\ \x00\x00\x07\x70\x00\x00\x00\x00\x00\x01\x00\x02\x86\xe8\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x07\x98\x00\x00\x00\x00\x00\x01\x00\x02\x91\x17\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ \x00\x00\x07\xc4\x00\x00\x00\x00\x00\x01\x00\x02\x9b\x5e\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x08\x00\x00\x00\x00\x00\x00\x01\x00\x02\x9d\x5f\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x08\x2c\x00\x00\x00\x00\x00\x01\x00\x02\xb8\xa4\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x08\x58\x00\x00\x00\x00\x00\x01\x00\x02\xbd\xed\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x46\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x47\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x03\x00\x00\x00\x47\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x08\x8c\x00\x00\x00\x00\x00\x01\x00\x02\xc1\x65\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x02\xca\x47\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4a\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x02\xcf\x7c\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4b\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x4b\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x4c\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x02\xcf\x7c\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x08\xe6\x00\x00\x00\x00\x00\x01\x00\x02\xd4\x77\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x09\x1e\x00\x00\x00\x00\x00\x01\x00\x02\xdd\x4b\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x09\x56\x00\x00\x00\x00\x00\x01\x00\x02\xe5\xef\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x09\x86\x00\x00\x00\x00\x00\x01\x00\x02\xed\x8e\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x09\xaa\x00\x00\x00\x00\x00\x01\x00\x02\xf5\x6a\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x09\xce\x00\x00\x00\x00\x00\x01\x00\x02\xfd\x20\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x0a\x02\x00\x00\x00\x00\x00\x01\x00\x03\x05\x2e\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x0a\x36\x00\x00\x00\x00\x00\x01\x00\x03\x0d\x10\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x0a\x6a\x00\x00\x00\x00\x00\x01\x00\x03\x14\xda\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x0a\xa4\x00\x00\x00\x00\x00\x01\x00\x03\x1c\x41\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x0a\xc8\x00\x00\x00\x00\x00\x01\x00\x03\x1f\xfe\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x0a\xf6\x00\x00\x00\x00\x00\x01\x00\x03\x23\xd7\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x59\ +\x00\x00\x08\xd8\x00\x00\x00\x00\x00\x01\x00\x02\xd9\x51\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x09\x00\x00\x00\x00\x00\x00\x01\x00\x02\xde\x4c\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x09\x38\x00\x00\x00\x00\x00\x01\x00\x02\xe7\x20\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x09\x70\x00\x00\x00\x00\x00\x01\x00\x02\xef\xc4\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x09\xa0\x00\x00\x00\x00\x00\x01\x00\x02\xf7\x63\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x09\xc4\x00\x00\x00\x00\x00\x01\x00\x02\xff\x3f\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x09\xe8\x00\x00\x00\x00\x00\x01\x00\x03\x06\xf5\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0a\x1c\x00\x00\x00\x00\x00\x01\x00\x03\x0f\x03\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0a\x50\x00\x00\x00\x00\x00\x01\x00\x03\x16\xe5\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0a\x84\x00\x00\x00\x00\x00\x01\x00\x03\x1e\xaf\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0a\xbe\x00\x00\x00\x00\x00\x01\x00\x03\x26\x16\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0a\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x29\xd3\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0b\x10\x00\x00\x00\x00\x00\x01\x00\x03\x2d\xac\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5a\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x08\x00\x00\x00\x5a\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x08\x00\x00\x00\x5b\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0b\x1c\x00\x00\x00\x00\x00\x01\x00\x03\x29\xe0\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x0b\x48\x00\x00\x00\x00\x00\x01\x00\x03\x4c\x64\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x0b\x76\x00\x00\x00\x00\x00\x01\x00\x03\x52\x51\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x0b\xa0\x00\x00\x00\x00\x00\x01\x00\x03\x54\x71\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x0b\xc8\x00\x00\x00\x00\x00\x01\x00\x03\x5d\x09\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x0b\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x6c\x52\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x0c\x0e\x00\x00\x00\x00\x00\x01\x00\x03\x72\xd0\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x0c\x36\x00\x00\x00\x00\x00\x01\x00\x03\x7d\x4a\ -\x00\x00\x01\x99\xe8\x24\xab\x4a\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x63\ +\x00\x00\x0b\x36\x00\x00\x00\x00\x00\x01\x00\x03\x33\xb5\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0b\x62\x00\x00\x00\x00\x00\x01\x00\x03\x56\x39\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0b\x90\x00\x00\x00\x00\x00\x01\x00\x03\x5c\x26\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0b\xba\x00\x00\x00\x00\x00\x01\x00\x03\x5e\x46\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0b\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x66\xde\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0b\xfc\x00\x00\x00\x00\x00\x01\x00\x03\x76\x27\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0c\x28\x00\x00\x00\x00\x00\x01\x00\x03\x7c\xa5\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x0c\x50\x00\x00\x00\x00\x00\x01\x00\x03\x87\x1f\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x64\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x64\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x65\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0c\x6c\x00\x00\x00\x00\x00\x01\x00\x03\x86\x91\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x0c\x9a\x00\x00\x00\x00\x00\x01\x00\x03\x89\x38\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x0c\xc8\x00\x01\x00\x00\x00\x01\x00\x03\x95\xb5\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x0c\xf4\x00\x00\x00\x00\x00\x01\x00\x03\xc3\x34\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x0d\x14\x00\x00\x00\x00\x00\x01\x00\x03\xc7\xeb\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x0d\x46\x00\x01\x00\x00\x00\x01\x00\x04\x20\xe4\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x0d\x78\x00\x00\x00\x00\x00\x01\x00\x04\x55\x7e\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x0d\x92\x00\x00\x00\x00\x00\x01\x00\x04\x5a\xc8\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x0d\xac\x00\x00\x00\x00\x00\x01\x00\x04\x60\x57\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x0d\xc6\x00\x00\x00\x00\x00\x01\x00\x04\x65\xc0\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x0d\xde\x00\x00\x00\x00\x00\x01\x00\x04\x71\x9e\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x0d\xfc\x00\x00\x00\x00\x00\x01\x00\x04\x77\xa2\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x71\ +\x00\x00\x0c\x86\x00\x00\x00\x00\x00\x01\x00\x03\x90\x66\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x0c\xb4\x00\x00\x00\x00\x00\x01\x00\x03\x93\x0d\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0c\xe2\x00\x01\x00\x00\x00\x01\x00\x03\x9f\x8a\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0d\x0e\x00\x00\x00\x00\x00\x01\x00\x03\xcd\x09\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x0d\x2e\x00\x00\x00\x00\x00\x01\x00\x03\xd1\xc0\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x0d\x60\x00\x01\x00\x00\x00\x01\x00\x04\x2a\xb9\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x0d\x92\x00\x00\x00\x00\x00\x01\x00\x04\x5f\x53\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0d\xac\x00\x00\x00\x00\x00\x01\x00\x04\x64\x9d\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0d\xc6\x00\x00\x00\x00\x00\x01\x00\x04\x6a\x2c\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0d\xe0\x00\x00\x00\x00\x00\x01\x00\x04\x6f\x95\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0d\xf8\x00\x00\x00\x00\x00\x01\x00\x04\x7b\x73\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0e\x16\x00\x00\x00\x00\x00\x01\x00\x04\x81\x77\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x72\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x72\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x73\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0e\x24\x00\x00\x00\x00\x00\x01\x00\x04\x7c\xd7\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x0e\x38\x00\x00\x00\x00\x00\x01\x00\x04\x82\xd4\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x0e\x4a\x00\x00\x00\x00\x00\x01\x00\x04\x84\x5a\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x0e\x5c\x00\x00\x00\x00\x00\x01\x00\x04\x8a\x54\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x77\ +\x00\x00\x0e\x3e\x00\x00\x00\x00\x00\x01\x00\x04\x86\xac\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0e\x52\x00\x00\x00\x00\x00\x01\x00\x04\x8c\xa9\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0e\x64\x00\x00\x00\x00\x00\x01\x00\x04\x8e\x2f\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0e\x76\x00\x00\x00\x00\x00\x01\x00\x04\x94\x29\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x78\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x78\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x79\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0e\x70\x00\x00\x00\x00\x00\x01\x00\x04\x8c\xaa\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x0e\x9c\x00\x00\x00\x00\x00\x01\x00\x04\x93\x91\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7b\ +\x00\x00\x0e\x8a\x00\x00\x00\x00\x00\x01\x00\x04\x96\x7f\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0e\xb6\x00\x00\x00\x00\x00\x01\x00\x04\x9d\x66\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7c\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x09\x00\x00\x00\x7c\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x09\x00\x00\x00\x7d\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0e\xc0\x00\x00\x00\x00\x00\x01\x00\x04\x9c\xf5\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x0e\xe0\x00\x01\x00\x00\x00\x01\x00\x04\x9f\xa1\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x0f\x04\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xf2\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x0f\x26\x00\x00\x00\x00\x00\x01\x00\x04\xb2\x97\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x0f\x42\x00\x00\x00\x00\x00\x01\x00\x04\xc3\x97\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x0f\x66\x00\x00\x00\x00\x00\x01\x00\x04\xcd\x18\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x0f\x86\x00\x00\x00\x00\x00\x01\x00\x04\xd2\xb5\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x0f\xae\x00\x00\x00\x00\x00\x01\x00\x04\xdc\x7e\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x0f\xce\x00\x00\x00\x00\x00\x01\x00\x04\xe0\x61\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x86\ +\x00\x00\x0e\xda\x00\x00\x00\x00\x00\x01\x00\x04\xa6\xca\ +\x00\x00\x01\x9b\x13\x3e\xbb\x62\ +\x00\x00\x0e\xfa\x00\x01\x00\x00\x00\x01\x00\x04\xa9\x76\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x0f\x1e\x00\x00\x00\x00\x00\x01\x00\x04\xb4\xc7\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0f\x40\x00\x00\x00\x00\x00\x01\x00\x04\xbc\x6c\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0f\x5c\x00\x00\x00\x00\x00\x01\x00\x04\xcd\x6c\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x0f\x80\x00\x00\x00\x00\x00\x01\x00\x04\xd6\xed\ +\x00\x00\x01\x9b\x13\x3e\xbb\x62\ +\x00\x00\x0f\xa0\x00\x00\x00\x00\x00\x01\x00\x04\xdc\x8a\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x0f\xc8\x00\x00\x00\x00\x00\x01\x00\x04\xe6\x53\ +\x00\x00\x01\x9b\x13\x3e\xbb\x62\ +\x00\x00\x0f\xe8\x00\x00\x00\x00\x00\x01\x00\x04\xea\x36\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x87\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x87\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x88\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0f\xea\x00\x00\x00\x00\x00\x01\x00\x04\xe6\x9c\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x10\x1a\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x32\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x10\x4a\x00\x00\x00\x00\x00\x01\x00\x04\xfc\x0e\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x10\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x02\x52\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x10\x98\x00\x00\x00\x00\x00\x01\x00\x05\x09\xdb\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x10\xc4\x00\x00\x00\x00\x00\x01\x00\x05\x10\x39\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x10\xfa\x00\x00\x00\x00\x00\x01\x00\x05\x18\x28\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x25\xcb\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x11\x18\x00\x00\x00\x00\x00\x01\x00\x05\x2b\x00\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x11\x32\x00\x00\x00\x00\x00\x01\x00\x05\x34\xd5\ -\x00\x00\x01\x99\xe8\x24\xab\x4a\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ +\x00\x00\x10\x04\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x71\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x10\x34\x00\x00\x00\x00\x00\x01\x00\x04\xfa\x07\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x10\x64\x00\x00\x00\x00\x00\x01\x00\x05\x05\xe3\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x10\x88\x00\x00\x00\x00\x00\x01\x00\x05\x0c\x27\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x10\xb2\x00\x00\x00\x00\x00\x01\x00\x05\x13\xb0\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x10\xde\x00\x00\x00\x00\x00\x01\x00\x05\x1a\x0e\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x11\x14\x00\x00\x00\x00\x00\x01\x00\x05\x21\xfd\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x2f\xa0\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x34\xd5\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x11\x32\x00\x00\x00\x00\x00\x01\x00\x05\x3e\xaa\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x11\x5a\x00\x00\x00\x00\x00\x01\x00\x05\x3c\x9f\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x95\ +\x00\x00\x11\x5a\x00\x00\x00\x00\x00\x01\x00\x05\x46\x74\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x96\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x28\x00\x00\x00\x96\ +\x00\x00\x01\x98\x00\x02\x00\x00\x00\x28\x00\x00\x00\x97\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x11\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x41\x65\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x11\x90\x00\x00\x00\x00\x00\x01\x00\x05\x49\x19\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x4b\x3e\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x11\xe0\x00\x00\x00\x00\x00\x01\x00\x05\x4c\xbe\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x11\xfc\x00\x00\x00\x00\x00\x01\x00\x05\x54\x6b\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x12\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x59\x40\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x12\x32\x00\x00\x00\x00\x00\x01\x00\x05\x5a\x30\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x12\x48\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x14\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x12\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x63\x4b\ -\x00\x00\x01\x99\x96\xf9\x85\x18\ -\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x05\x78\x58\ -\x00\x00\x01\x99\xe8\x24\xab\x4a\ -\x00\x00\x12\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x7d\x48\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x80\x47\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x86\x51\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x05\x89\xa0\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x93\x21\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\x99\x18\ -\x00\x00\x01\x99\x96\xf9\x85\x18\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9b\x29\ -\x00\x00\x01\x99\x96\xf9\x85\x18\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\xec\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x13\x88\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xa0\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xab\xee\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x13\xd4\x00\x00\x00\x00\x00\x01\x00\x05\xaf\x7c\ -\x00\x00\x01\x99\xed\x4d\xf1\x14\ -\x00\x00\x13\xfa\x00\x00\x00\x00\x00\x01\x00\x05\xb4\x1d\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x14\x0e\x00\x00\x00\x00\x00\x01\x00\x05\xbd\xef\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x14\x3a\x00\x00\x00\x00\x00\x01\x00\x05\xc3\x39\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x14\x62\x00\x00\x00\x00\x00\x01\x00\x05\xc9\x40\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x14\x78\x00\x00\x00\x00\x00\x01\x00\x05\xca\x24\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ -\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x05\xcc\x6d\ -\x00\x00\x01\x98\xe1\xb8\x63\x0e\ -\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x1d\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x14\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x61\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xd7\x8d\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x05\xdd\x4e\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x15\x2a\x00\x00\x00\x00\x00\x01\x00\x05\xde\x70\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x15\x48\x00\x00\x00\x00\x00\x01\x00\x05\xe4\x63\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x15\x68\x00\x00\x00\x00\x00\x01\x00\x05\xe7\x67\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x88\ -\x00\x00\x01\x98\xe1\xb8\x63\x0a\ -\x00\x00\x15\xaa\x00\x00\x00\x00\x00\x01\x00\x05\xeb\x5c\ -\x00\x00\x01\x98\xe1\xb8\x63\x06\ -\x00\x00\x15\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xf3\xc8\ -\x00\x00\x01\x99\x7d\x04\xc2\x80\ -\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x88\ -\x00\x00\x01\x98\xe1\xb8\x63\x02\ -\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x06\x00\xa3\ -\x00\x00\x01\x99\x4c\xf0\xd6\xbc\ -\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x01\xf6\ -\x00\x00\x01\x98\xe1\xb8\x62\xfe\ +\x00\x00\x11\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x4b\x3a\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x11\x90\x00\x00\x00\x00\x00\x01\x00\x05\x52\xee\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x55\x13\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x11\xe0\x00\x00\x00\x00\x00\x01\x00\x05\x56\x93\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x11\xfc\x00\x00\x00\x00\x00\x01\x00\x05\x5e\x40\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x12\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x63\x15\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x12\x32\x00\x00\x00\x00\x00\x01\x00\x05\x64\x05\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x12\x48\x00\x00\x00\x00\x00\x01\x00\x05\x66\xe9\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x12\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x6d\x20\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x05\x82\x2d\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x12\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x87\x1d\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x8a\x1c\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x90\x26\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x05\x93\x75\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x9c\xf6\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\xa2\xed\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\xa4\xfe\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xc1\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x13\x88\x00\x00\x00\x00\x00\x01\x00\x05\xb2\x75\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xb5\xc3\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x13\xd4\x00\x00\x00\x00\x00\x01\x00\x05\xb9\x51\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x13\xfa\x00\x00\x00\x00\x00\x01\x00\x05\xbd\xf2\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x14\x0e\x00\x00\x00\x00\x00\x01\x00\x05\xc7\xc4\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x14\x3a\x00\x00\x00\x00\x00\x01\x00\x05\xcd\x0e\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x14\x62\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x15\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x14\x78\x00\x00\x00\x00\x00\x01\x00\x05\xd3\xf9\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x42\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xdc\xf2\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x14\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xe0\x36\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xe1\x62\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x05\xe7\x23\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x15\x2a\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x45\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x15\x48\x00\x00\x00\x00\x00\x01\x00\x05\xee\x38\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x15\x68\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x3c\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xf2\x5d\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x15\xaa\x00\x00\x00\x00\x00\x01\x00\x05\xf5\x31\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x15\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xfd\x9d\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x06\x05\x5d\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x06\x0a\x78\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x0b\xcb\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ " qt_version = [int(v) for v in QtCore.qVersion().split('.')] From c3bfa568648ce68b75836e466151f92e4886339a Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Fri, 2 Jan 2026 12:23:30 +0000 Subject: [PATCH 23/70] Work display info UI (#140) * Bugfix: fixed total layer/current layer fallback * Add: added font size and family * bugfix: where the fallback was allways active * Refactor: ran ruff formatter --------- Signed-off-by: Hugo Costa Co-authored-by: Roberto Co-authored-by: Hugo Costa --- BlocksScreen/helper_methods.py | 5 +- .../lib/panels/widgets/jobStatusPage.py | 56 +++++++++++++------ BlocksScreen/lib/utils/display_button.py | 8 +++ 3 files changed, 50 insertions(+), 19 deletions(-) diff --git a/BlocksScreen/helper_methods.py b/BlocksScreen/helper_methods.py index 8de5fc13..dafabd96 100644 --- a/BlocksScreen/helper_methods.py +++ b/BlocksScreen/helper_methods.py @@ -254,7 +254,10 @@ def calculate_current_layer( """ if z_position == 0: return -1 - _current_layer = 1 + (z_position - first_layer_height) / layer_height + if z_position <= first_layer_height: + return 1 + + _current_layer = (z_position) / layer_height return int(_current_layer) diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index 9f74ba6e..d161fa22 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -58,6 +58,7 @@ class JobStatusWidget(QtWidgets.QWidget): def __init__(self, parent) -> None: super().__init__(parent) self.thumbnail_graphics = [] + self.layer_fallback = False self._setupUI() self.cancel_print_dialog = dialogPage.DialogPage(self) self.tune_menu_btn.clicked.connect(self.tune_clicked.emit) @@ -180,10 +181,8 @@ def on_print_start(self, file: str) -> None: @QtCore.pyqtSlot(dict, name="on_fileinfo") def on_fileinfo(self, fileinfo: dict) -> None: """Handle received file information/metadata""" - if not self.isVisible(): - return - self.total_layers = str(fileinfo.get("layer_count", "?")) - self.layer_display_button.setText("?") + self.total_layers = str(fileinfo.get("layer_count", "---")) + self.layer_display_button.setText("---") self.layer_display_button.secondary_text = str(self.total_layers) self.file_metadata = fileinfo self._load_thumbnails(*fileinfo.get("thumbnail_images", [])) @@ -269,13 +268,23 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: if not self.isVisible(): return if isinstance(value, dict): + self.layer_fallback = False if "total_layer" in value.keys(): - self.total_layers = value.get("total_layer", "?") - self.layer_display_button.secondary_text = str(self.total_layers) + self.total_layers = value["total_layer"] + if value["total_layer"] is not None: + self.layer_display_button.secondary_text = str(self.total_layers) + + else: + self.total_layers = "---" + self.layer_fallback = True + if "current_layer" in value.keys(): - _current_layer = value.get("current_layer", None) - if _current_layer: + if value["current_layer"] is not None: + _current_layer = value["current_layer"] self.layer_display_button.setText(f"{int(_current_layer)}") + else: + self.layer_display_button.setText("---") + self.layer_fallback = True elif isinstance(value, float): if "total_duration" in field: _time = estimate_print_time(int(value)) @@ -293,17 +302,28 @@ def on_gcode_move_update(self, field: str, value: list) -> None: return if "gcode_position" in field: if self._internal_print_status == "printing": - _current_layer = calculate_current_layer( - z_position=value[2], - object_height=float(self.file_metadata.get("object_height", -1.0)), - layer_height=float(self.file_metadata.get("layer_height", -1.0)), - first_layer_height=float( + if self.layer_fallback: + object_height = float(self.file_metadata.get("object_height", -1.0)) + layer_height = float(self.file_metadata.get("layer_height", -1.0)) + first_layer_height = float( self.file_metadata.get("first_layer_height", -1.0) - ), - ) - self.layer_display_button.setText( - f"{int(_current_layer)}" if _current_layer != -1 else "?" - ) + ) + _current_layer = calculate_current_layer( + z_position=value[2], + object_height=object_height, + layer_height=layer_height, + first_layer_height=first_layer_height, + ) + + total_layer = ( + (object_height) / layer_height if layer_height > 0 else -1 + ) + self.layer_display_button.secondary_text = ( + f"{int(total_layer)}" if total_layer != -1 else "---" + ) + self.layer_display_button.setText( + f"{int(_current_layer)}" if _current_layer != -1 else "---" + ) @QtCore.pyqtSlot(str, float, name="virtual_sdcard_update") @QtCore.pyqtSlot(str, bool, name="virtual_sdcard_update") diff --git a/BlocksScreen/lib/utils/display_button.py b/BlocksScreen/lib/utils/display_button.py index 1f798dfe..5bda40aa 100644 --- a/BlocksScreen/lib/utils/display_button.py +++ b/BlocksScreen/lib/utils/display_button.py @@ -174,6 +174,10 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: int(_mtl.width() / 2.0), _rect.height(), ) + font = QtGui.QFont() + font.setPointSize(12) + font.setFamily("Momcake-bold") + painter.setFont(font) painter.drawText( _ptl_rect, QtCore.Qt.TextFlag.TextShowMnemonic @@ -240,6 +244,10 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: ) else: + font = QtGui.QFont() + font.setPointSize(12) + font.setFamily("Momcake-bold") + painter.setFont(font) painter.drawText( _mtl, QtCore.Qt.TextFlag.TextShowMnemonic From 11c85729d63deb869be00ac4728110b9ec577143 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Mon, 5 Jan 2026 17:17:39 +0000 Subject: [PATCH 24/70] Work input shapper rework (#134) * ADD: added input shaper page * bugfix: button blocking clicks * ADD: added input shapper logic * ADD: added input shapper page to .ui * Add: basepopup.py Refactor: loadwidget to have similar logic to loadpage Removed: dialog page and loadPage * Refactor: change popup to Basepopup or/with loadwidget * Rev: removed misstype --------- Co-authored-by: Roberto --- BlocksScreen/lib/panels/controlTab.py | 35 +- BlocksScreen/lib/panels/filamentTab.py | 16 +- BlocksScreen/lib/panels/printTab.py | 24 +- BlocksScreen/lib/panels/utilitiesTab.py | 280 +++-- BlocksScreen/lib/panels/widgets/basePopup.py | 256 +++++ BlocksScreen/lib/panels/widgets/dialogPage.py | 167 --- .../lib/panels/widgets/inputshaperPage.py | 459 +++++++++ .../lib/panels/widgets/jobStatusPage.py | 4 +- BlocksScreen/lib/panels/widgets/loadPage.py | 180 ---- BlocksScreen/lib/panels/widgets/loadWidget.py | 145 ++- .../lib/panels/widgets/optionCardWidget.py | 3 + .../lib/panels/widgets/probeHelperPage.py | 12 +- BlocksScreen/lib/panels/widgets/updatePage.py | 16 +- BlocksScreen/lib/ui/utilitiesStackedWidget.ui | 968 +----------------- .../lib/ui/utilitiesStackedWidget_ui.py | 659 +++--------- 15 files changed, 1278 insertions(+), 1946 deletions(-) create mode 100644 BlocksScreen/lib/panels/widgets/basePopup.py delete mode 100644 BlocksScreen/lib/panels/widgets/dialogPage.py create mode 100644 BlocksScreen/lib/panels/widgets/inputshaperPage.py delete mode 100644 BlocksScreen/lib/panels/widgets/loadPage.py diff --git a/BlocksScreen/lib/panels/controlTab.py b/BlocksScreen/lib/panels/controlTab.py index 99f8a3c5..c17cb176 100644 --- a/BlocksScreen/lib/panels/controlTab.py +++ b/BlocksScreen/lib/panels/controlTab.py @@ -4,7 +4,8 @@ from functools import partial import re from lib.moonrakerComm import MoonWebSocket -from lib.panels.widgets.loadPage import LoadScreen +from lib.panels.widgets.loadWidget import LoadingOverlayWidget +from lib.panels.widgets.basePopup import BasePopup from lib.panels.widgets.numpadPage import CustomNumpad from lib.panels.widgets.printcorePage import SwapPrintcorePage from lib.panels.widgets.probeHelperPage import ProbeHelper @@ -79,8 +80,12 @@ def __init__( self.addWidget(self.probe_helper_page) self.printcores_page = SwapPrintcorePage(self) self.addWidget(self.printcores_page) - self.loadpage = LoadScreen(self, LoadScreen.AnimationGIF.DEFAULT) - self.addWidget(self.loadpage) + + self.loadscreen = BasePopup(self, floating=False, dialog=False) + self.loadwidget = LoadingOverlayWidget( + self, LoadingOverlayWidget.AnimationGIF.DEFAULT + ) + self.loadscreen.add_widget(self.loadwidget) self.sliderPage = SliderPage(self) self.addWidget(self.sliderPage) @@ -415,17 +420,17 @@ def handle_printcoreupdate(self, value: dict): return if value["swapping"] == "in_pos": - self.loadpage.hide() + self.loadscreen.hide() self.printcores_page.show() self.disable_popups.emit(True) self.printcores_page.setText( "Please Insert Print Core \n \n Afterwards click continue" ) if value["swapping"] == "unloading": - self.loadpage.set_status_message("Unloading print core") + self.loadwidget.set_status_message("Unloading print core") if value["swapping"] == "cleaning": - self.loadpage.set_status_message("Cleaning print core") + self.loadwidget.set_status_message("Cleaning print core") def _handle_gcode_response(self, messages: list): """Handle gcode response for Z-tilt adjustment""" @@ -448,21 +453,23 @@ def _handle_gcode_response(self, messages: list): probed_range = float(match.group(3)) tolerance = float(match.group(4)) if retries_done == retries_total: - self.loadpage.hide() + self.loadscreen.hide() return if probed_range < tolerance: - self.loadpage.hide() + self.loadscreen.hide() return - self.loadpage.set_status_message( + self.loadwidget.set_status_message( f"Retries: {retries_done}/{retries_total} | Range: {probed_range:.6f} | Tolerance: {tolerance:.6f}" ) def handle_ztilt(self): """Handle Z-Tilt Adjustment""" - self.loadpage.show() - self.loadpage.set_status_message("Please wait, performing Z-axis calibration.") + self.loadscreen.show() + self.loadwidget.set_status_message( + "Please wait, performing Z-axis calibration." + ) self.run_gcode_signal.emit("G28\nM400\nZ_TILT_ADJUST") @QtCore.pyqtSlot(str, name="on-klippy-status") @@ -479,8 +486,8 @@ def on_klippy_status(self, state: str): def show_swapcore(self): """Show swap printcore""" self.run_gcode_signal.emit("CHANGE_PRINTCORES") - self.loadpage.show() - self.loadpage.set_status_message("Preparing to swap print core") + self.loadscreen.show() + self.loadwidget.set_status_message("Preparing to swap print core") def handle_swapcore(self): """Handle swap printcore routine finish""" @@ -652,7 +659,7 @@ def on_toolhead_update(self, field: str, values: list) -> None: self.panel.mva_z_value_label.setText(f"{values[2]:.3f}") if values[0] == "252,50" and values[1] == "250" and values[2] == "50": - self.loadpage.hide + self.loadscreen.hide self.toolhead_info.update({f"{field}": values}) @QtCore.pyqtSlot(str, str, float, name="on-extruder-update") diff --git a/BlocksScreen/lib/panels/filamentTab.py b/BlocksScreen/lib/panels/filamentTab.py index 4ab358fa..1271375e 100644 --- a/BlocksScreen/lib/panels/filamentTab.py +++ b/BlocksScreen/lib/panels/filamentTab.py @@ -6,7 +6,8 @@ from lib.filament import Filament from lib.ui.filamentStackedWidget_ui import Ui_filamentStackedWidget -from lib.panels.widgets.loadPage import LoadScreen +from lib.panels.widgets.loadWidget import LoadingOverlayWidget +from lib.panels.widgets.basePopup import BasePopup from lib.panels.widgets.popupDialogWidget import Popup from PyQt6 import QtCore, QtGui, QtWidgets @@ -41,8 +42,11 @@ def __init__(self, parent: QtWidgets.QWidget, printer: Printer, ws, /) -> None: self.target_temp: int = 0 self.current_temp: int = 0 self.popup = Popup(self) - self.loadscreen = LoadScreen(self, LoadScreen.AnimationGIF.DEFAULT) - self.addWidget(self.loadscreen) + self.loadscreen = BasePopup(self, floating=False, dialog=False) + self.loadwidget = LoadingOverlayWidget( + self, LoadingOverlayWidget.AnimationGIF.DEFAULT + ) + self.loadscreen.add_widget(self.loadwidget) self.has_load_unload_objects = None self._filament_state = self.FilamentStates.UNKNOWN self._sensor_states = {} @@ -129,16 +133,16 @@ def on_extruder_update( if self.target_temp != 0: if self.current_temp == self.target_temp: - self.loadscreen.set_status_message("Extruder heated up \n Please wait") + self.loadwidget.set_status_message("Extruder heated up \n Please wait") return if field == "temperature": self.current_temp = round(new_value, 0) - self.loadscreen.set_status_message( + self.loadwidget.set_status_message( f"Heating up ({new_value}/{self.target_temp}) \n Please wait" ) if field == "target": self.target_temp = round(new_value, 0) - self.loadscreen.set_status_message("Heating up \n Please wait") + self.loadwidget.set_status_message("Heating up \n Please wait") @QtCore.pyqtSlot(bool, name="on_load_filament") def on_load_filament(self, status: bool): diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index 65f0030d..104a765a 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -14,8 +14,8 @@ from lib.panels.widgets.slider_selector_page import SliderPage from lib.utils.blocks_button import BlocksCustomButton from lib.panels.widgets.numpadPage import CustomNumpad -from lib.panels.widgets.loadPage import LoadScreen -from lib.panels.widgets.dialogPage import DialogPage +from lib.panels.widgets.loadWidget import LoadingOverlayWidget +from lib.panels.widgets.basePopup import BasePopup from configfile import BlocksScreenConfig, get_configparser from PyQt6 import QtCore, QtGui, QtWidgets @@ -85,14 +85,18 @@ def __init__( self.numpadPage = CustomNumpad(self) self.numpadPage.request_back.connect(self.back_button) self.addWidget(self.numpadPage) - self.loadscreen = LoadScreen(self, LoadScreen.AnimationGIF.DEFAULT) - self.addWidget(self.loadscreen) + + self.loadscreen = BasePopup(self, floating=False, dialog=False) + self.loadwidget = LoadingOverlayWidget( + self, LoadingOverlayWidget.AnimationGIF.DEFAULT + ) + self.loadscreen.add_widget(self.loadwidget) self.file_data: Files = file_data self.filesPage_widget = FilesPage(self) self.addWidget(self.filesPage_widget) - self.dialogPage = DialogPage(self) + self.BasePopup = BasePopup(self) self.confirmPage_widget = ConfirmWidget(self) self.addWidget(self.confirmPage_widget) @@ -295,18 +299,18 @@ def on_slidePage_request( @QtCore.pyqtSlot(str, name="delete_file") def delete_file(self, filename: str, directory: str = "gcodes") -> None: """Handle Delete file signal, shows confirmation dialog""" - self.dialogPage.set_message("Are you sure you want to delete this file?") - self.dialogPage.accepted.connect( + self.BasePopup.set_message("Are you sure you want to delete this file?") + self.BasePopup.accepted.connect( lambda: self._on_delete_file_confirmed(filename, directory) ) - self.dialogPage.open() + self.BasePopup.open() def _on_delete_file_confirmed(self, filename: str, directory: str) -> None: """Handle confirmed file deletion after user accepted the dialog""" self.file_data.on_request_delete_file(filename, directory) self.request_back.emit() self.filesPage_widget.reset_dir() - self.dialogPage.disconnect() + self.BasePopup.disconnect() def paintEvent(self, a0: QtGui.QPaintEvent) -> None: """Widget painting""" @@ -341,7 +345,7 @@ def handle_cancel_print(self) -> None: self.on_cancel_print.emit() self.loadscreen.show() self.loadscreen.setModal(True) - self.loadscreen.set_status_message("Cancelling print...\nPlease wait") + self.loadwidget.set_status_message("Cancelling print...\nPlease wait") def change_page(self, index: int) -> None: """Requests a page change page to the global manager diff --git a/BlocksScreen/lib/panels/utilitiesTab.py b/BlocksScreen/lib/panels/utilitiesTab.py index 4797b2ff..1c9d0fa2 100644 --- a/BlocksScreen/lib/panels/utilitiesTab.py +++ b/BlocksScreen/lib/panels/utilitiesTab.py @@ -1,11 +1,9 @@ -import csv import typing from dataclasses import dataclass from enum import Enum, auto from functools import partial from lib.moonrakerComm import MoonWebSocket -from lib.panels.widgets.loadPage import LoadScreen from lib.panels.widgets.troubleshootPage import TroubleshootPage from lib.panels.widgets.updatePage import UpdatePage from lib.printer import Printer @@ -14,6 +12,13 @@ from lib.utils.toggleAnimatedButton import ToggleAnimatedButton from PyQt6 import QtCore, QtGui, QtWidgets +from lib.panels.widgets.optionCardWidget import OptionCard +from lib.panels.widgets.inputshaperPage import InputShaperPage +from lib.panels.widgets.basePopup import BasePopup +from lib.panels.widgets.loadWidget import LoadingOverlayWidget + +import re + @dataclass class LedState: @@ -41,8 +46,6 @@ def get_gcode(self, name: str) -> str: class Process(Enum): - """Printer Process""" - FAN = auto() AXIS = auto() BED_HEATER = auto() @@ -113,25 +116,34 @@ def __init__( self.amount: int = 1 self.tb: bool = False self.cg = None + self.aut: bool = False # --- UI Setup --- self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.loadPage = LoadScreen(self) - self.addWidget(self.loadPage) + self.loadPage = BasePopup(self) + self.loadwidget = LoadingOverlayWidget( + self, LoadingOverlayWidget.AnimationGIF.DEFAULT + ) + self.loadPage.add_widget(self.loadwidget) self.update_page = UpdatePage(self) self.addWidget(self.update_page) - self.panel.utilities_input_shaper_btn.hide() + self.is_page = InputShaperPage(self) + self.addWidget(self.is_page) + + self.dialog_page = BasePopup(self, dialog=True, floating=True) + self.addWidget(self.dialog_page) + # --- Back Buttons --- for button in ( - self.panel.is_back_btn, self.panel.leds_back_btn, self.panel.info_back_btn, self.panel.leds_slider_back_btn, self.panel.input_shaper_back_btn, self.panel.routine_check_back_btn, self.update_page.update_back_btn, + self.is_page.update_back_btn, ): button.clicked.connect(self.back_button) @@ -145,7 +157,6 @@ def __init__( self._connect_page_change( self.panel.utilities_routine_check_btn, self.panel.routines_page ) - self._connect_page_change(self.panel.is_confirm_btn, self.panel.utilities_page) self._connect_page_change(self.panel.am_cancel, self.panel.utilities_page) self._connect_page_change(self.panel.axes_back_btn, self.panel.utilities_page) @@ -168,20 +179,6 @@ def __init__( self.panel.axis_y_btn.clicked.connect(partial(self.axis_maintenance, "y")) self.panel.axis_z_btn.clicked.connect(partial(self.axis_maintenance, "z")) - # --- Input Shaper --- - self.panel.is_X_startis_btn.clicked.connect( - partial(self.run_resonance_test, "x") - ) - self.panel.is_Y_startis_btn.clicked.connect( - partial(self.run_resonance_test, "y") - ) - self.panel.am_confirm.clicked.connect(self.apply_input_shaper_selection) - self.panel.isc_btn_group.buttonClicked.connect( - lambda btn: setattr(self, "ammount", int(btn.text())) - ) - self._connect_numpad_request(self.panel.isui_fq, "frequency", "Frequency") - self._connect_numpad_request(self.panel.isui_sm, "smoothing", "Smoothing") - self.panel.toggle_led_button.state = ToggleAnimatedButton.State.ON # --- LEDs --- @@ -193,6 +190,7 @@ def __init__( # --- Websocket/Printer Signals --- self.run_gcode_signal.connect(self.ws.api.run_gcode) + self.is_page.run_gcode_signal.connect(self.ws.api.run_gcode) self.subscribe_config[str, "PyQt_PyObject"].connect( self.printer.on_subscribe_config ) @@ -220,6 +218,7 @@ def __init__( self.update_page.request_refresh_update[str].connect( self.ws.api.refresh_update_status ) + self.printer.gcode_response.connect(self.handle_gcode_response) self.update_page.request_rollback_update.connect(self.ws.api.rollback_update) self.update_page.request_update_client.connect(self.ws.api.update_client) self.update_page.request_update_klipper.connect(self.ws.api.update_klipper) @@ -234,6 +233,156 @@ def __init__( QtGui.QPixmap(":/system/media/btn_icons/update-software-icon.svg") ) + self.automatic_is = OptionCard( + self, + "Automatic\nInput Shaper", + "Automatic Input Shaper", + QtGui.QPixmap(":/input_shaper/media/btn_icons/input_shaper_auto.svg"), + ) # type: ignore + self.automatic_is.setObjectName("Automatic_IS_Card") + self.panel.is_content_layout.addWidget( + self.automatic_is, alignment=QtCore.Qt.AlignmentFlag.AlignHCenter + ) + self.automatic_is.continue_clicked.connect( + lambda: self.handle_is("SHAPER_CALIBRATE") + ) + + self.manual_is = OptionCard( + self, + "Manual\nInput Shaper", + "Manual Input Shaper", + QtGui.QPixmap(":/input_shaper/media/btn_icons/input_shaper_manual.svg"), + ) # type: ignore + self.manual_is.setObjectName("Manual_IS_Card") + self.panel.is_content_layout.addWidget( + self.manual_is, alignment=QtCore.Qt.AlignmentFlag.AlignHCenter + ) + self.manual_is.continue_clicked.connect(lambda: self.handle_is("")) + + self.is_types: dict = {} + self.is_aut_types: dict = {} + + self.is_page.action_btn.clicked.connect( + lambda: self.change_page(self.indexOf(self.panel.input_shaper_page)) + ) + + def handle_gcode_response(self, data: list[str]) -> None: + """ + Parses a Klipper Input Shaper console message and updates self.is_types. + """ + + if not isinstance(data, list) or len(data) != 1 or not isinstance(data[0], str): + print( + f"WARNING: Invalid input format. Expected a list with one string. Received: {data}" + ) + return + + message = data[0] + + pattern_fitted = re.compile( + r"Fitted shaper '(?P\w+)' frequency = (?P[\d\.]+) Hz \(vibrations = (?P[\d\.]+)%" + ) + match_fitted = pattern_fitted.search(message) + + if match_fitted: + name = match_fitted.group("name") + freq = float(match_fitted.group("freq")) + vib = float(match_fitted.group("vib")) + current_data = self.is_types.get(name, {}) + current_data.update( + { + "frequency": freq, + "vibration": vib, + "max_accel": current_data.get("max_accel", 0.0), + } + ) + self.is_types[name] = current_data + + return + pattern_accel = re.compile( + r"To avoid too much smoothing with '(?P\w+)', suggested max_accel <= (?P[\d\.]+) mm/sec\^2" + ) + match_accel = pattern_accel.search(message) + + if match_accel: + name = match_accel.group("name") + accel = float(match_accel.group("accel")) + + if name in self.is_types and isinstance(self.is_types[name], dict): + self.is_types[name]["max_accel"] = accel + else: + self.is_types[name] = self.is_types.get(name, {}) + self.is_types[name]["max_accel"] = accel + return + + pattern_recommended = re.compile( + r"Recommended shaper_type_(?P[xy]) = (?P\w+), shaper_freq_(?P=axis) = (?P[\d\.]+) Hz" + ) + match_recommended = pattern_recommended.search(message) + if match_recommended: + axis = match_recommended.group("axis") + recommended_type = match_recommended.group("type") + self.is_types["Axis"] = axis + if self.aut: + self.is_aut_types[axis] = recommended_type + if len(self.is_aut_types) == 2: + self.run_gcode_signal.emit("SAVE_CONFIG") + self.loadPage.hide() + self.aut = False + return + return + + reordered = {recommended_type: self.is_types[recommended_type]} + for key, value in self.is_types.items(): + if key not in ("suggested_type", recommended_type, "Axis"): + reordered[key] = value + + self.is_page.set_type_dictionary(self.is_types) + first_key = next(iter(reordered.keys()), None) + for key in reordered.keys(): + if key == first_key: + self.is_page.add_type_entry(key, "Recommended type") + else: + self.is_page.add_type_entry(key) + + self.is_page.build_model_list() + self.loadPage.hide() + return + + def on_dialog_button_clicked(self, button_name: str) -> None: + print(button_name) + """Handle dialog button clicks""" + if button_name == "Confirm": + self.handle_is("SHAPER_CALIBRATE AXIS=Y") + elif button_name == "Cancel": + self.handle_is("SHAPER_CALIBRATE AXIS=X") + + def handle_is(self, gcode: str) -> None: + if gcode == "SHAPER_CALIBRATE": + self.run_gcode_signal.emit("G28\nM400") + self.aut = True + self.run_gcode_signal.emit(gcode) + if gcode == "": + print("manual Input Shaper Selected") + self.dialog_page.confirm_background_color("#dfdfdf") + self.dialog_page.cancel_background_color("#dfdfdf") + self.dialog_page.cancel_font_color("#000000") + self.dialog_page.confirm_font_color("#000000") + self.dialog_page.cancel_button_text("X axis") + self.dialog_page.confirm_button_text("Y axis") + self.dialog_page.set_message( + "Select the axis you want to execute the input shaper on:" + ) + self.dialog_page.show() + return + else: + self.run_gcode_signal.emit("G28\nM400") + self.run_gcode_signal.emit(gcode) + self.change_page(self.indexOf(self.is_page)) + + self.loadwidget.set_status_message("Running Input Shaper...") + self.loadPage.show() + @QtCore.pyqtSlot(list, name="on_object_list") def on_object_list(self, object_list: list) -> None: """Handle receiving printer object list""" @@ -287,21 +436,6 @@ def on_gcode_move_update(self, name: str, value: list) -> None: if name == "gcode_position": ... - def _connect_numpad_request(self, button: QtWidgets.QWidget, name: str, title: str): - if isinstance(button, QtWidgets.QPushButton): - button.clicked.connect( - lambda: self.request_numpad_signal.emit( - 3, name, title, self.handle_numpad_change, self - ) - ) - - def handle_numpad_change(self, name: str, new_value: typing.Union[int, float]): - """Handle numpad change""" - if name == "frequency": - self.panel.isui_fq.setText(f"Frequency: {new_value} Hz") - elif name == "smoothing": - self.panel.isui_sm.setText(f"Smoothing: {new_value}") - def run_routine(self, process: Process): """Run check routine for available processes""" self.current_process = process @@ -512,74 +646,6 @@ def save_led_state(self): led_state: LedState = self.objects["leds"][self.current_object] self.run_gcode_signal.emit(led_state.get_gcode(self.current_object)) - def run_resonance_test(self, axis: str) -> None: - """Perform Input Shaper Measure resonances test""" - self.axis_in = axis - path_map = { - "x": "/tmp/resonances_x_axis_data.csv", - "y": "/tmp/resonances_y_axis_data.csv", - } - if not (csv_path := path_map.get(axis)): - return - self.run_gcode_signal.emit(f"SHAPER_CALIBRATE AXIS={axis.upper()}") - self.data = self._parse_shaper_csv(csv_path) - for entry in self.data: - shaper = entry["shaper"] - panel_attr = f"am_{shaper}" - if hasattr(self.panel, panel_attr): - text = ( - f"Shaper: {shaper}, Freq: {entry['frequency']}Hz, Vibrations: {entry['vibrations']}%\n" - f"Smoothing: {entry['smoothing']}, Max Accel: {entry['max_accel']}mm/sec" - ) - getattr(self.panel, panel_attr).setText(text) - self.x_inputshaper[panel_attr] = entry - self.change_page(self.indexOf(self.panel.is_page)) - - def _parse_shaper_csv(self, file_path: str) -> list: - results = [] - try: - with open(file_path, newline="") as csvfile: - reader = csv.DictReader(csvfile) - for row in reader: - if row.get("shaper") and row.get("freq"): - results.append( - { - k: row.get(v, "N/A") - for k, v in { - "shaper": "shaper", - "frequency": "freq", - "vibrations": "vibrations", - "smoothing": "smoothing", - "max_accel": "max_accel", - }.items() - } - ) - except FileNotFoundError: - ... - except csv.Error: - ... - return results - - def apply_input_shaper_selection(self) -> None: - """Apply input shaper results""" - if not (checked_button := self.panel.is_btn_group.checkedButton()): - return - selected_name = checked_button.objectName() - if selected_name == "am_user_input": - self.change_page( - self.indexOf(self.panel.input_shaper_page) - ) # TEST: CHANGED THIS FROM input_shaper_user_input - return - if not (shaper_data := self.x_inputshaper.get(selected_name)): - return - gcode = ( - f"SET_INPUT_SHAPER SHAPER_TYPE={shaper_data['shaper']} " - f"SHAPER_FREQ_{self.axis_in.upper()}={shaper_data['frequency']} " - f"SHAPER_DAMPING_{self.axis_in.upper()}={shaper_data['smoothing']}" - ) - self.run_gcode_signal.emit(gcode) - self.change_page(self.indexOf(self.panel.utilities_page)) - def axis_maintenance(self, axis: str) -> None: """Routine, checks axis movement for printer debugging""" self.current_process = Process.AXIS_MAINTENANCE @@ -617,7 +683,7 @@ def troubleshoot_request(self) -> None: def show_waiting_page(self, page_to_go_to: int, label: str, time_ms: int): """Show placeholder page""" - self.loadPage.label.setText(label) + self.loadwidget.set_status_message(label) self.loadPage.show() QtCore.QTimer.singleShot(time_ms, lambda: self.change_page(page_to_go_to)) diff --git a/BlocksScreen/lib/panels/widgets/basePopup.py b/BlocksScreen/lib/panels/widgets/basePopup.py new file mode 100644 index 00000000..e9ebe5d3 --- /dev/null +++ b/BlocksScreen/lib/panels/widgets/basePopup.py @@ -0,0 +1,256 @@ +import typing + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class BasePopup(QtWidgets.QDialog): + """Simple popup with custom message and Confirm/Back buttons + + To assert if the user accepted or rejected the dialog connect to the **accepted()** or **rejected()** signals. + + The `finished()` signal can also be used to get the result of the dialog. This is emitted after + the accepted and rejected signals. + + + """ + + x_offset: float = 0.7 + y_offset: float = 0.7 + border_radius: int = 20 + border_margin: int = 5 + + def __init__( + self, + parent: QtWidgets.QWidget, # Make parent optional for easier testing + floating: bool = False, + dialog: bool = True, + ) -> None: + super().__init__(parent) + + self.setWindowFlags( + QtCore.Qt.WindowType.Popup | QtCore.Qt.WindowType.FramelessWindowHint + ) + self.floating = floating + self.dialog = dialog + + # Color Variables + self.btns_text_color = "#ffffff" + self.cancel_bk_color = "#F44336" + self.confirm_bk_color = "#4CAF50" + self.confirm_ft_color = "#ffffff" + self.cancel_ft_color = "#ffffff" + + self.setupUI() + self.update() + + if floating: + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) + else: + self.setStyleSheet( + """ + #MyParent { + background-image: url(:/background/media/1st_background.png); + } + """ + ) + + def _update_button_style(self) -> None: + """Applies the current color variables and adds the central border to the stylesheets.""" + if not self.dialog: + return + + self.confirm_button.clicked.connect(self.accept) + self.cancel_button.clicked.connect(self.reject) + + if not self.floating: + self.confirm_button.setStyleSheet( + f""" + background-color: {self.confirm_bk_color}; + color: {self.confirm_ft_color}; + border: none; + padding: 10px; + """ + ) + + self.cancel_button.setStyleSheet( + f""" + background-color: {self.cancel_bk_color}; + color: {self.cancel_ft_color}; + border: none; + padding: 10px; + """ + ) + else: + self.confirm_button.setStyleSheet( + f""" + background-color: {self.confirm_bk_color}; + color: {self.confirm_ft_color}; + border-top: none; + border-left: 2px solid #80807e;; + border-bottom: 2px solid #80807e; + border-right: 1px solid #80807e; + border-bottom-left-radius: 16px; + padding: 10px; + """ + ) + + self.cancel_button.setStyleSheet( + f""" + background-color: {self.cancel_bk_color}; + color: {self.cancel_ft_color}; + border-left: 1px solid #80807e;; + border-bottom: 2px solid #80807e; + border-right: 2px solid #80807e; + border-bottom-right-radius: 16px; + padding: 10px; + """ + ) + + def set_message(self, message: str) -> None: + self.label.setText(message) + + def cancel_button_text(self, text: str) -> None: + if not self.dialog: + return + self.cancel_button.setText(text) + + def confirm_button_text(self, text: str) -> None: + if not self.dialog: + return + self.confirm_button.setText(text) + + def cancel_background_color(self, color: str) -> None: + if not self.dialog: + return + self.cancel_bk_color = color + self._update_button_style() + + def confirm_background_color(self, color: str) -> None: + if not self.dialog: + return + self.confirm_bk_color = color + self._update_button_style() + + def cancel_font_color(self, color: str) -> None: + if not self.dialog: + return + self.cancel_ft_color = color + self._update_button_style() + + def confirm_font_color(self, color: str) -> None: + if not self.dialog: + return + self.confirm_ft_color = color + self._update_button_style() + + def add_widget(self, widget: QtWidgets.QWidget) -> None: + """Replace the label with a custom widget in the layout""" + + layout = self.vlayout + index = layout.indexOf(self.label) + self.label.setParent(None) + self.label.hide() + layout.insertWidget(index, widget) + widget.show() + + def _get_mainWindow_widget(self) -> typing.Optional[QtWidgets.QMainWindow]: + """Get the main application window""" + app_instance = QtWidgets.QApplication.instance() + if not app_instance: + return None + main_window = app_instance.activeWindow() + if main_window is None: + for widget in app_instance.allWidgets(): + if isinstance(widget, QtWidgets.QMainWindow): + main_window = widget + break + return main_window if isinstance(main_window, QtWidgets.QMainWindow) else None + + def _geometry_calc(self) -> None: + """Calculate dialog widget position relative to the window""" + main_window = self._get_mainWindow_widget() + if main_window is None: + return + + if self.floating: + width = int(main_window.width() * self.x_offset) + height = int(main_window.height() * self.y_offset) + x = int(main_window.geometry().x() + (main_window.width() - width) / 2) + y = int(main_window.geometry().y() + (main_window.height() - height) / 2) + else: + x = main_window.geometry().x() + y = main_window.geometry().y() + width = main_window.width() + height = main_window.height() + + self.setGeometry(x, y, width, height) + + def sizeHint(self) -> QtCore.QSize: + """Re-implemented method, widget size hint""" + popup_width = int(self.geometry().width()) + popup_height = int(self.geometry().height()) + popup_x = self.x() + popup_y = self.y() + (self.height() - popup_height) // 2 + self.move(popup_x, popup_y) + self.setFixedSize(popup_width, popup_height) + self.setMinimumSize(popup_width, popup_height) + return super().sizeHint() + + def open(self): + """Re-implemented method, open widget""" + self._geometry_calc() + return super().open() + + def show(self) -> None: + self._geometry_calc() + return super().show() + + def paintEvent(self, a0: QtGui.QPaintEvent | None) -> None: + """Re-implemented method, paint widget""" + if not self.floating: + return + + self._geometry_calc() + painter = QtGui.QPainter(self) + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) + rect = self.rect() + painter.setBrush(QtGui.QBrush(QtGui.QColor(63, 63, 63))) + border_color = QtGui.QColor(128, 128, 128) + pen = QtGui.QPen() + pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) + painter.setPen(QtGui.QPen(border_color, self.border_margin)) + painter.drawRoundedRect(rect, self.border_radius, self.border_radius) + painter.end() + + def setupUI(self) -> None: + self.vlayout = QtWidgets.QVBoxLayout(self) + self.setObjectName("MyParent") + self.label = QtWidgets.QLabel("Test Message", self) + font = QtGui.QFont() + font.setPointSize(25) + self.label.setFont(font) + self.label.setStyleSheet("color: #ffffff; background: transparent;") + self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.label.setWordWrap(True) + self.vlayout.addWidget(self.label) + if self.dialog: + self.hlauyout = QtWidgets.QHBoxLayout() + self.hlauyout.setContentsMargins(0, 0, 0, 0) + self.hlauyout.setSpacing(0) + self.vlayout.addLayout(self.hlauyout) + self.vlayout.setContentsMargins(0, 0, 0, 0) + self.confirm_button = QtWidgets.QPushButton("Confirm", self) + self.cancel_button = QtWidgets.QPushButton("Back", self) + + button_font = QtGui.QFont() + button_font.setPointSize(14) + self.confirm_button.setFont(button_font) + self.confirm_button.setMinimumHeight(60) + self.cancel_button.setFont(button_font) + self.cancel_button.setMinimumHeight(60) + self.confirm_button.setStyleSheet("background: transparent;") + self.cancel_button.setStyleSheet("background: transparent;") + self.hlauyout.addWidget(self.confirm_button) + self.hlauyout.addWidget(self.cancel_button) + + self._update_button_style() diff --git a/BlocksScreen/lib/panels/widgets/dialogPage.py b/BlocksScreen/lib/panels/widgets/dialogPage.py deleted file mode 100644 index 45d067a4..00000000 --- a/BlocksScreen/lib/panels/widgets/dialogPage.py +++ /dev/null @@ -1,167 +0,0 @@ -import typing - -from PyQt6 import QtCore, QtGui, QtWidgets - - -class DialogPage(QtWidgets.QDialog): - """Simple confirmation dialog with custom message and Confirm/Back buttons - - To assert if the user accepted or rejected the dialog connect to the **accepted()** or **rejected()** signals. - - The `finished()` signal can also be used to get the result of the dialog. This is emitted after - the accepted and rejected signals. - - - """ - - x_offset: float = 0.7 - y_offset: float = 0.7 - border_radius: int = 20 - border_margin: int = 5 - - def __init__( - self, - parent: QtWidgets.QWidget, - ) -> None: - super().__init__(parent) - self._setupUI() - self.setWindowFlags( - QtCore.Qt.WindowType.Popup | QtCore.Qt.WindowType.FramelessWindowHint - ) - self.setAttribute( - QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True - ) # Make background transparent - self.setWindowModality( # Force window modality to block input to other windows - QtCore.Qt.WindowModality.WindowModal - ) - self.confirm_button.clicked.connect(self.accept) - self.cancel_button.clicked.connect(self.reject) - self.setModal(True) - self.update() - - def set_message(self, message: str) -> None: - """Set dialog text message""" - self.label.setText(message) - - def _get_mainWindow_widget(self) -> typing.Optional[QtWidgets.QMainWindow]: - """Get the main application window""" - app_instance = QtWidgets.QApplication.instance() - if not app_instance: - return None - main_window = app_instance.activeWindow() - if main_window is None: - for widget in app_instance.allWidgets(): - if isinstance(widget, QtWidgets.QMainWindow): - main_window = widget - break - return main_window if isinstance(main_window, QtWidgets.QMainWindow) else None - - def _geometry_calc(self) -> None: - """Calculate dialog widget position relative to the window""" - main_window = self._get_mainWindow_widget() - width = int(main_window.width() * self.x_offset) - height = int(main_window.height() * self.y_offset) - x = int(main_window.geometry().x() + (main_window.width() - width) / 2) - y = int(main_window.geometry().y() + (main_window.height() - height) / 2) - self.setGeometry(x, y, width, height) - - def sizeHint(self) -> QtCore.QSize: - """Re-implemented method, widget size hint""" - popup_width = int(self.geometry().width()) - popup_height = int(self.geometry().height()) - popup_x = self.x() - popup_y = self.y() + (self.height() - popup_height) // 2 - self.move(popup_x, popup_y) - self.setFixedSize(popup_width, popup_height) - self.setMinimumSize(popup_width, popup_height) - return super().sizeHint() - - def resizeEvent(self, event: QtGui.QResizeEvent) -> None: - """Re-implemented method, handle resize event""" - super().resizeEvent(event) - main_window = self._get_mainWindow_widget() - if main_window is None: - return - width = int(main_window.width() * self.x_offset) - height = int(main_window.height() * self.y_offset) - label_x = (self.width() - width) // 2 - label_y = int(height / 4) - 20 # Move the label to the top (adjust as needed) - self.label.setGeometry(label_x, -label_y, width, height) - self.confirm_button.setGeometry( - int(0), self.height() - 70, int(self.width() / 2), 70 - ) - self.cancel_button.setGeometry( - int(self.width() / 2), - self.height() - 70, - int(self.width() / 2), - 70, - ) - - def open(self): - """Re-implemented method, open widget""" - self._geometry_calc() - return super().open() - - def show(self) -> None: - """Re-implemented method, show widget""" - self._geometry_calc() - return super().show() - - def paintEvent(self, event: QtGui.QPaintEvent) -> None: - """Re-implemented method, paint widget""" - self._geometry_calc() - painter = QtGui.QPainter(self) - painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) - rect = self.rect() - painter.setBrush( - QtGui.QBrush(QtGui.QColor(63, 63, 63)) - ) # Semi-transparent dark gray - border_color = QtGui.QColor(128, 128, 128) # Gray color - pen = QtGui.QPen() - pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) - painter.setPen(QtGui.QPen(border_color, self.border_margin)) - painter.drawRoundedRect(rect, self.border_radius, self.border_radius) - painter.end() - - def _setupUI(self) -> None: - self.label = QtWidgets.QLabel("Test", self) - font = QtGui.QFont() - font.setPointSize(25) - self.label.setFont(font) - self.label.setStyleSheet("color: #ffffff; background: transparent;") - self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.label.setWordWrap(True) - self.confirm_button = QtWidgets.QPushButton("Confirm", self) - self.cancel_button = QtWidgets.QPushButton("Back", self) - button_font = QtGui.QFont() - button_font.setPointSize(14) - self.confirm_button.setFont(button_font) - self.cancel_button.setFont(button_font) - self.confirm_button.setStyleSheet( - """ - background-color: #4CAF50; - color: white; - border: none; - border-bottom-left-radius: 20px; - padding: 10px; - """ - ) - self.cancel_button.setStyleSheet( - """ - background-color: #F44336; - color: white; - border: none; - border-bottom-right-radius: 20px; - padding: 10px; - """ - ) - # Position buttons - self.confirm_button.setGeometry( - int(0), self.height() - 70, int(self.width() / 2), 70 - ) - self.cancel_button.setGeometry( - int(self.width() / 2), - self.height() - 70, - int(self.width() / 2), - 70, - ) diff --git a/BlocksScreen/lib/panels/widgets/inputshaperPage.py b/BlocksScreen/lib/panels/widgets/inputshaperPage.py new file mode 100644 index 00000000..d96f543a --- /dev/null +++ b/BlocksScreen/lib/panels/widgets/inputshaperPage.py @@ -0,0 +1,459 @@ +from lib.panels.widgets.loadWidget import LoadingOverlayWidget +from lib.panels.widgets.basePopup import BasePopup +from lib.utils.blocks_button import BlocksCustomButton +from lib.utils.blocks_frame import BlocksCustomFrame +from lib.utils.icon_button import IconButton +from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem +from PyQt6 import QtCore, QtGui, QtWidgets + +import typing + + +class InputShaperPage(QtWidgets.QWidget): + """Update GUI Page, + retrieves from moonraker available clients and adds functionality + for updating or recovering them + """ + + run_gcode_signal: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="run-gcode" + ) + + def __init__(self, parent=None) -> None: + if parent: + super().__init__(parent) + else: + super().__init__() + self._setupUI() + self.selected_item: ListItem | None = None + self.ongoing_update: bool = False + self.type_dict: dict = {} + + self.loadscreen = BasePopup(self, floating=False, dialog=False) + self.loadwidget = LoadingOverlayWidget( + self, LoadingOverlayWidget.AnimationGIF.DEFAULT + ) + self.loadscreen.add_widget(self.loadwidget) + self.repeated_request_status = QtCore.QTimer() + self.repeated_request_status.setInterval(2000) # every 2 seconds + self.model = EntryListModel() + self.model.setParent(self.update_buttons_list_widget) + self.entry_delegate = EntryDelegate() + self.update_buttons_list_widget.setModel(self.model) + self.update_buttons_list_widget.setItemDelegate(self.entry_delegate) + self.entry_delegate.item_selected.connect(self.on_item_clicked) + self.update_back_btn.clicked.connect(self.reset_view_model) + + self.action_btn.clicked.connect(self.handle_ism_confirm) + + def handle_update_end(self) -> None: + """Handles update end signal + (closes loading page, returns to normal operation) + """ + if self.load_popup.isVisible(): + self.load_popup.close() + self.repeated_request_status.stop() + self.build_model_list() + + def handle_ongoing_update(self) -> None: + """Handled ongoing update signal, + calls loading page (blocks user interaction) + """ + self.loadwidget.set_status_message("Updating...") + self.load_popup.show() + self.repeated_request_status.start(2000) + + def reset_view_model(self) -> None: + """Clears items from ListView + (Resets `QAbstractListModel` by clearing entries) + """ + self.model.clear() + self.entry_delegate.clear() + + def deleteLater(self) -> None: + """Schedule the object for deletion, resets the list model first""" + self.reset_view_model() + return super().deleteLater() + + def showEvent(self, a0: QtGui.QShowEvent | None) -> None: + """Re-add clients to update list""" + return super().showEvent(a0) + + def build_model_list(self) -> None: + """Builds the model list (`self.model`) containing updatable clients""" + self.update_buttons_list_widget.blockSignals(True) + self.model.setData(self.model.index(0), True, EntryListModel.EnableRole) + self.on_item_clicked( + self.model.data(self.model.index(0), QtCore.Qt.ItemDataRole.UserRole) + ) + self.update_buttons_list_widget.blockSignals(False) + + def set_type_dictionary(self, dict) -> None: + """Receives the dictionary of input shaper types from the utilities tab""" + self.type_dict = dict + return + + @QtCore.pyqtSlot(ListItem, name="on-item-clicked") + def on_item_clicked(self, item: ListItem) -> None: + """Setup information for the currently clicked list item on the info box. + Keeps track of the list item + """ + self.currentItem: ListItem = item + if not item: + return + current_info = self.type_dict.get(self.currentItem.text, {}) + if not current_info: + return + + self.vib_label.setText(str("%.0f" % current_info.get("vibration", "N/A")) + "%") + self.sug_accel_label.setText( + str("%.0f" % current_info.get("max_accel", "N/A")) + "mm/s²" + ) + + self.action_btn.show() + + def handle_ism_confirm(self) -> None: + current_info = self.type_dict.get(self.currentItem.text, {}) + frequency = current_info.get("frequency", "N/A") + if self.type_dict["Axis"] == "x": + self.run_gcode_signal.emit( + f"SET_INPUT_SHAPER SHAPER_TYPE_X={self.currentItem.text} SHAPER_FREQ_X={frequency}" + ) + elif self.type_dict["Axis"] == "y": + self.run_gcode_signal.emit( + f"SET_INPUT_SHAPER SHAPER_TYPE_Y={self.currentItem.text} SHAPER_FREQ_Y={frequency}" + ) + + self.run_gcode_signal.emit("SAVE_CONFIG") + self.reset_view_model() + + def add_type_entry(self, cli_name: str, recommended: str = "") -> None: + """Adds a new item to the list model""" + item = ListItem( + text=cli_name, + right_text=recommended, + right_icon=QtGui.QPixmap(":/arrow_icons/media/btn_icons/right_arrow.svg"), + selected=False, + _lfontsize=17, + _rfontsize=9, + height=60, + allow_check=True, + notificate=False, + ) + self.model.add_item(item) + + def _setupUI(self) -> None: + """Setup UI for updatePage""" + font_id = QtGui.QFontDatabase.addApplicationFont( + ":/font/media/fonts for text/Momcake-Bold.ttf" + ) + font_family = QtGui.QFontDatabase.applicationFontFamilies(font_id)[0] + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + ) + sizePolicy.setHorizontalStretch(1) + sizePolicy.setVerticalStretch(1) + self.setSizePolicy(sizePolicy) + self.setMinimumSize(QtCore.QSize(710, 400)) + self.setMaximumSize(QtCore.QSize(720, 420)) + self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.update_page_content_layout = QtWidgets.QVBoxLayout() + self.update_page_content_layout.setContentsMargins(15, 15, 2, 2) + + self.header_content_layout = QtWidgets.QHBoxLayout() + self.header_content_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + + self.spacer_left = QtWidgets.QLabel(self) + self.spacer_left.setMinimumSize(QtCore.QSize(60, 60)) + self.spacer_left.setMaximumSize(QtCore.QSize(60, 60)) + self.header_content_layout.addWidget(self.spacer_left, 0) + self.header_title = QtWidgets.QLabel(self) + self.header_title.setMinimumSize(QtCore.QSize(100, 60)) + self.header_title.setMaximumSize(QtCore.QSize(16777215, 60)) + font = QtGui.QFont() + font.setFamily(font_family) + font.setPointSize(24) + palette = self.header_title.palette() + palette.setColor(palette.ColorRole.WindowText, QtGui.QColor("#FFFFFF")) + self.header_title.setFont(font) + self.header_title.setPalette(palette) + self.header_title.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) + self.header_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.header_title.setObjectName("header-title") + self.header_title.setText("Input Shaper") + self.header_content_layout.addWidget(self.header_title, 0) + self.update_back_btn = IconButton(self) + self.update_back_btn.setMinimumSize(QtCore.QSize(60, 60)) + self.update_back_btn.setMaximumSize(QtCore.QSize(60, 60)) + self.update_back_btn.setFlat(True) + self.update_back_btn.setPixmap(QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) + self.header_content_layout.addWidget(self.update_back_btn, 0) + self.update_page_content_layout.addLayout(self.header_content_layout, 0) + + self.main_content_layout = QtWidgets.QHBoxLayout() + self.main_content_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + self.update_buttons_frame = BlocksCustomFrame(self) + + self.update_buttons_frame.setMinimumSize(QtCore.QSize(300, 300)) + self.update_buttons_frame.setMaximumSize(QtCore.QSize(350, 500)) + + palette = QtGui.QPalette() + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorRole.Button, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) + palette.setBrush( + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorRole.Base, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorRole.Window, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorRole.Highlight, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorRole.Link, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Inactive, + QtGui.QPalette.ColorRole.Button, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) + palette.setBrush( + QtGui.QPalette.ColorGroup.Inactive, + QtGui.QPalette.ColorRole.Base, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Inactive, + QtGui.QPalette.ColorRole.Window, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Inactive, + QtGui.QPalette.ColorRole.Highlight, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Inactive, + QtGui.QPalette.ColorRole.Link, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.Button, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.Base, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.Window, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.Highlight, + brush, + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.Link, + brush, + ) + self.update_buttons_list_widget = QtWidgets.QListView(self.update_buttons_frame) + self.update_buttons_list_widget.setMouseTracking(True) + self.update_buttons_list_widget.setTabletTracking(True) + + self.update_buttons_list_widget.setPalette(palette) + self.update_buttons_list_widget.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.update_buttons_list_widget.setStyleSheet("background-color:transparent") + self.update_buttons_list_widget.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + self.update_buttons_list_widget.setMinimumSize(self.update_buttons_frame.size()) + self.update_buttons_list_widget.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.update_buttons_list_widget.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.update_buttons_list_widget.setHorizontalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.update_buttons_list_widget.setSizeAdjustPolicy( + QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents + ) + self.update_buttons_list_widget.setAutoScroll(False) + self.update_buttons_list_widget.setProperty("showDropIndicator", False) + self.update_buttons_list_widget.setDefaultDropAction( + QtCore.Qt.DropAction.IgnoreAction + ) + self.update_buttons_list_widget.setAlternatingRowColors(False) + self.update_buttons_list_widget.setSelectionMode( + QtWidgets.QAbstractItemView.SelectionMode.NoSelection + ) + self.update_buttons_list_widget.setSelectionBehavior( + QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems + ) + self.update_buttons_list_widget.setVerticalScrollMode( + QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel + ) + self.update_buttons_list_widget.setHorizontalScrollMode( + QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel + ) + QtWidgets.QScroller.grabGesture( + self.update_buttons_list_widget, + QtWidgets.QScroller.ScrollerGestureType.TouchGesture, + ) + QtWidgets.QScroller.grabGesture( + self.update_buttons_list_widget, + QtWidgets.QScroller.ScrollerGestureType.LeftMouseButtonGesture, + ) + self.update_buttons_layout = QtWidgets.QVBoxLayout() + self.update_buttons_layout.setContentsMargins(15, 20, 20, 5) + self.update_buttons_layout.addWidget(self.update_buttons_list_widget, 0) + self.update_buttons_frame.setLayout(self.update_buttons_layout) + + self.main_content_layout.addWidget(self.update_buttons_frame, 0) + + self.infobox_frame = BlocksCustomFrame() + self.infobox_frame.setMinimumSize(QtCore.QSize(250, 300)) + + self.info_box_layout = QtWidgets.QVBoxLayout() + self.info_box_layout.setContentsMargins(10, 10, 10, 10) + + font = QtGui.QFont() + font.setFamily(font_family) + font.setPointSize(20) + self.info_box = QtWidgets.QGridLayout() + self.info_box.setContentsMargins(0, 0, 0, 0) + + self.vib_title_label = QtWidgets.QLabel(self) + self.vib_title_label.setText("Vibrations: ") + self.vib_title_label.setMinimumSize(QtCore.QSize(60, 60)) + self.vib_title_label.setMaximumSize( + QtCore.QSize(int(self.infobox_frame.size().width() * 0.40), 9999) + ) + palette = self.vib_title_label.palette() + palette.setColor(palette.ColorRole.WindowText, QtGui.QColor("#FFFFFF")) + self.vib_title_label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter + ) + self.vib_title_label.setFont(font) + self.vib_title_label.setPalette(palette) + self.vib_title_label.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) + self.vib_label = QtWidgets.QLabel(self) + self.vib_label.setMinimumSize(QtCore.QSize(100, 60)) + self.vib_label.setMaximumSize(QtCore.QSize(16777215, 9999)) + palette = self.vib_label.palette() + palette.setColor(palette.ColorRole.WindowText, QtGui.QColor("#FFFFFF")) + self.vib_label.setFont(font) + self.vib_label.setPalette(palette) + self.vib_label.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) + self.vib_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.vib_label.setObjectName("version-tracking") + + self.info_box.addWidget(self.vib_title_label, 0, 0) + self.info_box.addWidget(self.vib_label, 0, 1) + + self.sug_accel_title_label = QtWidgets.QLabel(self) + self.sug_accel_title_label.setText("Sugested Max Acceleration:") + self.sug_accel_title_label.setMinimumSize(QtCore.QSize(60, 60)) + self.sug_accel_title_label.setMaximumSize( + QtCore.QSize(int(self.infobox_frame.size().width() * 0.40), 9999) + ) + palette = self.sug_accel_title_label.palette() + palette.setColor(palette.ColorRole.WindowText, QtGui.QColor("#FFFFFF")) + self.sug_accel_title_label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter + ) + self.sug_accel_title_label.setFont(font) + self.sug_accel_title_label.setPalette(palette) + self.sug_accel_title_label.setLayoutDirection( + QtCore.Qt.LayoutDirection.RightToLeft + ) + + self.sug_accel_label = QtWidgets.QLabel(self) + self.sug_accel_label.setMinimumSize(QtCore.QSize(100, 60)) + self.sug_accel_label.setMaximumSize( + QtCore.QSize(int(self.infobox_frame.size().width() * 0.60), 9999) + ) + palette = self.sug_accel_label.palette() + palette.setColor(palette.ColorRole.WindowText, QtGui.QColor("#FFFFFF")) + self.sug_accel_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.sug_accel_label.setFont(font) + self.sug_accel_label.setPalette(palette) + + self.info_box.addWidget(self.sug_accel_title_label, 1, 0) + self.info_box.addWidget(self.sug_accel_label, 1, 1) + + self.info_box_layout.addLayout(self.info_box, 1) + + self.button_box = QtWidgets.QVBoxLayout() + self.button_box.setContentsMargins(0, 0, 0, 0) + self.button_box.addSpacing(-1) + + self.action_btn = BlocksCustomButton() + self.action_btn.setMinimumSize(QtCore.QSize(200, 60)) + self.action_btn.setMaximumSize(QtCore.QSize(250, 60)) + font.setPointSize(20) + self.action_btn.setFont(font) + self.action_btn.setPalette(palette) + self.action_btn.setSizePolicy(sizePolicy) + self.action_btn.setText("Confirm") + self.action_btn.setPixmap(QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg")) + self.button_box.addWidget( + self.action_btn, + 0, + QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignBottom, + ) + + self.info_box_layout.addLayout( + self.button_box, + 0, + ) + self.infobox_frame.setLayout(self.info_box_layout) + self.main_content_layout.addWidget(self.infobox_frame, 1) + self.update_page_content_layout.addLayout(self.main_content_layout, 1) + self.setLayout(self.update_page_content_layout) diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index d161fa22..bbce76a3 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -2,7 +2,7 @@ import typing import events from helper_methods import calculate_current_layer, estimate_print_time -from lib.panels.widgets import dialogPage +from lib.panels.widgets.basePopup import BasePopup from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_label import BlocksLabel from lib.utils.blocks_progressbar import CustomProgressBar @@ -60,7 +60,7 @@ def __init__(self, parent) -> None: self.thumbnail_graphics = [] self.layer_fallback = False self._setupUI() - self.cancel_print_dialog = dialogPage.DialogPage(self) + self.cancel_print_dialog = BasePopup(self, floating=True) self.tune_menu_btn.clicked.connect(self.tune_clicked.emit) self.pause_printing_btn.clicked.connect(self.pause_resume_print) self.stop_printing_btn.clicked.connect(self.handleCancel) diff --git a/BlocksScreen/lib/panels/widgets/loadPage.py b/BlocksScreen/lib/panels/widgets/loadPage.py deleted file mode 100644 index f5fd7963..00000000 --- a/BlocksScreen/lib/panels/widgets/loadPage.py +++ /dev/null @@ -1,180 +0,0 @@ -import enum -from configfile import BlocksScreenConfig, get_configparser -from PyQt6 import QtCore, QtGui, QtWidgets - - -class LoadScreen(QtWidgets.QDialog): - class AnimationGIF(enum.Enum): - """Animation type""" - - DEFAULT = None - PLACEHOLDER = "" - - def __init__( - self, - parent: QtWidgets.QWidget, - anim_type: AnimationGIF = AnimationGIF.DEFAULT, - ) -> None: - super().__init__(parent) - - self.anim_type = anim_type - self._angle = 0 - self._span_angle = 90.0 - self._is_span_growing = True - self.min_length = 5.0 - self.max_length = 150.0 - self.length_step = 2.5 - - self.setStyleSheet( - "background-image: url(:/background/media/1st_background.png);" - ) - - self.setWindowFlags( - QtCore.Qt.WindowType.Popup | QtCore.Qt.WindowType.FramelessWindowHint - ) - self._setupUI() - config: BlocksScreenConfig = get_configparser() - try: - if config: - loading_config = config["loading"] - animation = loading_config.get( - str(self.anim_type.name), - default=LoadScreen.AnimationGIF.DEFAULT, - ) - except Exception: - self.anim_type = LoadScreen.AnimationGIF.DEFAULT - - self.timer = QtCore.QTimer(self) - self.timer.timeout.connect(self._update_animation) - - if self.anim_type == LoadScreen.AnimationGIF.PLACEHOLDER: - self.movie = QtGui.QMovie(animation) # Create QMovie object - self.gifshow.setMovie(self.movie) # Set QMovie to QLabel - self.movie.start() # Start the QMovie - - # Only start the animation timer if no GIF is provided - if self.anim_type == LoadScreen.AnimationGIF.DEFAULT: - self.timer.start(16) - - self.repaint() - - def set_status_message(self, message: str) -> None: - """Set widget status message""" - self.label.setText(message) - - def _geometry_calc(self) -> None: - """Calculate widget position relative to the screen""" - app_instance = QtWidgets.QApplication.instance() - main_window = app_instance.activeWindow() if app_instance else None - if main_window is None and app_instance: - for widget in app_instance.allWidgets(): - if isinstance(widget, QtWidgets.QMainWindow): - main_window = widget - x = main_window.geometry().x() - y = main_window.geometry().y() - width = main_window.width() - height = main_window.height() - - self.setGeometry(x, y, width, height) - - def close(self) -> bool: - """Re-implemented method, close widget""" - self.timer.stop() - self.label.setText("Loading...") - self._angle = 0 - # Stop the GIF animation if it was started - if self.anim_type != LoadScreen.AnimationGIF.DEFAULT: - self.gifshow.movie().stop() - return super().close() - - def _update_animation(self) -> None: - self._angle = (self._angle + 5) % 360 - if self._is_span_growing: - self._span_angle += self.length_step - if self._span_angle >= self.max_length: - self._span_angle = self.max_length - self._is_span_growing = False - else: - self._span_angle -= self.length_step - if self._span_angle <= self.min_length: - self._span_angle = self.min_length - self._is_span_growing = True - self.update() - - def sizeHint(self) -> QtCore.QSize: - """Re-implemented method, size hint""" - popup_width = int(self.geometry().width()) - popup_height = int(self.geometry().height()) - # Centering logic - popup_x = self.x() - popup_y = self.y() + (self.height() - popup_height) // 2 - self.move(popup_x, popup_y) - self.setFixedSize(popup_width, popup_height) - self.setMinimumSize(popup_width, popup_height) - return super().sizeHint() - - def paintEvent(self, a0: QtGui.QPaintEvent) -> None: - """Re-implemented method, paint widget""" - painter = QtGui.QPainter(self) - # loading circle draw - if self.anim_type == LoadScreen.AnimationGIF.DEFAULT: - painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) - painter.setRenderHint( - QtGui.QPainter.RenderHint.LosslessImageRendering, True - ) - painter.setRenderHint(QtGui.QPainter.RenderHint.SmoothPixmapTransform, True) - painter.setRenderHint(QtGui.QPainter.RenderHint.TextAntialiasing, True) - pen = QtGui.QPen() - pen.setWidth(8) - pen.setColor(QtGui.QColor("#ffffff")) - pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) - painter.setPen(pen) - - center_x = self.width() // 2 - center_y = int(self.height() * 0.4) - arc_size = 150 - - painter.translate(center_x, center_y) - painter.rotate(self._angle) - - arc_rect = QtCore.QRectF(-arc_size / 2, -arc_size / 2, arc_size, arc_size) - span_angle = int(self._span_angle * 16) - painter.drawArc(arc_rect, 0, span_angle) - - def resizeEvent(self, event: QtGui.QResizeEvent) -> None: - """Re-implemented method, handle widget resize event""" - super().resizeEvent(event) - label_width = self.width() - label_height = 100 - label_x = (self.width() - label_width) // 2 - label_y = int(self.height() * 0.65) - margin = 20 - # Center the GIF - gifshow_width = self.width() - margin * 2 - gifshow_height = self.height() - (self.height() - label_y) - margin - - self.gifshow.setGeometry(margin, margin, gifshow_width, gifshow_height) - - self.label.setGeometry(label_x, label_y, label_width, label_height) - - def show(self) -> None: - """Re-implemented method, show widget""" - self._geometry_calc() - # Start the animation timer only if no GIF is present - if self.anim_type == LoadScreen.AnimationGIF.DEFAULT: - self.timer.start() - self.repaint() - return super().show() - - def _setupUI(self) -> None: - self.gifshow = QtWidgets.QLabel("", self) - self.gifshow.setObjectName("gifshow") - self.gifshow.setStyleSheet("background: transparent;") - self.gifshow.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - - self.label = QtWidgets.QLabel("Test", self) - font = QtGui.QFont() - font.setPointSize(20) - self.label.setFont(font) - self.label.setStyleSheet("color: #ffffff; background: transparent;") - self.label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) diff --git a/BlocksScreen/lib/panels/widgets/loadWidget.py b/BlocksScreen/lib/panels/widgets/loadWidget.py index 9a5f3d68..c23d4a5d 100644 --- a/BlocksScreen/lib/panels/widgets/loadWidget.py +++ b/BlocksScreen/lib/panels/widgets/loadWidget.py @@ -1,10 +1,24 @@ from PyQt6 import QtCore, QtGui, QtWidgets +import enum +import os +from configfile import BlocksScreenConfig, get_configparser class LoadingOverlayWidget(QtWidgets.QLabel): + """ + A full-overlay widget to display a loading animation (GIF or spinning arc). + """ + + class AnimationGIF(enum.Enum): + """Animation type""" + + DEFAULT = None + PLACEHOLDER = "placeholder" + def __init__( self, parent: QtWidgets.QWidget, + initial_anim_type: AnimationGIF = AnimationGIF.PLACEHOLDER, ) -> None: super().__init__(parent) @@ -17,12 +31,66 @@ def __init__( self._setupUI() + config: BlocksScreenConfig = get_configparser() + animation_path = None + + if initial_anim_type == LoadingOverlayWidget.AnimationGIF.PLACEHOLDER: + animation_path = ( + "~/BlocksScreen/BlocksScreen/lib/ui/resources/intro_blocks.gif" + ) + self.anim_type = initial_anim_type + + else: + try: + loading_config = config.loading + animation_path = loading_config.get( + str(initial_anim_type.name), + ) + self.anim_type = initial_anim_type + except Exception: + self.anim_type = LoadingOverlayWidget.AnimationGIF.DEFAULT + + if ( + self.anim_type != LoadingOverlayWidget.AnimationGIF.DEFAULT + and animation_path + ): + abs_animation_path = os.path.expanduser(animation_path) + + self.movie = QtGui.QMovie(abs_animation_path) + + if self.movie.isValid(): + self.gifshow.setMovie(self.movie) + self.gifshow.setScaledContents(True) + self.movie.start() + self.gifshow.show() + else: + self.anim_type = LoadingOverlayWidget.AnimationGIF.DEFAULT + self.gifshow.hide() + self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self._update_animation) - self.timer.start(16) + + if self.anim_type == LoadingOverlayWidget.AnimationGIF.DEFAULT: + self.timer.start(16) + self.gifshow.hide() + self.label.setText("Loading...") self.repaint() + def set_animation_path(self, path: str) -> None: + """Set widget animation path""" + abs_animation_path = os.path.expanduser(path) + if os.path.isfile(abs_animation_path): + self.movie = QtGui.QMovie(abs_animation_path) + if self.movie.isValid(): + self.gifshow.setMovie(self.movie) + self.gifshow.setScaledContents(True) + self.movie.start() + self.gifshow.show() + self.anim_type = LoadingOverlayWidget.AnimationGIF.PLACEHOLDER + if self.timer.isActive(): + self.timer.stop() + def set_status_message(self, message: str) -> None: """Set widget message""" self.label.setText(message) @@ -32,6 +100,12 @@ def close(self) -> bool: self.timer.stop() self.label.setText("Loading...") self._angle = 0 + if ( + self.anim_type != LoadingOverlayWidget.AnimationGIF.DEFAULT + and hasattr(self, "movie") + and self.movie.isValid() + ): + self.movie.stop() return super().close() def _update_animation(self) -> None: @@ -48,55 +122,64 @@ def _update_animation(self) -> None: self._is_span_growing = True self.update() - def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + def paintEvent(self, a0: QtGui.QPaintEvent | None) -> None: """Re-implemented method, paint widget""" painter = QtGui.QPainter(self) - painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) - painter.setRenderHint(QtGui.QPainter.RenderHint.LosslessImageRendering, True) - painter.setRenderHint(QtGui.QPainter.RenderHint.SmoothPixmapTransform, True) - painter.setRenderHint(QtGui.QPainter.RenderHint.TextAntialiasing, True) - pen = QtGui.QPen() - pen.setWidth(8) - pen.setColor(QtGui.QColor("#ffffff")) - pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) - painter.setPen(pen) - - center_x = self.width() // 2 - center_y = int(self.height() * 0.4) - arc_size = 150 - - painter.translate(center_x, center_y) - painter.rotate(self._angle) - - arc_rect = QtCore.QRectF(-arc_size / 2, -arc_size / 2, arc_size, arc_size) - span_angle = int(self._span_angle * 16) - painter.drawArc(arc_rect, 0, span_angle) - - def resizeEvent(self, event: QtGui.QResizeEvent) -> None: - """Re-implemented method, handle resize event""" - super().resizeEvent(event) + if self.anim_type == LoadingOverlayWidget.AnimationGIF.DEFAULT: + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) + painter.setRenderHint( + QtGui.QPainter.RenderHint.LosslessImageRendering, True + ) + painter.setRenderHint(QtGui.QPainter.RenderHint.SmoothPixmapTransform, True) + painter.setRenderHint(QtGui.QPainter.RenderHint.TextAntialiasing, True) + pen = QtGui.QPen() + pen.setWidth(8) + pen.setColor(QtGui.QColor("#ffffff")) + pen.setCapStyle(QtCore.Qt.PenCapStyle.RoundCap) + painter.setPen(pen) + + center_x = self.width() // 2 + center_y = int(self.height() * 0.4) + arc_size = 150 + + painter.translate(center_x, center_y) + painter.rotate(self._angle) + + arc_rect = QtCore.QRectF(-arc_size / 2, -arc_size / 2, arc_size, arc_size) + span_angle = int(self._span_angle * 16) + painter.drawArc(arc_rect, 0, span_angle) + + super().paintEvent(a0) + + def resizeEvent(self, a0: QtGui.QResizeEvent | None) -> None: + """Re-implemented method, handle widget resize event""" + super().resizeEvent(a0) label_width = self.width() label_height = 100 label_x = (self.width() - label_width) // 2 label_y = int(self.height() * 0.65) margin = 20 - # Center the GIF - gifshow_width = self.width() - margin * 2 - gifshow_height = self.height() - (self.height() - label_y) - margin - self.gifshow.setGeometry(margin, margin, gifshow_width, gifshow_height) self.label.setGeometry(label_x, label_y, label_width, label_height) + gifshow_max_height = label_y - margin + size = min(self.width() - margin * 2, gifshow_max_height) + + gifshow_x = (self.width() - size) // 2 + gifshow_y = (gifshow_max_height - size) // 2 + + self.gifshow.setGeometry(gifshow_x, gifshow_y, size, size) def show(self) -> None: """Re-implemented method, show widget""" - self.timer.start() self.repaint() return super().show() def _setupUI(self) -> None: + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) self.gifshow = QtWidgets.QLabel("", self) self.gifshow.setObjectName("gifshow") self.gifshow.setStyleSheet("background: transparent;") self.gifshow.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.gifshow.hide() self.label = QtWidgets.QLabel(self) font = QtGui.QFont() diff --git a/BlocksScreen/lib/panels/widgets/optionCardWidget.py b/BlocksScreen/lib/panels/widgets/optionCardWidget.py index 8c79ee68..6fbfe20d 100644 --- a/BlocksScreen/lib/panels/widgets/optionCardWidget.py +++ b/BlocksScreen/lib/panels/widgets/optionCardWidget.py @@ -38,6 +38,9 @@ def __init__( self.line_separator.setAttribute( QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents ) + self.continue_button.setAttribute( + QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents + ) self.setMode(False) self.set_card_icon(icon) diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index cc0f1be8..6a632f4d 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -7,7 +7,8 @@ from lib.utils.check_button import BlocksCustomCheckButton from lib.utils.blocks_button import BlocksCustomButton -from lib.panels.widgets.loadPage import LoadScreen +from lib.panels.widgets.loadWidget import LoadingOverlayWidget +from lib.panels.widgets.basePopup import BasePopup class ProbeHelper(QtWidgets.QWidget): @@ -48,7 +49,12 @@ class ProbeHelper(QtWidgets.QWidget): def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__(parent) - self.Loadscreen = LoadScreen(self) + + self.Loadscreen = BasePopup(self) + self.loadwidget = LoadingOverlayWidget( + self, LoadingOverlayWidget.AnimationGIF.PLACEHOLDER + ) + self.Loadscreen.add_widget(self.loadwidget) self.setObjectName("probe_offset_page") self._setupUi() self.inductive_icon = QtGui.QPixmap( @@ -393,7 +399,7 @@ def handle_start_tool(self, sender: typing.Type[OptionCard]) -> None: i.setDisabled(True) self.Loadscreen.show() - self.Loadscreen.set_status_message("Homing Axes...") + self.loadwidget.set_status_message("Homing Axes...") if self.z_offset_safe_xy: self.run_gcode_signal.emit("G28\nM400") diff --git a/BlocksScreen/lib/panels/widgets/updatePage.py b/BlocksScreen/lib/panels/widgets/updatePage.py index 534bbabe..5ec9cebd 100644 --- a/BlocksScreen/lib/panels/widgets/updatePage.py +++ b/BlocksScreen/lib/panels/widgets/updatePage.py @@ -1,7 +1,8 @@ import copy import typing -from lib.panels.widgets.loadPage import LoadScreen +from lib.panels.widgets.basePopup import BasePopup +from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.icon_button import IconButton @@ -61,8 +62,11 @@ def __init__(self, parent=None) -> None: self.cli_tracking = {} self.selected_item: ListItem | None = None self.ongoing_update: bool = False - - self.load_popup: LoadScreen = LoadScreen(self) + self.load_popup = BasePopup(self, floating=False, dialog=False) + self.loadwidget = LoadingOverlayWidget( + self, LoadingOverlayWidget.AnimationGIF.DEFAULT + ) + self.load_popup.add_widget(self.loadwidget) self.repeated_request_status = QtCore.QTimer() self.repeated_request_status.setInterval(2000) # every 2 seconds self.model = EntryListModel() @@ -93,7 +97,7 @@ def handle_ongoing_update(self) -> None: """Handled ongoing update signal, calls loading page (blocks user interaction) """ - self.load_popup.set_status_message("Updating...") + self.loadwidget.set_status_message("Updating...") self.load_popup.show() self.repeated_request_status.start(2000) @@ -151,10 +155,10 @@ def on_update_clicked(self) -> None: else: self.request_update_client.emit(cli_name) - self.load_popup.set_status_message(f"Updating {cli_name}") + self.loadwidget.set_status_message(f"Updating {cli_name}") else: self.request_recover_repo[str, bool].emit(cli_name, True) - self.load_popup.set_status_message(f"Recovering {cli_name}") + self.loadwidget.set_status_message(f"Recovering {cli_name}") self.load_popup.show() self.request_update_status.emit(False) diff --git a/BlocksScreen/lib/ui/utilitiesStackedWidget.ui b/BlocksScreen/lib/ui/utilitiesStackedWidget.ui index 79a79b88..bcb4a7dc 100644 --- a/BlocksScreen/lib/ui/utilitiesStackedWidget.ui +++ b/BlocksScreen/lib/ui/utilitiesStackedWidget.ui @@ -32,7 +32,7 @@ StackedWidget - 9 + 6 @@ -798,6 +798,13 @@ Shaper + + + + Qt::Vertical + + + @@ -862,7 +869,7 @@ Shaper - + 0 0 @@ -1955,569 +1962,15 @@ Shaper - - - - - - - - 0 - 0 - - - - - 250 - 80 - - - - - 250 - 80 - - - - - Momcake - 19 - false - PreferAntialias - - - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - - - - Y - - - false - - - true - - - menu_btn - - - :/input_shaper/media/btn_icons/frequency_Y.svg - - - - - - - - 0 - 0 - - - - - 250 - 80 - - - - - 250 - 80 - - - - - Momcake - 19 - false - PreferAntialias - - - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - - - - X - - - false - - - true - - - menu_btn - - - :/input_shaper/media/btn_icons/frequency_X.svg - - - - - - - - - - 0 - 0 - - - - - 80 - 80 - - - - - 80 - 80 - - - - - Momcake - 20 - false - PreferAntialias - - - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - - - - 2 - - - true - - - false - - - true - - - menu_btn - - - :/button_borders/media/buttons/btn_part1.svg - - - :/button_borders/media/buttons/btn_part2.svg - - - :/button_borders/media/buttons/btn_part3.svg - - - normal - - - isc_btn_group - - - - - - - - 0 - 0 - - - - - 80 - 80 - - - - - 80 - 80 - - - - - Momcake - 20 - false - PreferAntialias - - - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - - - - 3 - - - true - - - false - - - true - - - menu_btn - - - :/button_borders/media/buttons/btn_part1.svg - - - :/button_borders/media/buttons/btn_part2.svg - - - :/button_borders/media/buttons/btn_part3.svg - - - normal - - - isc_btn_group - - - - - - - - 0 - 0 - - - - - 80 - 80 - - - - - 80 - 80 - - - - - Momcake - 20 - false - PreferAntialias - - - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - - - - 5 - - - true - - - false - - - true - - - menu_btn - - - :/button_borders/media/buttons/btn_part1.svg - - - :/button_borders/media/buttons/btn_part2.svg - - - :/button_borders/media/buttons/btn_part3.svg - - - normal - - - isc_btn_group - - - - - - - - 0 - 0 - - - - - 80 - 80 - - - - - 80 - 80 - - - - - Momcake - 20 - false - PreferAntialias - - - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - - - - 1 - - - true - - - true - - - false - - - true - - - menu_btn - - - :/button_borders/media/buttons/btn_part1.svg - - - :/button_borders/media/buttons/btn_part2.svg - - - :/button_borders/media/buttons/btn_part3.svg - - - normal - - - isc_btn_group - - - - - - - - 0 - 0 - - - - - 80 - 80 - - - - - 80 - 80 - - - - - Momcake - 20 - false - PreferAntialias - - - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - - - - 4 - - - true - - - false - - - true - - - menu_btn - - - :/button_borders/media/buttons/btn_part1.svg - - - :/button_borders/media/buttons/btn_part2.svg - - - :/button_borders/media/buttons/btn_part3.svg - - - normal - - - isc_btn_group - - - - - - - - 11 - - - - color:white - - - Times: - - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 60 - 20 - - - - - + - - - - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 20 - 24 - - - - + + - + @@ -2528,7 +1981,7 @@ Shaper 60 - 0 + 60 @@ -2537,89 +1990,28 @@ Shaper 60 - - Insert Best Text Here - - - Qt::AlignCenter - - - - - - - - - 0 - - - - - - 0 - 0 - - - - - 250 - 80 - - - - - 250 - 80 - - - Momcake - 19 - false - PreferAntialias + 20 - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - + color:white - ZV + Input Shaper - - true - - - false - - - true - - - menu_btn - - - + + Qt::AlignCenter - - is_btn_group - - - + + + + + + 0 @@ -2662,7 +2054,7 @@ Shaper - EI + MZV true @@ -2679,13 +2071,10 @@ Shaper - - is_btn_group - - - + + 0 @@ -2728,7 +2117,7 @@ Shaper - MZV + EI true @@ -2745,13 +2134,10 @@ Shaper - - is_btn_group - - + 0 @@ -2811,13 +2197,10 @@ Shaper - - is_btn_group - - - + + 0 @@ -2856,14 +2239,11 @@ Shaper Qt::LeftToRight - - false - - user input + ZV true @@ -2880,13 +2260,10 @@ Shaper - - is_btn_group - - - + + 0 @@ -2946,13 +2323,14 @@ Shaper - - is_btn_group - - - + + + + + + 0 @@ -2995,7 +2373,7 @@ Shaper - Confirm + Cancel false @@ -3007,12 +2385,12 @@ Shaper menu_btn - :/dialog/media/btn_icons/yes.svg + :/dialog/media/btn_icons/no.svg - - + + 0 @@ -3055,7 +2433,7 @@ Shaper - Cancel + Confirm false @@ -3067,7 +2445,7 @@ Shaper menu_btn - :/dialog/media/btn_icons/no.svg + :/dialog/media/btn_icons/yes.svg @@ -3120,255 +2498,23 @@ Shaper - - - - - - 0 - 0 - - - - - 250 - 80 - - - - - 250 - 80 - - - - - Momcake - 19 - false - PreferAntialias - - - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - - - - Confirm - - - false - - - true - - - menu_btn - - - :/dialog/media/btn_icons/yes.svg - - - - - - - - - - 0 - 0 - - - - - 250 - 80 - - - - - 250 - 80 - - - - - Momcake - 19 - false - PreferAntialias - - - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - - - - Smoothing - - - false - - - true - - - menu_btn - - - display_secondary - - - - - - - - - - - - 0 - 0 - - - - - 250 - 80 - - - - - 250 - 80 - - - - - Momcake - 19 - false - PreferAntialias - - - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - - - - Frequency - - - false - - - true - - - display_secondary - - - - - - - + + + - + 0 0 - - - 250 - 80 - - - - - 250 - 80 - - - - - Momcake - 19 - false - PreferAntialias - - - - false - - - true - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - - - - Back - - - false - - - true - - - menu_btn + + QFrame::StyledPanel - - :/ui/media/btn_icons/back.svg + + QFrame::Raised - - - @@ -3670,8 +2816,4 @@ Shaper - - - - diff --git a/BlocksScreen/lib/ui/utilitiesStackedWidget_ui.py b/BlocksScreen/lib/ui/utilitiesStackedWidget_ui.py index bb2ca93e..7c3c6223 100644 --- a/BlocksScreen/lib/ui/utilitiesStackedWidget_ui.py +++ b/BlocksScreen/lib/ui/utilitiesStackedWidget_ui.py @@ -1,4 +1,4 @@ -# Form implementation generated from reading ui file '/home/levi/main/Blocks_Screen/BlocksScreen/lib/ui/utilitiesStackedWidget.ui' +# Form implementation generated from reading ui file '/home/levi/BlocksScreen/BlocksScreen/lib/ui/utilitiesStackedWidget.ui' # # Created by: PyQt6 UI code generator 6.7.1 # @@ -325,6 +325,10 @@ def setupUi(self, utilitiesStackedWidget): self.verticalLayout_4.addLayout(self.leds_header_layout) spacerItem4 = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) self.verticalLayout_4.addItem(spacerItem4) + self.verticalScrollBar = QtWidgets.QScrollBar(parent=self.leds_page) + self.verticalScrollBar.setOrientation(QtCore.Qt.Orientation.Vertical) + self.verticalScrollBar.setObjectName("verticalScrollBar") + self.verticalLayout_4.addWidget(self.verticalScrollBar) self.leds_content_layout = QtWidgets.QGridLayout() self.leds_content_layout.setObjectName("leds_content_layout") self.leds_widget = QtWidgets.QWidget(parent=self.leds_page) @@ -346,7 +350,7 @@ def setupUi(self, utilitiesStackedWidget): spacerItem7 = QtWidgets.QSpacerItem(60, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) self.routines_header_layout.addItem(spacerItem7) self.routines_page_title = QtWidgets.QLabel(parent=self.routines_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.routines_page_title.sizePolicy().hasHeightForWidth()) @@ -769,391 +773,180 @@ def setupUi(self, utilitiesStackedWidget): self.verticalLayout_13.addLayout(self.is_header_layout) self.is_content_layout = QtWidgets.QHBoxLayout() self.is_content_layout.setObjectName("is_content_layout") - self.is_xy_layout = QtWidgets.QGridLayout() - self.is_xy_layout.setObjectName("is_xy_layout") - self.is_Y_startis_btn = BlocksCustomButton(parent=self.input_shaper_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.is_Y_startis_btn.sizePolicy().hasHeightForWidth()) - self.is_Y_startis_btn.setSizePolicy(sizePolicy) - self.is_Y_startis_btn.setMinimumSize(QtCore.QSize(250, 80)) - self.is_Y_startis_btn.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(19) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.is_Y_startis_btn.setFont(font) - self.is_Y_startis_btn.setMouseTracking(False) - self.is_Y_startis_btn.setTabletTracking(True) - self.is_Y_startis_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.is_Y_startis_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.is_Y_startis_btn.setStyleSheet("") - self.is_Y_startis_btn.setAutoDefault(False) - self.is_Y_startis_btn.setFlat(True) - self.is_Y_startis_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/input_shaper/media/btn_icons/frequency_Y.svg")) - self.is_Y_startis_btn.setObjectName("is_Y_startis_btn") - self.is_xy_layout.addWidget(self.is_Y_startis_btn, 1, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.is_X_startis_btn = BlocksCustomButton(parent=self.input_shaper_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.is_X_startis_btn.sizePolicy().hasHeightForWidth()) - self.is_X_startis_btn.setSizePolicy(sizePolicy) - self.is_X_startis_btn.setMinimumSize(QtCore.QSize(250, 80)) - self.is_X_startis_btn.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(19) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.is_X_startis_btn.setFont(font) - self.is_X_startis_btn.setMouseTracking(False) - self.is_X_startis_btn.setTabletTracking(True) - self.is_X_startis_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.is_X_startis_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.is_X_startis_btn.setStyleSheet("") - self.is_X_startis_btn.setAutoDefault(False) - self.is_X_startis_btn.setFlat(True) - self.is_X_startis_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/input_shaper/media/btn_icons/frequency_X.svg")) - self.is_X_startis_btn.setObjectName("is_X_startis_btn") - self.is_xy_layout.addWidget(self.is_X_startis_btn, 0, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.gridLayout = QtWidgets.QGridLayout() - self.gridLayout.setObjectName("gridLayout") - self.btn2 = BlocksCustomCheckButton(parent=self.input_shaper_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.btn2.sizePolicy().hasHeightForWidth()) - self.btn2.setSizePolicy(sizePolicy) - self.btn2.setMinimumSize(QtCore.QSize(80, 80)) - self.btn2.setMaximumSize(QtCore.QSize(80, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(20) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.btn2.setFont(font) - self.btn2.setMouseTracking(False) - self.btn2.setTabletTracking(True) - self.btn2.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.btn2.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.btn2.setStyleSheet("") - self.btn2.setCheckable(True) - self.btn2.setAutoDefault(False) - self.btn2.setFlat(True) - self.btn2.setProperty("borderLeftPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part1.svg")) - self.btn2.setProperty("borderCenterPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part2.svg")) - self.btn2.setProperty("borderRightPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part3.svg")) - self.btn2.setObjectName("btn2") - self.isc_btn_group = QtWidgets.QButtonGroup(utilitiesStackedWidget) - self.isc_btn_group.setObjectName("isc_btn_group") - self.isc_btn_group.addButton(self.btn2) - self.gridLayout.addWidget(self.btn2, 1, 1, 1, 1) - self.btn3 = BlocksCustomCheckButton(parent=self.input_shaper_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.btn3.sizePolicy().hasHeightForWidth()) - self.btn3.setSizePolicy(sizePolicy) - self.btn3.setMinimumSize(QtCore.QSize(80, 80)) - self.btn3.setMaximumSize(QtCore.QSize(80, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(20) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.btn3.setFont(font) - self.btn3.setMouseTracking(False) - self.btn3.setTabletTracking(True) - self.btn3.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.btn3.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.btn3.setStyleSheet("") - self.btn3.setCheckable(True) - self.btn3.setAutoDefault(False) - self.btn3.setFlat(True) - self.btn3.setProperty("borderLeftPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part1.svg")) - self.btn3.setProperty("borderCenterPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part2.svg")) - self.btn3.setProperty("borderRightPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part3.svg")) - self.btn3.setObjectName("btn3") - self.isc_btn_group.addButton(self.btn3) - self.gridLayout.addWidget(self.btn3, 2, 0, 1, 1) - self.btn5 = BlocksCustomCheckButton(parent=self.input_shaper_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.btn5.sizePolicy().hasHeightForWidth()) - self.btn5.setSizePolicy(sizePolicy) - self.btn5.setMinimumSize(QtCore.QSize(80, 80)) - self.btn5.setMaximumSize(QtCore.QSize(80, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(20) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.btn5.setFont(font) - self.btn5.setMouseTracking(False) - self.btn5.setTabletTracking(True) - self.btn5.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.btn5.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.btn5.setStyleSheet("") - self.btn5.setCheckable(True) - self.btn5.setAutoDefault(False) - self.btn5.setFlat(True) - self.btn5.setProperty("borderLeftPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part1.svg")) - self.btn5.setProperty("borderCenterPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part2.svg")) - self.btn5.setProperty("borderRightPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part3.svg")) - self.btn5.setObjectName("btn5") - self.isc_btn_group.addButton(self.btn5) - self.gridLayout.addWidget(self.btn5, 3, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.btn1 = BlocksCustomCheckButton(parent=self.input_shaper_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.btn1.sizePolicy().hasHeightForWidth()) - self.btn1.setSizePolicy(sizePolicy) - self.btn1.setMinimumSize(QtCore.QSize(80, 80)) - self.btn1.setMaximumSize(QtCore.QSize(80, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(20) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.btn1.setFont(font) - self.btn1.setMouseTracking(False) - self.btn1.setTabletTracking(True) - self.btn1.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.btn1.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.btn1.setStyleSheet("") - self.btn1.setCheckable(True) - self.btn1.setChecked(True) - self.btn1.setAutoDefault(False) - self.btn1.setFlat(True) - self.btn1.setProperty("borderLeftPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part1.svg")) - self.btn1.setProperty("borderCenterPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part2.svg")) - self.btn1.setProperty("borderRightPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part3.svg")) - self.btn1.setObjectName("btn1") - self.isc_btn_group.addButton(self.btn1) - self.gridLayout.addWidget(self.btn1, 1, 0, 1, 1) - self.btn4 = BlocksCustomCheckButton(parent=self.input_shaper_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.btn4.sizePolicy().hasHeightForWidth()) - self.btn4.setSizePolicy(sizePolicy) - self.btn4.setMinimumSize(QtCore.QSize(80, 80)) - self.btn4.setMaximumSize(QtCore.QSize(80, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(20) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.btn4.setFont(font) - self.btn4.setMouseTracking(False) - self.btn4.setTabletTracking(True) - self.btn4.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.btn4.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.btn4.setStyleSheet("") - self.btn4.setCheckable(True) - self.btn4.setAutoDefault(False) - self.btn4.setFlat(True) - self.btn4.setProperty("borderLeftPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part1.svg")) - self.btn4.setProperty("borderCenterPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part2.svg")) - self.btn4.setProperty("borderRightPixmap", QtGui.QPixmap(":/button_borders/media/buttons/btn_part3.svg")) - self.btn4.setObjectName("btn4") - self.isc_btn_group.addButton(self.btn4) - self.gridLayout.addWidget(self.btn4, 2, 1, 1, 1) - self.label = QtWidgets.QLabel(parent=self.input_shaper_page) - font = QtGui.QFont() - font.setPointSize(11) - self.label.setFont(font) - self.label.setStyleSheet("color:white") - self.label.setObjectName("label") - self.gridLayout.addWidget(self.label, 0, 0, 1, 2) - self.is_xy_layout.addLayout(self.gridLayout, 0, 2, 2, 1) - self.is_content_layout.addLayout(self.is_xy_layout) - spacerItem18 = QtWidgets.QSpacerItem(60, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.is_content_layout.addItem(spacerItem18) self.verticalLayout_13.addLayout(self.is_content_layout) utilitiesStackedWidget.addWidget(self.input_shaper_page) - self.is_page = QtWidgets.QWidget() - self.is_page.setObjectName("is_page") - self.verticalLayout_8 = QtWidgets.QVBoxLayout(self.is_page) - self.verticalLayout_8.setObjectName("verticalLayout_8") - spacerItem19 = QtWidgets.QSpacerItem(20, 24, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.verticalLayout_8.addItem(spacerItem19) + self.manual_is_res_page = QtWidgets.QWidget() + self.manual_is_res_page.setObjectName("manual_is_res_page") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.manual_is_res_page) + self.verticalLayout_3.setObjectName("verticalLayout_3") self.horizontalLayout_4 = QtWidgets.QHBoxLayout() self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.label_3 = QtWidgets.QLabel(parent=self.is_page) + self.label_3 = QtWidgets.QLabel(parent=self.manual_is_res_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.label_3.sizePolicy().hasHeightForWidth()) self.label_3.setSizePolicy(sizePolicy) - self.label_3.setMinimumSize(QtCore.QSize(60, 0)) + self.label_3.setMinimumSize(QtCore.QSize(60, 60)) self.label_3.setMaximumSize(QtCore.QSize(16777215, 60)) + font = QtGui.QFont() + font.setPointSize(20) + self.label_3.setFont(font) + self.label_3.setStyleSheet("color:white") self.label_3.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.label_3.setObjectName("label_3") - self.horizontalLayout_4.addWidget(self.label_3) - self.verticalLayout_8.addLayout(self.horizontalLayout_4) - self.gridLayout_4 = QtWidgets.QGridLayout() - self.gridLayout_4.setSpacing(0) - self.gridLayout_4.setObjectName("gridLayout_4") - self.am_zv = BlocksCustomCheckButton(parent=self.is_page) + self.horizontalLayout_4.addWidget(self.label_3, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.verticalLayout_3.addLayout(self.horizontalLayout_4) + self.gridLayout_3 = QtWidgets.QGridLayout() + self.gridLayout_3.setObjectName("gridLayout_3") + self.am_mzv_2 = BlocksCustomCheckButton(parent=self.manual_is_res_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.am_zv.sizePolicy().hasHeightForWidth()) - self.am_zv.setSizePolicy(sizePolicy) - self.am_zv.setMinimumSize(QtCore.QSize(250, 80)) - self.am_zv.setMaximumSize(QtCore.QSize(250, 80)) + sizePolicy.setHeightForWidth(self.am_mzv_2.sizePolicy().hasHeightForWidth()) + self.am_mzv_2.setSizePolicy(sizePolicy) + self.am_mzv_2.setMinimumSize(QtCore.QSize(250, 80)) + self.am_mzv_2.setMaximumSize(QtCore.QSize(250, 80)) font = QtGui.QFont() font.setFamily("Momcake") font.setPointSize(19) font.setItalic(False) font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.am_zv.setFont(font) - self.am_zv.setMouseTracking(False) - self.am_zv.setTabletTracking(True) - self.am_zv.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.am_zv.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.am_zv.setStyleSheet("") - self.am_zv.setCheckable(True) - self.am_zv.setAutoDefault(False) - self.am_zv.setFlat(True) - self.am_zv.setObjectName("am_zv") - self.is_btn_group = QtWidgets.QButtonGroup(utilitiesStackedWidget) - self.is_btn_group.setObjectName("is_btn_group") - self.is_btn_group.addButton(self.am_zv) - self.gridLayout_4.addWidget(self.am_zv, 0, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.am_ei = BlocksCustomCheckButton(parent=self.is_page) + self.am_mzv_2.setFont(font) + self.am_mzv_2.setMouseTracking(False) + self.am_mzv_2.setTabletTracking(True) + self.am_mzv_2.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.am_mzv_2.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.am_mzv_2.setStyleSheet("") + self.am_mzv_2.setCheckable(True) + self.am_mzv_2.setAutoDefault(False) + self.am_mzv_2.setFlat(True) + self.am_mzv_2.setObjectName("am_mzv_2") + self.gridLayout_3.addWidget(self.am_mzv_2, 0, 1, 1, 1) + self.am_ei_2 = BlocksCustomCheckButton(parent=self.manual_is_res_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.am_ei.sizePolicy().hasHeightForWidth()) - self.am_ei.setSizePolicy(sizePolicy) - self.am_ei.setMinimumSize(QtCore.QSize(250, 80)) - self.am_ei.setMaximumSize(QtCore.QSize(250, 80)) + sizePolicy.setHeightForWidth(self.am_ei_2.sizePolicy().hasHeightForWidth()) + self.am_ei_2.setSizePolicy(sizePolicy) + self.am_ei_2.setMinimumSize(QtCore.QSize(250, 80)) + self.am_ei_2.setMaximumSize(QtCore.QSize(250, 80)) font = QtGui.QFont() font.setFamily("Momcake") font.setPointSize(19) font.setItalic(False) font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.am_ei.setFont(font) - self.am_ei.setMouseTracking(False) - self.am_ei.setTabletTracking(True) - self.am_ei.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.am_ei.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.am_ei.setStyleSheet("") - self.am_ei.setCheckable(True) - self.am_ei.setAutoDefault(False) - self.am_ei.setFlat(True) - self.am_ei.setObjectName("am_ei") - self.is_btn_group.addButton(self.am_ei) - self.gridLayout_4.addWidget(self.am_ei, 0, 2, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.am_mzv = BlocksCustomCheckButton(parent=self.is_page) + self.am_ei_2.setFont(font) + self.am_ei_2.setMouseTracking(False) + self.am_ei_2.setTabletTracking(True) + self.am_ei_2.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.am_ei_2.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.am_ei_2.setStyleSheet("") + self.am_ei_2.setCheckable(True) + self.am_ei_2.setAutoDefault(False) + self.am_ei_2.setFlat(True) + self.am_ei_2.setObjectName("am_ei_2") + self.gridLayout_3.addWidget(self.am_ei_2, 1, 0, 1, 1) + self.am_3hump_ei_2 = BlocksCustomCheckButton(parent=self.manual_is_res_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.am_mzv.sizePolicy().hasHeightForWidth()) - self.am_mzv.setSizePolicy(sizePolicy) - self.am_mzv.setMinimumSize(QtCore.QSize(250, 80)) - self.am_mzv.setMaximumSize(QtCore.QSize(250, 80)) + sizePolicy.setHeightForWidth(self.am_3hump_ei_2.sizePolicy().hasHeightForWidth()) + self.am_3hump_ei_2.setSizePolicy(sizePolicy) + self.am_3hump_ei_2.setMinimumSize(QtCore.QSize(250, 80)) + self.am_3hump_ei_2.setMaximumSize(QtCore.QSize(250, 80)) font = QtGui.QFont() font.setFamily("Momcake") font.setPointSize(19) font.setItalic(False) font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.am_mzv.setFont(font) - self.am_mzv.setMouseTracking(False) - self.am_mzv.setTabletTracking(True) - self.am_mzv.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.am_mzv.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.am_mzv.setStyleSheet("") - self.am_mzv.setCheckable(True) - self.am_mzv.setAutoDefault(False) - self.am_mzv.setFlat(True) - self.am_mzv.setObjectName("am_mzv") - self.is_btn_group.addButton(self.am_mzv) - self.gridLayout_4.addWidget(self.am_mzv, 1, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.am_3hump_ei = BlocksCustomCheckButton(parent=self.is_page) + self.am_3hump_ei_2.setFont(font) + self.am_3hump_ei_2.setMouseTracking(False) + self.am_3hump_ei_2.setTabletTracking(True) + self.am_3hump_ei_2.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.am_3hump_ei_2.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.am_3hump_ei_2.setStyleSheet("") + self.am_3hump_ei_2.setCheckable(True) + self.am_3hump_ei_2.setAutoDefault(False) + self.am_3hump_ei_2.setFlat(True) + self.am_3hump_ei_2.setObjectName("am_3hump_ei_2") + self.gridLayout_3.addWidget(self.am_3hump_ei_2, 2, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) + self.am_zv_2 = BlocksCustomCheckButton(parent=self.manual_is_res_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.am_3hump_ei.sizePolicy().hasHeightForWidth()) - self.am_3hump_ei.setSizePolicy(sizePolicy) - self.am_3hump_ei.setMinimumSize(QtCore.QSize(250, 80)) - self.am_3hump_ei.setMaximumSize(QtCore.QSize(250, 80)) + sizePolicy.setHeightForWidth(self.am_zv_2.sizePolicy().hasHeightForWidth()) + self.am_zv_2.setSizePolicy(sizePolicy) + self.am_zv_2.setMinimumSize(QtCore.QSize(250, 80)) + self.am_zv_2.setMaximumSize(QtCore.QSize(250, 80)) font = QtGui.QFont() font.setFamily("Momcake") font.setPointSize(19) font.setItalic(False) font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.am_3hump_ei.setFont(font) - self.am_3hump_ei.setMouseTracking(False) - self.am_3hump_ei.setTabletTracking(True) - self.am_3hump_ei.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.am_3hump_ei.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.am_3hump_ei.setStyleSheet("") - self.am_3hump_ei.setCheckable(True) - self.am_3hump_ei.setAutoDefault(False) - self.am_3hump_ei.setFlat(True) - self.am_3hump_ei.setObjectName("am_3hump_ei") - self.is_btn_group.addButton(self.am_3hump_ei) - self.gridLayout_4.addWidget(self.am_3hump_ei, 2, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.am_user_input = BlocksCustomCheckButton(parent=self.is_page) + self.am_zv_2.setFont(font) + self.am_zv_2.setMouseTracking(False) + self.am_zv_2.setTabletTracking(True) + self.am_zv_2.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.am_zv_2.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.am_zv_2.setStyleSheet("") + self.am_zv_2.setCheckable(True) + self.am_zv_2.setAutoDefault(False) + self.am_zv_2.setFlat(True) + self.am_zv_2.setObjectName("am_zv_2") + self.gridLayout_3.addWidget(self.am_zv_2, 0, 0, 1, 1) + self.am_2hump_ei_2 = BlocksCustomCheckButton(parent=self.manual_is_res_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.am_user_input.sizePolicy().hasHeightForWidth()) - self.am_user_input.setSizePolicy(sizePolicy) - self.am_user_input.setMinimumSize(QtCore.QSize(250, 80)) - self.am_user_input.setMaximumSize(QtCore.QSize(250, 80)) + sizePolicy.setHeightForWidth(self.am_2hump_ei_2.sizePolicy().hasHeightForWidth()) + self.am_2hump_ei_2.setSizePolicy(sizePolicy) + self.am_2hump_ei_2.setMinimumSize(QtCore.QSize(250, 80)) + self.am_2hump_ei_2.setMaximumSize(QtCore.QSize(250, 80)) font = QtGui.QFont() font.setFamily("Momcake") font.setPointSize(19) font.setItalic(False) font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.am_user_input.setFont(font) - self.am_user_input.setMouseTracking(False) - self.am_user_input.setTabletTracking(True) - self.am_user_input.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.am_user_input.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.am_user_input.setAutoFillBackground(False) - self.am_user_input.setStyleSheet("") - self.am_user_input.setCheckable(True) - self.am_user_input.setAutoDefault(False) - self.am_user_input.setFlat(True) - self.am_user_input.setObjectName("am_user_input") - self.is_btn_group.addButton(self.am_user_input) - self.gridLayout_4.addWidget(self.am_user_input, 2, 2, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.am_2hump_ei = BlocksCustomCheckButton(parent=self.is_page) + self.am_2hump_ei_2.setFont(font) + self.am_2hump_ei_2.setMouseTracking(False) + self.am_2hump_ei_2.setTabletTracking(True) + self.am_2hump_ei_2.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.am_2hump_ei_2.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.am_2hump_ei_2.setStyleSheet("") + self.am_2hump_ei_2.setCheckable(True) + self.am_2hump_ei_2.setAutoDefault(False) + self.am_2hump_ei_2.setFlat(True) + self.am_2hump_ei_2.setObjectName("am_2hump_ei_2") + self.gridLayout_3.addWidget(self.am_2hump_ei_2, 1, 1, 1, 1) + self.verticalLayout_3.addLayout(self.gridLayout_3) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.am_cancel = BlocksCustomButton(parent=self.manual_is_res_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.am_2hump_ei.sizePolicy().hasHeightForWidth()) - self.am_2hump_ei.setSizePolicy(sizePolicy) - self.am_2hump_ei.setMinimumSize(QtCore.QSize(250, 80)) - self.am_2hump_ei.setMaximumSize(QtCore.QSize(250, 80)) + sizePolicy.setHeightForWidth(self.am_cancel.sizePolicy().hasHeightForWidth()) + self.am_cancel.setSizePolicy(sizePolicy) + self.am_cancel.setMinimumSize(QtCore.QSize(250, 80)) + self.am_cancel.setMaximumSize(QtCore.QSize(250, 80)) font = QtGui.QFont() font.setFamily("Momcake") font.setPointSize(19) font.setItalic(False) font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.am_2hump_ei.setFont(font) - self.am_2hump_ei.setMouseTracking(False) - self.am_2hump_ei.setTabletTracking(True) - self.am_2hump_ei.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.am_2hump_ei.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.am_2hump_ei.setStyleSheet("") - self.am_2hump_ei.setCheckable(True) - self.am_2hump_ei.setAutoDefault(False) - self.am_2hump_ei.setFlat(True) - self.am_2hump_ei.setObjectName("am_2hump_ei") - self.is_btn_group.addButton(self.am_2hump_ei) - self.gridLayout_4.addWidget(self.am_2hump_ei, 1, 2, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.am_confirm = BlocksCustomButton(parent=self.is_page) + self.am_cancel.setFont(font) + self.am_cancel.setMouseTracking(False) + self.am_cancel.setTabletTracking(True) + self.am_cancel.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.am_cancel.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.am_cancel.setStyleSheet("") + self.am_cancel.setAutoDefault(False) + self.am_cancel.setFlat(True) + self.am_cancel.setProperty("icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/no.svg")) + self.am_cancel.setObjectName("am_cancel") + self.horizontalLayout.addWidget(self.am_cancel) + self.am_confirm = BlocksCustomButton(parent=self.manual_is_res_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -1176,39 +969,15 @@ def setupUi(self, utilitiesStackedWidget): self.am_confirm.setFlat(True) self.am_confirm.setProperty("icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg")) self.am_confirm.setObjectName("am_confirm") - self.gridLayout_4.addWidget(self.am_confirm, 3, 2, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.am_cancel = BlocksCustomButton(parent=self.is_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.am_cancel.sizePolicy().hasHeightForWidth()) - self.am_cancel.setSizePolicy(sizePolicy) - self.am_cancel.setMinimumSize(QtCore.QSize(250, 80)) - self.am_cancel.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(19) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.am_cancel.setFont(font) - self.am_cancel.setMouseTracking(False) - self.am_cancel.setTabletTracking(True) - self.am_cancel.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.am_cancel.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.am_cancel.setStyleSheet("") - self.am_cancel.setAutoDefault(False) - self.am_cancel.setFlat(True) - self.am_cancel.setProperty("icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/no.svg")) - self.am_cancel.setObjectName("am_cancel") - self.gridLayout_4.addWidget(self.am_cancel, 3, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.verticalLayout_8.addLayout(self.gridLayout_4) - utilitiesStackedWidget.addWidget(self.is_page) + self.horizontalLayout.addWidget(self.am_confirm) + self.verticalLayout_3.addLayout(self.horizontalLayout) + utilitiesStackedWidget.addWidget(self.manual_is_res_page) self.user_input_page = QtWidgets.QWidget() self.user_input_page.setObjectName("user_input_page") self.verticalLayout_9 = QtWidgets.QVBoxLayout(self.user_input_page) self.verticalLayout_9.setObjectName("verticalLayout_9") - spacerItem20 = QtWidgets.QSpacerItem(20, 24, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.verticalLayout_9.addItem(spacerItem20) + spacerItem18 = QtWidgets.QSpacerItem(20, 24, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.verticalLayout_9.addItem(spacerItem18) self.horizontalLayout_5 = QtWidgets.QHBoxLayout() self.horizontalLayout_5.setObjectName("horizontalLayout_5") self.label_8 = QtWidgets.QLabel(parent=self.user_input_page) @@ -1222,112 +991,19 @@ def setupUi(self, utilitiesStackedWidget): self.label_8.setObjectName("label_8") self.horizontalLayout_5.addWidget(self.label_8) self.verticalLayout_9.addLayout(self.horizontalLayout_5) - self.gridLayout_5 = QtWidgets.QGridLayout() - self.gridLayout_5.setObjectName("gridLayout_5") - self.is_confirm_btn = BlocksCustomButton(parent=self.user_input_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.is_confirm_btn.sizePolicy().hasHeightForWidth()) - self.is_confirm_btn.setSizePolicy(sizePolicy) - self.is_confirm_btn.setMinimumSize(QtCore.QSize(250, 80)) - self.is_confirm_btn.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(19) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.is_confirm_btn.setFont(font) - self.is_confirm_btn.setMouseTracking(False) - self.is_confirm_btn.setTabletTracking(True) - self.is_confirm_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.is_confirm_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.is_confirm_btn.setStyleSheet("") - self.is_confirm_btn.setAutoDefault(False) - self.is_confirm_btn.setFlat(True) - self.is_confirm_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg")) - self.is_confirm_btn.setObjectName("is_confirm_btn") - self.gridLayout_5.addWidget(self.is_confirm_btn, 2, 2, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.horizontalLayout_8 = QtWidgets.QHBoxLayout() - self.horizontalLayout_8.setObjectName("horizontalLayout_8") - self.isui_sm = BlocksCustomButton(parent=self.user_input_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.isui_sm.sizePolicy().hasHeightForWidth()) - self.isui_sm.setSizePolicy(sizePolicy) - self.isui_sm.setMinimumSize(QtCore.QSize(250, 80)) - self.isui_sm.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(19) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.isui_sm.setFont(font) - self.isui_sm.setMouseTracking(False) - self.isui_sm.setTabletTracking(True) - self.isui_sm.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.isui_sm.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.isui_sm.setStyleSheet("") - self.isui_sm.setAutoDefault(False) - self.isui_sm.setFlat(True) - self.isui_sm.setObjectName("isui_sm") - self.horizontalLayout_8.addWidget(self.isui_sm, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.gridLayout_5.addLayout(self.horizontalLayout_8, 0, 2, 1, 1) - self.horizontalLayout_9 = QtWidgets.QHBoxLayout() - self.horizontalLayout_9.setObjectName("horizontalLayout_9") - self.isui_fq = BlocksCustomButton(parent=self.user_input_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.isui_fq.sizePolicy().hasHeightForWidth()) - self.isui_fq.setSizePolicy(sizePolicy) - self.isui_fq.setMinimumSize(QtCore.QSize(250, 80)) - self.isui_fq.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(19) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.isui_fq.setFont(font) - self.isui_fq.setMouseTracking(False) - self.isui_fq.setTabletTracking(True) - self.isui_fq.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.isui_fq.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.isui_fq.setStyleSheet("") - self.isui_fq.setAutoDefault(False) - self.isui_fq.setFlat(True) - self.isui_fq.setObjectName("isui_fq") - self.horizontalLayout_9.addWidget(self.isui_fq, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.gridLayout_5.addLayout(self.horizontalLayout_9, 1, 2, 1, 1) - self.is_back_btn = BlocksCustomButton(parent=self.user_input_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.MinimumExpanding) + self.verticalLayout_10 = QtWidgets.QVBoxLayout() + self.verticalLayout_10.setObjectName("verticalLayout_10") + self.frame_4 = QtWidgets.QFrame(parent=self.user_input_page) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.is_back_btn.sizePolicy().hasHeightForWidth()) - self.is_back_btn.setSizePolicy(sizePolicy) - self.is_back_btn.setMinimumSize(QtCore.QSize(250, 80)) - self.is_back_btn.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(19) - font.setItalic(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.is_back_btn.setFont(font) - self.is_back_btn.setMouseTracking(False) - self.is_back_btn.setTabletTracking(True) - self.is_back_btn.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.is_back_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.is_back_btn.setStyleSheet("") - self.is_back_btn.setAutoDefault(False) - self.is_back_btn.setFlat(True) - self.is_back_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) - self.is_back_btn.setObjectName("is_back_btn") - self.gridLayout_5.addWidget(self.is_back_btn, 2, 0, 1, 2, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.horizontalLayout_6 = QtWidgets.QHBoxLayout() - self.horizontalLayout_6.setObjectName("horizontalLayout_6") - self.gridLayout_5.addLayout(self.horizontalLayout_6, 0, 0, 2, 2) - self.verticalLayout_9.addLayout(self.gridLayout_5) + sizePolicy.setHeightForWidth(self.frame_4.sizePolicy().hasHeightForWidth()) + self.frame_4.setSizePolicy(sizePolicy) + self.frame_4.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_4.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame_4.setObjectName("frame_4") + self.verticalLayout_10.addWidget(self.frame_4) + self.verticalLayout_9.addLayout(self.verticalLayout_10) utilitiesStackedWidget.addWidget(self.user_input_page) self.leds_slider_page = QtWidgets.QWidget() self.leds_slider_page.setObjectName("leds_slider_page") @@ -1360,8 +1036,8 @@ def setupUi(self, utilitiesStackedWidget): self.leds_slider_header_layout = QtWidgets.QHBoxLayout(self.layoutWidget) self.leds_slider_header_layout.setContentsMargins(0, 0, 0, 0) self.leds_slider_header_layout.setObjectName("leds_slider_header_layout") - spacerItem21 = QtWidgets.QSpacerItem(60, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.leds_slider_header_layout.addItem(spacerItem21) + spacerItem19 = QtWidgets.QSpacerItem(60, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.leds_slider_header_layout.addItem(spacerItem19) self.leds_slider_tittle_label = QtWidgets.QLabel(parent=self.layoutWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Preferred) sizePolicy.setHorizontalStretch(0) @@ -1424,7 +1100,7 @@ def setupUi(self, utilitiesStackedWidget): utilitiesStackedWidget.addWidget(self.leds_slider_page) self.retranslateUi(utilitiesStackedWidget) - utilitiesStackedWidget.setCurrentIndex(9) + utilitiesStackedWidget.setCurrentIndex(6) QtCore.QMetaObject.connectSlotsByName(utilitiesStackedWidget) def retranslateUi(self, utilitiesStackedWidget): @@ -1495,53 +1171,22 @@ def retranslateUi(self, utilitiesStackedWidget): self.input_shaper_back_btn.setText(_translate("utilitiesStackedWidget", "Back")) self.input_shaper_back_btn.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) self.input_shaper_back_btn.setProperty("button_type", _translate("utilitiesStackedWidget", "icon")) - self.is_Y_startis_btn.setText(_translate("utilitiesStackedWidget", "Y")) - self.is_Y_startis_btn.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.is_X_startis_btn.setText(_translate("utilitiesStackedWidget", "X")) - self.is_X_startis_btn.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.btn2.setText(_translate("utilitiesStackedWidget", "2")) - self.btn2.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.btn2.setProperty("button_type", _translate("utilitiesStackedWidget", "normal")) - self.btn3.setText(_translate("utilitiesStackedWidget", "3")) - self.btn3.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.btn3.setProperty("button_type", _translate("utilitiesStackedWidget", "normal")) - self.btn5.setText(_translate("utilitiesStackedWidget", "5")) - self.btn5.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.btn5.setProperty("button_type", _translate("utilitiesStackedWidget", "normal")) - self.btn1.setText(_translate("utilitiesStackedWidget", "1")) - self.btn1.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.btn1.setProperty("button_type", _translate("utilitiesStackedWidget", "normal")) - self.btn4.setText(_translate("utilitiesStackedWidget", "4")) - self.btn4.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.btn4.setProperty("button_type", _translate("utilitiesStackedWidget", "normal")) - self.label.setText(_translate("utilitiesStackedWidget", "Times:")) - self.label_3.setText(_translate("utilitiesStackedWidget", "Insert Best Text Here")) - self.am_zv.setText(_translate("utilitiesStackedWidget", "ZV")) - self.am_zv.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.am_ei.setText(_translate("utilitiesStackedWidget", "EI")) - self.am_ei.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.am_mzv.setText(_translate("utilitiesStackedWidget", "MZV")) - self.am_mzv.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.am_3hump_ei.setText(_translate("utilitiesStackedWidget", "3hump_ei")) - self.am_3hump_ei.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.am_user_input.setText(_translate("utilitiesStackedWidget", "user input")) - self.am_user_input.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.am_2hump_ei.setText(_translate("utilitiesStackedWidget", "2hump_ei")) - self.am_2hump_ei.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.am_confirm.setText(_translate("utilitiesStackedWidget", "Confirm")) - self.am_confirm.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) + self.label_3.setText(_translate("utilitiesStackedWidget", "Input Shaper ")) + self.am_mzv_2.setText(_translate("utilitiesStackedWidget", "MZV")) + self.am_mzv_2.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) + self.am_ei_2.setText(_translate("utilitiesStackedWidget", "EI")) + self.am_ei_2.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) + self.am_3hump_ei_2.setText(_translate("utilitiesStackedWidget", "3hump_ei")) + self.am_3hump_ei_2.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) + self.am_zv_2.setText(_translate("utilitiesStackedWidget", "ZV")) + self.am_zv_2.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) + self.am_2hump_ei_2.setText(_translate("utilitiesStackedWidget", "2hump_ei")) + self.am_2hump_ei_2.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) self.am_cancel.setText(_translate("utilitiesStackedWidget", "Cancel")) self.am_cancel.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) + self.am_confirm.setText(_translate("utilitiesStackedWidget", "Confirm")) + self.am_confirm.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) self.label_8.setText(_translate("utilitiesStackedWidget", "User Input")) - self.is_confirm_btn.setText(_translate("utilitiesStackedWidget", "Confirm")) - self.is_confirm_btn.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.isui_sm.setText(_translate("utilitiesStackedWidget", "Smoothing")) - self.isui_sm.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) - self.isui_sm.setProperty("button_type", _translate("utilitiesStackedWidget", "display_secondary")) - self.isui_fq.setText(_translate("utilitiesStackedWidget", "Frequency")) - self.isui_fq.setProperty("button_type", _translate("utilitiesStackedWidget", "display_secondary")) - self.is_back_btn.setText(_translate("utilitiesStackedWidget", "Back")) - self.is_back_btn.setProperty("class", _translate("utilitiesStackedWidget", "menu_btn")) self.toggle_led_button.setText(_translate("utilitiesStackedWidget", "PushButton")) self.label_4.setText(_translate("utilitiesStackedWidget", "On")) self.label_5.setText(_translate("utilitiesStackedWidget", "Off")) From 5339d514b92533699271693c1eaefbb6defaf31a Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Mon, 12 Jan 2026 17:43:56 +0000 Subject: [PATCH 25/70] Work connnectivity update page (#139) * ADD: added update page to connection page * ADD: added reload button to updatepage Refactor : evertime it reload shows loadwidget Refactor: info frame layout * Refactor: when update page gets refreshed * Added: icon_button pressed state Refactor: resized icon buttons on connectionwindow * Rev: removed updatepage instances * Fix: fixed loadwidget not hiding * add: added update page instance * Refactor: ran ruff formatter * refactor: removed unused import * Refactor: ran ruff formatter --------- Co-authored-by: Roberto --- BlocksScreen/lib/moonrakerComm.py | 15 +- BlocksScreen/lib/panels/mainWindow.py | 43 +- BlocksScreen/lib/panels/utilitiesTab.py | 39 +- .../lib/panels/widgets/connectionPage.py | 61 +- BlocksScreen/lib/panels/widgets/updatePage.py | 88 +- BlocksScreen/lib/ui/connectionWindow.ui | 1145 +++++++++-------- BlocksScreen/lib/ui/connectionWindow_ui.py | 268 ++-- BlocksScreen/lib/utils/icon_button.py | 78 +- 8 files changed, 952 insertions(+), 785 deletions(-) diff --git a/BlocksScreen/lib/moonrakerComm.py b/BlocksScreen/lib/moonrakerComm.py index 78fba08e..ba298ba7 100644 --- a/BlocksScreen/lib/moonrakerComm.py +++ b/BlocksScreen/lib/moonrakerComm.py @@ -734,13 +734,16 @@ def update_status(self, refresh: bool = False) -> bool: @QtCore.pyqtSlot(name="update-refresh") @QtCore.pyqtSlot(str, name="update-refresh") - def refresh_update_status(self, name: str = "") -> bool: + def refresh_update_status(self, name: str = None) -> bool: """Refresh packages state""" - if not isinstance(name, str) or not name: - return False - return self._ws.send_request( - method="machine.update.refresh", params={"name": name} - ) + if isinstance(name, str): + return self._ws.send_request( + method="machine.update.refresh", params={"name": name} + ) + else: + return self._ws.send_request( + method="machine.update.refresh", + ) @QtCore.pyqtSlot(name="update-full") def full_update(self) -> bool: diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 18549a39..3c65bd7b 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -16,6 +16,7 @@ from lib.panels.widgets.popupDialogWidget import Popup from lib.printer import Printer from lib.ui.mainWindow_ui import Ui_MainWindow # With header +from lib.panels.widgets.updatePage import UpdatePage # from lib.ui.mainWindow_v2_ui import Ui_MainWindow # No header from lib.ui.resources.background_resources_rc import * @@ -58,6 +59,7 @@ class MainWindow(QtWidgets.QMainWindow): gcode_response = QtCore.pyqtSignal(list, name="gcode_response") handle_error_response = QtCore.pyqtSignal(list, name="handle_error_response") call_network_panel = QtCore.pyqtSignal(name="call-network-panel") + call_update_panel = QtCore.pyqtSignal(name="call-update-panel") on_update_message: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( dict, name="on-update-message" ) @@ -77,6 +79,8 @@ def __init__(self): self.index_stack = deque(maxlen=4) self.printer = Printer(self, self.ws) self.conn_window = ConnectionPage(self, self.ws) + self.up = UpdatePage(self) + self.up.hide() self.installEventFilter(self.conn_window) self.printPanel = PrintTab( self.ui.printTab, self.file_data, self.ws, self.printer @@ -152,7 +156,23 @@ def __init__(self): self.controlPanel.probe_helper_page.handle_error_response ) self.controlPanel.disable_popups.connect(self.popup_toggle) - self.on_update_message.connect(self.utilitiesPanel.on_update_message) + self.on_update_message.connect(self.up.handle_update_message) + self.up.request_full_update.connect(self.ws.api.full_update) + self.up.request_recover_repo[str].connect(self.ws.api.recover_corrupt_repo) + self.up.request_recover_repo[str, bool].connect( + self.ws.api.recover_corrupt_repo + ) + self.up.request_refresh_update.connect(self.ws.api.refresh_update_status) + self.up.request_refresh_update[str].connect(self.ws.api.refresh_update_status) + self.up.request_rollback_update.connect(self.ws.api.rollback_update) + self.up.request_update_client.connect(self.ws.api.update_client) + self.up.request_update_klipper.connect(self.ws.api.update_klipper) + self.up.request_update_moonraker.connect(self.ws.api.update_moonraker) + self.up.request_update_status.connect(self.ws.api.update_status) + self.up.request_update_system.connect(self.ws.api.update_system) + self.up.update_back_btn.clicked.connect(self.up.hide) + self.utilitiesPanel.show_update_page.connect(self.show_update_page) + self.conn_window.update_button_clicked.connect(self.show_update_page) self.ui.extruder_temp_display.display_format = "upper_downer" self.ui.bed_temp_display.display_format = "upper_downer" if self.config.has_section("server"): @@ -160,6 +180,27 @@ def __init__(self): self.bo_ws_startup.emit() self.reset_tab_indexes() + @QtCore.pyqtSlot(bool, name="show-update-page") + def show_update_page(self, fullscreen: bool): + """Slot for displaying update Panel""" + if not fullscreen: + self.up.setParent(self.ui.main_content_widget) + current_index = self.ui.main_content_widget.currentIndex() + tab_rect = self.ui.main_content_widget.tabBar().tabRect(current_index) + width = tab_rect.width() + _parent_size = self.up.parent().size() + self.up.setGeometry( + width, 0, _parent_size.width() - width, _parent_size.height() + ) + else: + self.up.setParent(self) + self.up.setGeometry(0, 0, self.width(), self.height()) + + self.up.raise_() + self.up.updateGeometry() + self.up.repaint() + self.up.show() + @QtCore.pyqtSlot(name="on-cancel-print") def on_cancel_print(self): """Slot for cancel print signal""" diff --git a/BlocksScreen/lib/panels/utilitiesTab.py b/BlocksScreen/lib/panels/utilitiesTab.py index 1c9d0fa2..a9b6652f 100644 --- a/BlocksScreen/lib/panels/utilitiesTab.py +++ b/BlocksScreen/lib/panels/utilitiesTab.py @@ -5,7 +5,6 @@ from lib.moonrakerComm import MoonWebSocket from lib.panels.widgets.troubleshootPage import TroubleshootPage -from lib.panels.widgets.updatePage import UpdatePage from lib.printer import Printer from lib.ui.utilitiesStackedWidget_ui import Ui_utilitiesStackedWidget from lib.utils.blocks_button import BlocksCustomButton @@ -87,6 +86,10 @@ class UtilitiesTab(QtWidgets.QStackedWidget): bool, name="update-available" ) + show_update_page: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + bool, name="show-update-page" + ) + def __init__( self, parent: QtWidgets.QWidget, ws: MoonWebSocket, printer: Printer ) -> None: @@ -126,8 +129,9 @@ def __init__( ) self.loadPage.add_widget(self.loadwidget) - self.update_page = UpdatePage(self) - self.addWidget(self.update_page) + self.panel.update_btn.clicked.connect( + lambda: self.show_update_page[bool].emit(False) + ) self.is_page = InputShaperPage(self) self.addWidget(self.is_page) @@ -142,14 +146,12 @@ def __init__( self.panel.leds_slider_back_btn, self.panel.input_shaper_back_btn, self.panel.routine_check_back_btn, - self.update_page.update_back_btn, self.is_page.update_back_btn, ): button.clicked.connect(self.back_button) # --- Page Navigation --- self._connect_page_change(self.panel.utilities_axes_btn, self.panel.axes_page) - self._connect_page_change(self.panel.update_btn, self.update_page) self._connect_page_change( self.panel.utilities_input_shaper_btn, self.panel.input_shaper_page ) @@ -202,33 +204,6 @@ def __init__( self.printer.printer_config.connect(self.on_printer_config_received) self.printer.gcode_move_update.connect(self.on_gcode_move_update) - # ---- Websocket connections ---- - - self.on_update_message.connect(self.update_page.handle_update_message) - self.update_page.request_full_update.connect(self.ws.api.full_update) - self.update_page.request_recover_repo[str].connect( - self.ws.api.recover_corrupt_repo - ) - self.update_page.request_recover_repo[str, bool].connect( - self.ws.api.recover_corrupt_repo - ) - self.update_page.request_refresh_update.connect( - self.ws.api.refresh_update_status - ) - self.update_page.request_refresh_update[str].connect( - self.ws.api.refresh_update_status - ) - self.printer.gcode_response.connect(self.handle_gcode_response) - self.update_page.request_rollback_update.connect(self.ws.api.rollback_update) - self.update_page.request_update_client.connect(self.ws.api.update_client) - self.update_page.request_update_klipper.connect(self.ws.api.update_klipper) - self.update_page.request_update_moonraker.connect(self.ws.api.update_moonraker) - self.update_page.request_update_status.connect(self.ws.api.update_status) - self.update_page.request_update_system.connect(self.ws.api.update_system) - self.update_page.update_available.connect(self.update_available.emit) - self.update_page.update_available.connect( - self.panel.update_btn.setShowNotification - ) self.panel.update_btn.setPixmap( QtGui.QPixmap(":/system/media/btn_icons/update-software-icon.svg") ) diff --git a/BlocksScreen/lib/panels/widgets/connectionPage.py b/BlocksScreen/lib/panels/widgets/connectionPage.py index 0b7e054d..1e9c32d1 100644 --- a/BlocksScreen/lib/panels/widgets/connectionPage.py +++ b/BlocksScreen/lib/panels/widgets/connectionPage.py @@ -13,11 +13,18 @@ class ConnectionPage(QtWidgets.QFrame): reboot_clicked = QtCore.pyqtSignal(name="reboot_clicked") restart_klipper_clicked = QtCore.pyqtSignal(name="restart_klipper_clicked") firmware_restart_clicked = QtCore.pyqtSignal(name="firmware_restart_clicked") + update_button_clicked = QtCore.pyqtSignal(bool, name="show-update-page") def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket, /): super().__init__(parent) + self.setMinimumSize(QtCore.QSize(800, 480)) self.panel = Ui_ConnectivityForm() self.panel.setupUi(self) + + self.panel.updatepageButton.clicked.connect( + lambda: self.update_button_clicked[bool].emit(True) + ) + self.ws = ws self._moonraker_status: str = "disconnected" self._klippy_state: str = "closed" @@ -55,9 +62,16 @@ def show_panel(self, reason: str | None = None): self.text_update() return False + def showEvent(self, a0: QtCore.QEvent | None): + """Handle show event""" + self.ws.api.refresh_update_status() + return super().showEvent(a0) + @QtCore.pyqtSlot(bool, name="on_klippy_connected") def on_klippy_connection(self, connected: bool): """Handle klippy connection state""" + self.dot_timer.stop() + self._klippy_connection = connected if not connected: self.panel.connectionTextBox.setText("Klipper Disconnected") @@ -69,6 +83,7 @@ def on_klippy_connection(self, connected: bool): @QtCore.pyqtSlot(str, name="on_klippy_state") def on_klippy_state(self, state: str): """Handle klippy state changes""" + self.dot_timer.stop() if state == "error": self.panel.connectionTextBox.setText("Klipper Connection Error") if not self.isVisible(): @@ -87,7 +102,6 @@ def on_klippy_state(self, state: str): self.panel.connectionTextBox.setText("Klipper Startup") elif state == "ready": self.panel.connectionTextBox.setText("Klipper Ready") - self.hide() @QtCore.pyqtSlot(int, name="on_websocket_connecting") @QtCore.pyqtSlot(str, name="on_websocket_connecting") @@ -98,21 +112,22 @@ def on_websocket_connecting(self, attempt: int): @QtCore.pyqtSlot(name="on_websocket_connection_achieved") def on_websocket_connection_achieved(self): """Handle websocket connected state""" + self.dot_timer.stop() self.panel.connectionTextBox.setText("Moonraker Connected\n Klippy not ready") - self.hide() - @QtCore.pyqtSlot(name="on_websocket_connection_lost") + @QtCore.pyqtSlot(name="on_websocket_connection_lzost") def on_websocket_connection_lost(self): """Handle websocket connection lost state""" if not self.isVisible(): self.show() + self.dot_timer.stop() self.text_update(text="Websocket lost") def text_update(self, text: int | str | None = None): """Update widget text""" if self.state == "shutdown" and self.message is not None: return False - + self.dot_timer.stop() logging.debug(f"[ConnectionWindowPanel] text_update: {text}") if text == "wb lost": self.panel.connectionTextBox.setText("Moonraker connection lost") @@ -125,41 +140,34 @@ def text_update(self, text: int | str | None = None): return True if isinstance(text, str): self.panel.connectionTextBox.setText( - f""" - Connection to Moonraker unavailable\nTry again by reconnecting or \nrestarting klipper\n{text} - """ + f"""Connection to Moonraker unavailable\nTry again by reconnecting or \nrestarting klipper\n{text}""" ) return True if isinstance(text, int): # * Websocket connection messages + + self.base_text = f"Attempting to reconnect to Moonraker.\n\nConnection try number: {text}" + if text == 0: - self.dot_timer.stop() self.panel.connectionTextBox.setText( - "Unable to Connect to Moonraker.\n\nTry again" + "Connection to Moonraker timeout \n \n please retry" ) - return False - - if text == 1: - if self.dot_timer.isActive(): - self.dot_timer.stop() - return - self.dot_timer.start() + return + self.dot_count = 0 - self.text2 = f"Attempting to reconnect to Moonraker.\n\nConnection try number: {text}" + self.dot_timer.start() + self._add_dot() return False def _add_dot(self): - if self.state == "shutdown" and self.message is not None: + """Add one dot per second (max 3).""" + self.dot_count += 1 + if self.dot_count > 3: self.dot_timer.stop() - return False - - if self.dot_count > 2: - self.dot_count = 1 - else: - self.dot_count += 1 + return dots = "." * self.dot_count + " " * (3 - self.dot_count) - self.panel.connectionTextBox.setText(f"{self.text2}{dots}") + self.panel.connectionTextBox.setText(f"{self.base_text}{dots}") @QtCore.pyqtSlot(str, str, name="webhooks_update") def webhook_update(self, state: str, message: str): @@ -171,16 +179,19 @@ def webhook_update(self, state: str, message: str): def eventFilter(self, object: QtCore.QObject, event: QtCore.QEvent) -> bool: """Re-implemented method, filter events""" if event.type() == KlippyDisconnected.type(): + self.dot_timer.stop() if not self.isVisible(): self.panel.connectionTextBox.setText("Klippy Disconnected") self.show() elif event.type() == KlippyReady.type(): + self.dot_timer.stop() self.panel.connectionTextBox.setText("Klippy Ready") self.hide() return False elif event.type() == KlippyShutdown.type(): + self.dot_timer.stop() if not self.isVisible(): self.panel.connectionTextBox.setText(f"{self.message}") self.show() diff --git a/BlocksScreen/lib/panels/widgets/updatePage.py b/BlocksScreen/lib/panels/widgets/updatePage.py index 5ec9cebd..4857f92e 100644 --- a/BlocksScreen/lib/panels/widgets/updatePage.py +++ b/BlocksScreen/lib/panels/widgets/updatePage.py @@ -82,6 +82,9 @@ def __init__(self, parent=None) -> None: self.repeated_request_status.timeout.connect( lambda: self.request_update_status.emit(False) ) + self.reload_btn.clicked.connect(self.on_request_reload) + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_StyledBackground, True) + self.show_loading(True) def handle_update_end(self) -> None: """Handles update end signal @@ -90,7 +93,7 @@ def handle_update_end(self) -> None: if self.load_popup.isVisible(): self.load_popup.close() self.repeated_request_status.stop() - self.request_refresh_update.emit() + self.on_request_reload() self.build_model_list() def handle_ongoing_update(self) -> None: @@ -101,6 +104,14 @@ def handle_ongoing_update(self) -> None: self.load_popup.show() self.repeated_request_status.start(2000) + def on_request_reload(self, service: str | None = None) -> None: + """Handles reload button click, requests update status refresh""" + self.show_loading(True) + if service: + self.request_refresh_update.emit([service]) + else: + self.request_refresh_update.emit() + def reset_view_model(self) -> None: """Clears items from ListView (Resets `QAbstractListModel` by clearing entries) @@ -116,6 +127,7 @@ def deleteLater(self) -> None: def showEvent(self, event: QtGui.QShowEvent | None) -> None: """Re-add clients to update list""" self.build_model_list() + return super().showEvent(event) def build_model_list(self) -> None: @@ -169,13 +181,15 @@ def on_item_clicked(self, item: ListItem) -> None: """ if not item: return + self.show_loading(False) cli_data = self.cli_tracking.get(item.text, {}) if not cli_data: self.version_tracking_info.setText("Missing, Cannot Update") self.selected_item = copy.copy(item) if item.text == "system": - self.remote_version_title.hide() - self.remote_version_tracking.hide() + self.no_update_placeholder.hide() + self.remote_version_title.setText("") + self.remote_version_tracking.setText("") updatable_packages = cli_data.get("package_count", 0) if updatable_packages == 0: self.version_title.hide() @@ -194,6 +208,7 @@ def on_item_clicked(self, item: ListItem) -> None: self.remote_version_tracking.hide() self.remote_version_title.show() self.remote_version_tracking.show() + self.remote_version_title.setText("Remote Version: ") self.remote_version_tracking.setText(_remote_version) _curr_version = cli_data.get("version", None) if not _curr_version: @@ -228,6 +243,17 @@ def on_item_clicked(self, item: ListItem) -> None: self.no_update_placeholder.hide() self.action_btn.show() + def show_loading(self, loading: bool = False) -> None: + """Show or hide loading overlay""" + self.loadwidget2.setVisible(loading) + self.update_buttons_list_widget.setVisible(not loading) + self.remote_version_title.setVisible(not loading) + self.remote_version_tracking.setVisible(not loading) + self.version_tracking_info.setVisible(not loading) + self.version_title.setVisible(not loading) + self.action_btn.setVisible(not loading) + self.no_update_placeholder.setVisible(not loading) + @QtCore.pyqtSlot(dict, name="handle-update-message") def handle_update_message(self, message: dict) -> None: """Handle receiving current state of each item update. @@ -248,6 +274,7 @@ def handle_update_message(self, message: dict) -> None: if not cli_version_info: return self.cli_tracking = cli_version_info + self.build_model_list() # Signal that updates exist (Used to render red dots) _update_avail = any( value @@ -285,14 +312,27 @@ def _setupUI(self) -> None: sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(1) self.setSizePolicy(sizePolicy) - self.setMinimumSize(QtCore.QSize(710, 400)) - self.setMaximumSize(QtCore.QSize(720, 420)) + self.setObjectName("updatePage") + self.setStyleSheet( + """#updatePage { + background-image: url(:/background/media/1st_background.png); + }""" + ) self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self.update_page_content_layout = QtWidgets.QVBoxLayout() - self.update_page_content_layout.setContentsMargins(15, 15, 2, 2) + self.update_page_content_layout.setContentsMargins(15, 15, 15, 15) self.header_content_layout = QtWidgets.QHBoxLayout() self.header_content_layout.setAlignment(QtCore.Qt.AlignmentFlag.AlignTop) + self.reload_btn = IconButton(self) + self.reload_btn.setMinimumSize(QtCore.QSize(60, 60)) + self.reload_btn.setMaximumSize(QtCore.QSize(60, 60)) + self.reload_btn.setFlat(True) + self.reload_btn.setPixmap(QtGui.QPixmap(":/ui/media/btn_icons/refresh.svg")) + self.header_content_layout.addWidget( + self.reload_btn + ) # alignment=QtCore.Qt.AlignmentFlag.AlignCenter) + self.header_title = QtWidgets.QLabel(self) self.header_title.setMinimumSize(QtCore.QSize(100, 60)) self.header_title.setMaximumSize(QtCore.QSize(16777215, 60)) @@ -304,16 +344,24 @@ def _setupUI(self) -> None: self.header_title.setFont(font) self.header_title.setPalette(palette) self.header_title.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.header_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.header_title.setObjectName("header-title") self.header_title.setText("Update Manager") - self.header_content_layout.addWidget(self.header_title, 0) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.header_title.setSizePolicy(sizePolicy) + self.header_content_layout.addWidget( + self.header_title, alignment=QtCore.Qt.AlignmentFlag.AlignCenter + ) self.update_back_btn = IconButton(self) self.update_back_btn.setMinimumSize(QtCore.QSize(60, 60)) self.update_back_btn.setMaximumSize(QtCore.QSize(60, 60)) self.update_back_btn.setFlat(True) self.update_back_btn.setPixmap(QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) - self.header_content_layout.addWidget(self.update_back_btn, 0) + self.header_content_layout.addWidget( + self.update_back_btn + ) # alignment=QtCore.Qt.AlignmentFlag.AlignCenter) self.update_page_content_layout.addLayout(self.header_content_layout, 0) self.main_content_layout = QtWidgets.QHBoxLayout() @@ -476,17 +524,22 @@ def _setupUI(self) -> None: QtWidgets.QScroller.ScrollerGestureType.LeftMouseButtonGesture, ) self.update_buttons_layout = QtWidgets.QVBoxLayout() - self.update_buttons_layout.setContentsMargins(15, 20, 20, 5) + self.update_buttons_layout.setContentsMargins(10, 10, 10, 10) self.update_buttons_layout.addWidget(self.update_buttons_list_widget, 0) + self.update_buttons_list_widget.hide() + self.loadwidget2 = LoadingOverlayWidget( + self, LoadingOverlayWidget.AnimationGIF.DEFAULT + ) + self.loadwidget2.setMinimumSize(self.update_buttons_frame.size()) + self.update_buttons_layout.addWidget(self.loadwidget2, 1) self.update_buttons_frame.setLayout(self.update_buttons_layout) self.main_content_layout.addWidget(self.update_buttons_frame, 0) self.infobox_frame = BlocksCustomFrame() - self.infobox_frame.setMinimumSize(QtCore.QSize(250, 300)) - self.info_box_layout = QtWidgets.QVBoxLayout() - self.info_box_layout.setContentsMargins(10, 0, 10, 0) + self.info_box_layout.setContentsMargins(10, 10, 10, 10) + self.infobox_frame.setLayout(self.info_box_layout) font = QtGui.QFont() font.setFamily(font_family) @@ -560,7 +613,7 @@ def _setupUI(self) -> None: self.action_btn = BlocksCustomButton() self.action_btn.setMinimumSize(QtCore.QSize(200, 60)) - self.action_btn.setMaximumSize(QtCore.QSize(250, 60)) + self.action_btn.setMaximumSize(QtCore.QSize(300, 60)) font.setPointSize(20) self.action_btn.setFont(font) self.action_btn.setPalette(palette) @@ -570,7 +623,7 @@ def _setupUI(self) -> None: QtGui.QPixmap(":/system/media/btn_icons/update-software-icon.svg") ) self.button_box.addWidget( - self.action_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + self.action_btn, 0, QtCore.Qt.AlignmentFlag.AlignCenter ) self.no_update_placeholder = QtWidgets.QLabel(self) self.no_update_placeholder.setMinimumSize(QtCore.QSize(200, 60)) @@ -583,16 +636,13 @@ def _setupUI(self) -> None: self.no_update_placeholder.setWordWrap(True) self.no_update_placeholder.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.info_box_layout.addWidget( - self.no_update_placeholder, 0, QtCore.Qt.AlignmentFlag.AlignBottom + self.no_update_placeholder, 0, QtCore.Qt.AlignmentFlag.AlignCenter ) - self.no_update_placeholder.hide() - self.info_box_layout.addLayout( self.button_box, 0, ) - self.infobox_frame.setLayout(self.info_box_layout) self.main_content_layout.addWidget(self.infobox_frame, 1) self.update_page_content_layout.addLayout(self.main_content_layout, 1) self.setLayout(self.update_page_content_layout) diff --git a/BlocksScreen/lib/ui/connectionWindow.ui b/BlocksScreen/lib/ui/connectionWindow.ui index 84558abd..f2bd4899 100644 --- a/BlocksScreen/lib/ui/connectionWindow.ui +++ b/BlocksScreen/lib/ui/connectionWindow.ui @@ -42,12 +42,8 @@ #ConnectivityForm{ - background-image: url(:/background/media/1st_background.png); -} - - - - +background-image: url(:/background/media/1st_background.png); +} @@ -115,551 +111,613 @@ 0 - - - - 623 - 10 - 154 - 80 - - - - - 0 - 0 - - - - - 154 - 80 - - - - - 154 - 80 - - - - - 80 - 80 - - - - - - - - - - - - 13 - - - - true - - - Qt::ClickFocus - - - false - - - - - - Retry - - - - :/system_icons/media/btn_icons/retry_connection.svg:/system_icons/media/btn_icons/retry_connection.svg - - - - 16 - 16 - - - - false - - + + 0 - + 0 - - false - - - false - - - true - - - bottom - - - :/system/media/btn_icons/restart_printer.svg - - - - 255 - 255 - 255 - - - - true - - - - - - 475 - 11 - 154 - 80 - - - - - 0 - 0 - - - - - 154 - 80 - - - - - 154 - 80 - - - - - 80 - 80 - - - - true - - - Qt::NoFocus - - - Qt::NoContextMenu - - - false - - - Wifi Settings - - - - :/system_icons/media/btn_icons/retry_connection.svg:/system_icons/media/btn_icons/retry_connection.svg - - - false - - - false - - - true - - - system_control_btn - - - :/network/media/btn_icons/wifi_config.svg - - - true - - - bottom - - - - - - 315 - 10 - 154 - 80 - - - - - 0 - 0 - - - - - 154 - 80 - - - - - 154 - 80 - - - - - 160 - 80 - - - - BlankCursor - - - true - - - Qt::NoFocus - - - Qt::NoContextMenu - - - false - - - Firmware Restart - - - - :/system_icons/media/btn_icons/firmware_restart.svg:/system_icons/media/btn_icons/firmware_restart.svg - - - false - - - false - - - true + + 5 - - :/system/media/btn_icons/restart_firmware.svg - - - true - - - bottom - - - - 255 - 255 - 255 - - - - - - - 157 - 10 - 154 - 80 - - - - - 0 - 0 - - - - - 154 - 80 - - - - - 154 - 80 - - - - - 80 - 80 - - - - BlankCursor - - - true - - - Qt::NoFocus - - - Qt::NoContextMenu - - - false - - - Reboot - - - - :/system_icons/media/btn_icons/firmware_restart.svg:/system_icons/media/btn_icons/firmware_restart.svg - - - false - - - false - - - true - - - :/system/media/btn_icons/reboot.svg - - - bottom - - - - 255 - 255 - 255 - - - - true - - - - - - 4 - 10 - 154 - 80 - - - - - 0 - 0 - - - - - 154 - 80 - - - - - 154 - 80 - - - - - 160 - 80 - - - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - - - false - PreferAntialias - false - - - - BlankCursor - - - true - - - Qt::NoFocus - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - false - - - - - - Restart Klipper - - - - :/system_icons/media/btn_icons/restart_klipper.svg - - - - - 46 - 42 - - - - false - - - false - - - false - - + 0 - + 0 - - false - - - true - - - :/system/media/btn_icons/restart_klipper.svg - - - true - - - bottom - - - - 255 - 255 - 255 - - - + + + + + 0 + 0 + + + + + 100 + 80 + + + + + 100 + 80 + + + + + 160 + 80 + + + + + + + + + 66 + 66 + 66 + + + + + + + 66 + 66 + 66 + + + + + + + 66 + 66 + 66 + + + + + + + + + 66 + 66 + 66 + + + + + + + 66 + 66 + 66 + + + + + + + 66 + 66 + 66 + + + + + + + + + 66 + 66 + 66 + + + + + + + 66 + 66 + 66 + + + + + + + 66 + 66 + 66 + + + + + + + + + false + PreferAntialias + false + + + + BlankCursor + + + true + + + Qt::NoFocus + + + Qt::NoContextMenu + + + Qt::LeftToRight + + + false + + + + + + Restart Klipper + + + + :/system_icons/media/btn_icons/restart_klipper.svg + + + + + 46 + 42 + + + + false + + + false + + + false + + + 0 + + + 0 + + + false + + + true + + + :/system/media/btn_icons/restart_klipper.svg + + + true + + + bottom + + + + 255 + 255 + 255 + + + + + + + + + 0 + 0 + + + + + 100 + 80 + + + + + 100 + 80 + + + + + 80 + 80 + + + + BlankCursor + + + true + + + Qt::NoFocus + + + Qt::NoContextMenu + + + false + + + Reboot + + + + :/system_icons/media/btn_icons/firmware_restart.svg:/system_icons/media/btn_icons/firmware_restart.svg + + + false + + + false + + + true + + + :/system/media/btn_icons/reboot.svg + + + bottom + + + + 255 + 255 + 255 + + + + true + + + + + + + + 0 + 0 + + + + + 100 + 80 + + + + + 100 + 80 + + + + + 160 + 80 + + + + BlankCursor + + + true + + + Qt::NoFocus + + + Qt::NoContextMenu + + + false + + + Firmware Restart + + + + :/system_icons/media/btn_icons/firmware_restart.svg:/system_icons/media/btn_icons/firmware_restart.svg + + + false + + + false + + + true + + + :/system/media/btn_icons/restart_firmware.svg + + + true + + + bottom + + + + 255 + 255 + 255 + + + + + + + + + 0 + 0 + + + + + 100 + 80 + + + + + 100 + 80 + + + + + 80 + 80 + + + + + + + + + + + + 13 + + + + true + + + Qt::ClickFocus + + + false + + + + + + Retry + + + + :/system_icons/media/btn_icons/retry_connection.svg:/system_icons/media/btn_icons/retry_connection.svg + + + + 16 + 16 + + + + false + + + 0 + + + 0 + + + false + + + false + + + true + + + bottom + + + :/system/media/btn_icons/restart_printer.svg + + + + 255 + 255 + 255 + + + + true + + + + + + + + 0 + 0 + + + + + 100 + 80 + + + + + 100 + 80 + + + + + 80 + 80 + + + + BlankCursor + + + true + + + Qt::NoFocus + + + Qt::NoContextMenu + + + false + + + Update page + + + + :/system_icons/media/btn_icons/firmware_restart.svg:/system_icons/media/btn_icons/firmware_restart.svg + + + false + + + false + + + true + + + :/system/media/btn_icons/update-software-icon.svg + + + bottom + + + + 255 + 255 + 255 + + + + true + + + + + + + + 0 + 0 + + + + + 100 + 80 + + + + + 100 + 80 + + + + + 80 + 80 + + + + true + + + Qt::NoFocus + + + Qt::NoContextMenu + + + false + + + Wifi Settings + + + + :/system_icons/media/btn_icons/retry_connection.svg:/system_icons/media/btn_icons/retry_connection.svg + + + false + + + false + + + true + + + system_control_btn + + + :/network/media/btn_icons/wifi_config.svg + + + true + + + bottom + + + + @@ -691,6 +749,9 @@ false + + + QFrame::NoFrame diff --git a/BlocksScreen/lib/ui/connectionWindow_ui.py b/BlocksScreen/lib/ui/connectionWindow_ui.py index 58c5945b..772dc227 100644 --- a/BlocksScreen/lib/ui/connectionWindow_ui.py +++ b/BlocksScreen/lib/ui/connectionWindow_ui.py @@ -1,4 +1,4 @@ -# Form implementation generated from reading ui file 'main/BlocksScreen/BlocksScreen/lib/ui/connectionWindow.ui' +# Form implementation generated from reading ui file '/home/levi/BlocksScreen/BlocksScreen/lib/ui/connectionWindow.ui' # # Created by: PyQt6 UI code generator 6.7.1 # @@ -24,12 +24,8 @@ def setupUi(self, ConnectivityForm): ConnectivityForm.setWindowOpacity(1.0) ConnectivityForm.setAutoFillBackground(False) ConnectivityForm.setStyleSheet("#ConnectivityForm{\n" -" background-image: url(:/background/media/1st_background.png);\n" -"}\n" -"\n" -"\n" -"\n" -"") +"background-image: url(:/background/media/1st_background.png);\n" +"}") ConnectivityForm.setProperty("class", "") self.cw_buttonFrame = BlocksCustomFrame(parent=ConnectivityForm) self.cw_buttonFrame.setGeometry(QtCore.QRect(10, 380, 780, 124)) @@ -53,117 +49,18 @@ def setupUi(self, ConnectivityForm): self.cw_buttonFrame.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) self.cw_buttonFrame.setLineWidth(0) self.cw_buttonFrame.setObjectName("cw_buttonFrame") - self.RetryConnectionButton = IconButton(parent=self.cw_buttonFrame) - self.RetryConnectionButton.setGeometry(QtCore.QRect(623, 10, 154, 80)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.RetryConnectionButton.sizePolicy().hasHeightForWidth()) - self.RetryConnectionButton.setSizePolicy(sizePolicy) - self.RetryConnectionButton.setMinimumSize(QtCore.QSize(154, 80)) - self.RetryConnectionButton.setMaximumSize(QtCore.QSize(154, 80)) - self.RetryConnectionButton.setBaseSize(QtCore.QSize(80, 80)) - palette = QtGui.QPalette() - self.RetryConnectionButton.setPalette(palette) - font = QtGui.QFont() - font.setPointSize(13) - self.RetryConnectionButton.setFont(font) - self.RetryConnectionButton.setTabletTracking(True) - self.RetryConnectionButton.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) - self.RetryConnectionButton.setAutoFillBackground(False) - self.RetryConnectionButton.setStyleSheet("") - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/retry_connection.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) - self.RetryConnectionButton.setIcon(icon) - self.RetryConnectionButton.setIconSize(QtCore.QSize(16, 16)) - self.RetryConnectionButton.setCheckable(False) - self.RetryConnectionButton.setAutoRepeatDelay(0) - self.RetryConnectionButton.setAutoRepeatInterval(0) - self.RetryConnectionButton.setAutoDefault(False) - self.RetryConnectionButton.setDefault(False) - self.RetryConnectionButton.setFlat(True) - self.RetryConnectionButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/restart_printer.svg")) - self.RetryConnectionButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) - self.RetryConnectionButton.setProperty("has_text", True) - self.RetryConnectionButton.setObjectName("RetryConnectionButton") - self.wifi_button = IconButton(parent=self.cw_buttonFrame) - self.wifi_button.setGeometry(QtCore.QRect(475, 11, 154, 80)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.wifi_button.sizePolicy().hasHeightForWidth()) - self.wifi_button.setSizePolicy(sizePolicy) - self.wifi_button.setMinimumSize(QtCore.QSize(154, 80)) - self.wifi_button.setMaximumSize(QtCore.QSize(154, 80)) - self.wifi_button.setBaseSize(QtCore.QSize(80, 80)) - self.wifi_button.setTabletTracking(True) - self.wifi_button.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.wifi_button.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.wifi_button.setAutoFillBackground(False) - self.wifi_button.setIcon(icon) - self.wifi_button.setAutoDefault(False) - self.wifi_button.setDefault(False) - self.wifi_button.setFlat(True) - self.wifi_button.setProperty("icon_pixmap", QtGui.QPixmap(":/network/media/btn_icons/wifi_config.svg")) - self.wifi_button.setProperty("has_text", True) - self.wifi_button.setObjectName("wifi_button") - self.FirmwareRestartButton = IconButton(parent=self.cw_buttonFrame) - self.FirmwareRestartButton.setGeometry(QtCore.QRect(315, 10, 154, 80)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.FirmwareRestartButton.sizePolicy().hasHeightForWidth()) - self.FirmwareRestartButton.setSizePolicy(sizePolicy) - self.FirmwareRestartButton.setMinimumSize(QtCore.QSize(154, 80)) - self.FirmwareRestartButton.setMaximumSize(QtCore.QSize(154, 80)) - self.FirmwareRestartButton.setBaseSize(QtCore.QSize(160, 80)) - self.FirmwareRestartButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) - self.FirmwareRestartButton.setTabletTracking(True) - self.FirmwareRestartButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.FirmwareRestartButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.FirmwareRestartButton.setAutoFillBackground(False) - icon1 = QtGui.QIcon() - icon1.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/firmware_restart.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) - self.FirmwareRestartButton.setIcon(icon1) - self.FirmwareRestartButton.setAutoDefault(False) - self.FirmwareRestartButton.setDefault(False) - self.FirmwareRestartButton.setFlat(True) - self.FirmwareRestartButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/restart_firmware.svg")) - self.FirmwareRestartButton.setProperty("has_text", True) - self.FirmwareRestartButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) - self.FirmwareRestartButton.setObjectName("FirmwareRestartButton") - self.RebootSystemButton = IconButton(parent=self.cw_buttonFrame) - self.RebootSystemButton.setGeometry(QtCore.QRect(157, 10, 154, 80)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.RebootSystemButton.sizePolicy().hasHeightForWidth()) - self.RebootSystemButton.setSizePolicy(sizePolicy) - self.RebootSystemButton.setMinimumSize(QtCore.QSize(154, 80)) - self.RebootSystemButton.setMaximumSize(QtCore.QSize(154, 80)) - self.RebootSystemButton.setBaseSize(QtCore.QSize(80, 80)) - self.RebootSystemButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) - self.RebootSystemButton.setTabletTracking(True) - self.RebootSystemButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.RebootSystemButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.RebootSystemButton.setAutoFillBackground(False) - self.RebootSystemButton.setIcon(icon1) - self.RebootSystemButton.setAutoDefault(False) - self.RebootSystemButton.setDefault(False) - self.RebootSystemButton.setFlat(True) - self.RebootSystemButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/reboot.svg")) - self.RebootSystemButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) - self.RebootSystemButton.setProperty("has_text", True) - self.RebootSystemButton.setObjectName("RebootSystemButton") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.cw_buttonFrame) + self.horizontalLayout.setContentsMargins(0, 5, 0, 0) + self.horizontalLayout.setSpacing(0) + self.horizontalLayout.setObjectName("horizontalLayout") self.RestartKlipperButton = IconButton(parent=self.cw_buttonFrame) - self.RestartKlipperButton.setGeometry(QtCore.QRect(4, 10, 154, 80)) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.RestartKlipperButton.sizePolicy().hasHeightForWidth()) self.RestartKlipperButton.setSizePolicy(sizePolicy) - self.RestartKlipperButton.setMinimumSize(QtCore.QSize(154, 80)) - self.RestartKlipperButton.setMaximumSize(QtCore.QSize(154, 80)) + self.RestartKlipperButton.setMinimumSize(QtCore.QSize(100, 80)) + self.RestartKlipperButton.setMaximumSize(QtCore.QSize(100, 80)) self.RestartKlipperButton.setBaseSize(QtCore.QSize(160, 80)) palette = QtGui.QPalette() brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) @@ -206,9 +103,9 @@ def setupUi(self, ConnectivityForm): self.RestartKlipperButton.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self.RestartKlipperButton.setAutoFillBackground(False) self.RestartKlipperButton.setStyleSheet("") - icon2 = QtGui.QIcon() - icon2.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/restart_klipper.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On) - self.RestartKlipperButton.setIcon(icon2) + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/restart_klipper.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On) + self.RestartKlipperButton.setIcon(icon) self.RestartKlipperButton.setIconSize(QtCore.QSize(46, 42)) self.RestartKlipperButton.setCheckable(False) self.RestartKlipperButton.setAutoRepeat(False) @@ -221,6 +118,132 @@ def setupUi(self, ConnectivityForm): self.RestartKlipperButton.setProperty("has_text", True) self.RestartKlipperButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) self.RestartKlipperButton.setObjectName("RestartKlipperButton") + self.horizontalLayout.addWidget(self.RestartKlipperButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) + self.RebootSystemButton = IconButton(parent=self.cw_buttonFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.RebootSystemButton.sizePolicy().hasHeightForWidth()) + self.RebootSystemButton.setSizePolicy(sizePolicy) + self.RebootSystemButton.setMinimumSize(QtCore.QSize(100, 80)) + self.RebootSystemButton.setMaximumSize(QtCore.QSize(100, 80)) + self.RebootSystemButton.setBaseSize(QtCore.QSize(80, 80)) + self.RebootSystemButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) + self.RebootSystemButton.setTabletTracking(True) + self.RebootSystemButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.RebootSystemButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.RebootSystemButton.setAutoFillBackground(False) + icon1 = QtGui.QIcon() + icon1.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/firmware_restart.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + self.RebootSystemButton.setIcon(icon1) + self.RebootSystemButton.setAutoDefault(False) + self.RebootSystemButton.setDefault(False) + self.RebootSystemButton.setFlat(True) + self.RebootSystemButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/reboot.svg")) + self.RebootSystemButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) + self.RebootSystemButton.setProperty("has_text", True) + self.RebootSystemButton.setObjectName("RebootSystemButton") + self.horizontalLayout.addWidget(self.RebootSystemButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) + self.FirmwareRestartButton = IconButton(parent=self.cw_buttonFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.FirmwareRestartButton.sizePolicy().hasHeightForWidth()) + self.FirmwareRestartButton.setSizePolicy(sizePolicy) + self.FirmwareRestartButton.setMinimumSize(QtCore.QSize(100, 80)) + self.FirmwareRestartButton.setMaximumSize(QtCore.QSize(100, 80)) + self.FirmwareRestartButton.setBaseSize(QtCore.QSize(160, 80)) + self.FirmwareRestartButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) + self.FirmwareRestartButton.setTabletTracking(True) + self.FirmwareRestartButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.FirmwareRestartButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.FirmwareRestartButton.setAutoFillBackground(False) + self.FirmwareRestartButton.setIcon(icon1) + self.FirmwareRestartButton.setAutoDefault(False) + self.FirmwareRestartButton.setDefault(False) + self.FirmwareRestartButton.setFlat(True) + self.FirmwareRestartButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/restart_firmware.svg")) + self.FirmwareRestartButton.setProperty("has_text", True) + self.FirmwareRestartButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) + self.FirmwareRestartButton.setObjectName("FirmwareRestartButton") + self.horizontalLayout.addWidget(self.FirmwareRestartButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) + self.RetryConnectionButton = IconButton(parent=self.cw_buttonFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.RetryConnectionButton.sizePolicy().hasHeightForWidth()) + self.RetryConnectionButton.setSizePolicy(sizePolicy) + self.RetryConnectionButton.setMinimumSize(QtCore.QSize(100, 80)) + self.RetryConnectionButton.setMaximumSize(QtCore.QSize(100, 80)) + self.RetryConnectionButton.setBaseSize(QtCore.QSize(80, 80)) + palette = QtGui.QPalette() + self.RetryConnectionButton.setPalette(palette) + font = QtGui.QFont() + font.setPointSize(13) + self.RetryConnectionButton.setFont(font) + self.RetryConnectionButton.setTabletTracking(True) + self.RetryConnectionButton.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) + self.RetryConnectionButton.setAutoFillBackground(False) + self.RetryConnectionButton.setStyleSheet("") + icon2 = QtGui.QIcon() + icon2.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/retry_connection.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + self.RetryConnectionButton.setIcon(icon2) + self.RetryConnectionButton.setIconSize(QtCore.QSize(16, 16)) + self.RetryConnectionButton.setCheckable(False) + self.RetryConnectionButton.setAutoRepeatDelay(0) + self.RetryConnectionButton.setAutoRepeatInterval(0) + self.RetryConnectionButton.setAutoDefault(False) + self.RetryConnectionButton.setDefault(False) + self.RetryConnectionButton.setFlat(True) + self.RetryConnectionButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/restart_printer.svg")) + self.RetryConnectionButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) + self.RetryConnectionButton.setProperty("has_text", True) + self.RetryConnectionButton.setObjectName("RetryConnectionButton") + self.horizontalLayout.addWidget(self.RetryConnectionButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) + self.updatepageButton = IconButton(parent=self.cw_buttonFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.updatepageButton.sizePolicy().hasHeightForWidth()) + self.updatepageButton.setSizePolicy(sizePolicy) + self.updatepageButton.setMinimumSize(QtCore.QSize(100, 80)) + self.updatepageButton.setMaximumSize(QtCore.QSize(100, 80)) + self.updatepageButton.setBaseSize(QtCore.QSize(80, 80)) + self.updatepageButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) + self.updatepageButton.setTabletTracking(True) + self.updatepageButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.updatepageButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.updatepageButton.setAutoFillBackground(False) + self.updatepageButton.setIcon(icon1) + self.updatepageButton.setAutoDefault(False) + self.updatepageButton.setDefault(False) + self.updatepageButton.setFlat(True) + self.updatepageButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/update-software-icon.svg")) + self.updatepageButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) + self.updatepageButton.setProperty("has_text", True) + self.updatepageButton.setObjectName("updatepageButton") + self.horizontalLayout.addWidget(self.updatepageButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) + self.wifi_button = IconButton(parent=self.cw_buttonFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.wifi_button.sizePolicy().hasHeightForWidth()) + self.wifi_button.setSizePolicy(sizePolicy) + self.wifi_button.setMinimumSize(QtCore.QSize(100, 80)) + self.wifi_button.setMaximumSize(QtCore.QSize(100, 80)) + self.wifi_button.setBaseSize(QtCore.QSize(80, 80)) + self.wifi_button.setTabletTracking(True) + self.wifi_button.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.wifi_button.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.wifi_button.setAutoFillBackground(False) + self.wifi_button.setIcon(icon2) + self.wifi_button.setAutoDefault(False) + self.wifi_button.setDefault(False) + self.wifi_button.setFlat(True) + self.wifi_button.setProperty("icon_pixmap", QtGui.QPixmap(":/network/media/btn_icons/wifi_config.svg")) + self.wifi_button.setProperty("has_text", True) + self.wifi_button.setObjectName("wifi_button") + self.horizontalLayout.addWidget(self.wifi_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) self.cw_Frame = QtWidgets.QFrame(parent=ConnectivityForm) self.cw_Frame.setGeometry(QtCore.QRect(0, 0, 800, 380)) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) @@ -231,6 +254,7 @@ def setupUi(self, ConnectivityForm): self.cw_Frame.setMinimumSize(QtCore.QSize(800, 380)) self.cw_Frame.setMaximumSize(QtCore.QSize(800, 380)) self.cw_Frame.setAutoFillBackground(False) + self.cw_Frame.setStyleSheet("") self.cw_Frame.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) self.cw_Frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) self.cw_Frame.setObjectName("cw_Frame") @@ -297,16 +321,18 @@ def setupUi(self, ConnectivityForm): def retranslateUi(self, ConnectivityForm): _translate = QtCore.QCoreApplication.translate ConnectivityForm.setWindowTitle(_translate("ConnectivityForm", "Form")) + self.RestartKlipperButton.setText(_translate("ConnectivityForm", "Restart Klipper")) + self.RestartKlipperButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) + self.RebootSystemButton.setText(_translate("ConnectivityForm", "Reboot")) + self.RebootSystemButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) + self.FirmwareRestartButton.setText(_translate("ConnectivityForm", "Firmware Restart")) + self.FirmwareRestartButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) self.RetryConnectionButton.setText(_translate("ConnectivityForm", "Retry ")) self.RetryConnectionButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) + self.updatepageButton.setText(_translate("ConnectivityForm", "Update page")) + self.updatepageButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) self.wifi_button.setText(_translate("ConnectivityForm", "Wifi Settings")) self.wifi_button.setProperty("class", _translate("ConnectivityForm", "system_control_btn")) self.wifi_button.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) - self.FirmwareRestartButton.setText(_translate("ConnectivityForm", "Firmware Restart")) - self.FirmwareRestartButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) - self.RebootSystemButton.setText(_translate("ConnectivityForm", "Reboot")) - self.RebootSystemButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) - self.RestartKlipperButton.setText(_translate("ConnectivityForm", "Restart Klipper")) - self.RestartKlipperButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.icon_button import IconButton diff --git a/BlocksScreen/lib/utils/icon_button.py b/BlocksScreen/lib/utils/icon_button.py index a60a7f1d..160f1f80 100644 --- a/BlocksScreen/lib/utils/icon_button.py +++ b/BlocksScreen/lib/utils/icon_button.py @@ -3,7 +3,7 @@ class IconButton(QtWidgets.QPushButton): - def __init__(self, parent: QtWidgets.QWidget) -> None: + def __init__(self, parent: QtWidgets.QWidget = None) -> None: super().__init__(parent) self.icon_pixmap: QtGui.QPixmap = QtGui.QPixmap() @@ -13,6 +13,7 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: self._name: str = "" self.text_color: QtGui.QColor = QtGui.QColor(255, 255, 255) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) + self.pressed_bg_color = QtGui.QColor(223, 223, 223, 70) # Set to solid white @property def name(self): @@ -42,6 +43,10 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: painter.setRenderHint(painter.RenderHint.SmoothPixmapTransform, True) painter.setRenderHint(painter.RenderHint.LosslessImageRendering, True) + if self.isDown(): + painter.setBrush(QtGui.QBrush(self.pressed_bg_color)) + painter.setPen(QtCore.Qt.PenStyle.NoPen) + painter.drawRoundedRect(self.rect().toRectF(), 6, 6) _pen = QtGui.QPen() _pen.setStyle(QtCore.Qt.PenStyle.NoPen) _pen.setColor(self.text_color) @@ -49,38 +54,36 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: painter.setPen(_pen) - # bg_color = ( - # QtGui.QColor(164, 164, 164) - # if self.isDown() - # else QtGui.QColor(223, 223, 223) - # ) - - # * Build icon - x = y = 15.0 if self.text_formatting else 5.0 - _icon_rect = QtCore.QRectF(0.0, 0.0, (self.width() - x), (self.height() - y)) - - _icon_scaled = self.icon_pixmap.scaled( - _icon_rect.size().toSize(), - QtCore.Qt.AspectRatioMode.KeepAspectRatio, - QtCore.Qt.TransformationMode.SmoothTransformation, - ) - # Calculate the actual QRect for the scaled pixmap (centering it if needed) - scaled_width = _icon_scaled.width() - scaled_height = _icon_scaled.height() - adjusted_x = (_icon_rect.width() - scaled_width) / 2.0 - adjusted_y = (_icon_rect.height() - scaled_height) / 2.0 - adjusted_icon_rect = QtCore.QRectF( - _icon_rect.x() + adjusted_x, - _icon_rect.y() + adjusted_y, - scaled_width, - scaled_height, - ) - - painter.drawPixmap( - adjusted_icon_rect, # Target area (center adjusted) - _icon_scaled, # Scaled pixmap - _icon_scaled.rect().toRectF(), # Entire source (scaled) pixmap - ) + y = 15.0 if self.text_formatting else 5.0 + if self.isDown(): + _icon_rect = QtCore.QRectF( + 2.5, 2.5, (self.width() - 5), (self.height() - 5 - y) + ) + else: + _icon_rect = QtCore.QRectF(0.0, 0.0, (self.width()), (self.height() - y)) + + if not self.icon_pixmap.isNull(): + _icon_scaled = self.icon_pixmap.scaled( + _icon_rect.size().toSize(), + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, + ) + scaled_width = _icon_scaled.width() + scaled_height = _icon_scaled.height() + adjusted_x = (_icon_rect.width() - scaled_width) / 2.0 + adjusted_y = (_icon_rect.height() - scaled_height) / 2.0 + adjusted_icon_rect = QtCore.QRectF( + _icon_rect.x() + adjusted_x, + _icon_rect.y() + adjusted_y, + scaled_width, + scaled_height, + ) + + painter.drawPixmap( + adjusted_icon_rect, + _icon_scaled, + _icon_scaled.rect().toRectF(), + ) if self.has_text: painter.setCompositionMode( @@ -99,9 +102,9 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: scaled_height, ) elif self.text_formatting == "bottom": - adjusted_x = (_icon_rect.width() - self.width() + 5.0) / 2.0 + # adjusted_x = 0#(_icon_rect.width() - self.width() + 5.0) / 2.0 adjusted_rectF = QtCore.QRectF( - _icon_rect.x() + adjusted_x, + 0, _icon_rect.height(), self.width(), self.height() - _icon_rect.height(), @@ -112,12 +115,9 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: painter.drawText( adjusted_rectF, - QtCore.Qt.TextFlag.TextSingleLine - | QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, + QtCore.Qt.TextFlag.TextSingleLine | QtCore.Qt.AlignmentFlag.AlignCenter, str(self.text()), ) - painter.setPen(QtCore.Qt.PenStyle.NoPen) painter.end() From 0e7a3e7881d98688ea12c156756e1e8f03959167 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Mon, 12 Jan 2026 17:53:33 +0000 Subject: [PATCH 26/70] Bugfix/tab unlocking (#147) * bugfix: fixed event config * Rev: removed on_cancel_print from handle cancel print --------- Co-authored-by: Roberto --- BlocksScreen/lib/panels/mainWindow.py | 9 ++++----- BlocksScreen/lib/panels/printTab.py | 1 - 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 3c65bd7b..f6fbc2f9 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -661,7 +661,6 @@ def event(self, event: QtCore.QEvent) -> bool: self.messageReceivedEvent(event) return True return False - if event.type() == events.PrintStart.type(): self.disable_tab_bar() self.ui.extruder_temp_display.clicked.disconnect() @@ -682,10 +681,10 @@ def event(self, event: QtCore.QEvent) -> bool: ) return False - if event.type() == ( - events.PrintError.type() - or events.PrintComplete.type() - or events.PrintCancelled.type() + if event.type() in ( + events.PrintError.type(), + events.PrintComplete.type(), + events.PrintCancelled.type(), ): self.enable_tab_bar() self.ui.extruder_temp_display.clicked.disconnect() diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index 104a765a..2a885362 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -342,7 +342,6 @@ def setProperty(self, name: str, value: typing.Any) -> bool: def handle_cancel_print(self) -> None: """Handles the print cancel action""" self.ws.api.cancel_print() - self.on_cancel_print.emit() self.loadscreen.show() self.loadscreen.setModal(True) self.loadwidget.set_status_message("Cancelling print...\nPlease wait") From 4ece7042a8edab1c7cba339a39462206efa928d0 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Mon, 12 Jan 2026 18:00:53 +0000 Subject: [PATCH 27/70] jobStatusPage: only load filedata when printer is printing (#150) Authored-by: Guilherme Costa --- BlocksScreen/lib/panels/widgets/jobStatusPage.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index bbce76a3..f19b5269 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -1,5 +1,6 @@ import logging import typing + import events from helper_methods import calculate_current_layer, estimate_print_time from lib.panels.widgets.basePopup import BasePopup @@ -181,6 +182,8 @@ def on_print_start(self, file: str) -> None: @QtCore.pyqtSlot(dict, name="on_fileinfo") def on_fileinfo(self, fileinfo: dict) -> None: """Handle received file information/metadata""" + if not self.isVisible(): + return self.total_layers = str(fileinfo.get("layer_count", "---")) self.layer_display_button.setText("---") self.layer_display_button.secondary_text = str(self.total_layers) From fa53e8c85ec9557c4c5270930fb7f83116549198 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Wed, 14 Jan 2026 10:25:35 +0000 Subject: [PATCH 28/70] Bugfix/inputshaper page (#148) * bugfix: multiple connects signals * bugifx: loadcreen having button * bugfix: fixed input shaper after merge * Bugfix: being able to click behind the popup * Refactor: ran ruff formatter --------- Authored-by: Roberto --- BlocksScreen/lib/panels/utilitiesTab.py | 20 +++++++++---------- BlocksScreen/lib/panels/widgets/basePopup.py | 13 ++++++------ .../lib/panels/widgets/inputshaperPage.py | 2 +- .../lib/panels/widgets/probeHelperPage.py | 2 +- 4 files changed, 17 insertions(+), 20 deletions(-) diff --git a/BlocksScreen/lib/panels/utilitiesTab.py b/BlocksScreen/lib/panels/utilitiesTab.py index a9b6652f..37fa6f61 100644 --- a/BlocksScreen/lib/panels/utilitiesTab.py +++ b/BlocksScreen/lib/panels/utilitiesTab.py @@ -123,7 +123,7 @@ def __init__( # --- UI Setup --- self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.loadPage = BasePopup(self) + self.loadPage = BasePopup(self, dialog=False) self.loadwidget = LoadingOverlayWidget( self, LoadingOverlayWidget.AnimationGIF.DEFAULT ) @@ -208,6 +208,7 @@ def __init__( QtGui.QPixmap(":/system/media/btn_icons/update-software-icon.svg") ) + # ---- Input Shaper ---- self.automatic_is = OptionCard( self, "Automatic\nInput Shaper", @@ -236,6 +237,12 @@ def __init__( self.is_types: dict = {} self.is_aut_types: dict = {} + self.dialog_page.accepted.connect( + lambda: self.handle_is("SHAPER_CALIBRATE AXIS=Y") + ) + self.dialog_page.rejected.connect( + lambda: self.handle_is("SHAPER_CALIBRATE AXIS=X") + ) self.is_page.action_btn.clicked.connect( lambda: self.change_page(self.indexOf(self.panel.input_shaper_page)) @@ -324,21 +331,12 @@ def handle_gcode_response(self, data: list[str]) -> None: self.loadPage.hide() return - def on_dialog_button_clicked(self, button_name: str) -> None: - print(button_name) - """Handle dialog button clicks""" - if button_name == "Confirm": - self.handle_is("SHAPER_CALIBRATE AXIS=Y") - elif button_name == "Cancel": - self.handle_is("SHAPER_CALIBRATE AXIS=X") - def handle_is(self, gcode: str) -> None: if gcode == "SHAPER_CALIBRATE": self.run_gcode_signal.emit("G28\nM400") self.aut = True self.run_gcode_signal.emit(gcode) - if gcode == "": - print("manual Input Shaper Selected") + elif gcode == "": self.dialog_page.confirm_background_color("#dfdfdf") self.dialog_page.cancel_background_color("#dfdfdf") self.dialog_page.cancel_font_color("#000000") diff --git a/BlocksScreen/lib/panels/widgets/basePopup.py b/BlocksScreen/lib/panels/widgets/basePopup.py index e9ebe5d3..a9a4d188 100644 --- a/BlocksScreen/lib/panels/widgets/basePopup.py +++ b/BlocksScreen/lib/panels/widgets/basePopup.py @@ -26,13 +26,13 @@ def __init__( dialog: bool = True, ) -> None: super().__init__(parent) - self.setWindowFlags( - QtCore.Qt.WindowType.Popup | QtCore.Qt.WindowType.FramelessWindowHint + QtCore.Qt.WindowType.Dialog + | QtCore.Qt.WindowType.FramelessWindowHint + | QtCore.Qt.WindowType.CustomizeWindowHint ) self.floating = floating self.dialog = dialog - # Color Variables self.btns_text_color = "#ffffff" self.cancel_bk_color = "#F44336" @@ -45,6 +45,7 @@ def __init__( if floating: self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) + self.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) else: self.setStyleSheet( """ @@ -59,9 +60,6 @@ def _update_button_style(self) -> None: if not self.dialog: return - self.confirm_button.clicked.connect(self.accept) - self.cancel_button.clicked.connect(self.reject) - if not self.floating: self.confirm_button.setStyleSheet( f""" @@ -252,5 +250,6 @@ def setupUI(self) -> None: self.cancel_button.setStyleSheet("background: transparent;") self.hlauyout.addWidget(self.confirm_button) self.hlauyout.addWidget(self.cancel_button) - + self.confirm_button.clicked.connect(self.accept) + self.cancel_button.clicked.connect(self.reject) self._update_button_style() diff --git a/BlocksScreen/lib/panels/widgets/inputshaperPage.py b/BlocksScreen/lib/panels/widgets/inputshaperPage.py index d96f543a..539dcaf4 100644 --- a/BlocksScreen/lib/panels/widgets/inputshaperPage.py +++ b/BlocksScreen/lib/panels/widgets/inputshaperPage.py @@ -398,7 +398,7 @@ def _setupUI(self) -> None: self.info_box.addWidget(self.vib_label, 0, 1) self.sug_accel_title_label = QtWidgets.QLabel(self) - self.sug_accel_title_label.setText("Sugested Max Acceleration:") + self.sug_accel_title_label.setText("Sugested Max\nAcceleration:") self.sug_accel_title_label.setMinimumSize(QtCore.QSize(60, 60)) self.sug_accel_title_label.setMaximumSize( QtCore.QSize(int(self.infobox_frame.size().width() * 0.40), 9999) diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index 6a632f4d..a3dea377 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -50,7 +50,7 @@ class ProbeHelper(QtWidgets.QWidget): def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__(parent) - self.Loadscreen = BasePopup(self) + self.Loadscreen = BasePopup(self, dialog=False) self.loadwidget = LoadingOverlayWidget( self, LoadingOverlayWidget.AnimationGIF.PLACEHOLDER ) From 1465ea1cd34514ce490ca84b6e3308da9f034ed0 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Wed, 14 Jan 2026 10:27:06 +0000 Subject: [PATCH 29/70] work popup features (#144) * Fix: fixed wrong if check for popups * feat: first version of userinput for popup * feat: added new error and info icons * feat: added ClearPixmap to icon_button * Refactor: finished popup userInput feat * Refactor: formated _handle_error_message message ignored unknow type popup * Refactor: popups only shows error messages and added popup whitelist * Refactor:run ruff formater * Rev: removed prints * refactor: removed bare except * bugfix: fixed wordwrap * Refactor: ran ruff formatter --------- Authored-by: Roberto --- BlocksScreen/lib/panels/mainWindow.py | 33 +- .../lib/panels/widgets/popupDialogWidget.py | 114 ++- .../lib/ui/resources/icon_resources.qrc | 4 +- .../lib/ui/resources/icon_resources_rc.py | 751 ++++++++---------- .../ui/resources/media/btn_icons/error.svg | 60 +- .../lib/ui/resources/media/btn_icons/info.svg | 14 +- BlocksScreen/lib/utils/icon_button.py | 5 + 7 files changed, 476 insertions(+), 505 deletions(-) diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index f6fbc2f9..63c53fa7 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -547,15 +547,13 @@ def _handle_notify_service_state_changed_message( """Handle websocket service messages""" entry = data.get("params") if entry: - if not self._popup_toggle: + if self._popup_toggle: return service_entry: dict = entry[0] service_name, service_info = service_entry.popitem() self.popup.new_message( message_type=Popup.MessageType.INFO, - message=f"""{service_name} service changed state to - {service_info.get("sub_state")} - """, + message=f"{service_name} service changed state to \n{service_info.get('sub_state')}", ) @api_handler @@ -564,15 +562,17 @@ def _handle_notify_gcode_response_message(self, method, data, metadata) -> None: _gcode_response = data.get("params") self.gcode_response[list].emit(_gcode_response) if _gcode_response: - if not self._popup_toggle: + if self._popup_toggle: return _gcode_msg_type, _message = str(_gcode_response[0]).split(" ", maxsplit=1) - _msg_type = Popup.MessageType.UNKNOWN - if _gcode_msg_type == "!!": + popupWhitelist = ["filament runout", "no filament"] + if _message.lower() in popupWhitelist or _gcode_msg_type == "!!": _msg_type = Popup.MessageType.ERROR - elif _gcode_msg_type == "//": - _msg_type = Popup.MessageType.INFO - self.popup.new_message(message_type=_msg_type, message=str(_message)) + self.popup.new_message( + message_type=_msg_type, + message=str(_message), + userInput=True, + ) @api_handler def _handle_error_message(self, method, data, metadata) -> None: @@ -581,17 +581,24 @@ def _handle_error_message(self, method, data, metadata) -> None: if "metadata" in data.get("message", "").lower(): # Quick fix, don't care about no metadata errors return - if not self._popup_toggle: + if self._popup_toggle: return + text = data + if isinstance(data, dict): + if "message" in data: + text = f"{data['message']}" + else: + text = data self.popup.new_message( message_type=Popup.MessageType.ERROR, - message=str(data), + message=str(text), + userInput=True, ) @api_handler def _handle_notify_cpu_throttled_message(self, method, data, metadata) -> None: """Handle websocket cpu throttled messages""" - if not self._popup_toggle: + if self._popup_toggle: return self.popup.new_message( message_type=Popup.MessageType.WARNING, diff --git a/BlocksScreen/lib/panels/widgets/popupDialogWidget.py b/BlocksScreen/lib/panels/widgets/popupDialogWidget.py index f878f612..69deec51 100644 --- a/BlocksScreen/lib/panels/widgets/popupDialogWidget.py +++ b/BlocksScreen/lib/panels/widgets/popupDialogWidget.py @@ -2,9 +2,7 @@ from collections import deque from typing import Deque from PyQt6 import QtCore, QtGui, QtWidgets - - -BASE_POPUP_TIMEOUT = 6000 +from lib.utils.icon_button import IconButton class Popup(QtWidgets.QDialog): @@ -25,9 +23,10 @@ class ColorCode(enum.Enum): def __init__(self, parent) -> None: super().__init__(parent) - self.popup_timeout = BASE_POPUP_TIMEOUT self.timeout_timer = QtCore.QTimer(self) + self.timeout_timer.setSingleShot(True) self.messages: Deque = deque() + self.isShown = False self.persistent_notifications: Deque = deque() self.message_type: Popup.MessageType = Popup.MessageType.INFO self.default_background_color = QtGui.QColor(164, 164, 164) @@ -48,20 +47,32 @@ def __init__(self, parent) -> None: self.slide_out_animation = QtCore.QPropertyAnimation(self, b"geometry") self.slide_out_animation.setDuration(200) self.slide_out_animation.setEasingCurve(QtCore.QEasingCurve.Type.InCubic) - self.slide_in_animation.finished.connect(self.on_slide_in_finished) + + self.SingleTime = QtCore.QTimer(self) + self.SingleTime.setInterval(5000) + self.SingleTime.setSingleShot(True) + self.SingleTime.timeout.connect(self._add_popup) + self.slide_out_animation.finished.connect(self.on_slide_out_finished) - self.timeout_timer.timeout.connect(self.slide_out_animation.start) + self.slide_in_animation.finished.connect(self.on_slide_in_finished) + self.timeout_timer.timeout.connect(lambda: self.slide_out_animation.start()) + self.actionbtn.clicked.connect(self.slide_out_animation.start) def on_slide_in_finished(self): """Handle slide in animation finished""" + if self.userInput: + return self.timeout_timer.start() def on_slide_out_finished(self): """Handle slide out animation finished""" - self.close() + self.hide() + self.isShown = False + self.timeout_timer.stop() self._add_popup() def _calculate_target_geometry(self) -> QtCore.QRect: + """Calculate on end posisition rect for popup""" app_instance = QtWidgets.QApplication.instance() main_window = app_instance.activeWindow() if app_instance else None if main_window is None and app_instance: @@ -73,7 +84,13 @@ def _calculate_target_geometry(self) -> QtCore.QRect: parent_rect = main_window.geometry() width = int(parent_rect.width() * 0.85) - height = min(self.text_label.rect().height(), self.icon_label.rect().height()) + height = ( + max( + self.text_label.height(), + self.icon_label.height(), + ) + + 10 + ) x = parent_rect.x() + (parent_rect.width() - width) // 2 y = parent_rect.y() + 20 @@ -89,20 +106,19 @@ def updateMask(self) -> None: def mousePressEvent(self, a0: QtGui.QMouseEvent) -> None: """Re-implemented method, handle mouse press events""" + if self.userInput: + return self.timeout_timer.stop() + self.slide_out_animation.setStartValue(self.slide_in_animation.currentValue()) + self.slide_in_animation.stop() self.slide_out_animation.start() - def set_timeout(self, value: int) -> None: - """Set popup timeout""" - if not isinstance(value, int): - raise ValueError("Expected type int ") - self.popup_timeout = value - def new_message( self, message_type: MessageType = MessageType.INFO, message: str = "", - timeout: int = 0, + timeout: int = 6000, + userInput: bool = False, ): """Create new popup message @@ -110,17 +126,29 @@ def new_message( message_type (MessageType, optional): Message Level, See `MessageType` Types. Defaults to MessageType.INFO. message (str, optional): The message. Defaults to "". timeout (int, optional): How long the message stays for, in milliseconds. Defaults to 0. + userInput (bool,optional): If the user is required to click to make the popup disappear. Defaults to False. Returns: _type_: _description_ """ self.messages.append( - {"message": message, "type": message_type, "timeout": timeout} + { + "message": message, + "type": message_type, + "timeout": timeout, + "userInput": userInput, + } ) return self._add_popup() def _add_popup(self) -> None: """Add popup to queue""" + if self.isShown: + if self.SingleTime.isActive(): + return + self.SingleTime.start() + return + if ( self.messages and self.slide_in_animation.state() @@ -131,7 +159,17 @@ def _add_popup(self) -> None: message_entry = self.messages.popleft() self.message_type = message_entry.get("type") message = message_entry.get("message") - self.text_label.setText(message) + timeout = message_entry.get("timeout") + self.timeout_timer.setInterval(timeout) + if message == self.text_label.text(): + self.messages = deque( + m for m in self.messages if m.get("message") != message + ) + return + self.userInput = message_entry.get("userInput") + self.text_label.setFixedHeight(60) + self.text_label.setFixedWidth(500) + match self.message_type: case Popup.MessageType.INFO: self.icon_label.setPixmap(self.info_icon) @@ -139,19 +177,31 @@ def _add_popup(self) -> None: self.icon_label.setPixmap(self.warning_icon) case Popup.MessageType.ERROR: self.icon_label.setPixmap(self.error_icon) - self.timeout_timer.setInterval(self.popup_timeout) + end_rect = self._calculate_target_geometry() - start_rect = end_rect.translated(0, -end_rect.height()) + start_rect = end_rect.translated(0, -end_rect.height() * 2) + self.slide_in_animation.setStartValue(start_rect) self.slide_in_animation.setEndValue(end_rect) self.slide_out_animation.setStartValue(end_rect) self.slide_out_animation.setEndValue(start_rect) - self.setGeometry(start_rect) - self.open() + if not self.userInput: + self.actionbtn.clearPixmap() + else: + self.actionbtn.setPixmap( + QtGui.QPixmap(":/arrow_icons/media/btn_icons/right_arrow.svg") + ) + self.setGeometry(end_rect) + self.text_label.setText(message) + self.text_label.setFixedHeight( + int(self.text_label.sizeHint().height() * 1.2) + ) + self.show() def showEvent(self, a0: QtGui.QShowEvent) -> None: """Re-implementation, widget show""" self.slide_in_animation.start() + self.isShown = True super().showEvent(a0) def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: @@ -183,22 +233,18 @@ def paintEvent(self, a0: QtGui.QPaintEvent) -> None: painter.drawRoundedRect(self.rect(), 10, 10) def _setupUI(self) -> None: - self.vertical_layout = QtWidgets.QVBoxLayout(self) - self.horizontal_layout = QtWidgets.QHBoxLayout() + self.horizontal_layout = QtWidgets.QHBoxLayout(self) self.horizontal_layout.setContentsMargins(5, 5, 5, 5) self.icon_label = QtWidgets.QLabel(self) self.icon_label.setFixedSize(QtCore.QSize(60, 60)) + self.icon_label.setMaximumSize(QtCore.QSize(60, 60)) self.icon_label.setScaledContents(True) - self.horizontal_layout.addWidget(self.icon_label) - self.text_label = QtWidgets.QLabel(self) + self.text_label.setStyleSheet("background: transparent; color:white") + self.text_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.text_label.setWordWrap(True) - self.text_label.setAlignment( - QtCore.Qt.AlignmentFlag.AlignVCenter | QtCore.Qt.AlignmentFlag.AlignHCenter - ) - font = self.text_label.font() font.setPixelSize(18) font.setFamily("sans-serif") @@ -209,9 +255,9 @@ def _setupUI(self) -> None: self.text_label.setPalette(palette) self.text_label.setFont(font) - self.spacer = QtWidgets.QSpacerItem(60, 60) + self.actionbtn = IconButton(self) + self.actionbtn.setMaximumSize(QtCore.QSize(60, 60)) - self.horizontal_layout.addWidget(self.text_label, 1) - self.horizontal_layout.addItem(self.spacer) - - self.vertical_layout.addLayout(self.horizontal_layout) + self.horizontal_layout.addWidget(self.icon_label) + self.horizontal_layout.addWidget(self.text_label) + self.horizontal_layout.addWidget(self.actionbtn) diff --git a/BlocksScreen/lib/ui/resources/icon_resources.qrc b/BlocksScreen/lib/ui/resources/icon_resources.qrc index a3bf6d57..8338a1da 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources.qrc +++ b/BlocksScreen/lib/ui/resources/icon_resources.qrc @@ -89,9 +89,9 @@ media/btn_icons/center_arrows.svg media/btn_icons/confirm_stl_window.svg media/btn_icons/horizontal_scroll_bar.svg - media/btn_icons/info.svg - media/btn_icons/error.svg media/btn_icons/info_prints.svg + media/btn_icons/error.svg + media/btn_icons/info.svg media/btn_icons/LCD_settings.svg media/btn_icons/LEDs.svg media/btn_icons/LEDs_off.svg diff --git a/BlocksScreen/lib/ui/resources/icon_resources_rc.py b/BlocksScreen/lib/ui/resources/icon_resources_rc.py index 1346a1f1..6481444f 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/icon_resources_rc.py @@ -22339,54 +22339,75 @@ \x20\x34\x38\x31\x2e\x32\x32\x20\x35\x31\x30\x2e\x37\x37\x20\x32\ \x38\x36\x2e\x30\x36\x20\x38\x39\x2e\x32\x33\x20\x31\x31\x34\x2e\ \x34\x31\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x02\xe0\ +\x00\x00\x04\x25\ \x3c\ -\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ -\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ -\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x65\x30\x65\x30\x64\x66\x3b\x7d\ -\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x31\x22\x20\x64\x3d\x22\x4d\x35\x36\x31\x2e\x34\x39\x2c\x33\x30\ -\x30\x63\x32\x2e\x36\x38\x2c\x31\x34\x31\x2e\x35\x32\x2d\x31\x31\ -\x38\x2e\x37\x39\x2c\x32\x36\x33\x2e\x36\x38\x2d\x32\x36\x34\x2e\ -\x36\x2c\x32\x36\x31\x2e\x36\x35\x2d\x31\x34\x31\x2e\x37\x31\x2d\ -\x32\x2d\x32\x35\x38\x2e\x35\x31\x2d\x31\x31\x38\x2e\x35\x39\x2d\ -\x32\x35\x38\x2e\x34\x32\x2d\x32\x36\x31\x2e\x38\x36\x43\x33\x38\ -\x2e\x35\x36\x2c\x31\x35\x34\x2e\x35\x35\x2c\x31\x35\x35\x2e\x36\ -\x38\x2c\x33\x38\x2e\x31\x36\x2c\x33\x30\x31\x2e\x35\x39\x2c\x33\ -\x38\x2e\x33\x32\x2c\x34\x34\x35\x2e\x34\x34\x2c\x33\x38\x2e\x34\ -\x37\x2c\x35\x36\x31\x2e\x34\x39\x2c\x31\x35\x35\x2e\x33\x33\x2c\ -\x35\x36\x31\x2e\x34\x39\x2c\x33\x30\x30\x5a\x4d\x32\x34\x37\x2e\ -\x32\x37\x2c\x33\x38\x35\x2e\x32\x32\x63\x30\x2c\x34\x36\x2e\x36\ -\x31\x2e\x31\x38\x2c\x39\x33\x2e\x32\x33\x2d\x2e\x31\x38\x2c\x31\ -\x33\x39\x2e\x38\x34\x2d\x2e\x30\x36\x2c\x37\x2e\x32\x39\x2c\x32\ -\x2c\x39\x2c\x39\x2c\x38\x2e\x38\x37\x2c\x32\x39\x2e\x36\x36\x2d\ -\x2e\x33\x39\x2c\x35\x39\x2e\x33\x33\x2d\x2e\x33\x2c\x38\x39\x2c\ -\x30\x2c\x35\x2e\x37\x33\x2e\x30\x35\x2c\x37\x2e\x37\x2d\x31\x2e\ -\x32\x35\x2c\x37\x2e\x36\x39\x2d\x37\x2e\x34\x33\x71\x2d\x2e\x33\ -\x31\x2d\x31\x34\x31\x2e\x36\x35\x2c\x30\x2d\x32\x38\x33\x2e\x33\ -\x63\x30\x2d\x36\x2e\x32\x31\x2d\x32\x2d\x37\x2e\x33\x36\x2d\x37\ -\x2e\x36\x35\x2d\x37\x2e\x33\x31\x2d\x32\x39\x2e\x36\x36\x2e\x32\ -\x36\x2d\x35\x39\x2e\x33\x34\x2e\x34\x36\x2d\x38\x39\x2d\x2e\x31\ -\x2d\x38\x2d\x2e\x31\x35\x2d\x39\x2e\x31\x32\x2c\x32\x2e\x35\x32\ -\x2d\x39\x2e\x30\x37\x2c\x39\x2e\x36\x34\x43\x32\x34\x37\x2e\x34\ -\x33\x2c\x32\x39\x32\x2c\x32\x34\x37\x2e\x32\x37\x2c\x33\x33\x38\ -\x2e\x36\x31\x2c\x32\x34\x37\x2e\x32\x37\x2c\x33\x38\x35\x2e\x32\ -\x32\x5a\x6d\x35\x34\x2e\x30\x38\x2d\x33\x30\x36\x63\x2d\x33\x30\ -\x2e\x31\x33\x2d\x2e\x34\x31\x2d\x35\x36\x2e\x33\x35\x2c\x31\x38\ -\x2d\x36\x33\x2e\x32\x36\x2c\x34\x34\x2e\x34\x33\x2d\x38\x2e\x33\ -\x2c\x33\x31\x2e\x37\x39\x2c\x31\x30\x2e\x30\x38\x2c\x36\x33\x2e\ -\x33\x34\x2c\x34\x32\x2e\x33\x32\x2c\x37\x32\x2e\x36\x33\x2c\x33\ -\x34\x2e\x38\x2c\x31\x30\x2c\x37\x31\x2e\x37\x33\x2d\x38\x2e\x31\ -\x31\x2c\x38\x31\x2e\x35\x35\x2d\x34\x30\x2e\x30\x35\x43\x33\x37\ -\x33\x2e\x38\x35\x2c\x31\x31\x37\x2e\x36\x33\x2c\x33\x34\x34\x2e\ -\x31\x31\x2c\x37\x39\x2e\x38\x35\x2c\x33\x30\x31\x2e\x33\x35\x2c\ -\x37\x39\x2e\x32\x37\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\ +\x61\x79\x65\x72\x5f\x31\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\ +\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\ +\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\ +\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x76\ +\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x36\x30\x30\x20\ +\x36\x30\x30\x22\x3e\x0a\x20\x20\x3c\x64\x65\x66\x73\x3e\x0a\x20\ +\x20\x20\x20\x3c\x73\x74\x79\x6c\x65\x3e\x0a\x20\x20\x20\x20\x20\ +\x20\x2e\x63\x6c\x73\x2d\x31\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x66\x69\x6c\x6c\x3a\x20\x23\x65\x30\x65\x30\x64\x66\x3b\ +\x0a\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x73\ +\x74\x79\x6c\x65\x3e\x0a\x20\x20\x3c\x2f\x64\x65\x66\x73\x3e\x0a\ +\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ +\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x33\x37\x2e\x39\x35\ +\x2c\x32\x35\x31\x2e\x37\x35\x63\x2d\x32\x34\x2e\x39\x32\x2e\x32\ +\x32\x2d\x34\x39\x2e\x38\x34\x2e\x33\x39\x2d\x37\x34\x2e\x37\x35\ +\x2d\x2e\x30\x38\x2d\x36\x2e\x37\x34\x2d\x2e\x31\x33\x2d\x37\x2e\ +\x36\x37\x2c\x32\x2e\x31\x32\x2d\x37\x2e\x36\x32\x2c\x38\x2e\x31\ +\x2e\x32\x37\x2c\x33\x39\x2e\x31\x35\x2e\x31\x34\x2c\x37\x38\x2e\ +\x33\x2e\x31\x34\x2c\x31\x31\x37\x2e\x34\x35\x73\x2e\x31\x35\x2c\ +\x37\x38\x2e\x33\x2d\x2e\x31\x35\x2c\x31\x31\x37\x2e\x34\x35\x63\ +\x2d\x2e\x30\x35\x2c\x36\x2e\x31\x33\x2c\x31\x2e\x37\x31\x2c\x37\ +\x2e\x35\x33\x2c\x37\x2e\x36\x2c\x37\x2e\x34\x36\x2c\x32\x34\x2e\ +\x39\x32\x2d\x2e\x33\x33\x2c\x34\x39\x2e\x38\x34\x2d\x2e\x32\x36\ +\x2c\x37\x34\x2e\x37\x36\x2d\x2e\x30\x34\x2c\x34\x2e\x38\x32\x2e\ +\x30\x34\x2c\x36\x2e\x34\x37\x2d\x31\x2e\x30\x35\x2c\x36\x2e\x34\ +\x36\x2d\x36\x2e\x32\x34\x2d\x2e\x31\x37\x2d\x37\x39\x2e\x33\x32\ +\x2d\x2e\x31\x37\x2d\x31\x35\x38\x2e\x36\x34\x2c\x30\x2d\x32\x33\ +\x37\x2e\x39\x36\x2e\x30\x31\x2d\x35\x2e\x32\x32\x2d\x31\x2e\x36\ +\x36\x2d\x36\x2e\x31\x39\x2d\x36\x2e\x34\x33\x2d\x36\x2e\x31\x34\ +\x5a\x22\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ +\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x35\ +\x37\x34\x2e\x32\x34\x2c\x33\x30\x35\x2e\x36\x34\x63\x30\x2d\x31\ +\x35\x31\x2e\x37\x34\x2d\x31\x32\x31\x2e\x37\x31\x2d\x32\x37\x34\ +\x2e\x32\x39\x2d\x32\x37\x32\x2e\x35\x37\x2d\x32\x37\x34\x2e\x34\ +\x36\x2d\x31\x35\x33\x2e\x30\x33\x2d\x2e\x31\x36\x2d\x32\x37\x35\ +\x2e\x38\x36\x2c\x31\x32\x31\x2e\x39\x2d\x32\x37\x35\x2e\x39\x36\ +\x2c\x32\x37\x34\x2e\x32\x34\x2d\x2e\x31\x2c\x31\x35\x30\x2e\x32\ +\x36\x2c\x31\x32\x32\x2e\x34\x2c\x32\x37\x32\x2e\x35\x36\x2c\x32\ +\x37\x31\x2e\x30\x32\x2c\x32\x37\x34\x2e\x36\x33\x2c\x31\x35\x32\ +\x2e\x39\x32\x2c\x32\x2e\x31\x33\x2c\x32\x38\x30\x2e\x33\x32\x2d\ +\x31\x32\x35\x2e\x39\x39\x2c\x32\x37\x37\x2e\x35\x31\x2d\x32\x37\ +\x34\x2e\x34\x5a\x4d\x32\x39\x37\x2e\x33\x38\x2c\x35\x32\x35\x2e\ +\x34\x31\x63\x2d\x31\x31\x39\x2e\x30\x33\x2d\x31\x2e\x36\x36\x2d\ +\x32\x31\x37\x2e\x31\x33\x2d\x39\x39\x2e\x36\x31\x2d\x32\x31\x37\ +\x2e\x30\x36\x2d\x32\x31\x39\x2e\x39\x35\x2e\x30\x38\x2d\x31\x32\ +\x32\x2c\x39\x38\x2e\x34\x35\x2d\x32\x31\x39\x2e\x37\x36\x2c\x32\ +\x32\x31\x2e\x30\x31\x2d\x32\x31\x39\x2e\x36\x33\x2c\x31\x32\x30\ +\x2e\x38\x32\x2e\x31\x33\x2c\x32\x31\x38\x2e\x33\x2c\x39\x38\x2e\ +\x32\x38\x2c\x32\x31\x38\x2e\x33\x2c\x32\x31\x39\x2e\x38\x31\x2c\ +\x32\x2e\x32\x35\x2c\x31\x31\x38\x2e\x38\x37\x2d\x39\x39\x2e\x37\ +\x38\x2c\x32\x32\x31\x2e\x34\x37\x2d\x32\x32\x32\x2e\x32\x35\x2c\ +\x32\x31\x39\x2e\x37\x37\x5a\x22\x2f\x3e\x0a\x20\x20\x3c\x70\x61\ +\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\ +\x20\x64\x3d\x22\x4d\x33\x30\x31\x2e\x31\x33\x2c\x31\x32\x30\x2e\ +\x32\x33\x63\x2d\x32\x35\x2e\x33\x31\x2d\x2e\x33\x35\x2d\x34\x37\ +\x2e\x33\x33\x2c\x31\x35\x2e\x31\x32\x2d\x35\x33\x2e\x31\x33\x2c\ +\x33\x37\x2e\x33\x32\x2d\x36\x2e\x39\x38\x2c\x32\x36\x2e\x37\x2c\ +\x38\x2e\x34\x36\x2c\x35\x33\x2e\x32\x2c\x33\x35\x2e\x35\x35\x2c\ +\x36\x31\x2e\x30\x31\x2c\x32\x39\x2e\x32\x33\x2c\x38\x2e\x34\x32\ +\x2c\x36\x30\x2e\x32\x35\x2d\x36\x2e\x38\x31\x2c\x36\x38\x2e\x35\ +\x2d\x33\x33\x2e\x36\x34\x2c\x39\x2e\x39\x38\x2d\x33\x32\x2e\x34\ +\x36\x2d\x31\x34\x2e\x39\x39\x2d\x36\x34\x2e\x31\x39\x2d\x35\x30\ +\x2e\x39\x31\x2d\x36\x34\x2e\x36\x38\x5a\x22\x2f\x3e\x0a\x3c\x2f\ +\x73\x76\x67\x3e\ \x00\x00\x06\x33\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -23113,160 +23134,74 @@ \x35\x33\x32\x2e\x39\x36\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\ \x34\x31\x38\x2e\x33\x36\x22\x20\x72\x78\x3d\x22\x32\x39\x2e\x31\ \x37\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x09\x7d\ +\x00\x00\x04\x1d\ \x3c\ \x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ \x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ -\x2d\x38\x22\x20\x73\x74\x61\x6e\x64\x61\x6c\x6f\x6e\x65\x3d\x22\ -\x6e\x6f\x22\x3f\x3e\x0a\x3c\x21\x2d\x2d\x20\x43\x72\x65\x61\x74\ -\x65\x64\x20\x77\x69\x74\x68\x20\x49\x6e\x6b\x73\x63\x61\x70\x65\ -\x20\x28\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\x6b\ -\x73\x63\x61\x70\x65\x2e\x6f\x72\x67\x2f\x29\x20\x2d\x2d\x3e\x0a\ -\x0a\x3c\x73\x76\x67\x0a\x20\x20\x20\x77\x69\x64\x74\x68\x3d\x22\ -\x31\x33\x38\x2e\x33\x39\x33\x39\x31\x6d\x6d\x22\x0a\x20\x20\x20\ -\x68\x65\x69\x67\x68\x74\x3d\x22\x31\x33\x38\x2e\x34\x37\x31\x30\ -\x35\x6d\x6d\x22\x0a\x20\x20\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x31\x33\x38\x2e\x33\x39\x33\x39\x31\x20\x31\ -\x33\x38\x2e\x34\x37\x31\x30\x35\x22\x0a\x20\x20\x20\x76\x65\x72\ -\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x31\x22\x0a\x20\x20\x20\x69\x64\ -\x3d\x22\x73\x76\x67\x31\x22\x0a\x20\x20\x20\x69\x6e\x6b\x73\x63\ -\x61\x70\x65\x3a\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\x34\ -\x20\x28\x38\x36\x61\x38\x61\x64\x37\x2c\x20\x32\x30\x32\x34\x2d\ -\x31\x30\x2d\x31\x31\x29\x22\x0a\x20\x20\x20\x73\x6f\x64\x69\x70\ -\x6f\x64\x69\x3a\x64\x6f\x63\x6e\x61\x6d\x65\x3d\x22\x65\x72\x72\ -\x6f\x72\x2e\x73\x76\x67\x22\x0a\x20\x20\x20\x78\x6d\x6c\x3a\x73\ -\x70\x61\x63\x65\x3d\x22\x70\x72\x65\x73\x65\x72\x76\x65\x22\x0a\ -\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x69\x6e\ -\x6b\x73\x63\x61\x70\x65\x2e\x6f\x72\x67\x2f\x6e\x61\x6d\x65\x73\ -\x70\x61\x63\x65\x73\x2f\x69\x6e\x6b\x73\x63\x61\x70\x65\x22\x0a\ -\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3a\x73\x6f\x64\x69\x70\x6f\x64\ -\x69\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x73\x6f\x64\x69\x70\x6f\ -\x64\x69\x2e\x73\x6f\x75\x72\x63\x65\x66\x6f\x72\x67\x65\x2e\x6e\ -\x65\x74\x2f\x44\x54\x44\x2f\x73\x6f\x64\x69\x70\x6f\x64\x69\x2d\ -\x30\x2e\x64\x74\x64\x22\x0a\x20\x20\x20\x78\x6d\x6c\x6e\x73\x3d\ -\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\ -\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x0a\x20\x20\x20\ -\x78\x6d\x6c\x6e\x73\x3a\x73\x76\x67\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x3e\x3c\x73\x6f\x64\x69\x70\x6f\x64\x69\ -\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x0a\x20\x20\x20\x20\x20\ -\x69\x64\x3d\x22\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x31\x22\x0a\ -\x20\x20\x20\x20\x20\x70\x61\x67\x65\x63\x6f\x6c\x6f\x72\x3d\x22\ -\x23\x66\x66\x66\x66\x66\x66\x22\x0a\x20\x20\x20\x20\x20\x62\x6f\ -\x72\x64\x65\x72\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x30\x30\x30\x30\ -\x30\x30\x22\x0a\x20\x20\x20\x20\x20\x62\x6f\x72\x64\x65\x72\x6f\ -\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x2e\x32\x35\x22\x0a\x20\x20\ -\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x73\x68\x6f\x77\ -\x70\x61\x67\x65\x73\x68\x61\x64\x6f\x77\x3d\x22\x32\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\x61\x67\ -\x65\x6f\x70\x61\x63\x69\x74\x79\x3d\x22\x30\x2e\x30\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\x61\x67\ -\x65\x63\x68\x65\x63\x6b\x65\x72\x62\x6f\x61\x72\x64\x3d\x22\x30\ -\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\ -\x64\x65\x73\x6b\x63\x6f\x6c\x6f\x72\x3d\x22\x23\x64\x31\x64\x31\ -\x64\x31\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x3a\x64\x6f\x63\x75\x6d\x65\x6e\x74\x2d\x75\x6e\x69\x74\x73\ -\x3d\x22\x6d\x6d\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\ -\x61\x70\x65\x3a\x7a\x6f\x6f\x6d\x3d\x22\x30\x2e\x37\x33\x34\x39\ -\x35\x33\x37\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\ -\x70\x65\x3a\x63\x78\x3d\x22\x34\x34\x32\x2e\x38\x38\x35\x30\x34\ -\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\ -\x63\x79\x3d\x22\x32\x39\x38\x2e\x36\x35\x38\x32\x37\x22\x0a\x20\ -\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\ -\x64\x6f\x77\x2d\x77\x69\x64\x74\x68\x3d\x22\x31\x39\x32\x30\x22\ -\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\ -\x69\x6e\x64\x6f\x77\x2d\x68\x65\x69\x67\x68\x74\x3d\x22\x31\x30\ -\x32\x37\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\ -\x65\x3a\x77\x69\x6e\x64\x6f\x77\x2d\x78\x3d\x22\x31\x39\x31\x32\ -\x22\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\ -\x77\x69\x6e\x64\x6f\x77\x2d\x79\x3d\x22\x32\x32\x22\x0a\x20\x20\ -\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x77\x69\x6e\x64\ -\x6f\x77\x2d\x6d\x61\x78\x69\x6d\x69\x7a\x65\x64\x3d\x22\x31\x22\ -\x0a\x20\x20\x20\x20\x20\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x63\ -\x75\x72\x72\x65\x6e\x74\x2d\x6c\x61\x79\x65\x72\x3d\x22\x73\x76\ -\x67\x31\x22\x3e\x3c\x69\x6e\x6b\x73\x63\x61\x70\x65\x3a\x70\x61\ -\x67\x65\x0a\x20\x20\x20\x20\x20\x20\x20\x78\x3d\x22\x30\x22\x0a\ -\x20\x20\x20\x20\x20\x20\x20\x79\x3d\x22\x30\x22\x0a\x20\x20\x20\ -\x20\x20\x20\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x33\x38\x2e\x33\ -\x39\x33\x39\x31\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x68\x65\x69\ -\x67\x68\x74\x3d\x22\x31\x33\x38\x2e\x34\x37\x31\x30\x35\x22\x0a\ -\x20\x20\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x67\x65\x33\ -\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x6d\x61\x72\x67\x69\x6e\x3d\ -\x22\x30\x22\x0a\x20\x20\x20\x20\x20\x20\x20\x62\x6c\x65\x65\x64\ -\x3d\x22\x30\x22\x20\x2f\x3e\x3c\x2f\x73\x6f\x64\x69\x70\x6f\x64\ -\x69\x3a\x6e\x61\x6d\x65\x64\x76\x69\x65\x77\x3e\x3c\x64\x65\x66\ -\x73\x0a\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x64\x65\x66\x73\x31\ -\x22\x3e\x3c\x73\x74\x79\x6c\x65\x0a\x20\x20\x20\x20\x20\x20\x20\ -\x69\x64\x3d\x22\x73\x74\x79\x6c\x65\x31\x22\x3e\x2e\x63\x6c\x73\ -\x2d\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x65\x30\x65\x30\x64\x66\x3b\ -\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\ -\x3c\x70\x61\x74\x68\x0a\x20\x20\x20\x20\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x31\x22\x0a\x20\x20\x20\x20\x20\x64\x3d\ -\x22\x4d\x20\x30\x2e\x30\x31\x31\x35\x31\x36\x30\x33\x2c\x36\x39\ -\x2e\x32\x33\x34\x38\x33\x31\x20\x43\x20\x2d\x30\x2e\x36\x39\x37\ -\x35\x36\x33\x39\x37\x2c\x33\x31\x2e\x37\x39\x31\x30\x30\x31\x20\ -\x33\x31\x2e\x34\x34\x31\x33\x37\x35\x2c\x2d\x30\x2e\x35\x33\x30\ -\x35\x30\x30\x31\x34\x20\x37\x30\x2e\x30\x32\x30\x32\x36\x35\x2c\ -\x30\x2e\x30\x30\x36\x35\x39\x39\x34\x36\x20\x31\x30\x37\x2e\x35\ -\x31\x34\x33\x38\x2c\x30\x2e\x35\x33\x35\x37\x36\x39\x34\x36\x20\ -\x31\x33\x38\x2e\x34\x31\x37\x37\x31\x2c\x33\x31\x2e\x33\x38\x33\ -\x35\x34\x31\x20\x31\x33\x38\x2e\x33\x39\x33\x39\x2c\x36\x39\x2e\ -\x32\x39\x30\x33\x39\x31\x20\x31\x33\x38\x2e\x33\x37\x30\x31\x2c\ -\x31\x30\x37\x2e\x37\x31\x38\x34\x38\x20\x31\x30\x37\x2e\x33\x38\ -\x32\x30\x39\x2c\x31\x33\x38\x2e\x35\x31\x33\x33\x33\x20\x36\x38\ -\x2e\x37\x37\x36\x37\x32\x35\x2c\x31\x33\x38\x2e\x34\x37\x31\x20\ -\x33\x30\x2e\x37\x31\x36\x34\x31\x35\x2c\x31\x33\x38\x2e\x34\x33\ -\x31\x33\x20\x30\x2e\x30\x31\x31\x35\x31\x36\x30\x33\x2c\x31\x30\ -\x37\x2e\x35\x31\x32\x31\x20\x30\x2e\x30\x31\x31\x35\x31\x36\x30\ -\x33\x2c\x36\x39\x2e\x32\x33\x34\x38\x33\x31\x20\x5a\x20\x4d\x20\ -\x38\x33\x2e\x31\x34\x38\x38\x39\x35\x2c\x34\x36\x2e\x36\x38\x37\ -\x30\x34\x31\x20\x63\x20\x30\x2c\x2d\x31\x32\x2e\x33\x33\x32\x32\ -\x33\x20\x2d\x30\x2e\x30\x34\x37\x36\x2c\x2d\x32\x34\x2e\x36\x36\ -\x37\x31\x31\x20\x30\x2e\x30\x34\x37\x36\x2c\x2d\x33\x36\x2e\x39\ -\x39\x39\x33\x34\x31\x36\x20\x30\x2e\x30\x31\x35\x39\x2c\x2d\x31\ -\x2e\x39\x32\x38\x38\x31\x20\x2d\x30\x2e\x35\x32\x39\x31\x36\x2c\ -\x2d\x32\x2e\x33\x38\x31\x32\x35\x20\x2d\x32\x2e\x33\x38\x31\x32\ -\x35\x2c\x2d\x32\x2e\x33\x34\x36\x38\x35\x20\x2d\x37\x2e\x38\x34\ -\x37\x35\x34\x2c\x30\x2e\x31\x30\x33\x31\x39\x20\x2d\x31\x35\x2e\ -\x36\x39\x37\x37\x33\x2c\x30\x2e\x30\x37\x39\x34\x20\x2d\x32\x33\ -\x2e\x35\x34\x37\x39\x31\x2c\x30\x20\x2d\x31\x2e\x35\x31\x36\x30\ -\x37\x2c\x2d\x30\x2e\x30\x31\x33\x32\x20\x2d\x32\x2e\x30\x33\x37\ -\x32\x39\x2c\x30\x2e\x33\x33\x30\x37\x33\x20\x2d\x32\x2e\x30\x33\ -\x34\x36\x35\x2c\x31\x2e\x39\x36\x35\x38\x35\x20\x71\x20\x30\x2e\ -\x30\x38\x32\x2c\x33\x37\x2e\x34\x37\x38\x32\x33\x31\x36\x20\x30\ -\x2c\x37\x34\x2e\x39\x35\x36\x34\x36\x31\x36\x20\x63\x20\x30\x2c\ -\x31\x2e\x36\x34\x33\x30\x36\x20\x30\x2e\x35\x32\x39\x31\x37\x2c\ -\x31\x2e\x39\x34\x37\x33\x34\x20\x32\x2e\x30\x32\x34\x30\x36\x2c\ -\x31\x2e\x39\x33\x34\x31\x31\x20\x37\x2e\x38\x34\x37\x35\x35\x2c\ -\x2d\x30\x2e\x30\x36\x38\x38\x20\x31\x35\x2e\x37\x30\x30\x33\x38\ -\x2c\x2d\x30\x2e\x31\x32\x31\x37\x31\x20\x32\x33\x2e\x35\x34\x37\ -\x39\x32\x2c\x30\x2e\x30\x32\x36\x35\x20\x32\x2e\x31\x31\x36\x36\ -\x37\x2c\x30\x2e\x30\x33\x39\x37\x20\x32\x2e\x34\x31\x33\x2c\x2d\ -\x30\x2e\x36\x36\x36\x37\x35\x20\x32\x2e\x33\x39\x39\x37\x37\x2c\ -\x2d\x32\x2e\x35\x35\x30\x35\x39\x20\x2d\x30\x2e\x30\x39\x37\x39\ -\x2c\x2d\x31\x32\x2e\x33\x32\x31\x36\x34\x20\x2d\x30\x2e\x30\x35\ -\x35\x36\x2c\x2d\x32\x34\x2e\x36\x35\x33\x38\x37\x20\x2d\x30\x2e\ -\x30\x35\x35\x36\x2c\x2d\x33\x36\x2e\x39\x38\x36\x31\x20\x7a\x20\ -\x6d\x20\x2d\x31\x34\x2e\x33\x30\x38\x36\x37\x2c\x38\x30\x2e\x39\ -\x36\x32\x34\x39\x39\x20\x63\x20\x37\x2e\x39\x37\x31\x39\x2c\x30\ -\x2e\x31\x30\x38\x34\x38\x20\x31\x34\x2e\x39\x30\x39\x32\x37\x2c\ -\x2d\x34\x2e\x37\x36\x32\x35\x20\x31\x36\x2e\x37\x33\x37\x35\x34\ -\x2c\x2d\x31\x31\x2e\x37\x35\x35\x34\x34\x20\x32\x2e\x31\x39\x36\ -\x30\x35\x2c\x2d\x38\x2e\x34\x31\x31\x31\x20\x2d\x32\x2e\x36\x36\ -\x37\x2c\x2d\x31\x36\x2e\x37\x35\x38\x37\x30\x37\x20\x2d\x31\x31\ -\x2e\x31\x39\x37\x31\x36\x2c\x2d\x31\x39\x2e\x32\x31\x36\x36\x38\ -\x37\x20\x2d\x39\x2e\x32\x30\x37\x35\x2c\x2d\x32\x2e\x36\x34\x35\ -\x38\x33\x20\x2d\x31\x38\x2e\x39\x37\x38\x35\x37\x2c\x32\x2e\x31\ -\x34\x35\x37\x37\x20\x2d\x32\x31\x2e\x35\x37\x36\x37\x37\x2c\x31\ -\x30\x2e\x35\x39\x36\x35\x36\x37\x20\x2d\x33\x2e\x31\x34\x35\x39\ -\x2c\x31\x30\x2e\x32\x31\x32\x39\x31\x20\x34\x2e\x37\x32\x32\x38\ -\x31\x2c\x32\x30\x2e\x32\x30\x38\x38\x37\x20\x31\x36\x2e\x30\x33\ -\x36\x33\x39\x2c\x32\x30\x2e\x33\x36\x32\x33\x33\x20\x7a\x22\x0a\ -\x20\x20\x20\x20\x20\x69\x64\x3d\x22\x70\x61\x74\x68\x31\x22\x0a\ -\x20\x20\x20\x20\x20\x73\x74\x79\x6c\x65\x3d\x22\x73\x74\x72\x6f\ -\x6b\x65\x2d\x77\x69\x64\x74\x68\x3a\x30\x2e\x32\x36\x34\x35\x38\ -\x33\x22\x20\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\x0a\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\ +\x61\x79\x65\x72\x5f\x31\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\ +\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\ +\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\ +\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x76\ +\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x36\x30\x30\x20\ +\x36\x30\x30\x22\x3e\x0a\x20\x20\x3c\x64\x65\x66\x73\x3e\x0a\x20\ +\x20\x20\x20\x3c\x73\x74\x79\x6c\x65\x3e\x0a\x20\x20\x20\x20\x20\ +\x20\x2e\x63\x6c\x73\x2d\x31\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x66\x69\x6c\x6c\x3a\x20\x23\x65\x30\x65\x30\x64\x66\x3b\ +\x0a\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x73\ +\x74\x79\x6c\x65\x3e\x0a\x20\x20\x3c\x2f\x64\x65\x66\x73\x3e\x0a\ +\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ +\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x36\x32\x2e\x30\x35\ +\x2c\x33\x35\x39\x2e\x35\x31\x63\x32\x34\x2e\x39\x32\x2d\x2e\x32\ +\x32\x2c\x34\x39\x2e\x38\x34\x2d\x2e\x33\x39\x2c\x37\x34\x2e\x37\ +\x35\x2e\x30\x38\x2c\x36\x2e\x37\x34\x2e\x31\x33\x2c\x37\x2e\x36\ +\x37\x2d\x32\x2e\x31\x32\x2c\x37\x2e\x36\x32\x2d\x38\x2e\x31\x2d\ +\x2e\x32\x37\x2d\x33\x39\x2e\x31\x35\x2d\x2e\x31\x34\x2d\x37\x38\ +\x2e\x33\x2d\x2e\x31\x34\x2d\x31\x31\x37\x2e\x34\x35\x73\x2d\x2e\ +\x31\x35\x2d\x37\x38\x2e\x33\x2e\x31\x35\x2d\x31\x31\x37\x2e\x34\ +\x35\x63\x2e\x30\x35\x2d\x36\x2e\x31\x33\x2d\x31\x2e\x37\x31\x2d\ +\x37\x2e\x35\x33\x2d\x37\x2e\x36\x2d\x37\x2e\x34\x36\x2d\x32\x34\ +\x2e\x39\x32\x2e\x33\x33\x2d\x34\x39\x2e\x38\x34\x2e\x32\x36\x2d\ +\x37\x34\x2e\x37\x36\x2e\x30\x34\x2d\x34\x2e\x38\x32\x2d\x2e\x30\ +\x34\x2d\x36\x2e\x34\x37\x2c\x31\x2e\x30\x35\x2d\x36\x2e\x34\x36\ +\x2c\x36\x2e\x32\x34\x2e\x31\x37\x2c\x37\x39\x2e\x33\x32\x2e\x31\ +\x37\x2c\x31\x35\x38\x2e\x36\x34\x2c\x30\x2c\x32\x33\x37\x2e\x39\ +\x36\x2d\x2e\x30\x31\x2c\x35\x2e\x32\x32\x2c\x31\x2e\x36\x36\x2c\ +\x36\x2e\x31\x39\x2c\x36\x2e\x34\x33\x2c\x36\x2e\x31\x34\x5a\x22\ +\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ +\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x35\x2e\ +\x37\x36\x2c\x33\x30\x35\x2e\x36\x32\x63\x30\x2c\x31\x35\x31\x2e\ +\x37\x34\x2c\x31\x32\x31\x2e\x37\x31\x2c\x32\x37\x34\x2e\x32\x39\ +\x2c\x32\x37\x32\x2e\x35\x37\x2c\x32\x37\x34\x2e\x34\x36\x2c\x31\ +\x35\x33\x2e\x30\x33\x2e\x31\x36\x2c\x32\x37\x35\x2e\x38\x36\x2d\ +\x31\x32\x31\x2e\x39\x2c\x32\x37\x35\x2e\x39\x36\x2d\x32\x37\x34\ +\x2e\x32\x34\x2e\x31\x2d\x31\x35\x30\x2e\x32\x36\x2d\x31\x32\x32\ +\x2e\x34\x2d\x32\x37\x32\x2e\x35\x36\x2d\x32\x37\x31\x2e\x30\x32\ +\x2d\x32\x37\x34\x2e\x36\x33\x43\x31\x35\x30\x2e\x33\x35\x2c\x32\ +\x39\x2e\x30\x38\x2c\x32\x32\x2e\x39\x35\x2c\x31\x35\x37\x2e\x32\ +\x2c\x32\x35\x2e\x37\x36\x2c\x33\x30\x35\x2e\x36\x32\x5a\x4d\x33\ +\x30\x32\x2e\x36\x32\x2c\x38\x35\x2e\x38\x35\x63\x31\x31\x39\x2e\ +\x30\x33\x2c\x31\x2e\x36\x36\x2c\x32\x31\x37\x2e\x31\x33\x2c\x39\ +\x39\x2e\x36\x31\x2c\x32\x31\x37\x2e\x30\x36\x2c\x32\x31\x39\x2e\ +\x39\x35\x2d\x2e\x30\x38\x2c\x31\x32\x32\x2d\x39\x38\x2e\x34\x35\ +\x2c\x32\x31\x39\x2e\x37\x36\x2d\x32\x32\x31\x2e\x30\x31\x2c\x32\ +\x31\x39\x2e\x36\x33\x2d\x31\x32\x30\x2e\x38\x32\x2d\x2e\x31\x33\ +\x2d\x32\x31\x38\x2e\x33\x2d\x39\x38\x2e\x32\x38\x2d\x32\x31\x38\ +\x2e\x33\x2d\x32\x31\x39\x2e\x38\x31\x2d\x32\x2e\x32\x35\x2d\x31\ +\x31\x38\x2e\x38\x37\x2c\x39\x39\x2e\x37\x38\x2d\x32\x32\x31\x2e\ +\x34\x37\x2c\x32\x32\x32\x2e\x32\x35\x2d\x32\x31\x39\x2e\x37\x37\ +\x5a\x22\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ +\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\ +\x39\x38\x2e\x38\x37\x2c\x34\x39\x31\x2e\x30\x33\x63\x32\x35\x2e\ +\x33\x31\x2e\x33\x35\x2c\x34\x37\x2e\x33\x33\x2d\x31\x35\x2e\x31\ +\x32\x2c\x35\x33\x2e\x31\x33\x2d\x33\x37\x2e\x33\x32\x2c\x36\x2e\ +\x39\x38\x2d\x32\x36\x2e\x37\x2d\x38\x2e\x34\x36\x2d\x35\x33\x2e\ +\x32\x2d\x33\x35\x2e\x35\x35\x2d\x36\x31\x2e\x30\x31\x2d\x32\x39\ +\x2e\x32\x33\x2d\x38\x2e\x34\x32\x2d\x36\x30\x2e\x32\x35\x2c\x36\ +\x2e\x38\x31\x2d\x36\x38\x2e\x35\x2c\x33\x33\x2e\x36\x34\x2d\x39\ +\x2e\x39\x38\x2c\x33\x32\x2e\x34\x36\x2c\x31\x34\x2e\x39\x39\x2c\ +\x36\x34\x2e\x31\x39\x2c\x35\x30\x2e\x39\x31\x2c\x36\x34\x2e\x36\ +\x38\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x05\xf3\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -26193,39 +26128,39 @@ \x00\x00\x11\xfc\x00\x00\x00\x00\x00\x01\x00\x05\x5e\x40\ \x00\x00\x12\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x63\x15\ \x00\x00\x12\x32\x00\x00\x00\x00\x00\x01\x00\x05\x64\x05\ -\x00\x00\x12\x48\x00\x00\x00\x00\x00\x01\x00\x05\x66\xe9\ -\x00\x00\x12\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x6d\x20\ -\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x05\x82\x2d\ -\x00\x00\x12\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x87\x1d\ -\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x8a\x1c\ -\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x90\x26\ -\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x05\x93\x75\ -\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x9c\xf6\ -\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\xa2\xed\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\xa4\xfe\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xc1\ -\x00\x00\x13\x88\x00\x00\x00\x00\x00\x01\x00\x05\xb2\x75\ -\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xb5\xc3\ -\x00\x00\x13\xd4\x00\x00\x00\x00\x00\x01\x00\x05\xb9\x51\ -\x00\x00\x13\xfa\x00\x00\x00\x00\x00\x01\x00\x05\xbd\xf2\ -\x00\x00\x14\x0e\x00\x00\x00\x00\x00\x01\x00\x05\xc7\xc4\ -\x00\x00\x14\x3a\x00\x00\x00\x00\x00\x01\x00\x05\xcd\x0e\ -\x00\x00\x14\x62\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x15\ -\x00\x00\x14\x78\x00\x00\x00\x00\x00\x01\x00\x05\xd3\xf9\ -\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x42\ -\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xdc\xf2\ -\x00\x00\x14\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xe0\x36\ -\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xe1\x62\ -\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x05\xe7\x23\ -\x00\x00\x15\x2a\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x45\ -\x00\x00\x15\x48\x00\x00\x00\x00\x00\x01\x00\x05\xee\x38\ -\x00\x00\x15\x68\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x3c\ -\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xf2\x5d\ -\x00\x00\x15\xaa\x00\x00\x00\x00\x00\x01\x00\x05\xf5\x31\ -\x00\x00\x15\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xfd\x9d\ -\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x06\x05\x5d\ -\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x06\x0a\x78\ -\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x0b\xcb\ +\x00\x00\x12\x48\x00\x00\x00\x00\x00\x01\x00\x05\x68\x2e\ +\x00\x00\x12\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x6e\x65\ +\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x05\x83\x72\ +\x00\x00\x12\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x88\x62\ +\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x8b\x61\ +\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x91\x6b\ +\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x05\x94\xba\ +\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x98\xdb\ +\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\x9e\xd2\ +\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\xe3\ +\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\xa4\xa6\ +\x00\x00\x13\x88\x00\x00\x00\x00\x00\x01\x00\x05\xae\x5a\ +\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xa8\ +\x00\x00\x13\xd4\x00\x00\x00\x00\x00\x01\x00\x05\xb5\x36\ +\x00\x00\x13\xfa\x00\x00\x00\x00\x00\x01\x00\x05\xb9\xd7\ +\x00\x00\x14\x0e\x00\x00\x00\x00\x00\x01\x00\x05\xc3\xa9\ +\x00\x00\x14\x3a\x00\x00\x00\x00\x00\x01\x00\x05\xc8\xf3\ +\x00\x00\x14\x62\x00\x00\x00\x00\x00\x01\x00\x05\xce\xfa\ +\x00\x00\x14\x78\x00\x00\x00\x00\x00\x01\x00\x05\xcf\xde\ +\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x05\xd2\x27\ +\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xd8\xd7\ +\x00\x00\x14\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xdc\x1b\ +\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xdd\x47\ +\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x05\xe3\x08\ +\x00\x00\x15\x2a\x00\x00\x00\x00\x00\x01\x00\x05\xe4\x2a\ +\x00\x00\x15\x48\x00\x00\x00\x00\x00\x01\x00\x05\xea\x1d\ +\x00\x00\x15\x68\x00\x00\x00\x00\x00\x01\x00\x05\xed\x21\ +\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xee\x42\ +\x00\x00\x15\xaa\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x16\ +\x00\x00\x15\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xf9\x82\ +\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x06\x01\x42\ +\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x06\x06\x5d\ +\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x07\xb0\ " qt_resource_struct_v2 = b"\ @@ -26266,75 +26201,75 @@ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x12\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x01\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x08\x66\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x01\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x09\x52\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x02\x20\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x35\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x02\x48\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x18\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x02\x70\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x06\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x02\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x14\x42\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x02\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf7\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x02\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x22\x41\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x01\x9b\x09\x08\xf9\x49\ \x00\x00\x03\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x26\x90\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x03\x36\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x6e\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x03\x52\x00\x00\x00\x00\x00\x01\x00\x00\x30\xca\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x03\x7a\x00\x00\x00\x00\x00\x01\x00\x00\x32\xb1\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x21\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x03\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x3a\x55\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ \x00\x00\x03\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x3c\xcf\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x03\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x3f\x43\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ \x00\x00\x03\xfe\x00\x00\x00\x00\x00\x01\x00\x00\x41\xbf\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x26\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x27\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x04\x20\x00\x00\x00\x00\x00\x01\x00\x00\x44\x3b\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x04\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x45\x3c\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x04\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x4a\x54\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ \x00\x00\x04\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x56\x16\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x04\xc4\x00\x00\x00\x00\x00\x01\x00\x00\x58\x86\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ \x00\x00\x04\xea\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x1e\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ \x00\x00\x05\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x62\x7a\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x05\x48\x00\x00\x00\x00\x00\x01\x00\x00\x69\x07\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x05\x64\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x03\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x05\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x6d\x01\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x32\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x33\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x05\xbe\x00\x01\x00\x00\x00\x01\x00\x00\x77\x6b\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ \x00\x00\x05\xd0\x00\x00\x00\x00\x00\x01\x00\x02\x3a\xc0\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x02\x00\x00\x00\x36\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x06\x00\x00\x00\x3f\ @@ -26342,275 +26277,275 @@ \x00\x00\x05\xe4\x00\x02\x00\x00\x00\x07\x00\x00\x00\x38\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x05\xf6\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ -\x00\x00\x01\x9a\x72\xe1\x95\x8f\ +\x00\x00\x01\x9b\x09\x08\xfa\x11\ \x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x49\xaa\ -\x00\x00\x01\x9a\x72\xe1\x95\x93\ +\x00\x00\x01\x9b\x09\x08\xfa\x11\ \x00\x00\x06\x5e\x00\x00\x00\x00\x00\x01\x00\x02\x53\xd3\ -\x00\x00\x01\x9a\x72\xe1\x95\x8f\ +\x00\x00\x01\x9b\x09\x08\xfa\x11\ \x00\x00\x06\x98\x00\x00\x00\x00\x00\x01\x00\x02\x5e\x2b\ -\x00\x00\x01\x9a\x72\xe1\x95\x93\ +\x00\x00\x01\x9b\x09\x08\xfa\x11\ \x00\x00\x06\xcc\x00\x00\x00\x00\x00\x01\x00\x02\x68\x43\ -\x00\x00\x01\x9a\x72\xe1\x95\x8f\ +\x00\x00\x01\x9b\x09\x08\xfa\x11\ \x00\x00\x07\x02\x00\x00\x00\x00\x00\x01\x00\x02\x72\x6c\ -\x00\x00\x01\x9a\x72\xe1\x95\x93\ +\x00\x00\x01\x9b\x09\x08\xfa\x11\ \x00\x00\x07\x3a\x00\x00\x00\x00\x00\x01\x00\x02\x7c\x82\ -\x00\x00\x01\x9a\x72\xe1\x95\x93\ +\x00\x00\x01\x9b\x09\x08\xfa\x11\ \x00\x00\x07\x70\x00\x00\x00\x00\x00\x01\x00\x02\x86\xe8\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x07\x98\x00\x00\x00\x00\x00\x01\x00\x02\x91\x17\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ \x00\x00\x07\xc4\x00\x00\x00\x00\x00\x01\x00\x02\x9b\x5e\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x08\x00\x00\x00\x00\x00\x00\x01\x00\x02\x9d\x5f\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x08\x2c\x00\x00\x00\x00\x00\x01\x00\x02\xb8\xa4\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x08\x58\x00\x00\x00\x00\x00\x01\x00\x02\xbd\xed\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x46\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x03\x00\x00\x00\x47\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x08\x8c\x00\x00\x00\x00\x00\x01\x00\x02\xc1\x65\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x02\xca\x47\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x02\xcf\x7c\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4b\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x4c\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x08\xd8\x00\x00\x00\x00\x00\x01\x00\x02\xd9\x51\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x09\x00\x00\x00\x00\x00\x00\x01\x00\x02\xde\x4c\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x09\x38\x00\x00\x00\x00\x00\x01\x00\x02\xe7\x20\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x09\x70\x00\x00\x00\x00\x00\x01\x00\x02\xef\xc4\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x09\xa0\x00\x00\x00\x00\x00\x01\x00\x02\xf7\x63\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x09\xc4\x00\x00\x00\x00\x00\x01\x00\x02\xff\x3f\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x09\xe8\x00\x00\x00\x00\x00\x01\x00\x03\x06\xf5\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x0a\x1c\x00\x00\x00\x00\x00\x01\x00\x03\x0f\x03\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x0a\x50\x00\x00\x00\x00\x00\x01\x00\x03\x16\xe5\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x0a\x84\x00\x00\x00\x00\x00\x01\x00\x03\x1e\xaf\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x0a\xbe\x00\x00\x00\x00\x00\x01\x00\x03\x26\x16\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x0a\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x29\xd3\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x0b\x10\x00\x00\x00\x00\x00\x01\x00\x03\x2d\xac\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5a\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x08\x00\x00\x00\x5b\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0b\x36\x00\x00\x00\x00\x00\x01\x00\x03\x33\xb5\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ \x00\x00\x0b\x62\x00\x00\x00\x00\x00\x01\x00\x03\x56\x39\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ \x00\x00\x0b\x90\x00\x00\x00\x00\x00\x01\x00\x03\x5c\x26\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x0b\xba\x00\x00\x00\x00\x00\x01\x00\x03\x5e\x46\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x0b\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x66\xde\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ \x00\x00\x0b\xfc\x00\x00\x00\x00\x00\x01\x00\x03\x76\x27\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ \x00\x00\x0c\x28\x00\x00\x00\x00\x00\x01\x00\x03\x7c\xa5\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ \x00\x00\x0c\x50\x00\x00\x00\x00\x00\x01\x00\x03\x87\x1f\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x64\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x65\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0c\x86\x00\x00\x00\x00\x00\x01\x00\x03\x90\x66\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x01\x9b\x09\x08\xf9\x49\ \x00\x00\x0c\xb4\x00\x00\x00\x00\x00\x01\x00\x03\x93\x0d\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x0c\xe2\x00\x01\x00\x00\x00\x01\x00\x03\x9f\x8a\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x0d\x0e\x00\x00\x00\x00\x00\x01\x00\x03\xcd\x09\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x01\x9b\x09\x08\xf9\x49\ \x00\x00\x0d\x2e\x00\x00\x00\x00\x00\x01\x00\x03\xd1\xc0\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x01\x9b\x09\x08\xf9\x49\ \x00\x00\x0d\x60\x00\x01\x00\x00\x00\x01\x00\x04\x2a\xb9\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x01\x9b\x09\x08\xf9\x49\ \x00\x00\x0d\x92\x00\x00\x00\x00\x00\x01\x00\x04\x5f\x53\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x0d\xac\x00\x00\x00\x00\x00\x01\x00\x04\x64\x9d\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x0d\xc6\x00\x00\x00\x00\x00\x01\x00\x04\x6a\x2c\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x0d\xe0\x00\x00\x00\x00\x00\x01\x00\x04\x6f\x95\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ \x00\x00\x0d\xf8\x00\x00\x00\x00\x00\x01\x00\x04\x7b\x73\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x0e\x16\x00\x00\x00\x00\x00\x01\x00\x04\x81\x77\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x72\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x73\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0e\x3e\x00\x00\x00\x00\x00\x01\x00\x04\x86\xac\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x0e\x52\x00\x00\x00\x00\x00\x01\x00\x04\x8c\xa9\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x0e\x64\x00\x00\x00\x00\x00\x01\x00\x04\x8e\x2f\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x0e\x76\x00\x00\x00\x00\x00\x01\x00\x04\x94\x29\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x78\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x79\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0e\x8a\x00\x00\x00\x00\x00\x01\x00\x04\x96\x7f\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x0e\xb6\x00\x00\x00\x00\x00\x01\x00\x04\x9d\x66\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7c\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x09\x00\x00\x00\x7d\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0e\xda\x00\x00\x00\x00\x00\x01\x00\x04\xa6\xca\ -\x00\x00\x01\x9b\x13\x3e\xbb\x62\ +\x00\x00\x01\x9b\x09\x08\xf9\x49\ \x00\x00\x0e\xfa\x00\x01\x00\x00\x00\x01\x00\x04\xa9\x76\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ \x00\x00\x0f\x1e\x00\x00\x00\x00\x00\x01\x00\x04\xb4\xc7\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ \x00\x00\x0f\x40\x00\x00\x00\x00\x00\x01\x00\x04\xbc\x6c\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x0f\x5c\x00\x00\x00\x00\x00\x01\x00\x04\xcd\x6c\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ \x00\x00\x0f\x80\x00\x00\x00\x00\x00\x01\x00\x04\xd6\xed\ -\x00\x00\x01\x9b\x13\x3e\xbb\x62\ +\x00\x00\x01\x9b\x09\x08\xf9\x49\ \x00\x00\x0f\xa0\x00\x00\x00\x00\x00\x01\x00\x04\xdc\x8a\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ \x00\x00\x0f\xc8\x00\x00\x00\x00\x00\x01\x00\x04\xe6\x53\ -\x00\x00\x01\x9b\x13\x3e\xbb\x62\ +\x00\x00\x01\x9b\x09\x08\xf9\x49\ \x00\x00\x0f\xe8\x00\x00\x00\x00\x00\x01\x00\x04\xea\x36\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x87\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x88\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x10\x04\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x71\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ \x00\x00\x10\x34\x00\x00\x00\x00\x00\x01\x00\x04\xfa\x07\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ \x00\x00\x10\x64\x00\x00\x00\x00\x00\x01\x00\x05\x05\xe3\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ \x00\x00\x10\x88\x00\x00\x00\x00\x00\x01\x00\x05\x0c\x27\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ \x00\x00\x10\xb2\x00\x00\x00\x00\x00\x01\x00\x05\x13\xb0\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x10\xde\x00\x00\x00\x00\x00\x01\x00\x05\x1a\x0e\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ \x00\x00\x11\x14\x00\x00\x00\x00\x00\x01\x00\x05\x21\xfd\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x2f\xa0\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x34\xd5\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x11\x32\x00\x00\x00\x00\x00\x01\x00\x05\x3e\xaa\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x11\x5a\x00\x00\x00\x00\x00\x01\x00\x05\x46\x74\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ \x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x96\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\x98\x00\x02\x00\x00\x00\x28\x00\x00\x00\x97\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x11\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x4b\x3a\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ \x00\x00\x11\x90\x00\x00\x00\x00\x00\x01\x00\x05\x52\xee\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x55\x13\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x11\xe0\x00\x00\x00\x00\x00\x01\x00\x05\x56\x93\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ \x00\x00\x11\xfc\x00\x00\x00\x00\x00\x01\x00\x05\x5e\x40\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ \x00\x00\x12\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x63\x15\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ \x00\x00\x12\x32\x00\x00\x00\x00\x00\x01\x00\x05\x64\x05\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x12\x48\x00\x00\x00\x00\x00\x01\x00\x05\x66\xe9\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x6d\x20\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x05\x82\x2d\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x12\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x87\x1d\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x8a\x1c\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x90\x26\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x05\x93\x75\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x9c\xf6\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\xa2\xed\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\xa4\xfe\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xc1\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x13\x88\x00\x00\x00\x00\x00\x01\x00\x05\xb2\x75\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xb5\xc3\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\xd4\x00\x00\x00\x00\x00\x01\x00\x05\xb9\x51\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x13\xfa\x00\x00\x00\x00\x00\x01\x00\x05\xbd\xf2\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x0e\x00\x00\x00\x00\x00\x01\x00\x05\xc7\xc4\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x14\x3a\x00\x00\x00\x00\x00\x01\x00\x05\xcd\x0e\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x62\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x15\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x78\x00\x00\x00\x00\x00\x01\x00\x05\xd3\xf9\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x42\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xdc\xf2\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xe0\x36\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xe1\x62\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x05\xe7\x23\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x2a\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x45\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x48\x00\x00\x00\x00\x00\x01\x00\x05\xee\x38\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x15\x68\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x3c\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xf2\x5d\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\xaa\x00\x00\x00\x00\x00\x01\x00\x05\xf5\x31\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x15\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xfd\x9d\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x06\x05\x5d\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x06\x0a\x78\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x0b\xcb\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x01\x9b\x8e\xa8\x75\xac\ +\x00\x00\x12\x48\x00\x00\x00\x00\x00\x01\x00\x05\x68\x2e\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ +\x00\x00\x12\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x6e\x65\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ +\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x05\x83\x72\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ +\x00\x00\x12\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x88\x62\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ +\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x8b\x61\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ +\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x91\x6b\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ +\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x05\x94\xba\ +\x00\x00\x01\x9b\x8e\xa8\x75\x96\ +\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x98\xdb\ +\x00\x00\x01\x9b\x09\x08\xf9\x49\ +\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\x9e\xd2\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ +\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\xe3\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ +\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\xa4\xa6\ +\x00\x00\x01\x9b\x09\x08\xf9\x49\ +\x00\x00\x13\x88\x00\x00\x00\x00\x00\x01\x00\x05\xae\x5a\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ +\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xa8\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ +\x00\x00\x13\xd4\x00\x00\x00\x00\x00\x01\x00\x05\xb5\x36\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ +\x00\x00\x13\xfa\x00\x00\x00\x00\x00\x01\x00\x05\xb9\xd7\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ +\x00\x00\x14\x0e\x00\x00\x00\x00\x00\x01\x00\x05\xc3\xa9\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ +\x00\x00\x14\x3a\x00\x00\x00\x00\x00\x01\x00\x05\xc8\xf3\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ +\x00\x00\x14\x62\x00\x00\x00\x00\x00\x01\x00\x05\xce\xfa\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ +\x00\x00\x14\x78\x00\x00\x00\x00\x00\x01\x00\x05\xcf\xde\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ +\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x05\xd2\x27\ +\x00\x00\x01\x9b\x09\x08\xf9\x59\ +\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xd8\xd7\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ +\x00\x00\x14\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xdc\x1b\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ +\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xdd\x47\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ +\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x05\xe3\x08\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ +\x00\x00\x15\x2a\x00\x00\x00\x00\x00\x01\x00\x05\xe4\x2a\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ +\x00\x00\x15\x48\x00\x00\x00\x00\x00\x01\x00\x05\xea\x1d\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ +\x00\x00\x15\x68\x00\x00\x00\x00\x00\x01\x00\x05\xed\x21\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ +\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xee\x42\ +\x00\x00\x01\x9b\x09\x08\xf9\x55\ +\x00\x00\x15\xaa\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x16\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ +\x00\x00\x15\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xf9\x82\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ +\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x06\x01\x42\ +\x00\x00\x01\x9b\x09\x08\xf9\x51\ +\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x06\x06\x5d\ +\x00\x00\x01\x9b\x09\x08\xf9\x4d\ +\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x07\xb0\ +\x00\x00\x01\x9b\x09\x08\xf9\x49\ " qt_version = [int(v) for v in QtCore.qVersion().split('.')] diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/error.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/error.svg index fbf6eca0..0bb1f19f 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/error.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/error.svg @@ -1,47 +1,13 @@ - - - - + + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/info.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/info.svg index 2404aa83..6a4426ae 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/info.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/info.svg @@ -1 +1,13 @@ - \ No newline at end of file + + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/utils/icon_button.py b/BlocksScreen/lib/utils/icon_button.py index 160f1f80..3880d285 100644 --- a/BlocksScreen/lib/utils/icon_button.py +++ b/BlocksScreen/lib/utils/icon_button.py @@ -29,6 +29,11 @@ def setPixmap(self, pixmap: QtGui.QPixmap) -> None: self.icon_pixmap = pixmap self.repaint() + def clearPixmap(self) -> None: + """Clear widget pixmap""" + self.icon_pixmap = QtGui.QPixmap() + self.repaint() + def setText(self, text: str) -> None: """Set widget text""" self._text = text From dd13e39e76f4bd455f94bad39e2a71712fc763f1 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Wed, 14 Jan 2026 11:13:09 +0000 Subject: [PATCH 30/70] =?UTF-8?q?Improvement/Apply=20Z=E2=80=91offset=20ch?= =?UTF-8?q?anges=20immediately,=20with=20an=20option=20to=20save=20permane?= =?UTF-8?q?ntly=20(#149)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * babystepPage: make buttons update instantly and respond to all Z-offset changes * babystepPage and printTab: bugfix showing the current saved z-offset * printTab: change save message * add missing icons from merge --------- Co-authored-by: Guilherme Costa --- BlocksScreen/lib/panels/printTab.py | 108 +- .../lib/panels/widgets/babystepPage.py | 97 +- .../lib/panels/widgets/jobStatusPage.py | 5 + .../lib/ui/resources/icon_resources.qrc | 4 + .../lib/ui/resources/icon_resources_rc.py | 1153 +++++++++-------- .../media/btn_icons/move_nozzle_away.svg | 15 + .../media/btn_icons/move_nozzle_close.svg | 12 + 7 files changed, 793 insertions(+), 601 deletions(-) create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/move_nozzle_away.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/move_nozzle_close.svg diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index 2a885362..6331585a 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -1,24 +1,28 @@ +import logging import os import typing from functools import partial -from lib.panels.widgets.babystepPage import BabystepPage -from lib.panels.widgets.tunePage import TuneWidget +from configfile import BlocksScreenConfig, get_configparser from lib.files import Files from lib.moonrakerComm import MoonWebSocket +from lib.panels.widgets.babystepPage import BabystepPage +from lib.panels.widgets.basePopup import BasePopup from lib.panels.widgets.confirmPage import ConfirmWidget from lib.panels.widgets.filesPage import FilesPage from lib.panels.widgets.jobStatusPage import JobStatusWidget +from lib.panels.widgets.loadWidget import LoadingOverlayWidget +from lib.panels.widgets.numpadPage import CustomNumpad from lib.panels.widgets.sensorsPanel import SensorsWindow -from lib.printer import Printer from lib.panels.widgets.slider_selector_page import SliderPage +from lib.panels.widgets.tunePage import TuneWidget +from lib.printer import Printer from lib.utils.blocks_button import BlocksCustomButton -from lib.panels.widgets.numpadPage import CustomNumpad -from lib.panels.widgets.loadWidget import LoadingOverlayWidget -from lib.panels.widgets.basePopup import BasePopup -from configfile import BlocksScreenConfig, get_configparser +from lib.utils.display_button import DisplayButton from PyQt6 import QtCore, QtGui, QtWidgets +logger = logging.getLogger(name="logs/BlocksScreen.log") + class PrintTab(QtWidgets.QStackedWidget): """QStackedWidget that contains the following widget panels: @@ -61,6 +65,8 @@ class PrintTab(QtWidgets.QStackedWidget): ) _z_offset: float = 0.0 + _active_z_offset: float = 0.0 + _finish_print_handled: bool = False def __init__( self, @@ -97,6 +103,7 @@ def __init__( self.addWidget(self.filesPage_widget) self.BasePopup = BasePopup(self) + self.BasePopup_z_offset = BasePopup(self, floating=True) self.confirmPage_widget = ConfirmWidget(self) self.addWidget(self.confirmPage_widget) @@ -137,7 +144,6 @@ def __init__( lambda: self.change_page(self.indexOf(self.jobStatusPage_widget)) ) self.jobStatusPage_widget.hide_request.connect( - # lambda: self.change_page(self.indexOf(self.panel.print_page)) lambda: self.change_page(self.indexOf(self.print_page)) ) self.jobStatusPage_widget.request_file_info.connect( @@ -148,6 +154,7 @@ def __init__( self.jobStatusPage_widget.print_resume.connect(self.ws.api.resume_print) self.jobStatusPage_widget.print_cancel.connect(self.handle_cancel_print) self.jobStatusPage_widget.print_pause.connect(self.ws.api.pause_print) + self.jobStatusPage_widget.print_finish.connect(self.finish_print_signal) self.jobStatusPage_widget.request_query_print_stats.connect( self.ws.api.object_query ) @@ -176,6 +183,7 @@ def __init__( self.printer.gcode_move_update[str, list].connect( self.jobStatusPage_widget.on_gcode_move_update ) + self.printer.request_available_objects_signal.connect(self.klipper_ready_signal) self.babystepPage = BabystepPage(self) self.babystepPage.request_back.connect(self.back_button) self.addWidget(self.babystepPage) @@ -203,6 +211,7 @@ def __init__( self.printer.gcode_move_update[str, list].connect( self.babystepPage.on_gcode_move_update ) + self.printer.gcode_move_update[str, list].connect(self.activate_save_button) self.tune_page.run_gcode.connect(self.ws.api.run_gcode) self.tune_page.request_sliderPage[str, int, "PyQt_PyObject"].connect( self.on_slidePage_request @@ -245,6 +254,8 @@ def __init__( self.run_gcode_signal.connect(self.ws.api.run_gcode) self.confirmPage_widget.on_delete.connect(self.delete_file) self.change_page(self.indexOf(self.print_page)) # force set the initial page + self.save_config_btn.clicked.connect(self.save_config) + self.BasePopup_z_offset.accepted.connect(self.update_configuration_file) @QtCore.pyqtSlot(str, dict, name="on_print_stats_update") @QtCore.pyqtSlot(str, float, name="on_print_stats_update") @@ -305,6 +316,38 @@ def delete_file(self, filename: str, directory: str = "gcodes") -> None: ) self.BasePopup.open() + def save_config(self) -> None: + """Handle Save configuration behaviour, shows confirmation dialog""" + if self._finish_print_handled: + self.run_gcode_signal.emit("Z_OFFSET_APPLY_PROBE") + self._z_offset = self._active_z_offset + self.babystepPage.bbp_z_offset_title_label.setText( + f"Z: {self._z_offset:.3f}mm" + ) + self.BasePopup_z_offset.set_message( + f"The Z‑Offset is now {self._active_z_offset:.3f} mm.\n" + "Would you like to save this change permanently?\n" + "The machine will restart." + ) + self.BasePopup_z_offset.cancel_button_text("Later") + self.BasePopup_z_offset.open() + + def update_configuration_file(self): + """Runs the `SAVE_CONFIG` gcode""" + self.run_gcode_signal.emit("Z_OFFSET_APPLY_PROBE") + self.run_gcode_signal.emit("SAVE_CONFIG") + self.BasePopup_z_offset.disconnect() + + @QtCore.pyqtSlot(str, list, name="activate_save_button") + def activate_save_button(self, name: str, value: list) -> None: + """Sync the `Save config` popup with the save_config_pending state""" + if not value: + return + + if name == "homing_origin": + self._active_z_offset = value[2] + self.save_config_btn.setVisible(value[2] != 0) + def _on_delete_file_confirmed(self, filename: str, directory: str) -> None: """Handle confirmed file deletion after user accepted the dialog""" self.file_data.on_request_delete_file(filename, directory) @@ -312,19 +355,6 @@ def _on_delete_file_confirmed(self, filename: str, directory: str) -> None: self.filesPage_widget.reset_dir() self.BasePopup.disconnect() - def paintEvent(self, a0: QtGui.QPaintEvent) -> None: - """Widget painting""" - if self.babystepPage.isVisible(): - _button_name_str = f"nozzle_offset_{self._z_offset}" - if hasattr(self, _button_name_str): - _button_attr = getattr(self, _button_name_str) - if callable(_button_attr) and isinstance( - _button_attr, BlocksCustomButton - ): - _button_attr.setChecked(True) - - return super().paintEvent(a0) - def setProperty(self, name: str, value: typing.Any) -> bool: """Intercept the set property method @@ -359,6 +389,21 @@ def back_button(self) -> None: """Goes back to the previous page""" self.request_back.emit() + @QtCore.pyqtSlot(name="klipper_ready_signal") + def klipper_ready_signal(self) -> None: + """React to klipper ready signal""" + self.babystepPage.baby_stepchange = False + self._finish_print_handled = False + + @QtCore.pyqtSlot(name="finish_print_signal") + def finish_print_signal(self) -> None: + """Behaviour when the print ends — but only once.""" + if self._finish_print_handled: + return + if self._active_z_offset != 0 and self.babystepPage.baby_stepchange: + self.save_config() + self._finish_print_handled = True + def setupMainPrintPage(self) -> None: """Setup UI for print page""" self.setObjectName("printStackedWidget") @@ -423,6 +468,27 @@ def setupMainPrintPage(self) -> None: "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/print.svg") ) self.main_print_btn.setObjectName("main_print_btn") + self.save_config_btn = DisplayButton(parent=self.print_page) + self.save_config_btn.setGeometry(QtCore.QRect(540, 20, 170, 50)) + font.setPointSize(8) + font.setFamily("Montserrat") + self.save_config_btn.setFont(font) + self.save_config_btn.setMouseTracking(False) + self.save_config_btn.setTabletTracking(True) + self.save_config_btn.setContextMenuPolicy( + QtCore.Qt.ContextMenuPolicy.NoContextMenu + ) + self.save_config_btn.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/save.svg") + ) + self.save_config_btn.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.save_config_btn.setStyleSheet("") + self.save_config_btn.setAutoDefault(False) + self.save_config_btn.setFlat(True) + self.save_config_btn.setMinimumSize(QtCore.QSize(170, 50)) + self.save_config_btn.setMaximumSize(QtCore.QSize(170, 50)) + self.save_config_btn.setText("Save\nZ-Offset") + self.save_config_btn.hide() self.main_text_label = QtWidgets.QLabel(parent=self.print_page) self.main_text_label.setEnabled(True) self.main_text_label.setGeometry(QtCore.QRect(105, 180, 500, 200)) diff --git a/BlocksScreen/lib/panels/widgets/babystepPage.py b/BlocksScreen/lib/panels/widgets/babystepPage.py index 6505c0aa..273e8f9c 100644 --- a/BlocksScreen/lib/panels/widgets/babystepPage.py +++ b/BlocksScreen/lib/panels/widgets/babystepPage.py @@ -1,9 +1,8 @@ import typing -from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_label import BlocksLabel -from lib.utils.icon_button import IconButton from lib.utils.check_button import BlocksCustomCheckButton +from lib.utils.icon_button import IconButton from PyQt6 import QtCore, QtGui, QtWidgets @@ -33,8 +32,18 @@ def __init__(self, parent) -> None: self.bbp_nozzle_offset_025.toggled.connect(self.handle_z_offset_change) self.bbp_nozzle_offset_05.toggled.connect(self.handle_z_offset_change) self.bbp_nozzle_offset_1.toggled.connect(self.handle_z_offset_change) + self._baby_stepchange = False - self.savebutton.clicked.connect(self.save_value) + @property + def baby_stepchange(self): + """Returns if the babystep was changed during print""" + return self._baby_stepchange + + @baby_stepchange.setter + def baby_stepchange(self, value: bool) -> None: + if not isinstance(value, bool): + raise ValueError("Value must be a bool") + self._baby_stepchange = value @QtCore.pyqtSlot(name="on_move_nozzle_close") def on_move_nozzle_close(self) -> None: @@ -42,9 +51,9 @@ def on_move_nozzle_close(self) -> None: by the amount set in **` self._z_offset`** """ self.run_gcode.emit( - f"SET_GCODE_OFFSET Z_ADJUST=-{self._z_offset}" # Z_ADJUST adds the value to the existing offset + f"SET_GCODE_OFFSET Z_ADJUST=-{self._z_offset} MOVE=1" # Z_ADJUST adds the value to the existing offset ) - self.savebutton.setVisible(True) + self._baby_stepchange = True @QtCore.pyqtSlot(name="on_move_nozzle_away") def on_move_nozzle_away(self) -> None: @@ -52,9 +61,9 @@ def on_move_nozzle_away(self) -> None: bed by **` self._z_offset`** amount """ self.run_gcode.emit( - f"SET_GCODE_OFFSET Z_ADJUST=+{self._z_offset}" # Z_ADJUST adds the value to the existing offset + f"SET_GCODE_OFFSET Z_ADJUST=+{self._z_offset} MOVE=1" # Z_ADJUST adds the value to the existing offset ) - self.savebutton.setVisible(True) + self._baby_stepchange = True @QtCore.pyqtSlot(name="handle_z_offset_change") def handle_z_offset_change(self) -> None: @@ -67,18 +76,11 @@ def handle_z_offset_change(self) -> None: Possible values are: 0.01, 0.025, 0.05, 0.1 **mm** """ - _possible_z_values: typing.List = [0.01, 0.025, 0.05, 0.1] _sender: QtCore.QObject | None = self.sender() if self._z_offset == float(_sender.text()[:-3]): return self._z_offset = float(_sender.text()[:-3]) - def save_value(self): - """Save new z offset value""" - self.run_gcode.emit("Z_OFFSET_APPLY_PROBE") - self.savebutton.setVisible(False) - self.bbp_z_offset_title_label.setText(self.bbp_z_offset_current_value.text()) - def on_gcode_move_update(self, name: str, value: list) -> None: """Handle gcode move updates""" if not value: @@ -87,8 +89,6 @@ def on_gcode_move_update(self, name: str, value: list) -> None: if name == "homing_origin": self._z_offset_text = value[2] self.bbp_z_offset_current_value.setText(f"Z: {self._z_offset_text:.3f}mm") - if self.bbp_z_offset_title_label.text() == "smth": - self.bbp_z_offset_title_label.setText(f"Z: {self._z_offset_text:.3f}mm") def setupUI(self): """Setup babystep page ui""" @@ -143,16 +143,6 @@ def setupUI(self): self.bbp_header_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.bbp_header_title.setObjectName("bbp_header_title") - self.savebutton = BlocksCustomButton(self) - self.savebutton.setGeometry(QtCore.QRect(460, 340, 200, 60)) - self.savebutton.setText("Save?") - self.savebutton.setObjectName("savebutton") - self.savebutton.setPixmap(QtGui.QPixmap(":/ui/media/btn_icons/save.svg")) - self.savebutton.setVisible(False) - font = QtGui.QFont() - font.setPointSize(15) - self.savebutton.setFont(font) - spacerItem = QtWidgets.QSpacerItem( 60, 20, @@ -234,6 +224,30 @@ def setupUI(self): QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) + # 0.05mm button + self.bbp_nozzle_offset_05 = BlocksCustomCheckButton( + parent=self.bbp_offset_steps_buttons_group_box + ) + self.bbp_nozzle_offset_05.setMinimumSize(QtCore.QSize(100, 70)) + self.bbp_nozzle_offset_05.setMaximumSize( + QtCore.QSize(100, 70) + ) # Increased max width by 5 pixels + self.bbp_nozzle_offset_05.setText("0.05 mm") + + font = QtGui.QFont() + font.setPointSize(14) + self.bbp_nozzle_offset_05.setFont(font) + self.bbp_nozzle_offset_05.setCheckable(True) + self.bbp_nozzle_offset_05.setFlat(True) + self.bbp_nozzle_offset_05.setProperty("button_type", "") + self.bbp_nozzle_offset_05.setObjectName("bbp_nozzle_offset_05") + self.bbp_offset_value_selector_group.addButton(self.bbp_nozzle_offset_05) + self.bbp_offset_steps_buttons.addWidget( + self.bbp_nozzle_offset_05, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + # Line separator for 0.1mm - set size policy to expanding horizontally # 0.01mm button @@ -260,30 +274,6 @@ def setupUI(self): QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) - # 0.05mm button - self.bbp_nozzle_offset_05 = BlocksCustomCheckButton( - parent=self.bbp_offset_steps_buttons_group_box - ) - self.bbp_nozzle_offset_05.setMinimumSize(QtCore.QSize(100, 70)) - self.bbp_nozzle_offset_05.setMaximumSize( - QtCore.QSize(100, 70) - ) # Increased max width by 5 pixels - self.bbp_nozzle_offset_05.setText("0.05 mm") - - font = QtGui.QFont() - font.setPointSize(14) - self.bbp_nozzle_offset_05.setFont(font) - self.bbp_nozzle_offset_05.setCheckable(True) - self.bbp_nozzle_offset_05.setFlat(True) - self.bbp_nozzle_offset_05.setProperty("button_type", "") - self.bbp_nozzle_offset_05.setObjectName("bbp_nozzle_offset_05") - self.bbp_offset_value_selector_group.addButton(self.bbp_nozzle_offset_05) - self.bbp_offset_steps_buttons.addWidget( - self.bbp_nozzle_offset_05, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - # 0.025mm button self.bbp_nozzle_offset_025 = BlocksCustomCheckButton( parent=self.bbp_offset_steps_buttons_group_box @@ -351,9 +341,8 @@ def setupUI(self): self.bbp_z_offset_title_label.setStyleSheet( "color: gray; background: transparent;" ) - self.bbp_z_offset_title_label.setText("Z-Offset") self.bbp_z_offset_title_label.setObjectName("bbp_z_offset_title_label") - self.bbp_z_offset_title_label.setText("smth") + self.bbp_z_offset_title_label.setText("Z: 0.000mm") self.bbp_z_offset_title_label.setGeometry(420, 270, 200, 30) # === END OF NEW LABEL === @@ -399,7 +388,7 @@ def setupUI(self): self.bbp_mvup.setText("") self.bbp_mvup.setFlat(True) self.bbp_mvup.setPixmap( - QtGui.QPixmap(":/arrow_icons/media/btn_icons/up_arrow.svg") + QtGui.QPixmap(":/baby_step/media/btn_icons/move_nozzle_close.svg") ) self.bbp_mvup.setObjectName("bbp_away_from_bed") self.bbp_option_button_group = QtWidgets.QButtonGroup(self) @@ -416,7 +405,7 @@ def setupUI(self): self.bbp_mvdown.setText("") self.bbp_mvdown.setFlat(True) self.bbp_mvdown.setPixmap( - QtGui.QPixmap(":/arrow_icons/media/btn_icons/down_arrow.svg") + QtGui.QPixmap(":/baby_step/media/btn_icons/move_nozzle_away.svg") ) self.bbp_mvdown.setObjectName("bbp_close_to_bed") self.bbp_option_button_group.addButton(self.bbp_mvdown) diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index f19b5269..bd5bbb8c 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -35,6 +35,9 @@ class JobStatusWidget(QtWidgets.QWidget): print_cancel: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="print_cancel" ) + print_finish: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + name="print_finish" + ) tune_clicked: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="tune_clicked" ) @@ -225,6 +228,8 @@ def _handle_print_state(self, state: str) -> None: self.show_request.emit() lstate = "start" elif lstate in invalid_states: + if lstate != "standby": + self.print_finish.emit() self._current_file_name = "" self._internal_print_status = "" self.total_layers = "?" diff --git a/BlocksScreen/lib/ui/resources/icon_resources.qrc b/BlocksScreen/lib/ui/resources/icon_resources.qrc index 8338a1da..a62dda06 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources.qrc +++ b/BlocksScreen/lib/ui/resources/icon_resources.qrc @@ -10,6 +10,10 @@ media/btn_icons/no_wifi.svg media/btn_icons/retry_wifi.svg + + media/btn_icons/move_nozzle_away.svg + media/btn_icons/move_nozzle_close.svg + media/btn_icons/blocks_contacts.svg media/btn_icons/logo_BLOCKS.svg diff --git a/BlocksScreen/lib/ui/resources/icon_resources_rc.py b/BlocksScreen/lib/ui/resources/icon_resources_rc.py index 6481444f..9df24546 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/icon_resources_rc.py @@ -9278,6 +9278,78 @@ \x37\x34\x2e\x32\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x39\x39\x2e\x36\ \x39\x2c\x37\x36\x2e\x32\x36\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\ \x3e\ +\x00\x00\x02\x27\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\ +\x61\x79\x65\x72\x5f\x31\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\ +\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\ +\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\ +\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x76\ +\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x36\x30\x30\x20\ +\x36\x30\x30\x22\x3e\x0a\x20\x20\x3c\x64\x65\x66\x73\x3e\x0a\x20\ +\x20\x20\x20\x3c\x73\x74\x79\x6c\x65\x3e\x0a\x20\x20\x20\x20\x20\ +\x20\x2e\x63\x6c\x73\x2d\x31\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x66\x69\x6c\x6c\x3a\x20\x23\x65\x30\x65\x30\x64\x66\x3b\ +\x0a\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x73\ +\x74\x79\x6c\x65\x3e\x0a\x20\x20\x3c\x2f\x64\x65\x66\x73\x3e\x0a\ +\x20\x20\x3c\x67\x3e\x0a\x20\x20\x20\x20\x3c\x72\x65\x63\x74\x20\ +\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x78\x3d\ +\x22\x32\x33\x38\x2e\x39\x31\x22\x20\x79\x3d\x22\x37\x33\x2e\x37\ +\x34\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x38\x31\x2e\x39\x32\x22\ +\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x34\x39\x33\x2e\x34\x37\x22\ +\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\ +\x73\x6c\x61\x74\x65\x28\x33\x30\x38\x2e\x35\x38\x20\x2d\x31\x30\ +\x34\x2e\x30\x34\x29\x20\x72\x6f\x74\x61\x74\x65\x28\x34\x35\x29\ +\x22\x2f\x3e\x0a\x20\x20\x20\x20\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\ +\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x70\ +\x6f\x69\x6e\x74\x73\x3d\x22\x33\x34\x31\x2e\x33\x38\x20\x39\x31\ +\x2e\x38\x32\x20\x35\x34\x34\x2e\x36\x32\x20\x35\x34\x2e\x30\x32\ +\x20\x35\x30\x37\x2e\x31\x34\x20\x32\x35\x38\x2e\x39\x37\x20\x33\ +\x34\x31\x2e\x33\x38\x20\x39\x31\x2e\x38\x32\x22\x2f\x3e\x0a\x20\ +\x20\x3c\x2f\x67\x3e\x0a\x20\x20\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\ +\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x70\ +\x6f\x69\x6e\x74\x73\x3d\x22\x32\x35\x38\x2e\x36\x32\x20\x35\x30\ +\x38\x2e\x31\x38\x20\x35\x35\x2e\x33\x38\x20\x35\x34\x35\x2e\x39\ +\x38\x20\x39\x32\x2e\x38\x36\x20\x33\x34\x31\x2e\x30\x33\x20\x32\ +\x35\x38\x2e\x36\x32\x20\x35\x30\x38\x2e\x31\x38\x22\x2f\x3e\x0a\ +\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x02\x02\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\ +\x61\x79\x65\x72\x5f\x31\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\ +\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\ +\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\ +\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x76\ +\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x36\x30\x30\x20\ +\x36\x30\x30\x22\x3e\x0a\x20\x20\x3c\x64\x65\x66\x73\x3e\x0a\x20\ +\x20\x20\x20\x3c\x73\x74\x79\x6c\x65\x3e\x0a\x20\x20\x20\x20\x20\ +\x20\x2e\x63\x6c\x73\x2d\x31\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x66\x69\x6c\x6c\x3a\x20\x23\x65\x30\x65\x30\x64\x66\x3b\ +\x0a\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x73\ +\x74\x79\x6c\x65\x3e\x0a\x20\x20\x3c\x2f\x64\x65\x66\x73\x3e\x0a\ +\x20\x20\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\x20\x63\x6c\x61\x73\x73\ +\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x70\x6f\x69\x6e\x74\x73\x3d\ +\x22\x35\x37\x38\x2e\x30\x35\x20\x37\x38\x2e\x33\x20\x35\x31\x39\ +\x2e\x36\x33\x20\x32\x30\x2e\x38\x36\x20\x33\x39\x39\x2e\x31\x37\ +\x20\x31\x34\x31\x2e\x33\x33\x20\x33\x34\x35\x2e\x34\x38\x20\x38\ +\x37\x2e\x31\x39\x20\x33\x30\x37\x2e\x39\x39\x20\x32\x39\x32\x2e\ +\x31\x33\x20\x35\x31\x31\x2e\x32\x34\x20\x32\x35\x34\x2e\x33\x34\ +\x20\x34\x35\x36\x2e\x38\x35\x20\x31\x39\x39\x2e\x35\x20\x35\x37\ +\x38\x2e\x30\x35\x20\x37\x38\x2e\x33\x22\x2f\x3e\x0a\x20\x20\x3c\ +\x70\x6f\x6c\x79\x67\x6f\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ +\x6c\x73\x2d\x31\x22\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x31\x35\ +\x2e\x33\x38\x20\x35\x33\x31\x2e\x33\x37\x20\x37\x33\x2e\x38\x20\ +\x35\x38\x38\x2e\x38\x31\x20\x31\x39\x39\x2e\x33\x33\x20\x34\x36\ +\x33\x2e\x32\x37\x20\x32\x35\x33\x2e\x33\x37\x20\x35\x31\x37\x2e\ +\x37\x36\x20\x32\x39\x30\x2e\x38\x36\x20\x33\x31\x32\x2e\x38\x31\ +\x20\x38\x37\x2e\x36\x31\x20\x33\x35\x30\x2e\x36\x31\x20\x31\x34\ +\x31\x2e\x36\x35\x20\x34\x30\x35\x2e\x31\x20\x31\x35\x2e\x33\x38\ +\x20\x35\x33\x31\x2e\x33\x37\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\ +\x3e\ \x00\x00\x0a\x60\ \x3c\ \x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ @@ -25305,6 +25377,10 @@ \x08\xf1\x7e\xd4\ \x00\x66\ \x00\x69\x00\x6c\x00\x61\x00\x6d\x00\x65\x00\x6e\x00\x74\x00\x5f\x00\x72\x00\x65\x00\x6c\x00\x61\x00\x74\x00\x65\x00\x64\ +\x00\x09\ +\x09\xf6\x7a\x20\ +\x00\x62\ +\x00\x61\x00\x62\x00\x79\x00\x5f\x00\x73\x00\x74\x00\x65\x00\x70\ \x00\x11\ \x0b\x8b\xba\x63\ \x00\x6c\ @@ -25460,6 +25536,16 @@ \x0c\x4a\x5a\x07\ \x00\x65\ \x00\x6e\x00\x67\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x14\ +\x02\x84\x9e\x27\ +\x00\x6d\ +\x00\x6f\x00\x76\x00\x65\x00\x5f\x00\x6e\x00\x6f\x00\x7a\x00\x7a\x00\x6c\x00\x65\x00\x5f\x00\x61\x00\x77\x00\x61\x00\x79\x00\x2e\ +\x00\x73\x00\x76\x00\x67\ +\x00\x15\ +\x06\x1c\x09\x47\ +\x00\x6d\ +\x00\x6f\x00\x76\x00\x65\x00\x5f\x00\x6e\x00\x6f\x00\x7a\x00\x7a\x00\x6c\x00\x65\x00\x5f\x00\x63\x00\x6c\x00\x6f\x00\x73\x00\x65\ +\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x06\ \x07\xb6\x68\x82\ \x00\x74\ @@ -25970,582 +26056,597 @@ " qt_resource_struct_v1 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0f\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x95\ -\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x86\ -\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7b\ -\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x77\ -\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x71\ -\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x63\ -\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x59\ -\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4a\ -\x00\x00\x00\xc0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x45\ -\x00\x00\x00\xdc\x00\x02\x00\x00\x00\x01\x00\x00\x00\x35\ -\x00\x00\x01\x02\x00\x02\x00\x00\x00\x01\x00\x00\x00\x31\ -\x00\x00\x01\x2a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x25\ -\x00\x00\x01\x50\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1f\ -\x00\x00\x01\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x10\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x11\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x12\ -\x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x08\x66\ -\x00\x00\x01\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x09\x52\ -\x00\x00\x02\x20\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x35\ -\x00\x00\x02\x48\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x18\ -\x00\x00\x02\x70\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x06\ -\x00\x00\x02\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x14\x42\ -\x00\x00\x02\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf7\ -\x00\x00\x02\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x22\x41\ -\x00\x00\x03\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x26\x90\ -\x00\x00\x03\x36\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x6e\ -\x00\x00\x03\x52\x00\x00\x00\x00\x00\x01\x00\x00\x30\xca\ -\x00\x00\x03\x7a\x00\x00\x00\x00\x00\x01\x00\x00\x32\xb1\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x21\ -\x00\x00\x03\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x3a\x55\ -\x00\x00\x03\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x3c\xcf\ -\x00\x00\x03\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x3f\x43\ -\x00\x00\x03\xfe\x00\x00\x00\x00\x00\x01\x00\x00\x41\xbf\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x26\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x27\ -\x00\x00\x04\x20\x00\x00\x00\x00\x00\x01\x00\x00\x44\x3b\ -\x00\x00\x04\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x45\x3c\ -\x00\x00\x04\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x4a\x54\ -\x00\x00\x04\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x56\x16\ -\x00\x00\x04\xc4\x00\x00\x00\x00\x00\x01\x00\x00\x58\x86\ -\x00\x00\x04\xea\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x1e\ -\x00\x00\x05\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x62\x7a\ -\x00\x00\x05\x48\x00\x00\x00\x00\x00\x01\x00\x00\x69\x07\ -\x00\x00\x05\x64\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x03\ -\x00\x00\x05\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x6d\x01\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x32\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x33\ -\x00\x00\x05\xbe\x00\x01\x00\x00\x00\x01\x00\x00\x77\x6b\ -\x00\x00\x05\xd0\x00\x00\x00\x00\x00\x01\x00\x02\x3a\xc0\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x02\x00\x00\x00\x36\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x06\x00\x00\x00\x3f\ -\x00\x00\x05\xe4\x00\x02\x00\x00\x00\x07\x00\x00\x00\x38\ -\x00\x00\x05\xf6\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ -\x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x49\xaa\ -\x00\x00\x06\x5e\x00\x00\x00\x00\x00\x01\x00\x02\x53\xd3\ -\x00\x00\x06\x98\x00\x00\x00\x00\x00\x01\x00\x02\x5e\x2b\ -\x00\x00\x06\xcc\x00\x00\x00\x00\x00\x01\x00\x02\x68\x43\ -\x00\x00\x07\x02\x00\x00\x00\x00\x00\x01\x00\x02\x72\x6c\ -\x00\x00\x07\x3a\x00\x00\x00\x00\x00\x01\x00\x02\x7c\x82\ -\x00\x00\x07\x70\x00\x00\x00\x00\x00\x01\x00\x02\x86\xe8\ -\x00\x00\x07\x98\x00\x00\x00\x00\x00\x01\x00\x02\x91\x17\ -\x00\x00\x07\xc4\x00\x00\x00\x00\x00\x01\x00\x02\x9b\x5e\ -\x00\x00\x08\x00\x00\x00\x00\x00\x00\x01\x00\x02\x9d\x5f\ -\x00\x00\x08\x2c\x00\x00\x00\x00\x00\x01\x00\x02\xb8\xa4\ -\x00\x00\x08\x58\x00\x00\x00\x00\x00\x01\x00\x02\xbd\xed\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x46\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x03\x00\x00\x00\x47\ -\x00\x00\x08\x8c\x00\x00\x00\x00\x00\x01\x00\x02\xc1\x65\ -\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x02\xca\x47\ -\x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x02\xcf\x7c\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4b\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x4c\ -\x00\x00\x08\xd8\x00\x00\x00\x00\x00\x01\x00\x02\xd9\x51\ -\x00\x00\x09\x00\x00\x00\x00\x00\x00\x01\x00\x02\xde\x4c\ -\x00\x00\x09\x38\x00\x00\x00\x00\x00\x01\x00\x02\xe7\x20\ -\x00\x00\x09\x70\x00\x00\x00\x00\x00\x01\x00\x02\xef\xc4\ -\x00\x00\x09\xa0\x00\x00\x00\x00\x00\x01\x00\x02\xf7\x63\ -\x00\x00\x09\xc4\x00\x00\x00\x00\x00\x01\x00\x02\xff\x3f\ -\x00\x00\x09\xe8\x00\x00\x00\x00\x00\x01\x00\x03\x06\xf5\ -\x00\x00\x0a\x1c\x00\x00\x00\x00\x00\x01\x00\x03\x0f\x03\ -\x00\x00\x0a\x50\x00\x00\x00\x00\x00\x01\x00\x03\x16\xe5\ -\x00\x00\x0a\x84\x00\x00\x00\x00\x00\x01\x00\x03\x1e\xaf\ -\x00\x00\x0a\xbe\x00\x00\x00\x00\x00\x01\x00\x03\x26\x16\ -\x00\x00\x0a\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x29\xd3\ -\x00\x00\x0b\x10\x00\x00\x00\x00\x00\x01\x00\x03\x2d\xac\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5a\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x08\x00\x00\x00\x5b\ -\x00\x00\x0b\x36\x00\x00\x00\x00\x00\x01\x00\x03\x33\xb5\ -\x00\x00\x0b\x62\x00\x00\x00\x00\x00\x01\x00\x03\x56\x39\ -\x00\x00\x0b\x90\x00\x00\x00\x00\x00\x01\x00\x03\x5c\x26\ -\x00\x00\x0b\xba\x00\x00\x00\x00\x00\x01\x00\x03\x5e\x46\ -\x00\x00\x0b\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x66\xde\ -\x00\x00\x0b\xfc\x00\x00\x00\x00\x00\x01\x00\x03\x76\x27\ -\x00\x00\x0c\x28\x00\x00\x00\x00\x00\x01\x00\x03\x7c\xa5\ -\x00\x00\x0c\x50\x00\x00\x00\x00\x00\x01\x00\x03\x87\x1f\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x64\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x65\ -\x00\x00\x0c\x86\x00\x00\x00\x00\x00\x01\x00\x03\x90\x66\ -\x00\x00\x0c\xb4\x00\x00\x00\x00\x00\x01\x00\x03\x93\x0d\ -\x00\x00\x0c\xe2\x00\x01\x00\x00\x00\x01\x00\x03\x9f\x8a\ -\x00\x00\x0d\x0e\x00\x00\x00\x00\x00\x01\x00\x03\xcd\x09\ -\x00\x00\x0d\x2e\x00\x00\x00\x00\x00\x01\x00\x03\xd1\xc0\ -\x00\x00\x0d\x60\x00\x01\x00\x00\x00\x01\x00\x04\x2a\xb9\ -\x00\x00\x0d\x92\x00\x00\x00\x00\x00\x01\x00\x04\x5f\x53\ -\x00\x00\x0d\xac\x00\x00\x00\x00\x00\x01\x00\x04\x64\x9d\ -\x00\x00\x0d\xc6\x00\x00\x00\x00\x00\x01\x00\x04\x6a\x2c\ -\x00\x00\x0d\xe0\x00\x00\x00\x00\x00\x01\x00\x04\x6f\x95\ -\x00\x00\x0d\xf8\x00\x00\x00\x00\x00\x01\x00\x04\x7b\x73\ -\x00\x00\x0e\x16\x00\x00\x00\x00\x00\x01\x00\x04\x81\x77\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x72\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x73\ -\x00\x00\x0e\x3e\x00\x00\x00\x00\x00\x01\x00\x04\x86\xac\ -\x00\x00\x0e\x52\x00\x00\x00\x00\x00\x01\x00\x04\x8c\xa9\ -\x00\x00\x0e\x64\x00\x00\x00\x00\x00\x01\x00\x04\x8e\x2f\ -\x00\x00\x0e\x76\x00\x00\x00\x00\x00\x01\x00\x04\x94\x29\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x78\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x79\ -\x00\x00\x0e\x8a\x00\x00\x00\x00\x00\x01\x00\x04\x96\x7f\ -\x00\x00\x0e\xb6\x00\x00\x00\x00\x00\x01\x00\x04\x9d\x66\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7c\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x09\x00\x00\x00\x7d\ -\x00\x00\x0e\xda\x00\x00\x00\x00\x00\x01\x00\x04\xa6\xca\ -\x00\x00\x0e\xfa\x00\x01\x00\x00\x00\x01\x00\x04\xa9\x76\ -\x00\x00\x0f\x1e\x00\x00\x00\x00\x00\x01\x00\x04\xb4\xc7\ -\x00\x00\x0f\x40\x00\x00\x00\x00\x00\x01\x00\x04\xbc\x6c\ -\x00\x00\x0f\x5c\x00\x00\x00\x00\x00\x01\x00\x04\xcd\x6c\ -\x00\x00\x0f\x80\x00\x00\x00\x00\x00\x01\x00\x04\xd6\xed\ -\x00\x00\x0f\xa0\x00\x00\x00\x00\x00\x01\x00\x04\xdc\x8a\ -\x00\x00\x0f\xc8\x00\x00\x00\x00\x00\x01\x00\x04\xe6\x53\ -\x00\x00\x0f\xe8\x00\x00\x00\x00\x00\x01\x00\x04\xea\x36\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x87\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x88\ -\x00\x00\x10\x04\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x71\ -\x00\x00\x10\x34\x00\x00\x00\x00\x00\x01\x00\x04\xfa\x07\ -\x00\x00\x10\x64\x00\x00\x00\x00\x00\x01\x00\x05\x05\xe3\ -\x00\x00\x10\x88\x00\x00\x00\x00\x00\x01\x00\x05\x0c\x27\ -\x00\x00\x10\xb2\x00\x00\x00\x00\x00\x01\x00\x05\x13\xb0\ -\x00\x00\x10\xde\x00\x00\x00\x00\x00\x01\x00\x05\x1a\x0e\ -\x00\x00\x11\x14\x00\x00\x00\x00\x00\x01\x00\x05\x21\xfd\ -\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x2f\xa0\ -\x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x34\xd5\ -\x00\x00\x11\x32\x00\x00\x00\x00\x00\x01\x00\x05\x3e\xaa\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ -\x00\x00\x11\x5a\x00\x00\x00\x00\x00\x01\x00\x05\x46\x74\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x96\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x28\x00\x00\x00\x97\ -\x00\x00\x11\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x4b\x3a\ -\x00\x00\x11\x90\x00\x00\x00\x00\x00\x01\x00\x05\x52\xee\ -\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x55\x13\ -\x00\x00\x11\xe0\x00\x00\x00\x00\x00\x01\x00\x05\x56\x93\ -\x00\x00\x11\xfc\x00\x00\x00\x00\x00\x01\x00\x05\x5e\x40\ -\x00\x00\x12\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x63\x15\ -\x00\x00\x12\x32\x00\x00\x00\x00\x00\x01\x00\x05\x64\x05\ -\x00\x00\x12\x48\x00\x00\x00\x00\x00\x01\x00\x05\x68\x2e\ -\x00\x00\x12\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x6e\x65\ -\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x05\x83\x72\ -\x00\x00\x12\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x88\x62\ -\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x8b\x61\ -\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x91\x6b\ -\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x05\x94\xba\ -\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x98\xdb\ -\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\x9e\xd2\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\xe3\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\xa4\xa6\ -\x00\x00\x13\x88\x00\x00\x00\x00\x00\x01\x00\x05\xae\x5a\ -\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xa8\ -\x00\x00\x13\xd4\x00\x00\x00\x00\x00\x01\x00\x05\xb5\x36\ -\x00\x00\x13\xfa\x00\x00\x00\x00\x00\x01\x00\x05\xb9\xd7\ -\x00\x00\x14\x0e\x00\x00\x00\x00\x00\x01\x00\x05\xc3\xa9\ -\x00\x00\x14\x3a\x00\x00\x00\x00\x00\x01\x00\x05\xc8\xf3\ -\x00\x00\x14\x62\x00\x00\x00\x00\x00\x01\x00\x05\xce\xfa\ -\x00\x00\x14\x78\x00\x00\x00\x00\x00\x01\x00\x05\xcf\xde\ -\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x05\xd2\x27\ -\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xd8\xd7\ -\x00\x00\x14\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xdc\x1b\ -\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xdd\x47\ -\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x05\xe3\x08\ -\x00\x00\x15\x2a\x00\x00\x00\x00\x00\x01\x00\x05\xe4\x2a\ -\x00\x00\x15\x48\x00\x00\x00\x00\x00\x01\x00\x05\xea\x1d\ -\x00\x00\x15\x68\x00\x00\x00\x00\x00\x01\x00\x05\xed\x21\ -\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xee\x42\ -\x00\x00\x15\xaa\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x16\ -\x00\x00\x15\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xf9\x82\ -\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x06\x01\x42\ -\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x06\x06\x5d\ -\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x07\xb0\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x10\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9a\ +\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x97\ +\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x8b\ +\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x80\ +\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7c\ +\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x76\ +\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x68\ +\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5e\ +\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4f\ +\x00\x00\x00\xc0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4a\ +\x00\x00\x00\xdc\x00\x02\x00\x00\x00\x01\x00\x00\x00\x3a\ +\x00\x00\x01\x02\x00\x02\x00\x00\x00\x01\x00\x00\x00\x36\ +\x00\x00\x01\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x32\ +\x00\x00\x01\x42\x00\x02\x00\x00\x00\x01\x00\x00\x00\x26\ +\x00\x00\x01\x68\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ +\x00\x00\x01\x84\x00\x02\x00\x00\x00\x01\x00\x00\x00\x11\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x12\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x13\ +\x00\x00\x01\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x08\x66\ +\x00\x00\x02\x10\x00\x00\x00\x00\x00\x01\x00\x00\x09\x52\ +\x00\x00\x02\x38\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x35\ +\x00\x00\x02\x60\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x18\ +\x00\x00\x02\x88\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x06\ +\x00\x00\x02\xbc\x00\x00\x00\x00\x00\x01\x00\x00\x14\x42\ +\x00\x00\x02\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf7\ +\x00\x00\x03\x12\x00\x00\x00\x00\x00\x01\x00\x00\x22\x41\ +\x00\x00\x03\x32\x00\x00\x00\x00\x00\x01\x00\x00\x26\x90\ +\x00\x00\x03\x4e\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x6e\ +\x00\x00\x03\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x30\xca\ +\x00\x00\x03\x92\x00\x00\x00\x00\x00\x01\x00\x00\x32\xb1\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x21\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x22\ +\x00\x00\x03\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x3a\x55\ +\x00\x00\x03\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x3c\xcf\ +\x00\x00\x03\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x3f\x43\ +\x00\x00\x04\x16\x00\x00\x00\x00\x00\x01\x00\x00\x41\xbf\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x27\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x28\ +\x00\x00\x04\x38\x00\x00\x00\x00\x00\x01\x00\x00\x44\x3b\ +\x00\x00\x04\x52\x00\x00\x00\x00\x00\x01\x00\x00\x45\x3c\ +\x00\x00\x04\x82\x00\x00\x00\x00\x00\x01\x00\x00\x4a\x54\ +\x00\x00\x04\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x56\x16\ +\x00\x00\x04\xdc\x00\x00\x00\x00\x00\x01\x00\x00\x58\x86\ +\x00\x00\x05\x02\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x1e\ +\x00\x00\x05\x26\x00\x00\x00\x00\x00\x01\x00\x00\x62\x7a\ +\x00\x00\x05\x60\x00\x00\x00\x00\x00\x01\x00\x00\x69\x07\ +\x00\x00\x05\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x03\ +\x00\x00\x05\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x6d\x01\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x33\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x34\ +\x00\x00\x05\xd6\x00\x01\x00\x00\x00\x01\x00\x00\x77\x6b\ +\x00\x00\x05\xe8\x00\x00\x00\x00\x00\x01\x00\x02\x3a\xc0\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x37\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x38\ +\x00\x00\x05\xfc\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ +\x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x41\x71\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3b\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x44\ +\x00\x00\x06\x5a\x00\x02\x00\x00\x00\x07\x00\x00\x00\x3d\ +\x00\x00\x06\x6c\x00\x00\x00\x00\x00\x01\x00\x02\x43\x77\ +\x00\x00\x06\xa0\x00\x00\x00\x00\x00\x01\x00\x02\x4d\xdb\ +\x00\x00\x06\xd4\x00\x00\x00\x00\x00\x01\x00\x02\x58\x04\ +\x00\x00\x07\x0e\x00\x00\x00\x00\x00\x01\x00\x02\x62\x5c\ +\x00\x00\x07\x42\x00\x00\x00\x00\x00\x01\x00\x02\x6c\x74\ +\x00\x00\x07\x78\x00\x00\x00\x00\x00\x01\x00\x02\x76\x9d\ +\x00\x00\x07\xb0\x00\x00\x00\x00\x00\x01\x00\x02\x80\xb3\ +\x00\x00\x07\xe6\x00\x00\x00\x00\x00\x01\x00\x02\x8b\x19\ +\x00\x00\x08\x0e\x00\x00\x00\x00\x00\x01\x00\x02\x95\x48\ +\x00\x00\x08\x3a\x00\x00\x00\x00\x00\x01\x00\x02\x9f\x8f\ +\x00\x00\x08\x76\x00\x00\x00\x00\x00\x01\x00\x02\xa1\x90\ +\x00\x00\x08\xa2\x00\x00\x00\x00\x00\x01\x00\x02\xbc\xd5\ +\x00\x00\x08\xce\x00\x00\x00\x00\x00\x01\x00\x02\xc2\x1e\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4b\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x03\x00\x00\x00\x4c\ +\x00\x00\x09\x02\x00\x00\x00\x00\x00\x01\x00\x02\xc5\x96\ +\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x02\xce\x78\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x02\xd3\xad\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x50\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x51\ +\x00\x00\x09\x4e\x00\x00\x00\x00\x00\x01\x00\x02\xdd\x82\ +\x00\x00\x09\x76\x00\x00\x00\x00\x00\x01\x00\x02\xe2\x7d\ +\x00\x00\x09\xae\x00\x00\x00\x00\x00\x01\x00\x02\xeb\x51\ +\x00\x00\x09\xe6\x00\x00\x00\x00\x00\x01\x00\x02\xf3\xf5\ +\x00\x00\x0a\x16\x00\x00\x00\x00\x00\x01\x00\x02\xfb\x94\ +\x00\x00\x0a\x3a\x00\x00\x00\x00\x00\x01\x00\x03\x03\x70\ +\x00\x00\x0a\x5e\x00\x00\x00\x00\x00\x01\x00\x03\x0b\x26\ +\x00\x00\x0a\x92\x00\x00\x00\x00\x00\x01\x00\x03\x13\x34\ +\x00\x00\x0a\xc6\x00\x00\x00\x00\x00\x01\x00\x03\x1b\x16\ +\x00\x00\x0a\xfa\x00\x00\x00\x00\x00\x01\x00\x03\x22\xe0\ +\x00\x00\x0b\x34\x00\x00\x00\x00\x00\x01\x00\x03\x2a\x47\ +\x00\x00\x0b\x58\x00\x00\x00\x00\x00\x01\x00\x03\x2e\x04\ +\x00\x00\x0b\x86\x00\x00\x00\x00\x00\x01\x00\x03\x31\xdd\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5f\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x08\x00\x00\x00\x60\ +\x00\x00\x0b\xac\x00\x00\x00\x00\x00\x01\x00\x03\x37\xe6\ +\x00\x00\x0b\xd8\x00\x00\x00\x00\x00\x01\x00\x03\x5a\x6a\ +\x00\x00\x0c\x06\x00\x00\x00\x00\x00\x01\x00\x03\x60\x57\ +\x00\x00\x0c\x30\x00\x00\x00\x00\x00\x01\x00\x03\x62\x77\ +\x00\x00\x0c\x58\x00\x00\x00\x00\x00\x01\x00\x03\x6b\x0f\ +\x00\x00\x0c\x72\x00\x00\x00\x00\x00\x01\x00\x03\x7a\x58\ +\x00\x00\x0c\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x80\xd6\ +\x00\x00\x0c\xc6\x00\x00\x00\x00\x00\x01\x00\x03\x8b\x50\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x69\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x6a\ +\x00\x00\x0c\xfc\x00\x00\x00\x00\x00\x01\x00\x03\x94\x97\ +\x00\x00\x0d\x2a\x00\x00\x00\x00\x00\x01\x00\x03\x97\x3e\ +\x00\x00\x0d\x58\x00\x01\x00\x00\x00\x01\x00\x03\xa3\xbb\ +\x00\x00\x0d\x84\x00\x00\x00\x00\x00\x01\x00\x03\xd1\x3a\ +\x00\x00\x0d\xa4\x00\x00\x00\x00\x00\x01\x00\x03\xd5\xf1\ +\x00\x00\x0d\xd6\x00\x01\x00\x00\x00\x01\x00\x04\x2e\xea\ +\x00\x00\x0e\x08\x00\x00\x00\x00\x00\x01\x00\x04\x63\x84\ +\x00\x00\x0e\x22\x00\x00\x00\x00\x00\x01\x00\x04\x68\xce\ +\x00\x00\x0e\x3c\x00\x00\x00\x00\x00\x01\x00\x04\x6e\x5d\ +\x00\x00\x0e\x56\x00\x00\x00\x00\x00\x01\x00\x04\x73\xc6\ +\x00\x00\x0e\x6e\x00\x00\x00\x00\x00\x01\x00\x04\x7f\xa4\ +\x00\x00\x0e\x8c\x00\x00\x00\x00\x00\x01\x00\x04\x85\xa8\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x77\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x78\ +\x00\x00\x0e\xb4\x00\x00\x00\x00\x00\x01\x00\x04\x8a\xdd\ +\x00\x00\x0e\xc8\x00\x00\x00\x00\x00\x01\x00\x04\x90\xda\ +\x00\x00\x0e\xda\x00\x00\x00\x00\x00\x01\x00\x04\x92\x60\ +\x00\x00\x0e\xec\x00\x00\x00\x00\x00\x01\x00\x04\x98\x5a\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7d\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x7e\ +\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x01\x00\x04\x9a\xb0\ +\x00\x00\x0f\x2c\x00\x00\x00\x00\x00\x01\x00\x04\xa1\x97\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x81\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x09\x00\x00\x00\x82\ +\x00\x00\x0f\x50\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xfb\ +\x00\x00\x0f\x70\x00\x01\x00\x00\x00\x01\x00\x04\xad\xa7\ +\x00\x00\x0f\x94\x00\x00\x00\x00\x00\x01\x00\x04\xb8\xf8\ +\x00\x00\x0f\xb6\x00\x00\x00\x00\x00\x01\x00\x04\xc0\x9d\ +\x00\x00\x0f\xd2\x00\x00\x00\x00\x00\x01\x00\x04\xd1\x9d\ +\x00\x00\x0f\xf6\x00\x00\x00\x00\x00\x01\x00\x04\xdb\x1e\ +\x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe0\xbb\ +\x00\x00\x10\x3e\x00\x00\x00\x00\x00\x01\x00\x04\xea\x84\ +\x00\x00\x10\x5e\x00\x00\x00\x00\x00\x01\x00\x04\xee\x67\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x8c\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x8d\ +\x00\x00\x10\x7a\x00\x00\x00\x00\x00\x01\x00\x04\xf4\xa2\ +\x00\x00\x10\xaa\x00\x00\x00\x00\x00\x01\x00\x04\xfe\x38\ +\x00\x00\x10\xda\x00\x00\x00\x00\x00\x01\x00\x05\x0a\x14\ +\x00\x00\x10\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x10\x58\ +\x00\x00\x11\x28\x00\x00\x00\x00\x00\x01\x00\x05\x17\xe1\ +\x00\x00\x11\x54\x00\x00\x00\x00\x00\x01\x00\x05\x1e\x3f\ +\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x26\x2e\ +\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x33\xd1\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x39\x06\ +\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x42\xdb\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x98\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x99\ +\x00\x00\x11\xd0\x00\x00\x00\x00\x00\x01\x00\x05\x4a\xa5\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9b\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\x9c\ +\x00\x00\x11\xf0\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6b\ +\x00\x00\x12\x06\x00\x00\x00\x00\x00\x01\x00\x05\x57\x1f\ +\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x59\x44\ +\x00\x00\x12\x56\x00\x00\x00\x00\x00\x01\x00\x05\x5a\xc4\ +\x00\x00\x12\x72\x00\x00\x00\x00\x00\x01\x00\x05\x62\x71\ +\x00\x00\x12\x92\x00\x00\x00\x00\x00\x01\x00\x05\x67\x46\ +\x00\x00\x12\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x68\x36\ +\x00\x00\x12\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x6c\x5f\ +\x00\x00\x12\xe4\x00\x00\x00\x00\x00\x01\x00\x05\x72\x96\ +\x00\x00\x12\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x87\xa3\ +\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x8c\x93\ +\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\x8f\x92\ +\x00\x00\x13\x4c\x00\x00\x00\x00\x00\x01\x00\x05\x95\x9c\ +\x00\x00\x13\x7e\x00\x00\x00\x00\x00\x01\x00\x05\x98\xeb\ +\x00\x00\x13\x96\x00\x00\x00\x00\x00\x01\x00\x05\x9d\x0c\ +\x00\x00\x13\xac\x00\x00\x00\x00\x00\x01\x00\x05\xa3\x03\ +\x00\x00\x13\xc0\x00\x00\x00\x00\x00\x01\x00\x05\xa5\x14\ +\x00\x00\x13\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xd7\ +\x00\x00\x13\xfe\x00\x00\x00\x00\x00\x01\x00\x05\xb2\x8b\ +\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb5\xd9\ +\x00\x00\x14\x4a\x00\x00\x00\x00\x00\x01\x00\x05\xb9\x67\ +\x00\x00\x14\x70\x00\x00\x00\x00\x00\x01\x00\x05\xbe\x08\ +\x00\x00\x14\x84\x00\x00\x00\x00\x00\x01\x00\x05\xc7\xda\ +\x00\x00\x14\xb0\x00\x00\x00\x00\x00\x01\x00\x05\xcd\x24\ +\x00\x00\x14\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x2b\ +\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xd4\x0f\ +\x00\x00\x15\x1a\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x58\ +\x00\x00\x15\x30\x00\x00\x00\x00\x00\x01\x00\x05\xdd\x08\ +\x00\x00\x15\x4c\x00\x00\x00\x00\x00\x01\x00\x05\xe0\x4c\ +\x00\x00\x15\x64\x00\x00\x00\x00\x00\x01\x00\x05\xe1\x78\ +\x00\x00\x15\x7e\x00\x00\x00\x00\x00\x01\x00\x05\xe7\x39\ +\x00\x00\x15\xa0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x5b\ +\x00\x00\x15\xbe\x00\x00\x00\x00\x00\x01\x00\x05\xee\x4e\ +\x00\x00\x15\xde\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x52\ +\x00\x00\x16\x00\x00\x00\x00\x00\x00\x01\x00\x05\xf2\x73\ +\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x05\xf5\x47\ +\x00\x00\x16\x4e\x00\x00\x00\x00\x00\x01\x00\x05\xfd\xb3\ +\x00\x00\x16\x72\x00\x00\x00\x00\x00\x01\x00\x06\x05\x73\ +\x00\x00\x16\x96\x00\x00\x00\x00\x00\x01\x00\x06\x0a\x8e\ +\x00\x00\x16\xbe\x00\x00\x00\x00\x00\x01\x00\x06\x0b\xe1\ " qt_resource_struct_v2 = b"\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x0f\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x10\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9a\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x97\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x8b\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x95\ +\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x80\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ +\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7c\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x86\ +\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x76\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7b\ +\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x68\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x77\ +\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5e\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x71\ +\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4f\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x63\ +\x00\x00\x00\xc0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4a\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x59\ +\x00\x00\x00\xdc\x00\x02\x00\x00\x00\x01\x00\x00\x00\x3a\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4a\ +\x00\x00\x01\x02\x00\x02\x00\x00\x00\x01\x00\x00\x00\x36\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\xc0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x45\ +\x00\x00\x01\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x32\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\xdc\x00\x02\x00\x00\x00\x01\x00\x00\x00\x35\ +\x00\x00\x01\x42\x00\x02\x00\x00\x00\x01\x00\x00\x00\x26\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x02\x00\x02\x00\x00\x00\x01\x00\x00\x00\x31\ +\x00\x00\x01\x68\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x2a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x25\ +\x00\x00\x01\x84\x00\x02\x00\x00\x00\x01\x00\x00\x00\x11\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x50\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1f\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x12\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x10\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x13\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x11\ +\x00\x00\x01\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x08\x66\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x02\x10\x00\x00\x00\x00\x00\x01\x00\x00\x09\x52\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x02\x38\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x35\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x02\x60\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x18\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x02\x88\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x06\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x02\xbc\x00\x00\x00\x00\x00\x01\x00\x00\x14\x42\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x02\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf7\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x03\x12\x00\x00\x00\x00\x00\x01\x00\x00\x22\x41\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x03\x32\x00\x00\x00\x00\x00\x01\x00\x00\x26\x90\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x03\x4e\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x6e\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x03\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x30\xca\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x03\x92\x00\x00\x00\x00\x00\x01\x00\x00\x32\xb1\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x21\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x12\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x22\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x01\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x08\x66\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x01\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x09\x52\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x02\x20\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x35\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x02\x48\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x18\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x02\x70\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x06\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x02\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x14\x42\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x02\xd0\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf7\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x02\xfa\x00\x00\x00\x00\x00\x01\x00\x00\x22\x41\ -\x00\x00\x01\x9b\x09\x08\xf9\x49\ -\x00\x00\x03\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x26\x90\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x03\x36\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x6e\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x03\x52\x00\x00\x00\x00\x00\x01\x00\x00\x30\xca\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x03\x7a\x00\x00\x00\x00\x00\x01\x00\x00\x32\xb1\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ +\x00\x00\x03\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x3a\x55\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x03\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x3c\xcf\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x03\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x3f\x43\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x04\x16\x00\x00\x00\x00\x00\x01\x00\x00\x41\xbf\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x27\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x21\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x28\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x03\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x3a\x55\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x03\xbe\x00\x00\x00\x00\x00\x01\x00\x00\x3c\xcf\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x03\xe0\x00\x00\x00\x00\x00\x01\x00\x00\x3f\x43\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x03\xfe\x00\x00\x00\x00\x00\x01\x00\x00\x41\xbf\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x26\ +\x00\x00\x04\x38\x00\x00\x00\x00\x00\x01\x00\x00\x44\x3b\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x04\x52\x00\x00\x00\x00\x00\x01\x00\x00\x45\x3c\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x04\x82\x00\x00\x00\x00\x00\x01\x00\x00\x4a\x54\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x04\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x56\x16\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x04\xdc\x00\x00\x00\x00\x00\x01\x00\x00\x58\x86\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x05\x02\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x1e\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x05\x26\x00\x00\x00\x00\x00\x01\x00\x00\x62\x7a\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x05\x60\x00\x00\x00\x00\x00\x01\x00\x00\x69\x07\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x05\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x03\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x05\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x6d\x01\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x33\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x27\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x34\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x04\x20\x00\x00\x00\x00\x00\x01\x00\x00\x44\x3b\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x04\x3a\x00\x00\x00\x00\x00\x01\x00\x00\x45\x3c\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x04\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x4a\x54\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x04\x9a\x00\x00\x00\x00\x00\x01\x00\x00\x56\x16\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x04\xc4\x00\x00\x00\x00\x00\x01\x00\x00\x58\x86\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x04\xea\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x1e\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x05\x0e\x00\x00\x00\x00\x00\x01\x00\x00\x62\x7a\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x05\x48\x00\x00\x00\x00\x00\x01\x00\x00\x69\x07\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x05\x64\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x03\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x05\x8c\x00\x00\x00\x00\x00\x01\x00\x00\x6d\x01\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x32\ +\x00\x00\x05\xd6\x00\x01\x00\x00\x00\x01\x00\x00\x77\x6b\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x05\xe8\x00\x00\x00\x00\x00\x01\x00\x02\x3a\xc0\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x37\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x33\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x38\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x05\xbe\x00\x01\x00\x00\x00\x01\x00\x00\x77\x6b\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x05\xd0\x00\x00\x00\x00\x00\x01\x00\x02\x3a\xc0\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x02\x00\x00\x00\x36\ +\x00\x00\x05\xfc\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ +\x00\x00\x01\x9b\xbc\x28\x2f\x35\ +\x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x41\x71\ +\x00\x00\x01\x9b\xbc\x28\x2f\x35\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3b\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x06\x00\x00\x00\x3f\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x44\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x05\xe4\x00\x02\x00\x00\x00\x07\x00\x00\x00\x38\ +\x00\x00\x06\x5a\x00\x02\x00\x00\x00\x07\x00\x00\x00\x3d\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x05\xf6\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ -\x00\x00\x01\x9b\x09\x08\xfa\x11\ -\x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x49\xaa\ -\x00\x00\x01\x9b\x09\x08\xfa\x11\ -\x00\x00\x06\x5e\x00\x00\x00\x00\x00\x01\x00\x02\x53\xd3\ -\x00\x00\x01\x9b\x09\x08\xfa\x11\ -\x00\x00\x06\x98\x00\x00\x00\x00\x00\x01\x00\x02\x5e\x2b\ -\x00\x00\x01\x9b\x09\x08\xfa\x11\ -\x00\x00\x06\xcc\x00\x00\x00\x00\x00\x01\x00\x02\x68\x43\ -\x00\x00\x01\x9b\x09\x08\xfa\x11\ -\x00\x00\x07\x02\x00\x00\x00\x00\x00\x01\x00\x02\x72\x6c\ -\x00\x00\x01\x9b\x09\x08\xfa\x11\ -\x00\x00\x07\x3a\x00\x00\x00\x00\x00\x01\x00\x02\x7c\x82\ -\x00\x00\x01\x9b\x09\x08\xfa\x11\ -\x00\x00\x07\x70\x00\x00\x00\x00\x00\x01\x00\x02\x86\xe8\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x07\x98\x00\x00\x00\x00\x00\x01\x00\x02\x91\x17\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x07\xc4\x00\x00\x00\x00\x00\x01\x00\x02\x9b\x5e\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x08\x00\x00\x00\x00\x00\x00\x01\x00\x02\x9d\x5f\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x08\x2c\x00\x00\x00\x00\x00\x01\x00\x02\xb8\xa4\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x08\x58\x00\x00\x00\x00\x00\x01\x00\x02\xbd\xed\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x46\ +\x00\x00\x06\x6c\x00\x00\x00\x00\x00\x01\x00\x02\x43\x77\ +\x00\x00\x01\x9a\x72\xe1\x95\x8f\ +\x00\x00\x06\xa0\x00\x00\x00\x00\x00\x01\x00\x02\x4d\xdb\ +\x00\x00\x01\x9a\x72\xe1\x95\x93\ +\x00\x00\x06\xd4\x00\x00\x00\x00\x00\x01\x00\x02\x58\x04\ +\x00\x00\x01\x9a\x72\xe1\x95\x8f\ +\x00\x00\x07\x0e\x00\x00\x00\x00\x00\x01\x00\x02\x62\x5c\ +\x00\x00\x01\x9a\x72\xe1\x95\x93\ +\x00\x00\x07\x42\x00\x00\x00\x00\x00\x01\x00\x02\x6c\x74\ +\x00\x00\x01\x9a\x72\xe1\x95\x8f\ +\x00\x00\x07\x78\x00\x00\x00\x00\x00\x01\x00\x02\x76\x9d\ +\x00\x00\x01\x9a\x72\xe1\x95\x93\ +\x00\x00\x07\xb0\x00\x00\x00\x00\x00\x01\x00\x02\x80\xb3\ +\x00\x00\x01\x9a\x72\xe1\x95\x93\ +\x00\x00\x07\xe6\x00\x00\x00\x00\x00\x01\x00\x02\x8b\x19\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x08\x0e\x00\x00\x00\x00\x00\x01\x00\x02\x95\x48\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x08\x3a\x00\x00\x00\x00\x00\x01\x00\x02\x9f\x8f\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x08\x76\x00\x00\x00\x00\x00\x01\x00\x02\xa1\x90\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x08\xa2\x00\x00\x00\x00\x00\x01\x00\x02\xbc\xd5\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x08\xce\x00\x00\x00\x00\x00\x01\x00\x02\xc2\x1e\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4b\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x03\x00\x00\x00\x47\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x03\x00\x00\x00\x4c\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x08\x8c\x00\x00\x00\x00\x00\x01\x00\x02\xc1\x65\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x02\xca\x47\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x02\xcf\x7c\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4b\ +\x00\x00\x09\x02\x00\x00\x00\x00\x00\x01\x00\x02\xc5\x96\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x02\xce\x78\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x02\xd3\xad\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x50\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x4c\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x51\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x08\xd8\x00\x00\x00\x00\x00\x01\x00\x02\xd9\x51\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x09\x00\x00\x00\x00\x00\x00\x01\x00\x02\xde\x4c\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x09\x38\x00\x00\x00\x00\x00\x01\x00\x02\xe7\x20\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x09\x70\x00\x00\x00\x00\x00\x01\x00\x02\xef\xc4\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x09\xa0\x00\x00\x00\x00\x00\x01\x00\x02\xf7\x63\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x09\xc4\x00\x00\x00\x00\x00\x01\x00\x02\xff\x3f\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x09\xe8\x00\x00\x00\x00\x00\x01\x00\x03\x06\xf5\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x0a\x1c\x00\x00\x00\x00\x00\x01\x00\x03\x0f\x03\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x0a\x50\x00\x00\x00\x00\x00\x01\x00\x03\x16\xe5\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x0a\x84\x00\x00\x00\x00\x00\x01\x00\x03\x1e\xaf\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x0a\xbe\x00\x00\x00\x00\x00\x01\x00\x03\x26\x16\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x0a\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x29\xd3\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x0b\x10\x00\x00\x00\x00\x00\x01\x00\x03\x2d\xac\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5a\ +\x00\x00\x09\x4e\x00\x00\x00\x00\x00\x01\x00\x02\xdd\x82\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x09\x76\x00\x00\x00\x00\x00\x01\x00\x02\xe2\x7d\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x09\xae\x00\x00\x00\x00\x00\x01\x00\x02\xeb\x51\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x09\xe6\x00\x00\x00\x00\x00\x01\x00\x02\xf3\xf5\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0a\x16\x00\x00\x00\x00\x00\x01\x00\x02\xfb\x94\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0a\x3a\x00\x00\x00\x00\x00\x01\x00\x03\x03\x70\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0a\x5e\x00\x00\x00\x00\x00\x01\x00\x03\x0b\x26\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0a\x92\x00\x00\x00\x00\x00\x01\x00\x03\x13\x34\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0a\xc6\x00\x00\x00\x00\x00\x01\x00\x03\x1b\x16\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0a\xfa\x00\x00\x00\x00\x00\x01\x00\x03\x22\xe0\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0b\x34\x00\x00\x00\x00\x00\x01\x00\x03\x2a\x47\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0b\x58\x00\x00\x00\x00\x00\x01\x00\x03\x2e\x04\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0b\x86\x00\x00\x00\x00\x00\x01\x00\x03\x31\xdd\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5f\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x08\x00\x00\x00\x5b\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x08\x00\x00\x00\x60\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0b\x36\x00\x00\x00\x00\x00\x01\x00\x03\x33\xb5\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x0b\x62\x00\x00\x00\x00\x00\x01\x00\x03\x56\x39\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x0b\x90\x00\x00\x00\x00\x00\x01\x00\x03\x5c\x26\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x0b\xba\x00\x00\x00\x00\x00\x01\x00\x03\x5e\x46\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x0b\xe2\x00\x00\x00\x00\x00\x01\x00\x03\x66\xde\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x0b\xfc\x00\x00\x00\x00\x00\x01\x00\x03\x76\x27\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x0c\x28\x00\x00\x00\x00\x00\x01\x00\x03\x7c\xa5\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x0c\x50\x00\x00\x00\x00\x00\x01\x00\x03\x87\x1f\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x64\ +\x00\x00\x0b\xac\x00\x00\x00\x00\x00\x01\x00\x03\x37\xe6\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0b\xd8\x00\x00\x00\x00\x00\x01\x00\x03\x5a\x6a\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0c\x06\x00\x00\x00\x00\x00\x01\x00\x03\x60\x57\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0c\x30\x00\x00\x00\x00\x00\x01\x00\x03\x62\x77\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0c\x58\x00\x00\x00\x00\x00\x01\x00\x03\x6b\x0f\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0c\x72\x00\x00\x00\x00\x00\x01\x00\x03\x7a\x58\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0c\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x80\xd6\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x0c\xc6\x00\x00\x00\x00\x00\x01\x00\x03\x8b\x50\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x69\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x65\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x6a\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0c\x86\x00\x00\x00\x00\x00\x01\x00\x03\x90\x66\ -\x00\x00\x01\x9b\x09\x08\xf9\x49\ -\x00\x00\x0c\xb4\x00\x00\x00\x00\x00\x01\x00\x03\x93\x0d\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x0c\xe2\x00\x01\x00\x00\x00\x01\x00\x03\x9f\x8a\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x0d\x0e\x00\x00\x00\x00\x00\x01\x00\x03\xcd\x09\ -\x00\x00\x01\x9b\x09\x08\xf9\x49\ -\x00\x00\x0d\x2e\x00\x00\x00\x00\x00\x01\x00\x03\xd1\xc0\ -\x00\x00\x01\x9b\x09\x08\xf9\x49\ -\x00\x00\x0d\x60\x00\x01\x00\x00\x00\x01\x00\x04\x2a\xb9\ -\x00\x00\x01\x9b\x09\x08\xf9\x49\ -\x00\x00\x0d\x92\x00\x00\x00\x00\x00\x01\x00\x04\x5f\x53\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x0d\xac\x00\x00\x00\x00\x00\x01\x00\x04\x64\x9d\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x0d\xc6\x00\x00\x00\x00\x00\x01\x00\x04\x6a\x2c\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x0d\xe0\x00\x00\x00\x00\x00\x01\x00\x04\x6f\x95\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x0d\xf8\x00\x00\x00\x00\x00\x01\x00\x04\x7b\x73\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x0e\x16\x00\x00\x00\x00\x00\x01\x00\x04\x81\x77\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x72\ +\x00\x00\x0c\xfc\x00\x00\x00\x00\x00\x01\x00\x03\x94\x97\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x0d\x2a\x00\x00\x00\x00\x00\x01\x00\x03\x97\x3e\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0d\x58\x00\x01\x00\x00\x00\x01\x00\x03\xa3\xbb\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0d\x84\x00\x00\x00\x00\x00\x01\x00\x03\xd1\x3a\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x0d\xa4\x00\x00\x00\x00\x00\x01\x00\x03\xd5\xf1\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x0d\xd6\x00\x01\x00\x00\x00\x01\x00\x04\x2e\xea\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x0e\x08\x00\x00\x00\x00\x00\x01\x00\x04\x63\x84\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0e\x22\x00\x00\x00\x00\x00\x01\x00\x04\x68\xce\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0e\x3c\x00\x00\x00\x00\x00\x01\x00\x04\x6e\x5d\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0e\x56\x00\x00\x00\x00\x00\x01\x00\x04\x73\xc6\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0e\x6e\x00\x00\x00\x00\x00\x01\x00\x04\x7f\xa4\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0e\x8c\x00\x00\x00\x00\x00\x01\x00\x04\x85\xa8\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x77\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x04\x00\x00\x00\x73\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x78\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0e\x3e\x00\x00\x00\x00\x00\x01\x00\x04\x86\xac\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x0e\x52\x00\x00\x00\x00\x00\x01\x00\x04\x8c\xa9\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x0e\x64\x00\x00\x00\x00\x00\x01\x00\x04\x8e\x2f\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x0e\x76\x00\x00\x00\x00\x00\x01\x00\x04\x94\x29\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x78\ +\x00\x00\x0e\xb4\x00\x00\x00\x00\x00\x01\x00\x04\x8a\xdd\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0e\xc8\x00\x00\x00\x00\x00\x01\x00\x04\x90\xda\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0e\xda\x00\x00\x00\x00\x00\x01\x00\x04\x92\x60\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0e\xec\x00\x00\x00\x00\x00\x01\x00\x04\x98\x5a\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7d\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x02\x00\x00\x00\x79\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x7e\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0e\x8a\x00\x00\x00\x00\x00\x01\x00\x04\x96\x7f\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x0e\xb6\x00\x00\x00\x00\x00\x01\x00\x04\x9d\x66\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7c\ +\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x01\x00\x04\x9a\xb0\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x0f\x2c\x00\x00\x00\x00\x00\x01\x00\x04\xa1\x97\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x81\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x09\x00\x00\x00\x7d\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x09\x00\x00\x00\x82\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0e\xda\x00\x00\x00\x00\x00\x01\x00\x04\xa6\xca\ -\x00\x00\x01\x9b\x09\x08\xf9\x49\ -\x00\x00\x0e\xfa\x00\x01\x00\x00\x00\x01\x00\x04\xa9\x76\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x0f\x1e\x00\x00\x00\x00\x00\x01\x00\x04\xb4\xc7\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x0f\x40\x00\x00\x00\x00\x00\x01\x00\x04\xbc\x6c\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x0f\x5c\x00\x00\x00\x00\x00\x01\x00\x04\xcd\x6c\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x0f\x80\x00\x00\x00\x00\x00\x01\x00\x04\xd6\xed\ -\x00\x00\x01\x9b\x09\x08\xf9\x49\ -\x00\x00\x0f\xa0\x00\x00\x00\x00\x00\x01\x00\x04\xdc\x8a\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x0f\xc8\x00\x00\x00\x00\x00\x01\x00\x04\xe6\x53\ -\x00\x00\x01\x9b\x09\x08\xf9\x49\ -\x00\x00\x0f\xe8\x00\x00\x00\x00\x00\x01\x00\x04\xea\x36\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x87\ +\x00\x00\x0f\x50\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xfb\ +\x00\x00\x01\x9b\x7f\x73\xe2\xad\ +\x00\x00\x0f\x70\x00\x01\x00\x00\x00\x01\x00\x04\xad\xa7\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x0f\x94\x00\x00\x00\x00\x00\x01\x00\x04\xb8\xf8\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x0f\xb6\x00\x00\x00\x00\x00\x01\x00\x04\xc0\x9d\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0f\xd2\x00\x00\x00\x00\x00\x01\x00\x04\xd1\x9d\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x0f\xf6\x00\x00\x00\x00\x00\x01\x00\x04\xdb\x1e\ +\x00\x00\x01\x9b\x7f\x73\xe2\xad\ +\x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe0\xbb\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x10\x3e\x00\x00\x00\x00\x00\x01\x00\x04\xea\x84\ +\x00\x00\x01\x9b\x7f\x73\xe2\xad\ +\x00\x00\x10\x5e\x00\x00\x00\x00\x00\x01\x00\x04\xee\x67\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x8c\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x88\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x8d\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x10\x04\x00\x00\x00\x00\x00\x01\x00\x04\xf0\x71\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x10\x34\x00\x00\x00\x00\x00\x01\x00\x04\xfa\x07\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x10\x64\x00\x00\x00\x00\x00\x01\x00\x05\x05\xe3\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x10\x88\x00\x00\x00\x00\x00\x01\x00\x05\x0c\x27\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x10\xb2\x00\x00\x00\x00\x00\x01\x00\x05\x13\xb0\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x10\xde\x00\x00\x00\x00\x00\x01\x00\x05\x1a\x0e\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x11\x14\x00\x00\x00\x00\x00\x01\x00\x05\x21\xfd\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x08\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x2f\xa0\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x08\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x34\xd5\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x11\x32\x00\x00\x00\x00\x00\x01\x00\x05\x3e\xaa\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ +\x00\x00\x10\x7a\x00\x00\x00\x00\x00\x01\x00\x04\xf4\xa2\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x10\xaa\x00\x00\x00\x00\x00\x01\x00\x04\xfe\x38\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x10\xda\x00\x00\x00\x00\x00\x01\x00\x05\x0a\x14\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x10\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x10\x58\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x11\x28\x00\x00\x00\x00\x00\x01\x00\x05\x17\xe1\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x11\x54\x00\x00\x00\x00\x00\x01\x00\x05\x1e\x3f\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x26\x2e\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x33\xd1\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x39\x06\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x42\xdb\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x98\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x99\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x11\x5a\x00\x00\x00\x00\x00\x01\x00\x05\x46\x74\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x01\x88\x00\x02\x00\x00\x00\x01\x00\x00\x00\x96\ +\x00\x00\x11\xd0\x00\x00\x00\x00\x00\x01\x00\x05\x4a\xa5\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9b\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\x98\x00\x02\x00\x00\x00\x28\x00\x00\x00\x97\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\x9c\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x11\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x4b\x3a\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x11\x90\x00\x00\x00\x00\x00\x01\x00\x05\x52\xee\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x55\x13\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x11\xe0\x00\x00\x00\x00\x00\x01\x00\x05\x56\x93\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x11\xfc\x00\x00\x00\x00\x00\x01\x00\x05\x5e\x40\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x12\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x63\x15\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x12\x32\x00\x00\x00\x00\x00\x01\x00\x05\x64\x05\ -\x00\x00\x01\x9b\x8e\xa8\x75\xac\ -\x00\x00\x12\x48\x00\x00\x00\x00\x00\x01\x00\x05\x68\x2e\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x12\x6e\x00\x00\x00\x00\x00\x01\x00\x05\x6e\x65\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x12\x88\x00\x00\x00\x00\x00\x01\x00\x05\x83\x72\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x12\xaa\x00\x00\x00\x00\x00\x01\x00\x05\x88\x62\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x12\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x8b\x61\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x91\x6b\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x13\x08\x00\x00\x00\x00\x00\x01\x00\x05\x94\xba\ -\x00\x00\x01\x9b\x8e\xa8\x75\x96\ -\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x98\xdb\ -\x00\x00\x01\x9b\x09\x08\xf9\x49\ -\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\x9e\xd2\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\xe3\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\xa4\xa6\ -\x00\x00\x01\x9b\x09\x08\xf9\x49\ -\x00\x00\x13\x88\x00\x00\x00\x00\x00\x01\x00\x05\xae\x5a\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x13\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xa8\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x13\xd4\x00\x00\x00\x00\x00\x01\x00\x05\xb5\x36\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x13\xfa\x00\x00\x00\x00\x00\x01\x00\x05\xb9\xd7\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x14\x0e\x00\x00\x00\x00\x00\x01\x00\x05\xc3\xa9\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x14\x3a\x00\x00\x00\x00\x00\x01\x00\x05\xc8\xf3\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x14\x62\x00\x00\x00\x00\x00\x01\x00\x05\xce\xfa\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x14\x78\x00\x00\x00\x00\x00\x01\x00\x05\xcf\xde\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x14\xa4\x00\x00\x00\x00\x00\x01\x00\x05\xd2\x27\ -\x00\x00\x01\x9b\x09\x08\xf9\x59\ -\x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xd8\xd7\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x14\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xdc\x1b\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xdd\x47\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x15\x08\x00\x00\x00\x00\x00\x01\x00\x05\xe3\x08\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x15\x2a\x00\x00\x00\x00\x00\x01\x00\x05\xe4\x2a\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x15\x48\x00\x00\x00\x00\x00\x01\x00\x05\xea\x1d\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x15\x68\x00\x00\x00\x00\x00\x01\x00\x05\xed\x21\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x05\xee\x42\ -\x00\x00\x01\x9b\x09\x08\xf9\x55\ -\x00\x00\x15\xaa\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x16\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x15\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xf9\x82\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x15\xfc\x00\x00\x00\x00\x00\x01\x00\x06\x01\x42\ -\x00\x00\x01\x9b\x09\x08\xf9\x51\ -\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x06\x06\x5d\ -\x00\x00\x01\x9b\x09\x08\xf9\x4d\ -\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x07\xb0\ -\x00\x00\x01\x9b\x09\x08\xf9\x49\ +\x00\x00\x11\xf0\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6b\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x12\x06\x00\x00\x00\x00\x00\x01\x00\x05\x57\x1f\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x59\x44\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x12\x56\x00\x00\x00\x00\x00\x01\x00\x05\x5a\xc4\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x12\x72\x00\x00\x00\x00\x00\x01\x00\x05\x62\x71\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x12\x92\x00\x00\x00\x00\x00\x01\x00\x05\x67\x46\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x12\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x68\x36\ +\x00\x00\x01\x9b\xbc\x0f\x8a\x2e\ +\x00\x00\x12\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x6c\x5f\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x12\xe4\x00\x00\x00\x00\x00\x01\x00\x05\x72\x96\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x12\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x87\xa3\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x8c\x93\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\x8f\x92\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x13\x4c\x00\x00\x00\x00\x00\x01\x00\x05\x95\x9c\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x13\x7e\x00\x00\x00\x00\x00\x01\x00\x05\x98\xeb\ +\x00\x00\x01\x9b\xbc\x0f\x8a\x2e\ +\x00\x00\x13\x96\x00\x00\x00\x00\x00\x01\x00\x05\x9d\x0c\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x13\xac\x00\x00\x00\x00\x00\x01\x00\x05\xa3\x03\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x13\xc0\x00\x00\x00\x00\x00\x01\x00\x05\xa5\x14\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x13\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xd7\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ +\x00\x00\x13\xfe\x00\x00\x00\x00\x00\x01\x00\x05\xb2\x8b\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb5\xd9\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x14\x4a\x00\x00\x00\x00\x00\x01\x00\x05\xb9\x67\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x14\x70\x00\x00\x00\x00\x00\x01\x00\x05\xbe\x08\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x14\x84\x00\x00\x00\x00\x00\x01\x00\x05\xc7\xda\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x14\xb0\x00\x00\x00\x00\x00\x01\x00\x05\xcd\x24\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x14\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x2b\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xd4\x0f\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x15\x1a\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x58\ +\x00\x00\x01\x9a\x72\xe1\x94\x5b\ +\x00\x00\x15\x30\x00\x00\x00\x00\x00\x01\x00\x05\xdd\x08\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x15\x4c\x00\x00\x00\x00\x00\x01\x00\x05\xe0\x4c\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x15\x64\x00\x00\x00\x00\x00\x01\x00\x05\xe1\x78\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x15\x7e\x00\x00\x00\x00\x00\x01\x00\x05\xe7\x39\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x15\xa0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x5b\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x15\xbe\x00\x00\x00\x00\x00\x01\x00\x05\xee\x4e\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x15\xde\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x52\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x16\x00\x00\x00\x00\x00\x00\x01\x00\x05\xf2\x73\ +\x00\x00\x01\x9a\x72\xe1\x94\x57\ +\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x05\xf5\x47\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x16\x4e\x00\x00\x00\x00\x00\x01\x00\x05\xfd\xb3\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x16\x72\x00\x00\x00\x00\x00\x01\x00\x06\x05\x73\ +\x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x16\x96\x00\x00\x00\x00\x00\x01\x00\x06\x0a\x8e\ +\x00\x00\x01\x9a\x72\xe1\x94\x4f\ +\x00\x00\x16\xbe\x00\x00\x00\x00\x00\x01\x00\x06\x0b\xe1\ +\x00\x00\x01\x9a\x72\xe1\x94\x4b\ " qt_version = [int(v) for v in QtCore.qVersion().split('.')] diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/move_nozzle_away.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/move_nozzle_away.svg new file mode 100644 index 00000000..ac52e9a5 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/move_nozzle_away.svg @@ -0,0 +1,15 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/move_nozzle_close.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/move_nozzle_close.svg new file mode 100644 index 00000000..6b633e62 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/move_nozzle_close.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file From 95d956f5eb96518119c70b51ac3a0a0e58241c11 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Wed, 14 Jan 2026 11:38:41 +0000 Subject: [PATCH 31/70] Work network priority (#122) * ADD: added priority to 'get_saved_networks' * ADD: added priority buttons ADD: added details page to saved network Refacotr: refactor how edit on saved is made * ADD: added option to set an Tittle * UPD: changed some text * Refactor: Ran Ruff formatter * Refactor: some butttons text and group name --------- Co-authored-by: Roberto Co-authored-by: Hugo Costa --- BlocksScreen/lib/network.py | 3 + BlocksScreen/lib/panels/networkWindow.py | 60 +- BlocksScreen/lib/ui/wifiConnectivityWindow.ui | 1170 +++++++++++------ .../lib/ui/wifiConnectivityWindow_ui.py | 515 +++++--- BlocksScreen/lib/utils/blocks_frame.py | 95 +- 5 files changed, 1233 insertions(+), 610 deletions(-) diff --git a/BlocksScreen/lib/network.py b/BlocksScreen/lib/network.py index 312b60e8..51b87074 100644 --- a/BlocksScreen/lib/network.py +++ b/BlocksScreen/lib/network.py @@ -731,6 +731,9 @@ def get_saved_networks( "mode": network_properties["802-11-wireless"][ "mode" ], + "priority": network_properties["connection"].get( + "autoconnect-priority", (None, None) + )[1], } if network_properties["connection"]["type"][1] == "802-11-wireless" diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 57617efe..125d07ba 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -223,7 +223,7 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: ) ) self.delete_network_signal.connect(self.delete_network) - self.panel.saved_connection_change_password_field.returnPressed.connect( + self.panel.snd_back.clicked.connect( lambda: self.update_network( ssid=self.panel.saved_connection_network_name.text(), password=self.panel.saved_connection_change_password_field.text(), @@ -316,8 +316,23 @@ def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") ) - self.panel.network_activate_btn.clicked.connect(self.saved_wifi_option_selected) - self.panel.network_delete_btn.clicked.connect(self.saved_wifi_option_selected) + self.panel.network_details_btn.setPixmap( + QtGui.QPixmap(":/ui/media/btn_icons/printer_settings.svg") + ) + + self.panel.snd_back.clicked.connect( + lambda: self.setCurrentIndex(self.indexOf(self.panel.saved_connection_page)) + ) + self.panel.network_details_btn.clicked.connect( + lambda: self.setCurrentIndex(self.indexOf(self.panel.saved_details_page)) + ) + + self.panel.network_activate_btn.clicked.connect( + lambda: self.saved_wifi_option_selected() + ) + self.panel.network_delete_btn.clicked.connect( + lambda: self.saved_wifi_option_selected() + ) self.network_list_worker.build() self.request_network_scan.emit() @@ -801,10 +816,16 @@ def update_network( if not self.sdbus_network.is_known(ssid): return + checked_btn = self.panel.priority_btn_group.checkedButton() + if checked_btn == self.panel.high_priority_btn: + priority = 90 + elif checked_btn == self.panel.low_priority_btn: + priority = 20 + else: + priority = 50 + self.sdbus_network.update_connection_settings( - ssid=ssid, - password=password, - new_ssid=new_ssid, + ssid=ssid, password=password, new_ssid=new_ssid, priority=priority ) QtCore.QTimer().singleShot(10000, lambda: self.network_list_worker.build()) self.setCurrentIndex(self.indexOf(self.panel.network_list_page)) @@ -838,14 +859,35 @@ def handle_network_list(self, data: typing.List[typing.Tuple]) -> None: def handle_button_click(self, ssid: str): """Handles pressing a network""" - if ssid in self.sdbus_network.get_saved_ssid_names(): + _saved_ssids = self.sdbus_network.get_saved_networks() + if any(item["ssid"] == ssid for item in _saved_ssids): self.setCurrentIndex(self.indexOf(self.panel.saved_connection_page)) self.panel.saved_connection_network_name.setText(str(ssid)) + self.panel.snd_name.setText(str(ssid)) + + # find the entry for this SSID + entry = next((item for item in _saved_ssids if item["ssid"] == ssid), None) + + logger.debug(_saved_ssids) + + if entry is not None: + priority = entry.get("priority") + + if priority == 90: + self.panel.hig_priorrity_btn.setChecked(True) + elif priority == 20: + self.panel.low_priorrity_btn.setChecked(True) + else: + self.panel.med_priorrity_btn.setChecked(True) + _curr_ssid = self.sdbus_network.get_current_ssid() if _curr_ssid != str(ssid): - self.panel.network_activate_btn.show() + self.panel.network_activate_btn.setDisabled(False) + self.panel.sn_info.setText("Saved Network") else: - self.panel.network_activate_btn.hide() + self.panel.network_activate_btn.setDisabled(True) + self.panel.sn_info.setText("Active Network") + self.panel.frame.repaint() else: diff --git a/BlocksScreen/lib/ui/wifiConnectivityWindow.ui b/BlocksScreen/lib/ui/wifiConnectivityWindow.ui index 289cbe08..871ba2dd 100644 --- a/BlocksScreen/lib/ui/wifiConnectivityWindow.ui +++ b/BlocksScreen/lib/ui/wifiConnectivityWindow.ui @@ -40,7 +40,7 @@ - 2 + 4 @@ -1066,7 +1066,56 @@ using the buttons on the side. - + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 60 + 20 + + + + + + + + + 0 + 0 + + + + + 16777215 + 60 + + + + + 20 + + + + color: rgb(255, 255, 255); + + + + + + false + + + Qt::AlignCenter + + + + + 60 @@ -1080,7 +1129,7 @@ using the buttons on the side. - Delete + Back true @@ -1092,12 +1141,524 @@ using the buttons on the side. icon - :/ui/media/btn_icons/indf_svg.svg + :/ui/media/btn_icons/back.svg + + + + - + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 20 + 20 + + + + + + + + + + + + + 0 + 0 + + + + + 400 + 16777215 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + + + 120 + 120 + 120 + + + + + + + 120 + 120 + 120 + + + + + + + + + 15 + + + + Signal +Strength + + + Qt::AlignCenter + + + + + + + + 250 + 0 + + + + + 11 + + + + color: rgb(255, 255, 255); + + + TextLabel + + + Qt::AlignCenter + + + + + + + + + Qt::Horizontal + + + + + + + + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + + + 120 + 120 + 120 + + + + + + + 120 + 120 + 120 + + + + + + + + + 15 + + + + Security +Type + + + Qt::AlignCenter + + + + + + + + 250 + 0 + + + + + 11 + + + + color: rgb(255, 255, 255); + + + TextLabel + + + Qt::AlignCenter + + + + + + + + + Qt::Horizontal + + + + + + + + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + + + 120 + 120 + 120 + + + + + + + 120 + 120 + 120 + + + + + + + + + 15 + + + + Status + + + Qt::AlignCenter + + + + + + + + 250 + 0 + + + + + 11 + + + + color: rgb(255, 255, 255); + + + TextLabel + + + Qt::AlignCenter + + + + + + + + + + + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + + + + 250 + 80 + + + + + 250 + 80 + + + + + 15 + + + + Connect + + + true + + + + + + + + 250 + 80 + + + + + 250 + 80 + + + + + 15 + + + + Details + + + true + + + + + + + + 250 + 80 + + + + + 250 + 80 + + + + + 15 + + + + Forget + + + true + + + + + + + + + + + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Minimum + + + + 60 + 60 + + + + + + + + + 0 + 0 + + 16777215 @@ -1182,7 +1743,7 @@ using the buttons on the side. - + 60 @@ -1215,9 +1776,9 @@ using the buttons on the side. - + - + Qt::Vertical @@ -1227,19 +1788,25 @@ using the buttons on the side. 20 - 30 + 20 - + 0 0 + + + 0 + 70 + + 16777215 @@ -1252,7 +1819,7 @@ using the buttons on the side. QFrame::Raised - + 0 @@ -1261,9 +1828,9 @@ using the buttons on the side. 62 - + - + @@ -1399,394 +1966,189 @@ Password - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 200 - 150 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 0 - 20 - 201 - 119 - - - - - - - - 0 - 0 - - - - - 100 - 16777215 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 120 - 120 - 120 - - - - - - - 120 - 120 - 120 - - - - - - - - - 15 - - - - Signal -Strength - - - Qt::AlignCenter - - - - - - - - 150 - 0 - - - - - 150 - 16777215 - - - - Qt::Horizontal - - - - - - - - 20 - - - - color:white - - - TextLabel - - - Qt::AlignCenter - - - - - - - - - - - - 200 - 100 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - 80 - 80 - - - - - 80 - 80 - - - - - - - true - - - - - - - - 60 - 60 - - - - - 80 - 80 - - - - - - - true - - - - - - + - - - - 0 - 0 - - - - - 0 - 0 - - - - - 200 - 150 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 0 - 20 - 201 - 119 - - - - - - - - 100 - 16777215 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 120 - 120 - 120 - - - - - - - 120 - 120 - 120 - - - - - - - - - 15 - - - - Security -Type - - - Qt::AlignCenter - - - - - - - - 150 - 0 - - - - Qt::Horizontal - - - - - - - - 20 - - - - color:white - - - TextLabel - - - Qt::AlignCenter - - - - - - + + + + + + 0 + 0 + + + + + 400 + 160 + + + + + 400 + 99999 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + Network priority + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 10 + 10 + + + + + + + + + + + 100 + 100 + + + + + 100 + 100 + + + + Low + + + true + + + true + + + true + + + back_btn + + + icon + + + :/ui/media/btn_icons/indf_svg.svg + + + priority_btn_group + + + + + + + + 100 + 100 + + + + + 100 + 100 + + + + Medium + + + true + + + true + + + true + + + true + + + back_btn + + + icon + + + :/ui/media/btn_icons/indf_svg.svg + + + priority_btn_group + + + + + + + + 100 + 100 + + + + + 100 + 100 + + + + High + + + true + + + false + + + true + + + true + + + back_btn + + + icon + + + :/ui/media/btn_icons/indf_svg.svg + + + priority_btn_group + + + + + + + + + @@ -2754,6 +3116,11 @@ Type QLabel
lib.panels.widgets.loadWidget
+ + GroupButton + QPushButton +
lib.utils.group_button
+
@@ -2762,4 +3129,7 @@ Type + + + diff --git a/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py b/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py index fccaf1ae..1af9f761 100644 --- a/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py +++ b/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py @@ -1,4 +1,4 @@ -# Form implementation generated from reading ui file '/home/levi/main/BlocksScreen/BlocksScreen/lib/ui/wifiConnectivityWindow.ui' +# Form implementation generated from reading ui file '/home/levi/BlocksScreen/BlocksScreen/lib/ui/wifiConnectivityWindow.ui' # # Created by: PyQt6 UI code generator 6.7.1 # @@ -435,15 +435,55 @@ def setupUi(self, wifi_stacked_page): self.verticalLayout_11.setObjectName("verticalLayout_11") self.horizontalLayout_7 = QtWidgets.QHBoxLayout() self.horizontalLayout_7.setObjectName("horizontalLayout_7") - self.saved_connection_delete_network_button = IconButton(parent=self.saved_connection_page) - self.saved_connection_delete_network_button.setMinimumSize(QtCore.QSize(60, 60)) - self.saved_connection_delete_network_button.setMaximumSize(QtCore.QSize(60, 60)) - self.saved_connection_delete_network_button.setFlat(True) - self.saved_connection_delete_network_button.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg")) - self.saved_connection_delete_network_button.setObjectName("saved_connection_delete_network_button") - self.horizontalLayout_7.addWidget(self.saved_connection_delete_network_button) + spacerItem4 = QtWidgets.QSpacerItem(60, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_7.addItem(spacerItem4) self.saved_connection_network_name = QtWidgets.QLabel(parent=self.saved_connection_page) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.saved_connection_network_name.sizePolicy().hasHeightForWidth()) + self.saved_connection_network_name.setSizePolicy(sizePolicy) self.saved_connection_network_name.setMaximumSize(QtCore.QSize(16777215, 60)) + font = QtGui.QFont() + font.setPointSize(20) + self.saved_connection_network_name.setFont(font) + self.saved_connection_network_name.setStyleSheet("color: rgb(255, 255, 255);") + self.saved_connection_network_name.setText("") + self.saved_connection_network_name.setScaledContents(False) + self.saved_connection_network_name.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.saved_connection_network_name.setObjectName("saved_connection_network_name") + self.horizontalLayout_7.addWidget(self.saved_connection_network_name) + self.saved_connection_back_button = IconButton(parent=self.saved_connection_page) + self.saved_connection_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.saved_connection_back_button.setMaximumSize(QtCore.QSize(60, 60)) + self.saved_connection_back_button.setFlat(True) + self.saved_connection_back_button.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) + self.saved_connection_back_button.setObjectName("saved_connection_back_button") + self.horizontalLayout_7.addWidget(self.saved_connection_back_button, 0, QtCore.Qt.AlignmentFlag.AlignRight) + self.verticalLayout_11.addLayout(self.horizontalLayout_7) + self.verticalLayout_5 = QtWidgets.QVBoxLayout() + self.verticalLayout_5.setObjectName("verticalLayout_5") + spacerItem5 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.verticalLayout_5.addItem(spacerItem5) + self.horizontalLayout_9 = QtWidgets.QHBoxLayout() + self.horizontalLayout_9.setObjectName("horizontalLayout_9") + self.verticalLayout_2 = QtWidgets.QVBoxLayout() + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.frame = BlocksCustomFrame(parent=self.saved_connection_page) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.frame.sizePolicy().hasHeightForWidth()) + self.frame.setSizePolicy(sizePolicy) + self.frame.setMaximumSize(QtCore.QSize(400, 16777215)) + self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame.setObjectName("frame") + self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.frame) + self.verticalLayout_6.setObjectName("verticalLayout_6") + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.netlist_strength_label_2 = QtWidgets.QLabel(parent=self.frame) palette = QtGui.QPalette() brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) @@ -463,42 +503,31 @@ def setupUi(self, wifi_stacked_page): brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.saved_connection_network_name.setPalette(palette) + self.netlist_strength_label_2.setPalette(palette) font = QtGui.QFont() - font.setPointSize(20) - self.saved_connection_network_name.setFont(font) - self.saved_connection_network_name.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.saved_connection_network_name.setObjectName("saved_connection_network_name") - self.horizontalLayout_7.addWidget(self.saved_connection_network_name) - self.saved_connection_back_button = IconButton(parent=self.saved_connection_page) - self.saved_connection_back_button.setMinimumSize(QtCore.QSize(60, 60)) - self.saved_connection_back_button.setMaximumSize(QtCore.QSize(60, 60)) - self.saved_connection_back_button.setFlat(True) - self.saved_connection_back_button.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) - self.saved_connection_back_button.setObjectName("saved_connection_back_button") - self.horizontalLayout_7.addWidget(self.saved_connection_back_button) - self.verticalLayout_11.addLayout(self.horizontalLayout_7) - self.verticalLayout_5 = QtWidgets.QVBoxLayout() - self.verticalLayout_5.setObjectName("verticalLayout_5") - spacerItem4 = QtWidgets.QSpacerItem(20, 30, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.verticalLayout_5.addItem(spacerItem4) - self.frame_5 = BlocksCustomFrame(parent=self.saved_connection_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.frame_5.sizePolicy().hasHeightForWidth()) - self.frame_5.setSizePolicy(sizePolicy) - self.frame_5.setMaximumSize(QtCore.QSize(16777215, 70)) - self.frame_5.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_5.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_5.setObjectName("frame_5") - self.layoutWidget_5 = QtWidgets.QWidget(parent=self.frame_5) - self.layoutWidget_5.setGeometry(QtCore.QRect(0, 0, 776, 62)) - self.layoutWidget_5.setObjectName("layoutWidget_5") - self.horizontalLayout_8 = QtWidgets.QHBoxLayout(self.layoutWidget_5) - self.horizontalLayout_8.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout_8.setObjectName("horizontalLayout_8") - self.saved_connection_change_password_label_2 = QtWidgets.QLabel(parent=self.layoutWidget_5) + font.setPointSize(15) + self.netlist_strength_label_2.setFont(font) + self.netlist_strength_label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_strength_label_2.setObjectName("netlist_strength_label_2") + self.horizontalLayout.addWidget(self.netlist_strength_label_2) + self.saved_connection_signal_strength_info_frame = QtWidgets.QLabel(parent=self.frame) + self.saved_connection_signal_strength_info_frame.setMinimumSize(QtCore.QSize(250, 0)) + font = QtGui.QFont() + font.setPointSize(11) + self.saved_connection_signal_strength_info_frame.setFont(font) + self.saved_connection_signal_strength_info_frame.setStyleSheet("color: rgb(255, 255, 255);") + self.saved_connection_signal_strength_info_frame.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.saved_connection_signal_strength_info_frame.setObjectName("saved_connection_signal_strength_info_frame") + self.horizontalLayout.addWidget(self.saved_connection_signal_strength_info_frame) + self.verticalLayout_6.addLayout(self.horizontalLayout) + self.line_4 = QtWidgets.QFrame(parent=self.frame) + self.line_4.setFrameShape(QtWidgets.QFrame.Shape.HLine) + self.line_4.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + self.line_4.setObjectName("line_4") + self.verticalLayout_6.addWidget(self.line_4) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.netlist_security_label_2 = QtWidgets.QLabel(parent=self.frame) palette = QtGui.QPalette() brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) @@ -518,55 +547,31 @@ def setupUi(self, wifi_stacked_page): brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.saved_connection_change_password_label_2.setPalette(palette) + self.netlist_security_label_2.setPalette(palette) font = QtGui.QFont() font.setPointSize(15) - self.saved_connection_change_password_label_2.setFont(font) - self.saved_connection_change_password_label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.saved_connection_change_password_label_2.setObjectName("saved_connection_change_password_label_2") - self.horizontalLayout_8.addWidget(self.saved_connection_change_password_label_2, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) - self.saved_connection_change_password_field = BlocksCustomLinEdit(parent=self.layoutWidget_5) - self.saved_connection_change_password_field.setMinimumSize(QtCore.QSize(500, 60)) - self.saved_connection_change_password_field.setMaximumSize(QtCore.QSize(500, 16777215)) + self.netlist_security_label_2.setFont(font) + self.netlist_security_label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_security_label_2.setObjectName("netlist_security_label_2") + self.horizontalLayout_2.addWidget(self.netlist_security_label_2) + self.saved_connection_security_type_info_label = QtWidgets.QLabel(parent=self.frame) + self.saved_connection_security_type_info_label.setMinimumSize(QtCore.QSize(250, 0)) font = QtGui.QFont() - font.setPointSize(12) - self.saved_connection_change_password_field.setFont(font) - self.saved_connection_change_password_field.setObjectName("saved_connection_change_password_field") - self.horizontalLayout_8.addWidget(self.saved_connection_change_password_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.saved_connection_change_password_view = IconButton(parent=self.layoutWidget_5) - self.saved_connection_change_password_view.setMinimumSize(QtCore.QSize(60, 60)) - self.saved_connection_change_password_view.setMaximumSize(QtCore.QSize(60, 60)) - self.saved_connection_change_password_view.setFlat(True) - self.saved_connection_change_password_view.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg")) - self.saved_connection_change_password_view.setObjectName("saved_connection_change_password_view") - self.horizontalLayout_8.addWidget(self.saved_connection_change_password_view, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) - self.verticalLayout_5.addWidget(self.frame_5) - self.horizontalLayout_9 = QtWidgets.QHBoxLayout() - self.horizontalLayout_9.setObjectName("horizontalLayout_9") - self.frame_3 = BlocksCustomFrame(parent=self.saved_connection_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.frame_3.sizePolicy().hasHeightForWidth()) - self.frame_3.setSizePolicy(sizePolicy) - self.frame_3.setMinimumSize(QtCore.QSize(0, 0)) - self.frame_3.setMaximumSize(QtCore.QSize(200, 150)) - self.frame_3.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_3.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_3.setObjectName("frame_3") - self.layoutWidget_3 = QtWidgets.QWidget(parent=self.frame_3) - self.layoutWidget_3.setGeometry(QtCore.QRect(0, 20, 201, 119)) - self.layoutWidget_3.setObjectName("layoutWidget_3") - self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.layoutWidget_3) - self.verticalLayout_6.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_6.setObjectName("verticalLayout_6") - self.sabed_connection_signal_strength_label = QtWidgets.QLabel(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.sabed_connection_signal_strength_label.sizePolicy().hasHeightForWidth()) - self.sabed_connection_signal_strength_label.setSizePolicy(sizePolicy) - self.sabed_connection_signal_strength_label.setMaximumSize(QtCore.QSize(100, 16777215)) + font.setPointSize(11) + self.saved_connection_security_type_info_label.setFont(font) + self.saved_connection_security_type_info_label.setStyleSheet("color: rgb(255, 255, 255);") + self.saved_connection_security_type_info_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.saved_connection_security_type_info_label.setObjectName("saved_connection_security_type_info_label") + self.horizontalLayout_2.addWidget(self.saved_connection_security_type_info_label) + self.verticalLayout_6.addLayout(self.horizontalLayout_2) + self.line_5 = QtWidgets.QFrame(parent=self.frame) + self.line_5.setFrameShape(QtWidgets.QFrame.Shape.HLine) + self.line_5.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + self.line_5.setObjectName("line_5") + self.verticalLayout_6.addWidget(self.line_5) + self.horizontalLayout_8 = QtWidgets.QHBoxLayout() + self.horizontalLayout_8.setObjectName("horizontalLayout_8") + self.netlist_security_label_4 = QtWidgets.QLabel(parent=self.frame) palette = QtGui.QPalette() brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) @@ -586,70 +591,77 @@ def setupUi(self, wifi_stacked_page): brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.sabed_connection_signal_strength_label.setPalette(palette) + self.netlist_security_label_4.setPalette(palette) font = QtGui.QFont() font.setPointSize(15) - self.sabed_connection_signal_strength_label.setFont(font) - self.sabed_connection_signal_strength_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.sabed_connection_signal_strength_label.setObjectName("sabed_connection_signal_strength_label") - self.verticalLayout_6.addWidget(self.sabed_connection_signal_strength_label, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignBottom) - self.line_4 = QtWidgets.QFrame(parent=self.layoutWidget_3) - self.line_4.setMinimumSize(QtCore.QSize(150, 0)) - self.line_4.setMaximumSize(QtCore.QSize(150, 16777215)) - self.line_4.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.line_4.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line_4.setObjectName("line_4") - self.verticalLayout_6.addWidget(self.line_4, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.saved_connection_signal_strength_info_frame = QtWidgets.QLabel(parent=self.layoutWidget_3) + self.netlist_security_label_4.setFont(font) + self.netlist_security_label_4.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_security_label_4.setObjectName("netlist_security_label_4") + self.horizontalLayout_8.addWidget(self.netlist_security_label_4) + self.sn_info = QtWidgets.QLabel(parent=self.frame) + self.sn_info.setMinimumSize(QtCore.QSize(250, 0)) font = QtGui.QFont() - font.setPointSize(20) - self.saved_connection_signal_strength_info_frame.setFont(font) - self.saved_connection_signal_strength_info_frame.setStyleSheet("color:white") - self.saved_connection_signal_strength_info_frame.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.saved_connection_signal_strength_info_frame.setObjectName("saved_connection_signal_strength_info_frame") - self.verticalLayout_6.addWidget(self.saved_connection_signal_strength_info_frame, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.horizontalLayout_9.addWidget(self.frame_3) - self.frame = BlocksCustomFrame(parent=self.saved_connection_page) - self.frame.setMaximumSize(QtCore.QSize(200, 100)) - self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame.setObjectName("frame") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.frame) - self.horizontalLayout.setObjectName("horizontalLayout") - self.network_activate_btn = IconButton(parent=self.frame) - self.network_activate_btn.setMinimumSize(QtCore.QSize(80, 80)) - self.network_activate_btn.setMaximumSize(QtCore.QSize(80, 80)) - self.network_activate_btn.setText("") + font.setPointSize(11) + self.sn_info.setFont(font) + self.sn_info.setStyleSheet("color: rgb(255, 255, 255);") + self.sn_info.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.sn_info.setObjectName("sn_info") + self.horizontalLayout_8.addWidget(self.sn_info) + self.verticalLayout_6.addLayout(self.horizontalLayout_8) + self.verticalLayout_2.addWidget(self.frame) + self.horizontalLayout_9.addLayout(self.verticalLayout_2) + self.frame_8 = BlocksCustomFrame(parent=self.saved_connection_page) + self.frame_8.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_8.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame_8.setObjectName("frame_8") + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.frame_8) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.network_activate_btn = BlocksCustomButton(parent=self.frame_8) + self.network_activate_btn.setMinimumSize(QtCore.QSize(250, 80)) + self.network_activate_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.network_activate_btn.setFont(font) self.network_activate_btn.setFlat(True) self.network_activate_btn.setObjectName("network_activate_btn") - self.horizontalLayout.addWidget(self.network_activate_btn) - self.network_delete_btn = IconButton(parent=self.frame) - self.network_delete_btn.setMinimumSize(QtCore.QSize(60, 60)) - self.network_delete_btn.setMaximumSize(QtCore.QSize(80, 80)) - self.network_delete_btn.setText("") + self.verticalLayout_4.addWidget(self.network_activate_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.network_details_btn = BlocksCustomButton(parent=self.frame_8) + self.network_details_btn.setMinimumSize(QtCore.QSize(250, 80)) + self.network_details_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.network_details_btn.setFont(font) + self.network_details_btn.setFlat(True) + self.network_details_btn.setObjectName("network_details_btn") + self.verticalLayout_4.addWidget(self.network_details_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) + self.network_delete_btn = BlocksCustomButton(parent=self.frame_8) + self.network_delete_btn.setMinimumSize(QtCore.QSize(250, 80)) + self.network_delete_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.network_delete_btn.setFont(font) self.network_delete_btn.setFlat(True) self.network_delete_btn.setObjectName("network_delete_btn") - self.horizontalLayout.addWidget(self.network_delete_btn) - self.horizontalLayout_9.addWidget(self.frame) - self.frame_4 = BlocksCustomFrame(parent=self.saved_connection_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Expanding) + self.verticalLayout_4.addWidget(self.network_delete_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.horizontalLayout_9.addWidget(self.frame_8) + self.verticalLayout_5.addLayout(self.horizontalLayout_9) + self.verticalLayout_11.addLayout(self.verticalLayout_5) + wifi_stacked_page.addWidget(self.saved_connection_page) + self.saved_details_page = QtWidgets.QWidget() + self.saved_details_page.setObjectName("saved_details_page") + self.verticalLayout_19 = QtWidgets.QVBoxLayout(self.saved_details_page) + self.verticalLayout_19.setObjectName("verticalLayout_19") + self.horizontalLayout_14 = QtWidgets.QHBoxLayout() + self.horizontalLayout_14.setObjectName("horizontalLayout_14") + spacerItem6 = QtWidgets.QSpacerItem(60, 60, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_14.addItem(spacerItem6) + self.snd_name = QtWidgets.QLabel(parent=self.saved_details_page) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.frame_4.sizePolicy().hasHeightForWidth()) - self.frame_4.setSizePolicy(sizePolicy) - self.frame_4.setMinimumSize(QtCore.QSize(0, 0)) - self.frame_4.setMaximumSize(QtCore.QSize(200, 150)) - self.frame_4.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_4.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_4.setObjectName("frame_4") - self.layoutWidget_4 = QtWidgets.QWidget(parent=self.frame_4) - self.layoutWidget_4.setGeometry(QtCore.QRect(0, 20, 201, 119)) - self.layoutWidget_4.setObjectName("layoutWidget_4") - self.verticalLayout_7 = QtWidgets.QVBoxLayout(self.layoutWidget_4) - self.verticalLayout_7.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_7.setObjectName("verticalLayout_7") - self.saved_connection_security_type_label = QtWidgets.QLabel(parent=self.layoutWidget_4) - self.saved_connection_security_type_label.setMaximumSize(QtCore.QSize(100, 16777215)) + sizePolicy.setHeightForWidth(self.snd_name.sizePolicy().hasHeightForWidth()) + self.snd_name.setSizePolicy(sizePolicy) + self.snd_name.setMaximumSize(QtCore.QSize(16777215, 60)) palette = QtGui.QPalette() brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) @@ -669,39 +681,154 @@ def setupUi(self, wifi_stacked_page): brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.saved_connection_security_type_label.setPalette(palette) + self.snd_name.setPalette(palette) + font = QtGui.QFont() + font.setPointSize(20) + self.snd_name.setFont(font) + self.snd_name.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.snd_name.setObjectName("snd_name") + self.horizontalLayout_14.addWidget(self.snd_name) + self.snd_back = IconButton(parent=self.saved_details_page) + self.snd_back.setMinimumSize(QtCore.QSize(60, 60)) + self.snd_back.setMaximumSize(QtCore.QSize(60, 60)) + self.snd_back.setFlat(True) + self.snd_back.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) + self.snd_back.setObjectName("snd_back") + self.horizontalLayout_14.addWidget(self.snd_back) + self.verticalLayout_19.addLayout(self.horizontalLayout_14) + self.verticalLayout_8 = QtWidgets.QVBoxLayout() + self.verticalLayout_8.setObjectName("verticalLayout_8") + spacerItem7 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.verticalLayout_8.addItem(spacerItem7) + self.frame_9 = BlocksCustomFrame(parent=self.saved_details_page) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.frame_9.sizePolicy().hasHeightForWidth()) + self.frame_9.setSizePolicy(sizePolicy) + self.frame_9.setMinimumSize(QtCore.QSize(0, 70)) + self.frame_9.setMaximumSize(QtCore.QSize(16777215, 70)) + self.frame_9.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_9.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame_9.setObjectName("frame_9") + self.layoutWidget_8 = QtWidgets.QWidget(parent=self.frame_9) + self.layoutWidget_8.setGeometry(QtCore.QRect(0, 0, 776, 62)) + self.layoutWidget_8.setObjectName("layoutWidget_8") + self.horizontalLayout_10 = QtWidgets.QHBoxLayout(self.layoutWidget_8) + self.horizontalLayout_10.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_10.setObjectName("horizontalLayout_10") + self.saved_connection_change_password_label_3 = QtWidgets.QLabel(parent=self.layoutWidget_8) + palette = QtGui.QPalette() + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) + self.saved_connection_change_password_label_3.setPalette(palette) font = QtGui.QFont() font.setPointSize(15) - self.saved_connection_security_type_label.setFont(font) - self.saved_connection_security_type_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.saved_connection_security_type_label.setObjectName("saved_connection_security_type_label") - self.verticalLayout_7.addWidget(self.saved_connection_security_type_label, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignBottom) - self.line_5 = QtWidgets.QFrame(parent=self.layoutWidget_4) - self.line_5.setMinimumSize(QtCore.QSize(150, 0)) - self.line_5.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.line_5.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line_5.setObjectName("line_5") - self.verticalLayout_7.addWidget(self.line_5, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.saved_connection_security_type_info_label = QtWidgets.QLabel(parent=self.layoutWidget_4) + self.saved_connection_change_password_label_3.setFont(font) + self.saved_connection_change_password_label_3.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.saved_connection_change_password_label_3.setObjectName("saved_connection_change_password_label_3") + self.horizontalLayout_10.addWidget(self.saved_connection_change_password_label_3, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.saved_connection_change_password_field = BlocksCustomLinEdit(parent=self.layoutWidget_8) + self.saved_connection_change_password_field.setMinimumSize(QtCore.QSize(500, 60)) + self.saved_connection_change_password_field.setMaximumSize(QtCore.QSize(500, 16777215)) font = QtGui.QFont() - font.setPointSize(20) - self.saved_connection_security_type_info_label.setFont(font) - self.saved_connection_security_type_info_label.setStyleSheet("color:white") - self.saved_connection_security_type_info_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.saved_connection_security_type_info_label.setObjectName("saved_connection_security_type_info_label") - self.verticalLayout_7.addWidget(self.saved_connection_security_type_info_label, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.horizontalLayout_9.addWidget(self.frame_4) - self.verticalLayout_5.addLayout(self.horizontalLayout_9) - self.verticalLayout_11.addLayout(self.verticalLayout_5) - wifi_stacked_page.addWidget(self.saved_connection_page) + font.setPointSize(12) + self.saved_connection_change_password_field.setFont(font) + self.saved_connection_change_password_field.setObjectName("saved_connection_change_password_field") + self.horizontalLayout_10.addWidget(self.saved_connection_change_password_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) + self.saved_connection_change_password_view = IconButton(parent=self.layoutWidget_8) + self.saved_connection_change_password_view.setMinimumSize(QtCore.QSize(60, 60)) + self.saved_connection_change_password_view.setMaximumSize(QtCore.QSize(60, 60)) + self.saved_connection_change_password_view.setFlat(True) + self.saved_connection_change_password_view.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg")) + self.saved_connection_change_password_view.setObjectName("saved_connection_change_password_view") + self.horizontalLayout_10.addWidget(self.saved_connection_change_password_view, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.verticalLayout_8.addWidget(self.frame_9) + self.horizontalLayout_13 = QtWidgets.QHBoxLayout() + self.horizontalLayout_13.setObjectName("horizontalLayout_13") + self.verticalLayout_13 = QtWidgets.QVBoxLayout() + self.verticalLayout_13.setObjectName("verticalLayout_13") + self.frame_12 = BlocksCustomFrame(parent=self.saved_details_page) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.frame_12.sizePolicy().hasHeightForWidth()) + self.frame_12.setSizePolicy(sizePolicy) + self.frame_12.setMinimumSize(QtCore.QSize(400, 160)) + self.frame_12.setMaximumSize(QtCore.QSize(400, 99999)) + self.frame_12.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_12.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame_12.setObjectName("frame_12") + self.verticalLayout_17 = QtWidgets.QVBoxLayout(self.frame_12) + self.verticalLayout_17.setObjectName("verticalLayout_17") + spacerItem8 = QtWidgets.QSpacerItem(10, 10, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.verticalLayout_17.addItem(spacerItem8) + self.horizontalLayout_4 = QtWidgets.QHBoxLayout() + self.horizontalLayout_4.setObjectName("horizontalLayout_4") + self.low_priority_btn = GroupButton(parent=self.frame_12) + self.low_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) + self.low_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) + self.low_priority_btn.setCheckable(True) + self.low_priority_btn.setAutoExclusive(True) + self.low_priority_btn.setFlat(True) + self.low_priority_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg")) + self.low_priority_btn.setObjectName("low_priority_btn") + self.priority_btn_group = QtWidgets.QButtonGroup(wifi_stacked_page) + self.priority_btn_group.setObjectName("priority_btn_group") + self.priority_btn_group.addButton(self.low_priority_btn) + self.horizontalLayout_4.addWidget(self.low_priority_btn) + self.med_priority_btn = GroupButton(parent=self.frame_12) + self.med_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) + self.med_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) + self.med_priority_btn.setCheckable(True) + self.med_priority_btn.setChecked(True) + self.med_priority_btn.setAutoExclusive(True) + self.med_priority_btn.setFlat(True) + self.med_priority_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg")) + self.med_priority_btn.setObjectName("med_priority_btn") + self.priority_btn_group.addButton(self.med_priority_btn) + self.horizontalLayout_4.addWidget(self.med_priority_btn) + self.high_priority_btn = GroupButton(parent=self.frame_12) + self.high_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) + self.high_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) + self.high_priority_btn.setCheckable(True) + self.high_priority_btn.setChecked(False) + self.high_priority_btn.setAutoExclusive(True) + self.high_priority_btn.setFlat(True) + self.high_priority_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg")) + self.high_priority_btn.setObjectName("high_priority_btn") + self.priority_btn_group.addButton(self.high_priority_btn) + self.horizontalLayout_4.addWidget(self.high_priority_btn) + self.verticalLayout_17.addLayout(self.horizontalLayout_4) + self.verticalLayout_13.addWidget(self.frame_12, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.horizontalLayout_13.addLayout(self.verticalLayout_13) + self.verticalLayout_8.addLayout(self.horizontalLayout_13) + self.verticalLayout_19.addLayout(self.verticalLayout_8) + wifi_stacked_page.addWidget(self.saved_details_page) self.hotspot_page = QtWidgets.QWidget() self.hotspot_page.setObjectName("hotspot_page") self.verticalLayout_12 = QtWidgets.QVBoxLayout(self.hotspot_page) self.verticalLayout_12.setObjectName("verticalLayout_12") self.hospot_page_header_layout = QtWidgets.QHBoxLayout() self.hospot_page_header_layout.setObjectName("hospot_page_header_layout") - spacerItem5 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.hospot_page_header_layout.addItem(spacerItem5) + spacerItem9 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.hospot_page_header_layout.addItem(spacerItem9) self.hotspot_header_title = QtWidgets.QLabel(parent=self.hotspot_page) palette = QtGui.QPalette() brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) @@ -740,8 +867,8 @@ def setupUi(self, wifi_stacked_page): self.hotspot_page_content_layout = QtWidgets.QVBoxLayout() self.hotspot_page_content_layout.setContentsMargins(-1, 5, -1, 5) self.hotspot_page_content_layout.setObjectName("hotspot_page_content_layout") - spacerItem6 = QtWidgets.QSpacerItem(20, 50, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.hotspot_page_content_layout.addItem(spacerItem6) + spacerItem10 = QtWidgets.QSpacerItem(20, 50, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.hotspot_page_content_layout.addItem(spacerItem10) self.frame_6 = BlocksCustomFrame(parent=self.hotspot_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) sizePolicy.setHorizontalStretch(0) @@ -807,11 +934,11 @@ def setupUi(self, wifi_stacked_page): self.hotspot_name_input_field.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password) self.hotspot_name_input_field.setObjectName("hotspot_name_input_field") self.horizontalLayout_11.addWidget(self.hotspot_name_input_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) - spacerItem7 = QtWidgets.QSpacerItem(60, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.horizontalLayout_11.addItem(spacerItem7) + spacerItem11 = QtWidgets.QSpacerItem(60, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.horizontalLayout_11.addItem(spacerItem11) self.hotspot_page_content_layout.addWidget(self.frame_6) - spacerItem8 = QtWidgets.QSpacerItem(773, 128, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.hotspot_page_content_layout.addItem(spacerItem8) + spacerItem12 = QtWidgets.QSpacerItem(773, 128, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.hotspot_page_content_layout.addItem(spacerItem12) self.frame_7 = BlocksCustomFrame(parent=self.hotspot_page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) sizePolicy.setHorizontalStretch(0) @@ -1017,7 +1144,7 @@ def setupUi(self, wifi_stacked_page): wifi_stacked_page.addWidget(self.hotspot_page) self.retranslateUi(wifi_stacked_page) - wifi_stacked_page.setCurrentIndex(2) + wifi_stacked_page.setCurrentIndex(4) QtCore.QMetaObject.connectSlotsByName(wifi_stacked_page) def retranslateUi(self, wifi_stacked_page): @@ -1052,24 +1179,39 @@ def retranslateUi(self, wifi_stacked_page): self.add_network_password_view.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.add_network_password_view.setProperty("button_type", _translate("wifi_stacked_page", "icon")) self.add_network_validation_button.setText(_translate("wifi_stacked_page", "Activate")) - self.saved_connection_delete_network_button.setText(_translate("wifi_stacked_page", "Delete")) - self.saved_connection_delete_network_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) - self.saved_connection_delete_network_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.saved_connection_network_name.setText(_translate("wifi_stacked_page", "SSID")) self.saved_connection_back_button.setText(_translate("wifi_stacked_page", "Back")) self.saved_connection_back_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.saved_connection_back_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.saved_connection_change_password_label_2.setText(_translate("wifi_stacked_page", "Change\n" -"Password")) - self.saved_connection_change_password_view.setText(_translate("wifi_stacked_page", "View")) - self.saved_connection_change_password_view.setProperty("class", _translate("wifi_stacked_page", "back_btn")) - self.saved_connection_change_password_view.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.sabed_connection_signal_strength_label.setText(_translate("wifi_stacked_page", "Signal\n" + self.netlist_strength_label_2.setText(_translate("wifi_stacked_page", "Signal\n" "Strength")) self.saved_connection_signal_strength_info_frame.setText(_translate("wifi_stacked_page", "TextLabel")) - self.saved_connection_security_type_label.setText(_translate("wifi_stacked_page", "Security\n" + self.netlist_security_label_2.setText(_translate("wifi_stacked_page", "Security\n" "Type")) self.saved_connection_security_type_info_label.setText(_translate("wifi_stacked_page", "TextLabel")) + self.netlist_security_label_4.setText(_translate("wifi_stacked_page", "Status")) + self.sn_info.setText(_translate("wifi_stacked_page", "TextLabel")) + self.network_activate_btn.setText(_translate("wifi_stacked_page", "Connect")) + self.network_details_btn.setText(_translate("wifi_stacked_page", "Details")) + self.network_delete_btn.setText(_translate("wifi_stacked_page", "Forget")) + self.snd_name.setText(_translate("wifi_stacked_page", "SSID")) + self.snd_back.setText(_translate("wifi_stacked_page", "Back")) + self.snd_back.setProperty("class", _translate("wifi_stacked_page", "back_btn")) + self.snd_back.setProperty("button_type", _translate("wifi_stacked_page", "icon")) + self.saved_connection_change_password_label_3.setText(_translate("wifi_stacked_page", "Change\n" +"Password")) + self.saved_connection_change_password_view.setText(_translate("wifi_stacked_page", "View")) + self.saved_connection_change_password_view.setProperty("class", _translate("wifi_stacked_page", "back_btn")) + self.saved_connection_change_password_view.setProperty("button_type", _translate("wifi_stacked_page", "icon")) + self.frame_12.setProperty("text", _translate("wifi_stacked_page", "Network priority")) + self.low_priority_btn.setText(_translate("wifi_stacked_page", "Low")) + self.low_priority_btn.setProperty("class", _translate("wifi_stacked_page", "back_btn")) + self.low_priority_btn.setProperty("button_type", _translate("wifi_stacked_page", "icon")) + self.med_priority_btn.setText(_translate("wifi_stacked_page", "Medium")) + self.med_priority_btn.setProperty("class", _translate("wifi_stacked_page", "back_btn")) + self.med_priority_btn.setProperty("button_type", _translate("wifi_stacked_page", "icon")) + self.high_priority_btn.setText(_translate("wifi_stacked_page", "High")) + self.high_priority_btn.setProperty("class", _translate("wifi_stacked_page", "back_btn")) + self.high_priority_btn.setProperty("button_type", _translate("wifi_stacked_page", "icon")) self.hotspot_header_title.setText(_translate("wifi_stacked_page", "Hotspot")) self.hotspot_back_button.setText(_translate("wifi_stacked_page", "Back")) self.hotspot_back_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) @@ -1085,4 +1227,5 @@ def retranslateUi(self, wifi_stacked_page): from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.blocks_linedit import BlocksCustomLinEdit from lib.utils.blocks_togglebutton import NetworkWidgetbuttons +from lib.utils.group_button import GroupButton from lib.utils.icon_button import IconButton diff --git a/BlocksScreen/lib/utils/blocks_frame.py b/BlocksScreen/lib/utils/blocks_frame.py index 6783ad8a..7de7514e 100644 --- a/BlocksScreen/lib/utils/blocks_frame.py +++ b/BlocksScreen/lib/utils/blocks_frame.py @@ -1,29 +1,94 @@ -from PyQt6.QtWidgets import QFrame -from PyQt6.QtGui import QPainter, QPen, QBrush, QColor -from PyQt6.QtCore import QRectF +from PyQt6 import QtCore, QtGui, QtWidgets +import typing -class BlocksCustomFrame(QFrame): +class BlocksCustomFrame(QtWidgets.QFrame): def __init__(self, parent=None): super().__init__(parent) - self._radius = 20 + + self._radius = 10 + self._left_line_width = 15 + self._is_centered = False + self.text = "" + + self.setMinimumHeight(40) + self.setMinimumWidth(300) def setRadius(self, radius: int): """Set widget frame radius""" self._radius = radius self.update() - def radius(self): - """Get widget frame radius""" - return self._radius + def setLeftLineWidth(self, width: int): + """Set widget left line width""" + self._left_line_width = width + self.update() + + def setCentered(self, centered: bool): + """Set if text is centered or left-aligned""" + self._is_centered = centered + self.update() - def paintEvent(self, event): - """Re-implemented method, paint widget""" - painter = QPainter(self) - painter.setRenderHint(QPainter.RenderHint.Antialiasing) - rect = QRectF(self.rect()) - pen = QPen(QColor(20, 20, 20, 70)) + def setProperty(self, name: str | None, value: typing.Any) -> bool: + if name == "text": + self.text = value + self.update() + return True + return super().setProperty(name, value) + + def paintEvent(self, a0): + painter = QtGui.QPainter(self) + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing) + + rect = QtCore.QRectF(self.rect()) + pen = QtGui.QPen(QtGui.QColor(20, 20, 20, 70)) pen.setWidth(2) painter.setPen(pen) - painter.setBrush(QBrush(QColor(50, 50, 50, 100))) + painter.setBrush(QtGui.QBrush(QtGui.QColor(50, 50, 50, 100))) painter.drawRoundedRect(rect.adjusted(1, 1, -1, -1), self._radius, self._radius) + + if self.text: + painter.setPen(QtGui.QColor("white")) + font = QtGui.QFont() + font.setPointSize(12) + painter.setFont(font) + fm = painter.fontMetrics() + text_width = fm.horizontalAdvance(self.text) + baseline = fm.ascent() + + margin = 10 + spacing = 8 + line_center_y = margin + baseline // 2 + + if self._is_centered: + left_line_width = self._left_line_width + right_line_width = self._left_line_width + + total_content_width = ( + left_line_width + spacing + text_width + spacing + right_line_width + ) + + start_x = (self.width() - total_content_width) // 2 + x = max(margin, start_x) + + else: + left_line_width = self._left_line_width + x = margin + right_line_width = 0 + + small_rect = QtCore.QRect(x, line_center_y - 1, left_line_width, 3) + painter.fillRect(small_rect, QtGui.QColor("white")) + x += left_line_width + spacing + + painter.drawText(x, margin + baseline, self.text) + x += text_width + spacing + + if self._is_centered: + big_rect_width = right_line_width + else: + remaining_width = self.width() - x - margin + big_rect_width = max(0, remaining_width) + + big_rect = QtCore.QRect(x, line_center_y - 1, big_rect_width, 3) + + painter.fillRect(big_rect, QtGui.QColor("white")) From 102a7466217387e3d227a65b7773105ee0acc581 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Wed, 14 Jan 2026 11:42:56 +0000 Subject: [PATCH 32/70] Bugfix: fixed loadwidget default being placeholder (gif) (#145) Co-authored-by: Roberto Co-authored-by: Hugo Costa --- BlocksScreen/lib/panels/widgets/loadWidget.py | 2 +- BlocksScreen/lib/panels/widgets/probeHelperPage.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/BlocksScreen/lib/panels/widgets/loadWidget.py b/BlocksScreen/lib/panels/widgets/loadWidget.py index c23d4a5d..1119bd14 100644 --- a/BlocksScreen/lib/panels/widgets/loadWidget.py +++ b/BlocksScreen/lib/panels/widgets/loadWidget.py @@ -18,7 +18,7 @@ class AnimationGIF(enum.Enum): def __init__( self, parent: QtWidgets.QWidget, - initial_anim_type: AnimationGIF = AnimationGIF.PLACEHOLDER, + initial_anim_type: AnimationGIF = AnimationGIF.DEFAULT, ) -> None: super().__init__(parent) diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index a3dea377..77e17d11 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -52,7 +52,7 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: self.Loadscreen = BasePopup(self, dialog=False) self.loadwidget = LoadingOverlayWidget( - self, LoadingOverlayWidget.AnimationGIF.PLACEHOLDER + self, LoadingOverlayWidget.AnimationGIF.DEFAULT ) self.Loadscreen.add_widget(self.loadwidget) self.setObjectName("probe_offset_page") From a9a486bad7832299656f3c6f1b119f5bd7fb74f6 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Wed, 14 Jan 2026 13:35:59 +0000 Subject: [PATCH 33/70] Bugfix/after merge fix (#151) * bugfix: fixed wrong imports * bugfix: wrong button name --------- Co-authored-by: Roberto --- BlocksScreen/lib/panels/networkWindow.py | 7 +++---- BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py | 8 ++++---- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 125d07ba..c15b4f75 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -874,12 +874,11 @@ def handle_button_click(self, ssid: str): priority = entry.get("priority") if priority == 90: - self.panel.hig_priorrity_btn.setChecked(True) + self.panel.high_priority_btn.setChecked(True) elif priority == 20: - self.panel.low_priorrity_btn.setChecked(True) + self.panel.low_priority_btn.setChecked(True) else: - self.panel.med_priorrity_btn.setChecked(True) - + self.panel.med_priority_btn.setChecked(True) _curr_ssid = self.sdbus_network.get_current_ssid() if _curr_ssid != str(ssid): self.panel.network_activate_btn.setDisabled(False) diff --git a/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py b/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py index 1af9f761..e66053a9 100644 --- a/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py +++ b/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py @@ -781,7 +781,7 @@ def setupUi(self, wifi_stacked_page): self.verticalLayout_17.addItem(spacerItem8) self.horizontalLayout_4 = QtWidgets.QHBoxLayout() self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.low_priority_btn = GroupButton(parent=self.frame_12) + self.low_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) self.low_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) self.low_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) self.low_priority_btn.setCheckable(True) @@ -793,7 +793,7 @@ def setupUi(self, wifi_stacked_page): self.priority_btn_group.setObjectName("priority_btn_group") self.priority_btn_group.addButton(self.low_priority_btn) self.horizontalLayout_4.addWidget(self.low_priority_btn) - self.med_priority_btn = GroupButton(parent=self.frame_12) + self.med_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) self.med_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) self.med_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) self.med_priority_btn.setCheckable(True) @@ -804,7 +804,7 @@ def setupUi(self, wifi_stacked_page): self.med_priority_btn.setObjectName("med_priority_btn") self.priority_btn_group.addButton(self.med_priority_btn) self.horizontalLayout_4.addWidget(self.med_priority_btn) - self.high_priority_btn = GroupButton(parent=self.frame_12) + self.high_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) self.high_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) self.high_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) self.high_priority_btn.setCheckable(True) @@ -1227,5 +1227,5 @@ def retranslateUi(self, wifi_stacked_page): from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.blocks_linedit import BlocksCustomLinEdit from lib.utils.blocks_togglebutton import NetworkWidgetbuttons -from lib.utils.group_button import GroupButton +from lib.utils.check_button import BlocksCustomCheckButton from lib.utils.icon_button import IconButton From 1a3f83e9c2f66158336cca7746e6d208b1dd6e19 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Wed, 14 Jan 2026 17:52:51 +0000 Subject: [PATCH 34/70] Fans controlling UI wasnt working (#153) Co-authored-by: Guilherme Costa --- BlocksScreen/helper_methods.py | 3 +- BlocksScreen/lib/panels/controlTab.py | 67 ++++++++++----------- BlocksScreen/lib/panels/widgets/tunePage.py | 5 +- 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/BlocksScreen/helper_methods.py b/BlocksScreen/helper_methods.py index dafabd96..b4dafc0f 100644 --- a/BlocksScreen/helper_methods.py +++ b/BlocksScreen/helper_methods.py @@ -7,14 +7,13 @@ import ctypes -import os import enum import logging +import os import pathlib import struct import typing - try: ctypes.cdll.LoadLibrary("libXext.so.6") libxext = ctypes.CDLL("libXext.so.6") diff --git a/BlocksScreen/lib/panels/controlTab.py b/BlocksScreen/lib/panels/controlTab.py index c17cb176..5d1b9b80 100644 --- a/BlocksScreen/lib/panels/controlTab.py +++ b/BlocksScreen/lib/panels/controlTab.py @@ -1,24 +1,23 @@ from __future__ import annotations +import re import typing from functools import partial -import re + +from helper_methods import normalize from lib.moonrakerComm import MoonWebSocket -from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.panels.widgets.basePopup import BasePopup +from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.panels.widgets.numpadPage import CustomNumpad +from lib.panels.widgets.optionCardWidget import OptionCard +from lib.panels.widgets.popupDialogWidget import Popup from lib.panels.widgets.printcorePage import SwapPrintcorePage from lib.panels.widgets.probeHelperPage import ProbeHelper +from lib.panels.widgets.slider_selector_page import SliderPage from lib.printer import Printer from lib.ui.controlStackedWidget_ui import Ui_controlStackedWidget -from PyQt6 import QtCore, QtGui, QtWidgets - -from lib.panels.widgets.popupDialogWidget import Popup from lib.utils.display_button import DisplayButton -from lib.panels.widgets.slider_selector_page import SliderPage - -from lib.panels.widgets.optionCardWidget import OptionCard -from helper_methods import normalize +from PyQt6 import QtCore, QtGui, QtWidgets class ControlTab(QtWidgets.QStackedWidget): @@ -274,6 +273,12 @@ def __init__( ) ) + self.path = { + "fan_cage": QtGui.QPixmap(":/fan_related/media/btn_icons/fan_cage.svg"), + "blower": QtGui.QPixmap(":/fan_related/media/btn_icons/blower.svg"), + "fan": QtGui.QPixmap(":/fan_related/media/btn_icons/fan.svg"), + } + self.panel.cp_z_tilt_btn.clicked.connect(lambda: self.handle_ztilt()) self.printcores_page.pc_accept.clicked.connect(self.handle_swapcore) @@ -306,23 +311,25 @@ def on_fan_object_update( if "speed" not in field: return - if name == "fan_generic Auxiliary_Cooling_Fans": - name = "Auxiliary\ncooling fans" - elif name == "fan_generic CHAMBER_EXHAUST": - name = "Exhaust Fan" - elif name == "fan_generic Part_Cooling_Fan": - name = "Cooling fan" - else: - name = name.removeprefix("fan_generic") - fan_card = self.tune_display_buttons.get(name) + fields = name.split() + first_field = fields[0] + second_field = fields[1] if len(fields) > 1 else None + name = second_field.replace("_", " ") if second_field else name - if fan_card is None: - icon_path = ( - ":/temperature_related/media/btn_icons/blower.svg" - if "blower" in name.lower() - else ":/temperature_related/media/btn_icons/fan.svg" - ) - icon = QtGui.QPixmap(icon_path) + fan_card = self.tune_display_buttons.get(name) + if fan_card is None and first_field in ( + "fan", + "fan_generic", + ): + icon = self.path.get("fan") + if second_field: + second_field = second_field.lower() + pattern_blower = r"(?:^|_)(?:blower|auxiliary)(?:_|$)" + pattern_exhaust = r"(?:^|_)exhaust(?:_|$)" + if re.search(pattern_blower, second_field): + icon = self.path.get("blower") + elif re.search(pattern_exhaust, second_field): + icon = self.path.get("fan_cage") card = OptionCard(self, name, str(name), icon) # type: ignore card.setObjectName(str(name)) @@ -379,20 +386,12 @@ def on_slider_change(self, name: str, new_value: int) -> None: if "speed" in name.lower(): self.speed_factor_override = new_value / 100 self.run_gcode_signal.emit(f"M220 S{new_value}") - - if name == "Auxiliary\ncooling fans": - name = "Auxiliary_Cooling_Fans" - elif name == "Exhaust Fan": - name = "CHAMBER_EXHAUST" - elif name == "Cooling fan": - name = "Part_Cooling_Fan" - else: - ... if name.lower() == "fan": self.run_gcode_signal.emit( f"M106 S{int(round((normalize(float(new_value / 100), 0.0, 1.0, 0, 255))))}" ) # [0, 255] Range else: + name = name.replace(" ", "_") self.run_gcode_signal.emit( f'SET_FAN_SPEED FAN="{name}" SPEED={float(new_value / 100.00)}' ) # [0.0, 1.0] Range diff --git a/BlocksScreen/lib/panels/widgets/tunePage.py b/BlocksScreen/lib/panels/widgets/tunePage.py index 0fc8faf7..09b5868d 100644 --- a/BlocksScreen/lib/panels/widgets/tunePage.py +++ b/BlocksScreen/lib/panels/widgets/tunePage.py @@ -95,6 +95,7 @@ def on_slider_change(self, name: str, new_value: int) -> None: f"M106 S{int(round((normalize(float(new_value / 100), 0.0, 1.0, 0, 255))))}" ) # [0, 255] Range else: + name = name.replace(" ", "_") self.run_gcode.emit( f"SET_FAN_SPEED FAN={name} SPEED={float(new_value / 100.00)}" ) # [0.0, 1.0] Range @@ -113,7 +114,7 @@ def on_fan_object_update( """ fields = name.split() first_field = fields[0] - second_field = fields[1].lower() if len(fields) > 1 else None + second_field = fields[1] if len(fields) > 1 else None if "speed" in field: if not self.tune_display_buttons.get(name, None) and first_field in ( "fan", @@ -126,6 +127,8 @@ def on_fan_object_update( _new_display_button.setParent(self) _new_display_button.icon_pixmap = self.path.get("fan") if second_field: + name = second_field.replace("_", " ") + second_field = second_field.lower() if re.search(pattern_blower, second_field): _new_display_button.icon_pixmap = self.path.get("blower") elif re.search(pattern_exhaust, second_field): From 92e7e54bfde28cc8e07c5fe7f6b7c8241753eb5c Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Wed, 14 Jan 2026 17:54:53 +0000 Subject: [PATCH 35/70] Bugfix/update page & Popup logic (#154) * add: added popup cap * bugfix: made update page hide * bugfix: fixed broken popup logic --------- Co-authored-by: Roberto --- BlocksScreen/lib/panels/mainWindow.py | 16 +++++++++------- .../lib/panels/widgets/popupDialogWidget.py | 3 +++ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 63c53fa7..3ea87f52 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -308,6 +308,7 @@ def reset_tab_indexes(self): Used to grantee all tabs reset to their first page once the user leaves the tab """ + self.up.hide() self.printPanel.setCurrentIndex(0) self.filamentPanel.setCurrentIndex(0) self.controlPanel.setCurrentIndex(0) @@ -566,13 +567,14 @@ def _handle_notify_gcode_response_message(self, method, data, metadata) -> None: return _gcode_msg_type, _message = str(_gcode_response[0]).split(" ", maxsplit=1) popupWhitelist = ["filament runout", "no filament"] - if _message.lower() in popupWhitelist or _gcode_msg_type == "!!": - _msg_type = Popup.MessageType.ERROR - self.popup.new_message( - message_type=_msg_type, - message=str(_message), - userInput=True, - ) + if _message.lower() not in popupWhitelist or _gcode_msg_type != "!!": + return + + self.popup.new_message( + message_type=Popup.MessageType.ERROR, + message=str(_message), + userInput=True, + ) @api_handler def _handle_error_message(self, method, data, metadata) -> None: diff --git a/BlocksScreen/lib/panels/widgets/popupDialogWidget.py b/BlocksScreen/lib/panels/widgets/popupDialogWidget.py index 69deec51..3696c90e 100644 --- a/BlocksScreen/lib/panels/widgets/popupDialogWidget.py +++ b/BlocksScreen/lib/panels/widgets/popupDialogWidget.py @@ -131,6 +131,9 @@ def new_message( Returns: _type_: _description_ """ + if len(self.messages) == 4: + return + self.messages.append( { "message": message, From ca26de51b6d8fa390768fb7f0d851375febfb49f Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Thu, 15 Jan 2026 10:53:11 +0000 Subject: [PATCH 36/70] bugfix: ipv4 ip command error fix (#155) Co-authored-by: Guilherme Costa --- BlocksScreen/lib/panels/networkWindow.py | 39 ++++++++++-------------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index c15b4f75..f9636a82 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -1,13 +1,13 @@ import logging -import typing import subprocess # nosec: B404 +import typing from functools import partial from lib.network import SdbusNetworkManagerAsync +from lib.panels.widgets.keyboardPage import CustomQwertyKeyboard from lib.panels.widgets.popupDialogWidget import Popup from lib.ui.wifiConnectivityWindow_ui import Ui_wifi_stacked_page from lib.utils.list_button import ListCustomButton -from lib.panels.widgets.keyboardPage import CustomQwertyKeyboard from PyQt6 import QtCore, QtGui, QtWidgets logger = logging.getLogger("logs/BlocksScreen.log") @@ -630,22 +630,7 @@ def get_hotspot_ip_via_shell(self): Returns: The IP address string (e.g., '10.42.0.1') or None if not found. """ - command = [ - "ip", - "a", - "show", - "wlan0", - " |", - "grep", - " 'inet '", - "|", - "awk", - " '{{print $2}}'", - "|", - "cut", - "-d/", - "-f1", - ] + command = ["ip", "-4", "addr", "show", "wlan0"] try: result = subprocess.run( # nosec: B603 command, @@ -654,9 +639,6 @@ def get_hotspot_ip_via_shell(self): check=True, timeout=5, ) - ip_addr = result.stdout.strip() - if ip_addr and len(ip_addr.split(".")) == 4: - return ip_addr except subprocess.CalledProcessError as e: logging.error( "Caught exception (exit code %d) failed to run command: %s \nStderr: %s", @@ -664,10 +646,21 @@ def get_hotspot_ip_via_shell(self): command, e.stderr.strip(), ) - raise + return "" + except FileNotFoundError: + logging.error("Command not found") + return "" except subprocess.TimeoutExpired as e: logging.error("Caught exception, failed to run command %s", e) - raise + return "" + + for line in result.stdout.splitlines(): + line = line.strip() + if line.startswith("inet "): + ip_address = line.split()[1].split("/")[0] + return ip_address + logging.error("No IPv4 address found in output for wlan0") + return "" def close(self) -> bool: """Close class, close network module""" From 0dd5d86bb49bfaa118558ff952b45a3931a9cdbf Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Fri, 16 Jan 2026 15:49:57 +0000 Subject: [PATCH 37/70] bugfix: fans_widget on tunepage are stacked (#156) Co-authored-by: Guilherme Costa --- BlocksScreen/lib/panels/widgets/tunePage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/BlocksScreen/lib/panels/widgets/tunePage.py b/BlocksScreen/lib/panels/widgets/tunePage.py index 09b5868d..301c5cd1 100644 --- a/BlocksScreen/lib/panels/widgets/tunePage.py +++ b/BlocksScreen/lib/panels/widgets/tunePage.py @@ -115,6 +115,7 @@ def on_fan_object_update( fields = name.split() first_field = fields[0] second_field = fields[1] if len(fields) > 1 else None + name = second_field.replace("_", " ") if second_field else name if "speed" in field: if not self.tune_display_buttons.get(name, None) and first_field in ( "fan", From a5f360043f29be7c88d05f60cfac96be7dda0350 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Mon, 19 Jan 2026 10:25:30 +0000 Subject: [PATCH 38/70] bugfix ztilt loadscreen (#158) * Add z_tilt object update handler and corresponding signal * bugfix: fixed ztilt hiding on wrong moment * Refactor: ran ruff formatter --------- Co-authored-by: HugoCLSC Co-authored-by: Roberto --- BlocksScreen/lib/panels/controlTab.py | 12 +++++++----- BlocksScreen/lib/panels/mainWindow.py | 6 ++++++ BlocksScreen/lib/printer.py | 8 +++++++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/BlocksScreen/lib/panels/controlTab.py b/BlocksScreen/lib/panels/controlTab.py index 5d1b9b80..a957bea0 100644 --- a/BlocksScreen/lib/panels/controlTab.py +++ b/BlocksScreen/lib/panels/controlTab.py @@ -287,7 +287,7 @@ def __init__( self.ws.klippy_state_signal.connect(self.probe_helper_page.on_klippy_status) self.printer.on_printcore_update.connect(self.handle_printcoreupdate) self.printer.gcode_response.connect(self._handle_gcode_response) - + self.printer.z_tilt_update.connect(self._handle_z_tilt_object_update) # self.panel.cp_printer_settings_btn.hide() self.panel.temperature_cooldown_btn.hide() self.panel.cooldown_btn.hide() @@ -296,6 +296,12 @@ def __init__( self.printer.fan_update[str, str, float].connect(self.on_fan_object_update) self.printer.fan_update[str, str, int].connect(self.on_fan_object_update) + def _handle_z_tilt_object_update(self, value, state): + if state: + self.ztilt_state = state + if self.loadscreen.isVisible(): + self.loadscreen.hide() + @QtCore.pyqtSlot(str, str, float, name="on_fan_update") @QtCore.pyqtSlot(str, str, int, name="on_fan_update") def on_fan_object_update( @@ -455,10 +461,6 @@ def _handle_gcode_response(self, messages: list): self.loadscreen.hide() return - if probed_range < tolerance: - self.loadscreen.hide() - return - self.loadwidget.set_status_message( f"Retries: {retries_done}/{retries_total} | Range: {probed_range:.6f} | Tolerance: {tolerance:.6f}" ) diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 3ea87f52..66627b5e 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -575,6 +575,9 @@ def _handle_notify_gcode_response_message(self, method, data, metadata) -> None: message=str(_message), userInput=True, ) + if not self.controlPanel.ztilt_state: + if self.controlPanel.loadscreen.isVisible(): + self.controlPanel.loadscreen.hide() @api_handler def _handle_error_message(self, method, data, metadata) -> None: @@ -596,6 +599,9 @@ def _handle_error_message(self, method, data, metadata) -> None: message=str(text), userInput=True, ) + if not self.controlPanel.ztilt_state: + if self.controlPanel.loadscreen.isVisible(): + self.controlPanel.loadscreen.hide() @api_handler def _handle_notify_cpu_throttled_message(self, method, data, metadata) -> None: diff --git a/BlocksScreen/lib/printer.py b/BlocksScreen/lib/printer.py index 52706fd5..5889c19d 100644 --- a/BlocksScreen/lib/printer.py +++ b/BlocksScreen/lib/printer.py @@ -76,7 +76,9 @@ class Printer(QtCore.QObject): configfile_update: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( dict, name="configfile_update" ) - + z_tilt_update: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, bool, name="z_tilt_update" + ) config_subscription: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( [dict], [list], @@ -466,6 +468,10 @@ def _heater_fan_object_updated(self, value: dict, fan_name: str = "") -> None: _names = ["heater_fan", fan_name] # object_name = " ".join(_names) + def _z_tilt_object_updated(self, value: dict, name: str = "") -> None: + if value["applied"]: + self.z_tilt_update[str, bool].emit("applied", value["applied"]) + def _idle_timeout_object_updated( self, value: dict, name: str = "idle_timeout" ) -> None: From a17c8eb3de0ec9e4c253f2b60a4eee21be834ea0 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Mon, 19 Jan 2026 19:41:37 +0000 Subject: [PATCH 39/70] bugfix: inputshaper load not hiding (#161) Co-authored-by: Roberto --- BlocksScreen/lib/panels/utilitiesTab.py | 1 + 1 file changed, 1 insertion(+) diff --git a/BlocksScreen/lib/panels/utilitiesTab.py b/BlocksScreen/lib/panels/utilitiesTab.py index 37fa6f61..c6453eae 100644 --- a/BlocksScreen/lib/panels/utilitiesTab.py +++ b/BlocksScreen/lib/panels/utilitiesTab.py @@ -199,6 +199,7 @@ def __init__( self.subscribe_config[list, "PyQt_PyObject"].connect( self.printer.on_subscribe_config ) + self.printer.gcode_response.connect(self.handle_gcode_response) # --- Initialize Printer Communication --- self.printer.printer_config.connect(self.on_printer_config_received) From 14a60849af1aec01ba716587082926868d9ee1c7 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Tue, 20 Jan 2026 13:24:26 +0000 Subject: [PATCH 40/70] bugfix/popup show right arrow (#163) Co-authored-by: Guilherme Costa --- BlocksScreen/lib/panels/widgets/popupDialogWidget.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/BlocksScreen/lib/panels/widgets/popupDialogWidget.py b/BlocksScreen/lib/panels/widgets/popupDialogWidget.py index 3696c90e..d565bd3a 100644 --- a/BlocksScreen/lib/panels/widgets/popupDialogWidget.py +++ b/BlocksScreen/lib/panels/widgets/popupDialogWidget.py @@ -1,8 +1,9 @@ import enum from collections import deque from typing import Deque -from PyQt6 import QtCore, QtGui, QtWidgets + from lib.utils.icon_button import IconButton +from PyQt6 import QtCore, QtGui, QtWidgets class Popup(QtWidgets.QDialog): @@ -188,12 +189,9 @@ def _add_popup(self) -> None: self.slide_in_animation.setEndValue(end_rect) self.slide_out_animation.setStartValue(end_rect) self.slide_out_animation.setEndValue(start_rect) - if not self.userInput: - self.actionbtn.clearPixmap() - else: - self.actionbtn.setPixmap( - QtGui.QPixmap(":/arrow_icons/media/btn_icons/right_arrow.svg") - ) + self.actionbtn.setPixmap( + QtGui.QPixmap(":/arrow_icons/media/btn_icons/right_arrow.svg") + ) self.setGeometry(end_rect) self.text_label.setText(message) self.text_label.setFixedHeight( From d0144bf2bf3145a47b4c2ac1289882ffd2f92e89 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Tue, 20 Jan 2026 13:27:26 +0000 Subject: [PATCH 41/70] swap lower and raise nozzle icons (#164) Co-authored-by: Guilherme Costa --- .../lib/panels/widgets/probeHelperPage.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index 77e17d11..9cf25e59 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -1,14 +1,13 @@ import typing +from lib.panels.widgets.basePopup import BasePopup +from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.panels.widgets.optionCardWidget import OptionCard -from PyQt6 import QtCore, QtGui, QtWidgets +from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_label import BlocksLabel -from lib.utils.icon_button import IconButton from lib.utils.check_button import BlocksCustomCheckButton -from lib.utils.blocks_button import BlocksCustomButton - -from lib.panels.widgets.loadWidget import LoadingOverlayWidget -from lib.panels.widgets.basePopup import BasePopup +from lib.utils.icon_button import IconButton +from PyQt6 import QtCore, QtGui, QtWidgets class ProbeHelper(QtWidgets.QWidget): @@ -890,7 +889,7 @@ def _setupUi(self) -> None: font.setPointSize(14) self.current_offset_info.setFont(font) self.current_offset_info.setStyleSheet("background: transparent; color: white;") - self.current_offset_info.setText("Z:0mm") + self.current_offset_info.setText("Z:0.000mm") self.current_offset_info.setPixmap( QtGui.QPixmap(":/graphics/media/btn_icons/z_offset_adjust.svg") ) @@ -917,7 +916,7 @@ def _setupUi(self) -> None: self.mb_lower_nozzle.setText("") self.mb_lower_nozzle.setFlat(True) self.mb_lower_nozzle.setPixmap( - QtGui.QPixmap(":/arrow_icons/media/btn_icons/up_arrow.svg") + QtGui.QPixmap(":/baby_step/media/btn_icons/move_nozzle_close.svg") ) self.mb_lower_nozzle.setObjectName("bbp_away_from_bed") self.bbp_option_button_group = QtWidgets.QButtonGroup(self) @@ -936,7 +935,7 @@ def _setupUi(self) -> None: self.mb_raise_nozzle.setText("") self.mb_raise_nozzle.setFlat(True) self.mb_raise_nozzle.setPixmap( - QtGui.QPixmap(":/arrow_icons/media/btn_icons/down_arrow.svg") + QtGui.QPixmap(":/baby_step/media/btn_icons/move_nozzle_away.svg") ) self.mb_raise_nozzle.setObjectName("bbp_close_to_bed") self.bbp_option_button_group.addButton(self.mb_raise_nozzle) From d62879fe45c97e9f546f90564667e7d3448e3f32 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Tue, 20 Jan 2026 13:33:11 +0000 Subject: [PATCH 42/70] Refactor loadscreen on the project (#165) * Refactor: added single instance of loadScreen on all project bugfix fixed conenctionpage below load page * Refactor: ran ruff formatter --------- Signed-off-by: Hugo Costa Co-authored-by: Roberto --- BlocksScreen/lib/panels/controlTab.py | 41 +++++-------- BlocksScreen/lib/panels/filamentTab.py | 57 +++++++++---------- BlocksScreen/lib/panels/mainWindow.py | 47 ++++++++++++--- BlocksScreen/lib/panels/printTab.py | 12 +--- BlocksScreen/lib/panels/utilitiesTab.py | 21 +++---- .../lib/panels/widgets/connectionPage.py | 2 + .../lib/panels/widgets/inputshaperPage.py | 15 +---- .../lib/panels/widgets/probeHelperPage.py | 13 ++--- BlocksScreen/lib/panels/widgets/updatePage.py | 19 ++----- 9 files changed, 106 insertions(+), 121 deletions(-) diff --git a/BlocksScreen/lib/panels/controlTab.py b/BlocksScreen/lib/panels/controlTab.py index a957bea0..d5b5d6f5 100644 --- a/BlocksScreen/lib/panels/controlTab.py +++ b/BlocksScreen/lib/panels/controlTab.py @@ -6,8 +6,6 @@ from helper_methods import normalize from lib.moonrakerComm import MoonWebSocket -from lib.panels.widgets.basePopup import BasePopup -from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.panels.widgets.numpadPage import CustomNumpad from lib.panels.widgets.optionCardWidget import OptionCard from lib.panels.widgets.popupDialogWidget import Popup @@ -47,6 +45,7 @@ class ControlTab(QtWidgets.QStackedWidget): request_file_info: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( str, name="request-file-info" ) + call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") tune_display_buttons: dict = {} card_options: dict = {} @@ -70,6 +69,7 @@ def __init__( self.extruder_info: dict = {} self.bed_info: dict = {} self.toolhead_info: dict = {} + self.ztilt_state = False self.extrude_length: int = 10 self.extrude_feedrate: int = 2 self.extrude_page_message: str = "" @@ -77,15 +77,10 @@ def __init__( self.move_speed: float = 25.0 self.probe_helper_page = ProbeHelper(self) self.addWidget(self.probe_helper_page) + self.probe_helper_page.call_load_panel.connect(self.call_load_panel) self.printcores_page = SwapPrintcorePage(self) self.addWidget(self.printcores_page) - self.loadscreen = BasePopup(self, floating=False, dialog=False) - self.loadwidget = LoadingOverlayWidget( - self, LoadingOverlayWidget.AnimationGIF.DEFAULT - ) - self.loadscreen.add_widget(self.loadwidget) - self.sliderPage = SliderPage(self) self.addWidget(self.sliderPage) self.sliderPage.request_back.connect(self.back_button) @@ -299,8 +294,7 @@ def __init__( def _handle_z_tilt_object_update(self, value, state): if state: self.ztilt_state = state - if self.loadscreen.isVisible(): - self.loadscreen.hide() + self.call_load_panel.emit(False, "") @QtCore.pyqtSlot(str, str, float, name="on_fan_update") @QtCore.pyqtSlot(str, str, int, name="on_fan_update") @@ -425,17 +419,16 @@ def handle_printcoreupdate(self, value: dict): return if value["swapping"] == "in_pos": - self.loadscreen.hide() + self.call_load_panel.emit(False, "") self.printcores_page.show() self.disable_popups.emit(True) self.printcores_page.setText( "Please Insert Print Core \n \n Afterwards click continue" ) if value["swapping"] == "unloading": - self.loadwidget.set_status_message("Unloading print core") - + self.call_load_panel.emit(True, "Unloading print core") if value["swapping"] == "cleaning": - self.loadwidget.set_status_message("Cleaning print core") + self.call_load_panel.emit(True, "Cleaning print core") def _handle_gcode_response(self, messages: list): """Handle gcode response for Z-tilt adjustment""" @@ -458,20 +451,17 @@ def _handle_gcode_response(self, messages: list): probed_range = float(match.group(3)) tolerance = float(match.group(4)) if retries_done == retries_total: - self.loadscreen.hide() + self.call_load_panel.emit(False, "") return - - self.loadwidget.set_status_message( - f"Retries: {retries_done}/{retries_total} | Range: {probed_range:.6f} | Tolerance: {tolerance:.6f}" + self.call_load_panel.emit( + True, + f"Retries: {retries_done}/{retries_total} | Range: {probed_range:.6f} | Tolerance: {tolerance:.6f}", ) def handle_ztilt(self): """Handle Z-Tilt Adjustment""" - self.loadscreen.show() - self.loadwidget.set_status_message( - "Please wait, performing Z-axis calibration." - ) - self.run_gcode_signal.emit("G28\nM400\nZ_TILT_ADJUST") + self.call_load_panel.emit(True, "Please wait, performing Z-axis calibration.") + self.run_gcode_signal.emit("Z_TILT_ADJUST") @QtCore.pyqtSlot(str, name="on-klippy-status") def on_klippy_status(self, state: str): @@ -487,8 +477,7 @@ def on_klippy_status(self, state: str): def show_swapcore(self): """Show swap printcore""" self.run_gcode_signal.emit("CHANGE_PRINTCORES") - self.loadscreen.show() - self.loadwidget.set_status_message("Preparing to swap print core") + self.call_load_panel.emit(True, "Preparing to swap print core") def handle_swapcore(self): """Handle swap printcore routine finish""" @@ -660,7 +649,7 @@ def on_toolhead_update(self, field: str, values: list) -> None: self.panel.mva_z_value_label.setText(f"{values[2]:.3f}") if values[0] == "252,50" and values[1] == "250" and values[2] == "50": - self.loadscreen.hide + self.call_load_panel.emit(False, "") self.toolhead_info.update({f"{field}": values}) @QtCore.pyqtSlot(str, str, float, name="on-extruder-update") diff --git a/BlocksScreen/lib/panels/filamentTab.py b/BlocksScreen/lib/panels/filamentTab.py index 1271375e..4b368bd8 100644 --- a/BlocksScreen/lib/panels/filamentTab.py +++ b/BlocksScreen/lib/panels/filamentTab.py @@ -6,8 +6,6 @@ from lib.filament import Filament from lib.ui.filamentStackedWidget_ui import Ui_filamentStackedWidget -from lib.panels.widgets.loadWidget import LoadingOverlayWidget -from lib.panels.widgets.basePopup import BasePopup from lib.panels.widgets.popupDialogWidget import Popup from PyQt6 import QtCore, QtGui, QtWidgets @@ -19,6 +17,7 @@ class FilamentTab(QtWidgets.QStackedWidget): request_change_page = QtCore.pyqtSignal(int, int, name="request_change_page") request_toolhead_count = QtCore.pyqtSignal(int, name="toolhead_number_received") run_gcode = QtCore.pyqtSignal(str, name="run_gcode") + call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") class FilamentTypes(enum.Enum): PLA = Filament(name="PLA", temperature=220) @@ -42,11 +41,6 @@ def __init__(self, parent: QtWidgets.QWidget, printer: Printer, ws, /) -> None: self.target_temp: int = 0 self.current_temp: int = 0 self.popup = Popup(self) - self.loadscreen = BasePopup(self, floating=False, dialog=False) - self.loadwidget = LoadingOverlayWidget( - self, LoadingOverlayWidget.AnimationGIF.DEFAULT - ) - self.loadscreen.add_widget(self.loadwidget) self.has_load_unload_objects = None self._filament_state = self.FilamentStates.UNKNOWN self._sensor_states = {} @@ -130,19 +124,25 @@ def on_extruder_update( """Handle extruder update""" if not self.isVisible: return - - if self.target_temp != 0: - if self.current_temp == self.target_temp: - self.loadwidget.set_status_message("Extruder heated up \n Please wait") - return - if field == "temperature": - self.current_temp = round(new_value, 0) - self.loadwidget.set_status_message( - f"Heating up ({new_value}/{self.target_temp}) \n Please wait" - ) - if field == "target": - self.target_temp = round(new_value, 0) - self.loadwidget.set_status_message("Heating up \n Please wait") + if not self.loadignore or not self.unloadignore: + if self.target_temp != 0: + if self.current_temp == self.target_temp: + if self.isVisible: + self.call_load_panel.emit( + True, "Extruder heated up \n Please wait" + ) + return + if field == "temperature": + self.current_temp = round(new_value, 0) + if self.isVisible: + self.call_load_panel.emit( + True, + f"Heating up ({new_value}/{self.target_temp}) \n Please wait", + ) + if field == "target": + self.target_temp = round(new_value, 0) + if self.isVisible: + self.call_load_panel.emit(True, "Heating up \n Please wait") @QtCore.pyqtSlot(bool, name="on_load_filament") def on_load_filament(self, status: bool): @@ -150,14 +150,13 @@ def on_load_filament(self, status: bool): if self.loadignore: self.loadignore = False return - if not self.isVisible: return if status: - self.loadscreen.show() + self.call_load_panel.emit(True, "Loading Filament") else: self.target_temp = 0 - self.loadscreen.hide() + self.call_load_panel.emit(False, "") self._filament_state = self.FilamentStates.LOADED self.handle_filament_state() @@ -167,14 +166,12 @@ def on_unload_filament(self, status: bool): if self.unloadignore: self.unloadignore = False return - if not self.isVisible: return - if status: - self.loadscreen.show() + self.call_load_panel.emit(True, "Unloading Filament") else: - self.loadscreen.hide() + self.call_load_panel.emit(False, "") self.target_temp = 0 self._filament_state = self.FilamentStates.UNLOADED self.handle_filament_state() @@ -197,7 +194,8 @@ def load_filament(self, toolhead: int = 0, temp: int = 220) -> None: message="Filament is already loaded.", ) return - self.loadscreen.show() + self.loadignore = False + self.call_load_panel.emit(True, "Loading Filament") self.run_gcode.emit(f"LOAD_FILAMENT TOOLHEAD=load_toolhead TEMPERATURE={temp}") @QtCore.pyqtSlot(str, int, name="unload_filament") @@ -220,7 +218,8 @@ def unload_filament(self, toolhead: int = 0, temp: int = 220) -> None: return self.find_routine_objects() - self.loadscreen.show() + self.unload_filament = False + self.call_load_panel.emit(True, "Unloading Filament") self.run_gcode.emit(f"UNLOAD_FILAMENT TEMPERATURE={temp}") def handle_filament_state(self): diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 66627b5e..32355803 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -17,6 +17,8 @@ from lib.printer import Printer from lib.ui.mainWindow_ui import Ui_MainWindow # With header from lib.panels.widgets.updatePage import UpdatePage +from lib.panels.widgets.basePopup import BasePopup +from lib.panels.widgets.loadWidget import LoadingOverlayWidget # from lib.ui.mainWindow_v2_ui import Ui_MainWindow # No header from lib.ui.resources.background_resources_rc import * @@ -63,6 +65,7 @@ class MainWindow(QtWidgets.QMainWindow): on_update_message: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( dict, name="on-update-message" ) + call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") def __init__(self): super(MainWindow, self).__init__() @@ -140,6 +143,7 @@ def __init__(self): slot=self.mc.restart_klipper_service ) self.conn_window.reboot_clicked.connect(slot=self.mc.machine_restart) + self.printer_object_report_signal.connect( self.printer.on_object_report_received ) @@ -175,11 +179,46 @@ def __init__(self): self.conn_window.update_button_clicked.connect(self.show_update_page) self.ui.extruder_temp_display.display_format = "upper_downer" self.ui.bed_temp_display.display_format = "upper_downer" + + self.controlPanel.call_load_panel.connect(self.show_LoadScreen) + self.filamentPanel.call_load_panel.connect(self.show_LoadScreen) + self.printPanel.call_load_panel.connect(self.show_LoadScreen) + self.utilitiesPanel.call_load_panel.connect(self.show_LoadScreen) + self.conn_window.call_load_panel.connect(self.show_LoadScreen) + + self.loadscreen = BasePopup(self, floating=False, dialog=False) + self.loadwidget = LoadingOverlayWidget( + self, LoadingOverlayWidget.AnimationGIF.DEFAULT + ) + self.loadscreen.add_widget(self.loadwidget) if self.config.has_section("server"): # @ Start websocket connection with moonraker self.bo_ws_startup.emit() self.reset_tab_indexes() + @QtCore.pyqtSlot(bool, str, name="show-load-page") + def show_LoadScreen(self, show: bool = True, msg: str = ""): + _sender = self.sender() + + if _sender == self.filamentPanel: + if not self.filamentPanel.isVisible(): + return + if _sender == self.controlPanel: + if not self.controlPanel.isVisible(): + return + if _sender == self.printPanel: + if not self.printPanel.isVisible(): + return + if _sender == self.utilitiesPanel: + if not self.utilitiesPanel.isVisible(): + return + + self.loadwidget.set_status_message(msg) + if show: + self.loadscreen.show() + else: + self.loadscreen.hide() + @QtCore.pyqtSlot(bool, name="show-update-page") def show_update_page(self, fullscreen: bool): """Slot for displaying update Panel""" @@ -365,7 +404,7 @@ def global_change_page(self, tab_index: int, panel_index: int) -> None: "Panel page index expected type int, %s", str(type(panel_index)) ) - self.printPanel.loadscreen.hide() + self.show_LoadScreen(False) current_page = [ self.ui.main_content_widget.currentIndex(), self.current_panel_index(), @@ -575,9 +614,6 @@ def _handle_notify_gcode_response_message(self, method, data, metadata) -> None: message=str(_message), userInput=True, ) - if not self.controlPanel.ztilt_state: - if self.controlPanel.loadscreen.isVisible(): - self.controlPanel.loadscreen.hide() @api_handler def _handle_error_message(self, method, data, metadata) -> None: @@ -599,9 +635,6 @@ def _handle_error_message(self, method, data, metadata) -> None: message=str(text), userInput=True, ) - if not self.controlPanel.ztilt_state: - if self.controlPanel.loadscreen.isVisible(): - self.controlPanel.loadscreen.hide() @api_handler def _handle_notify_cpu_throttled_message(self, method, data, metadata) -> None: diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index 6331585a..7431938e 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -11,7 +11,6 @@ from lib.panels.widgets.confirmPage import ConfirmWidget from lib.panels.widgets.filesPage import FilesPage from lib.panels.widgets.jobStatusPage import JobStatusWidget -from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.panels.widgets.numpadPage import CustomNumpad from lib.panels.widgets.sensorsPanel import SensorsWindow from lib.panels.widgets.slider_selector_page import SliderPage @@ -63,6 +62,7 @@ class PrintTab(QtWidgets.QStackedWidget): on_cancel_print: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="on_cancel_print" ) + call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") _z_offset: float = 0.0 _active_z_offset: float = 0.0 @@ -92,12 +92,6 @@ def __init__( self.numpadPage.request_back.connect(self.back_button) self.addWidget(self.numpadPage) - self.loadscreen = BasePopup(self, floating=False, dialog=False) - self.loadwidget = LoadingOverlayWidget( - self, LoadingOverlayWidget.AnimationGIF.DEFAULT - ) - self.loadscreen.add_widget(self.loadwidget) - self.file_data: Files = file_data self.filesPage_widget = FilesPage(self) self.addWidget(self.filesPage_widget) @@ -372,9 +366,7 @@ def setProperty(self, name: str, value: typing.Any) -> bool: def handle_cancel_print(self) -> None: """Handles the print cancel action""" self.ws.api.cancel_print() - self.loadscreen.show() - self.loadscreen.setModal(True) - self.loadwidget.set_status_message("Cancelling print...\nPlease wait") + self.call_load_panel.emit(True, "Cancelling print...\nPlease wait") def change_page(self, index: int) -> None: """Requests a page change page to the global manager diff --git a/BlocksScreen/lib/panels/utilitiesTab.py b/BlocksScreen/lib/panels/utilitiesTab.py index c6453eae..6cff5f27 100644 --- a/BlocksScreen/lib/panels/utilitiesTab.py +++ b/BlocksScreen/lib/panels/utilitiesTab.py @@ -14,7 +14,6 @@ from lib.panels.widgets.optionCardWidget import OptionCard from lib.panels.widgets.inputshaperPage import InputShaperPage from lib.panels.widgets.basePopup import BasePopup -from lib.panels.widgets.loadWidget import LoadingOverlayWidget import re @@ -89,6 +88,7 @@ class UtilitiesTab(QtWidgets.QStackedWidget): show_update_page: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( bool, name="show-update-page" ) + call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") def __init__( self, parent: QtWidgets.QWidget, ws: MoonWebSocket, printer: Printer @@ -123,17 +123,12 @@ def __init__( # --- UI Setup --- self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.loadPage = BasePopup(self, dialog=False) - self.loadwidget = LoadingOverlayWidget( - self, LoadingOverlayWidget.AnimationGIF.DEFAULT - ) - self.loadPage.add_widget(self.loadwidget) - self.panel.update_btn.clicked.connect( lambda: self.show_update_page[bool].emit(False) ) self.is_page = InputShaperPage(self) + self.is_page.call_load_panel.connect(self.call_load_panel) self.addWidget(self.is_page) self.dialog_page = BasePopup(self, dialog=True, floating=True) @@ -310,7 +305,7 @@ def handle_gcode_response(self, data: list[str]) -> None: self.is_aut_types[axis] = recommended_type if len(self.is_aut_types) == 2: self.run_gcode_signal.emit("SAVE_CONFIG") - self.loadPage.hide() + self.call_load_panel.emit(False, "") self.aut = False return return @@ -329,7 +324,7 @@ def handle_gcode_response(self, data: list[str]) -> None: self.is_page.add_type_entry(key) self.is_page.build_model_list() - self.loadPage.hide() + self.call_load_panel.emit(False, "") return def handle_is(self, gcode: str) -> None: @@ -354,8 +349,7 @@ def handle_is(self, gcode: str) -> None: self.run_gcode_signal.emit(gcode) self.change_page(self.indexOf(self.is_page)) - self.loadwidget.set_status_message("Running Input Shaper...") - self.loadPage.show() + self.call_load_panel.emit(True, "Running Input Shaper...") @QtCore.pyqtSlot(list, name="on_object_list") def on_object_list(self, object_list: list) -> None: @@ -657,8 +651,7 @@ def troubleshoot_request(self) -> None: def show_waiting_page(self, page_to_go_to: int, label: str, time_ms: int): """Show placeholder page""" - self.loadwidget.set_status_message(label) - self.loadPage.show() + self.call_load_panel.emit(True, label) QtCore.QTimer.singleShot(time_ms, lambda: self.change_page(page_to_go_to)) def _connect_page_change(self, button: QtWidgets.QWidget, page: QtWidgets.QWidget): @@ -667,7 +660,7 @@ def _connect_page_change(self, button: QtWidgets.QWidget, page: QtWidgets.QWidge def change_page(self, index: int): """Request change page by index""" - self.loadPage.hide() + self.call_load_panel.emit(False, "") self.troubleshoot_page.hide() if index < self.count(): self.request_change_page.emit(3, index) diff --git a/BlocksScreen/lib/panels/widgets/connectionPage.py b/BlocksScreen/lib/panels/widgets/connectionPage.py index 1e9c32d1..9403d290 100644 --- a/BlocksScreen/lib/panels/widgets/connectionPage.py +++ b/BlocksScreen/lib/panels/widgets/connectionPage.py @@ -14,6 +14,7 @@ class ConnectionPage(QtWidgets.QFrame): restart_klipper_clicked = QtCore.pyqtSignal(name="restart_klipper_clicked") firmware_restart_clicked = QtCore.pyqtSignal(name="firmware_restart_clicked") update_button_clicked = QtCore.pyqtSignal(bool, name="show-update-page") + call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket, /): super().__init__(parent) @@ -65,6 +66,7 @@ def show_panel(self, reason: str | None = None): def showEvent(self, a0: QtCore.QEvent | None): """Handle show event""" self.ws.api.refresh_update_status() + self.call_load_panel.emit(False, "") return super().showEvent(a0) @QtCore.pyqtSlot(bool, name="on_klippy_connected") diff --git a/BlocksScreen/lib/panels/widgets/inputshaperPage.py b/BlocksScreen/lib/panels/widgets/inputshaperPage.py index 539dcaf4..ead82cfb 100644 --- a/BlocksScreen/lib/panels/widgets/inputshaperPage.py +++ b/BlocksScreen/lib/panels/widgets/inputshaperPage.py @@ -1,5 +1,3 @@ -from lib.panels.widgets.loadWidget import LoadingOverlayWidget -from lib.panels.widgets.basePopup import BasePopup from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.icon_button import IconButton @@ -18,6 +16,7 @@ class InputShaperPage(QtWidgets.QWidget): run_gcode_signal: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( str, name="run-gcode" ) + call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") def __init__(self, parent=None) -> None: if parent: @@ -28,12 +27,6 @@ def __init__(self, parent=None) -> None: self.selected_item: ListItem | None = None self.ongoing_update: bool = False self.type_dict: dict = {} - - self.loadscreen = BasePopup(self, floating=False, dialog=False) - self.loadwidget = LoadingOverlayWidget( - self, LoadingOverlayWidget.AnimationGIF.DEFAULT - ) - self.loadscreen.add_widget(self.loadwidget) self.repeated_request_status = QtCore.QTimer() self.repeated_request_status.setInterval(2000) # every 2 seconds self.model = EntryListModel() @@ -50,8 +43,7 @@ def handle_update_end(self) -> None: """Handles update end signal (closes loading page, returns to normal operation) """ - if self.load_popup.isVisible(): - self.load_popup.close() + self.call_load_panel.emit(False, "Updating...") self.repeated_request_status.stop() self.build_model_list() @@ -59,8 +51,7 @@ def handle_ongoing_update(self) -> None: """Handled ongoing update signal, calls loading page (blocks user interaction) """ - self.loadwidget.set_status_message("Updating...") - self.load_popup.show() + self.call_load_panel.emit(True, "Updating...") self.repeated_request_status.start(2000) def reset_view_model(self) -> None: diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index 9cf25e59..a105037b 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -10,6 +10,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets + class ProbeHelper(QtWidgets.QWidget): request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="request_back" @@ -35,6 +36,7 @@ class ProbeHelper(QtWidgets.QWidget): request_page_view: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="request_page_view" ) + call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") distances = ["0.01", ".025", "0.1", "0.5", "1"] _calibration_commands: list = [] @@ -49,11 +51,6 @@ class ProbeHelper(QtWidgets.QWidget): def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__(parent) - self.Loadscreen = BasePopup(self, dialog=False) - self.loadwidget = LoadingOverlayWidget( - self, LoadingOverlayWidget.AnimationGIF.DEFAULT - ) - self.Loadscreen.add_widget(self.loadwidget) self.setObjectName("probe_offset_page") self._setupUi() self.inductive_icon = QtGui.QPixmap( @@ -397,9 +394,7 @@ def handle_start_tool(self, sender: typing.Type[OptionCard]) -> None: for i in self.card_options.values(): i.setDisabled(True) - self.Loadscreen.show() - self.loadwidget.set_status_message("Homing Axes...") - + self.call_load_panel.emit(True, "Homing Axes...") if self.z_offset_safe_xy: self.run_gcode_signal.emit("G28\nM400") self._move_to_pos(self.z_offset_safe_xy[0], self.z_offset_safe_xy[1], 100) @@ -533,7 +528,7 @@ def _toggle_tool_buttons(self, state: bool) -> None: if state: for i in self.card_options.values(): i.setDisabled(False) - self.Loadscreen.hide() + self.call_load_panel.emit(False, "") self.po_back_button.setEnabled(False) self.po_back_button.hide() self.po_header_title.setEnabled(False) diff --git a/BlocksScreen/lib/panels/widgets/updatePage.py b/BlocksScreen/lib/panels/widgets/updatePage.py index 4857f92e..b91e41d3 100644 --- a/BlocksScreen/lib/panels/widgets/updatePage.py +++ b/BlocksScreen/lib/panels/widgets/updatePage.py @@ -1,7 +1,6 @@ import copy import typing -from lib.panels.widgets.basePopup import BasePopup from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_frame import BlocksCustomFrame @@ -52,6 +51,7 @@ class UpdatePage(QtWidgets.QWidget): update_available: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( bool, name="update-available" ) + call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") def __init__(self, parent=None) -> None: if parent: @@ -62,11 +62,6 @@ def __init__(self, parent=None) -> None: self.cli_tracking = {} self.selected_item: ListItem | None = None self.ongoing_update: bool = False - self.load_popup = BasePopup(self, floating=False, dialog=False) - self.loadwidget = LoadingOverlayWidget( - self, LoadingOverlayWidget.AnimationGIF.DEFAULT - ) - self.load_popup.add_widget(self.loadwidget) self.repeated_request_status = QtCore.QTimer() self.repeated_request_status.setInterval(2000) # every 2 seconds self.model = EntryListModel() @@ -90,8 +85,7 @@ def handle_update_end(self) -> None: """Handles update end signal (closes loading page, returns to normal operation) """ - if self.load_popup.isVisible(): - self.load_popup.close() + self.call_load_panel.emit(False, "") self.repeated_request_status.stop() self.on_request_reload() self.build_model_list() @@ -100,8 +94,7 @@ def handle_ongoing_update(self) -> None: """Handled ongoing update signal, calls loading page (blocks user interaction) """ - self.loadwidget.set_status_message("Updating...") - self.load_popup.show() + self.call_load_panel.emit(True, "Updating...") self.repeated_request_status.start(2000) def on_request_reload(self, service: str | None = None) -> None: @@ -166,12 +159,10 @@ def on_update_clicked(self) -> None: self.request_update_moonraker.emit() else: self.request_update_client.emit(cli_name) - - self.loadwidget.set_status_message(f"Updating {cli_name}") + self.call_load_panel.emit(True, f"Updating {cli_name}") else: self.request_recover_repo[str, bool].emit(cli_name, True) - self.loadwidget.set_status_message(f"Recovering {cli_name}") - self.load_popup.show() + self.call_load_panel.emit(True, f"Recovering {cli_name}") self.request_update_status.emit(False) @QtCore.pyqtSlot(ListItem, name="on-item-clicked") From 5a6732cc80d15d1995acf6264edc58d4c73b55a4 Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Wed, 21 Jan 2026 10:43:38 +0000 Subject: [PATCH 43/70] Deleted Unused `ztilt_state` variable from control tab (#166) The variable `ztilt_state` was left behing during another PR, it should have been deleted. Now it is --- BlocksScreen/lib/panels/controlTab.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/BlocksScreen/lib/panels/controlTab.py b/BlocksScreen/lib/panels/controlTab.py index d5b5d6f5..1c8eb30b 100644 --- a/BlocksScreen/lib/panels/controlTab.py +++ b/BlocksScreen/lib/panels/controlTab.py @@ -69,7 +69,6 @@ def __init__( self.extruder_info: dict = {} self.bed_info: dict = {} self.toolhead_info: dict = {} - self.ztilt_state = False self.extrude_length: int = 10 self.extrude_feedrate: int = 2 self.extrude_page_message: str = "" @@ -293,7 +292,6 @@ def __init__( def _handle_z_tilt_object_update(self, value, state): if state: - self.ztilt_state = state self.call_load_panel.emit(False, "") @QtCore.pyqtSlot(str, str, float, name="on_fan_update") From 8ba3d6321b13019c67a43a6bbab4aad1c69ff42e Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Wed, 28 Jan 2026 11:32:25 +0000 Subject: [PATCH 44/70] ADD: Additional load messages (#169) Co-authored-by: Roberto --- BlocksScreen/lib/panels/controlTab.py | 1 + .../lib/panels/widgets/probeHelperPage.py | 29 +++++++++++++++++-- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/BlocksScreen/lib/panels/controlTab.py b/BlocksScreen/lib/panels/controlTab.py index 1c8eb30b..0f77cd24 100644 --- a/BlocksScreen/lib/panels/controlTab.py +++ b/BlocksScreen/lib/panels/controlTab.py @@ -99,6 +99,7 @@ def __init__( self.probe_helper_page.subscribe_config[list, "PyQt_PyObject"].connect( self.printer.on_subscribe_config ) + self.printer.extruder_update.connect(self.probe_helper_page.on_extruder_update) self.printer.gcode_move_update.connect( self.probe_helper_page.on_gcode_move_update ) diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index a105037b..6ce968d3 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -1,7 +1,5 @@ import typing -from lib.panels.widgets.basePopup import BasePopup -from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.panels.widgets.optionCardWidget import OptionCard from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_label import BlocksLabel @@ -10,7 +8,6 @@ from PyQt6 import QtCore, QtGui, QtWidgets - class ProbeHelper(QtWidgets.QWidget): request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( name="request_back" @@ -86,6 +83,8 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: self.update() self.block_z = False self.block_list = False + self.target_temp = 0 + self.current_temp = 0 def on_klippy_status(self, state: str): """Handle Klippy status event change""" @@ -411,6 +410,30 @@ def handle_start_tool(self, sender: typing.Type[OptionCard]) -> None: return self.run_gcode_signal.emit(_cmd) + @QtCore.pyqtSlot(str, str, float, name="on_extruder_update") + def on_extruder_update( + self, extruder_name: str, field: str, new_value: float + ) -> None: + """Handle extruder update""" + if not self.helper_initialize: + return + if self.target_temp != 0: + if self.current_temp == self.target_temp: + if self.isVisible: + self.call_load_panel.emit(True, "Extruder heated up \n Please wait") + return + if field == "temperature": + self.current_temp = round(new_value, 0) + if self.isVisible: + self.call_load_panel.emit( + True, + f"Heating up ({new_value}/{self.target_temp}) \n Please wait", + ) + if field == "target": + self.target_temp = round(new_value, 0) + if self.isVisible: + self.call_load_panel.emit(True, "Cleaning the nozzle \n Please wait") + @QtCore.pyqtSlot(name="handle_accept") def handle_accept(self) -> None: """Accepts the configured value from the calibration""" From 72fb8a0231ca94f6f1b92422c50d113e6ac51e24 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Wed, 4 Feb 2026 11:24:22 +0000 Subject: [PATCH 45/70] Refactor `NetworkWindow` (#174) * refactor: change network list to listview * Refactor: Refac to MVC view with Controller being runnables on a threadpoll * UPD: Regenerated icon_resources_rc * networkWindow.py: refactor to include listview wifiConnectivityWindow.ui: change horizontalLayout to a vertical layout with a listview and a vertical scrollbar wifiConnectivityWindow.py: generated file from QtDesigner with some optimizations Signed-off-by: Guilherme Costa * networkWindow: rebase merge conflits fix and cleanup Signed-off-by: Guilherme Costa * networkWindow.py: added missing right icon Signed-off-by: Guilherme Costa * Fix typo * networkWindow.py: optimize and bugfix on self.paths Signed-off-by: Guilherme Costa * networkWindow.py: comments cleanup * networkWindow.py: fix missing formatting * Revert accidental changes to requirements.txt * networkwindo.py: between 5 and 25 show only one bar icon * bugfix: fixed wrong imports * bugfix: wrong button name * resolve merge conflits * add missing docstrings * add missing docstrings * separation between saved and unsaved network and update code * refactor network window file * Add hidden network page, fix scrollbar behaviour at borders remove the need to use wificonnectivitywindow_ui * cleanup of unused code * fix code formatation * changed QtWidgets.QApplication.processEvents for repaint * delete unused files * fix formatting issues and logic to parse sensors * fix code formatation --------- Signed-off-by: Guilherme Costa Co-authored-by: Roberto Martins Co-authored-by: Guilherme Costa Co-authored-by: HugoCLSC Co-authored-by: Roberto --- BlocksScreen/lib/network.py | 96 +- BlocksScreen/lib/panels/networkWindow.py | 3978 +++++++++++++---- .../lib/panels/widgets/sensorsPanel.py | 17 +- BlocksScreen/lib/ui/connectionWindow.ui | 938 ---- BlocksScreen/lib/ui/connectionWindow_ui.py | 338 -- .../lib/ui/resources/icon_resources.qrc | 13 +- .../lib/ui/resources/icon_resources_rc.py | 1815 ++++++-- .../resources/media/btn_icons/0bar_wifi.svg | 1 + .../media/btn_icons/0bar_wifi_protected.svg | 1 + .../resources/media/btn_icons/1bar_wifi.svg | 2 +- .../media/btn_icons/1bar_wifi_protected.svg | 1 + .../resources/media/btn_icons/2bar_wifi.svg | 2 +- .../media/btn_icons/2bar_wifi_protected.svg | 1 + .../resources/media/btn_icons/3bar_wifi.svg | 2 +- .../media/btn_icons/3bar_wifi_protected.svg | 1 + .../resources/media/btn_icons/4bar_wifi.svg | 1 + .../media/btn_icons/4bar_wifi_protected.svg | 1 + BlocksScreen/lib/ui/wifiConnectivityWindow.ui | 63 +- .../lib/ui/wifiConnectivityWindow_ui.py | 122 +- BlocksScreen/lib/utils/blocks_Scrollbar.py | 38 +- BlocksScreen/lib/utils/blocks_linedit.py | 139 +- BlocksScreen/lib/utils/list_model.py | 9 + 22 files changed, 4899 insertions(+), 2680 deletions(-) delete mode 100644 BlocksScreen/lib/ui/connectionWindow.ui delete mode 100644 BlocksScreen/lib/ui/connectionWindow_ui.py create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/0bar_wifi.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/0bar_wifi_protected.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi_protected.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi_protected.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi_protected.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi_protected.svg diff --git a/BlocksScreen/lib/network.py b/BlocksScreen/lib/network.py index 51b87074..61ea4078 100644 --- a/BlocksScreen/lib/network.py +++ b/BlocksScreen/lib/network.py @@ -354,13 +354,15 @@ def get_wired_interfaces(self) -> typing.List[dbusNm.NetworkDeviceWired]: filter( lambda path: path, filter( - lambda device: asyncio.run_coroutine_threadsafe( - dbusNm.NetworkDeviceGeneric( - bus=self.system_dbus, device_path=device - ).device_type.get_async(), - self.loop, - ).result(timeout=2) - == dbusNm.enums.DeviceType.ETHERNET, + lambda device: ( + asyncio.run_coroutine_threadsafe( + dbusNm.NetworkDeviceGeneric( + bus=self.system_dbus, device_path=device + ).device_type.get_async(), + self.loop, + ).result(timeout=2) + == dbusNm.enums.DeviceType.ETHERNET + ), devices, ), ), @@ -386,13 +388,15 @@ def get_wireless_interfaces( filter( lambda path: path, filter( - lambda device: asyncio.run_coroutine_threadsafe( - dbusNm.NetworkDeviceGeneric( - bus=self.system_dbus, device_path=device - ).device_type.get_async(), - self.loop, - ).result(timeout=3) - == dbusNm.enums.DeviceType.WIFI, + lambda device: ( + asyncio.run_coroutine_threadsafe( + dbusNm.NetworkDeviceGeneric( + bus=self.system_dbus, device_path=device + ).device_type.get_async(), + self.loop, + ).result(timeout=3) + == dbusNm.enums.DeviceType.WIFI + ), devices, ), ), @@ -472,6 +476,68 @@ def get_current_ip_addr(self) -> str: return [address_data["address"][1] for address_data in addr_data][0] except IndexError as e: logger.error("List out of index %s", e) + except Exception as e: + logger.error("Error getting current IP address: %s", e) + return "" + + def get_device_ip_by_interface(self, interface_name: str = "wlan0") -> str: + """Get IPv4 address for a specific interface via NetworkManager D-Bus. + + This method retrieves the IP address directly from a specific network + interface, useful for getting hotspot IP when it's the active connection + on that interface. + + Args: + interface_name: The network interface name (e.g., "wlan0", "eth0") + + Returns: + str: The IPv4 address or empty string if not found + """ + if not self.nm: + return "" + + try: + devices_future = asyncio.run_coroutine_threadsafe( + self.nm.get_devices(), self.loop + ) + devices = devices_future.result(timeout=2) + + for device_path in devices: + device = dbusNm.NetworkDeviceGeneric( + bus=self.system_dbus, device_path=device_path + ) + + # Check if this is the interface we want + iface_future = asyncio.run_coroutine_threadsafe( + device.interface.get_async(), self.loop + ) + iface = iface_future.result(timeout=2) + + if iface != interface_name: + continue + + # Get IP4Config path + ip4_path_future = asyncio.run_coroutine_threadsafe( + device.ip4_config.get_async(), self.loop + ) + ip4_path = ip4_path_future.result(timeout=2) + + if not ip4_path or ip4_path == "/": + return "" + + # Get address data + ip4_config = dbusNm.IPv4Config(bus=self.system_dbus, ip4_path=ip4_path) + addr_data_future = asyncio.run_coroutine_threadsafe( + ip4_config.address_data.get_async(), self.loop + ) + addr_data = addr_data_future.result(timeout=2) + + if addr_data and len(addr_data) > 0: + return addr_data[0]["address"][1] + + except Exception as e: + logger.error("Failed to get IP for interface %s: %s", interface_name, e) + return "" async def _gather_primary_interface( @@ -821,7 +887,7 @@ def get_saved_ssid_names(self) -> typing.List[str]: return [] return list( map( - lambda saved_network: (saved_network.get("ssid", None)), + lambda saved_network: saved_network.get("ssid", None), _saved_networks, ) ) diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index f9636a82..19574cf5 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -1,1167 +1,3235 @@ import logging -import subprocess # nosec: B404 -import typing +import threading from functools import partial +from typing import ( + Any, + Callable, + Dict, + List, + NamedTuple, + Optional, +) from lib.network import SdbusNetworkManagerAsync from lib.panels.widgets.keyboardPage import CustomQwertyKeyboard +from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.panels.widgets.popupDialogWidget import Popup -from lib.ui.wifiConnectivityWindow_ui import Ui_wifi_stacked_page -from lib.utils.list_button import ListCustomButton +from lib.utils.blocks_button import BlocksCustomButton +from lib.utils.blocks_frame import BlocksCustomFrame +from lib.utils.blocks_linedit import BlocksCustomLinEdit +from lib.utils.blocks_Scrollbar import CustomScrollBar +from lib.utils.blocks_togglebutton import NetworkWidgetbuttons +from lib.utils.check_button import BlocksCustomCheckButton +from lib.utils.icon_button import IconButton +from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem from PyQt6 import QtCore, QtGui, QtWidgets +from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal logger = logging.getLogger("logs/BlocksScreen.log") -class BuildNetworkList(QtCore.QThread): - """Retrieves information from sdbus interface about scanned networks""" +LOAD_TIMEOUT_MS = 30_000 +NETWORK_CONNECT_DELAY_MS = 5_000 +NETWORK_LIST_REFRESH_MS = 10_000 +STATUS_CHECK_INTERVAL_MS = 2_000 +DEFAULT_POLL_INTERVAL_MS = 10_000 - scan_result: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - dict, name="scan-results" - ) - finished_network_list_build: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - list, name="finished-network-list-build" - ) +SIGNAL_EXCELLENT_THRESHOLD = 75 +SIGNAL_GOOD_THRESHOLD = 50 +SIGNAL_FAIR_THRESHOLD = 25 +SIGNAL_MINIMUM_THRESHOLD = 5 - def __init__(self) -> None: +PRIORITY_HIGH = 90 +PRIORITY_MEDIUM = 50 +PRIORITY_LOW = 20 + +SEPARATOR_SIGNAL_VALUE = -10 +PRIVACY_BIT = 1 + +# SSIDs that indicate hidden networks +HIDDEN_NETWORK_INDICATORS = ("", "UNKNOWN", "", None) + + +class NetworkInfo(NamedTuple): + """Information about a network.""" + + signal: int + status: str + is_open: bool = False + is_saved: bool = False + is_hidden: bool = False # Added flag for hidden networks + + +class NetworkScanResult(NamedTuple): + """Result of a network scan.""" + + ssid: str + signal: int + status: str + is_open: bool = False + + +class NetworkScanRunnable(QRunnable): + """Runnable for scanning networks in background thread.""" + + class Signals(QObject): + """Signals for network scan results.""" + + scan_results = pyqtSignal(dict, name="scan-results") + finished_network_list_build = pyqtSignal( + list, name="finished-network-list-build" + ) + error = pyqtSignal(str) + + def __init__(self, nm: SdbusNetworkManagerAsync) -> None: + """Initialize the network scan runnable.""" super().__init__() - self.mutex = QtCore.QMutex() - self.condition = QtCore.QWaitCondition() - self.restart = False - self.mutex.unlock() - self.network_items_list = [] - self.nm = SdbusNetworkManagerAsync() - if not self.nm: - logger.error( - "Cannot scan for networks, parent does not have \ - sdbus_network ('SdbusNetworkManagerAsync' instance class)" - ) - return - logger.info("Network Scanner Thread Initiated") + self._nm = nm + self.signals = NetworkScanRunnable.Signals() + + def run(self) -> None: + """Execute the network scan.""" + try: + self._nm.rescan_networks() + saved_ssids = self._nm.get_saved_ssid_names() + available = self._get_available_networks() + data_dict = self._build_data_dict(available, saved_ssids) + self.signals.scan_results.emit(data_dict) + items = self._build_network_list(data_dict) + self.signals.finished_network_list_build.emit(items) + except Exception as e: + logger.error("Error scanning networks", exc_info=True) + self.signals.error.emit(str(e)) + + def _get_available_networks(self) -> Dict[str, Dict]: + """Get available networks from NetworkManager.""" + if self._nm.check_wifi_interface(): + return self._nm.get_available_networks() or {} + return {} + + def _build_data_dict( + self, available: Dict[str, Dict], saved_ssids: List[str] + ) -> Dict[str, Dict]: + """Build data dictionary from available networks.""" + data_dict: Dict[str, Dict] = {} + for ssid, props in available.items(): + signal = int(props.get("signal_level", 0)) + sec_tuple = props.get("security", (0, 0, 0)) + caps_value = sec_tuple[2] if len(sec_tuple) > 2 else 0 + is_open = (caps_value & PRIVACY_BIT) == 0 + # Check if this is a hidden network + is_hidden = ssid in HIDDEN_NETWORK_INDICATORS or not ssid.strip() + data_dict[ssid] = { + "signal_level": signal, + "is_saved": ssid in saved_ssids, + "is_open": is_open, + "is_hidden": is_hidden, + } + return data_dict + + def _build_network_list(self, data_dict: Dict[str, Dict]) -> List[tuple]: + """Build sorted network list for display.""" + current_ssid = self._nm.get_current_ssid() + + saved_nets = [ + (ssid, info["signal_level"], info["is_open"], info.get("is_hidden", False)) + for ssid, info in data_dict.items() + if info["is_saved"] + ] + unsaved_nets = [ + (ssid, info["signal_level"], info["is_open"], info.get("is_hidden", False)) + for ssid, info in data_dict.items() + if not info["is_saved"] + ] + + saved_nets.sort(key=lambda x: -x[1]) + unsaved_nets.sort(key=lambda x: -x[1]) + + items: List[tuple] = [] + + for ssid, signal, is_open, is_hidden in saved_nets: + status = "Active" if ssid == current_ssid else "Saved" + items.append((ssid, signal, status, is_open, True, is_hidden)) + + for ssid, signal, is_open, is_hidden in unsaved_nets: + status = "Open" if is_open else "Protected" + items.append((ssid, signal, status, is_open, False, is_hidden)) + + return items + + +class BuildNetworkList(QtCore.QObject): + """Worker class for building network lists with polling support.""" + + scan_results = pyqtSignal(dict, name="scan-results") + finished_network_list_build = pyqtSignal(list, name="finished-network-list-build") + error = pyqtSignal(str) + + def __init__( + self, + nm: SdbusNetworkManagerAsync, + poll_interval_ms: int = DEFAULT_POLL_INTERVAL_MS, + ) -> None: + """Initialize the network list builder.""" + super().__init__() + self._nm = nm + self._threadpool = QThreadPool.globalInstance() + self._poll_interval_ms = poll_interval_ms + self._is_scanning = False + self._scan_lock = threading.Lock() + self._timer = QtCore.QTimer(self) + self._timer.setSingleShot(True) + self._timer.timeout.connect(self._do_scan) + + def start_polling(self) -> None: + """Start periodic network scanning.""" + self._schedule_next_scan() + + def stop_polling(self) -> None: + """Stop periodic network scanning.""" + self._timer.stop() def build(self) -> None: - """Starts QThread""" - with QtCore.QMutexLocker(self.mutex): - if not self.isRunning(): - self.start(QtCore.QThread.Priority.LowPriority) - else: - self.restart = True - self.condition.wakeOne() + """Trigger immediate network scan.""" + self._do_scan() + + def _schedule_next_scan(self) -> None: + """Schedule the next network scan.""" + self._timer.start(self._poll_interval_ms) + + def _on_task_finished(self, items: List) -> None: + """Handle scan completion.""" + with self._scan_lock: + self._is_scanning = False + self.finished_network_list_build.emit(items) + self._schedule_next_scan() + + def _on_task_scan_results(self, data_dict: Dict) -> None: + """Handle scan results.""" + self.scan_results.emit(data_dict) + + def _on_task_error(self, err: str) -> None: + """Handle scan error.""" + with self._scan_lock: + self._is_scanning = False + self.error.emit(err) + self._schedule_next_scan() + + def _do_scan(self) -> None: + """Execute network scan in background thread.""" + with self._scan_lock: + if self._is_scanning: + return + self._is_scanning = True - def stop(self): - """Stops QThread execution""" - self.mutex.lock() - self.condition.wakeOne() - self.mutex.unlock() - self.deleteLater() + task = NetworkScanRunnable(self._nm) + task.signals.finished_network_list_build.connect(self._on_task_finished) + task.signals.scan_results.connect(self._on_task_scan_results) + task.signals.error.connect(self._on_task_error) + self._threadpool.start(task) - def run(self) -> None: - """BuildNetworkList main thread logic""" - logger.debug("Scanning and building network list") - while True: - self.mutex.lock() - self.network_items_list.clear() - self.nm.rescan_networks() - saved_ssids = self.nm.get_saved_ssid_names() - saved_networks = self.nm.get_saved_networks() - unsaved_networks = [] - networks = [] - if self.nm.check_wifi_interface(): - available_networks = self.nm.get_available_networks() - if not available_networks: # Skip everything if no networks exist - logger.debug("No available networks after scan") - self.finished_network_list_build.emit(self.network_items_list) - return - for ssid_key in available_networks: - properties = available_networks.get(ssid_key, {}) - signal = int(properties.get("signal_level", 0)) - networks.append( - { - "ssid": ssid_key if ssid_key else "UNKNOWN", - "signal": signal, - "is_saved": bool(ssid_key in saved_ssids), - } - ) - if networks: - saved_networks = sorted( - [n for n in networks if n["is_saved"]], - key=lambda x: -x["signal"], - ) - unsaved_networks = sorted( - [n for n in networks if not n["is_saved"]], - key=lambda x: -x["signal"], - ) - elif saved_networks: - saved_networks = sorted([n for n in saved_networks], key=lambda x: -1) - if saved_networks: - for net in saved_networks: - if "ap" in net.get("mode", ""): - return - ssid = net.get("ssid", "UNKNOWN") - signal = ( - self.nm.get_connection_signal_by_ssid(ssid) - if ssid != "UNKNOWN" - else 0 - ) - if ssid == self.nm.get_current_ssid(): - self.network_items_list.append((ssid, signal, "Active")) - else: - self.network_items_list.append((ssid, signal, "Saved")) - if saved_networks and unsaved_networks: # Separator - self.network_items_list.append("separator") - if unsaved_networks: - for net in unsaved_networks: - ssid = net.get("ssid", "UNKNOWN") - signal = ( - self.nm.get_connection_signal_by_ssid(ssid) - if ssid != "UNKNOWN" - else 0 - ) - self.network_items_list.append((ssid, signal, "Protected")) - # Add a dummy blank space at the end if there are any unsaved networks - if unsaved_networks: - self.network_items_list.append("blank") - - self.finished_network_list_build.emit(self.network_items_list) - if not self.restart: - self.condition.wait(self.mutex) - self.restart = False - self.mutex.unlock() + +class WifiIconProvider: + """Provider for Wi-Fi signal strength icons.""" + + def __init__(self) -> None: + """Initialize icon paths.""" + self._paths = { + (0, False): ":/network/media/btn_icons/0bar_wifi.svg", + (1, False): ":/network/media/btn_icons/1bar_wifi.svg", + (2, False): ":/network/media/btn_icons/2bar_wifi.svg", + (3, False): ":/network/media/btn_icons/3bar_wifi.svg", + (4, False): ":/network/media/btn_icons/4bar_wifi.svg", + (0, True): ":/network/media/btn_icons/0bar_wifi_protected.svg", + (1, True): ":/network/media/btn_icons/1bar_wifi_protected.svg", + (2, True): ":/network/media/btn_icons/2bar_wifi_protected.svg", + (3, True): ":/network/media/btn_icons/3bar_wifi_protected.svg", + (4, True): ":/network/media/btn_icons/4bar_wifi_protected.svg", + } + + def get_pixmap(self, signal: int, status: str) -> QtGui.QPixmap: + """Get pixmap for given signal strength and status.""" + bars = self._signal_to_bars(signal) + is_protected = status == "Protected" + key = (bars, is_protected) + path = self._paths.get(key, self._paths[(0, False)]) + return QtGui.QPixmap(path) + + @staticmethod + def _signal_to_bars(signal: int) -> int: + """Convert signal strength to bar count.""" + if signal < SIGNAL_MINIMUM_THRESHOLD: + return 0 + elif signal >= SIGNAL_EXCELLENT_THRESHOLD: + return 4 + elif signal >= SIGNAL_GOOD_THRESHOLD: + return 3 + elif signal > SIGNAL_FAIR_THRESHOLD: + return 2 + else: + return 1 class NetworkControlWindow(QtWidgets.QStackedWidget): - """Network Control panel Widget""" - - request_network_scan = QtCore.pyqtSignal(name="scan-network") - new_ip_signal = QtCore.pyqtSignal(str, name="ip-address-change") - get_hotspot_ssid = QtCore.pyqtSignal(str, name="hotspot-ssid-name") - delete_network_signal = QtCore.pyqtSignal(str, name="delete-network") - - def __init__(self, parent: typing.Optional[QtWidgets.QWidget], /) -> None: - super(NetworkControlWindow, self).__init__(parent) - self.background: typing.Optional[QtGui.QPixmap] = None - self.panel = Ui_wifi_stacked_page() - self.panel.setupUi(self) - self.popup = Popup(self) - self.sdbus_network = SdbusNetworkManagerAsync() - self.start: bool = True - self.saved_network = dict - - self._load_timer = QtCore.QTimer() - self._load_timer.setSingleShot(True) - self._load_timer.timeout.connect(self._handle_load_timeout) + """Main network control window widget.""" + + request_network_scan = pyqtSignal(name="scan-network") + new_ip_signal = pyqtSignal(str, name="ip-address-change") + get_hotspot_ssid = pyqtSignal(str, name="hotspot-ssid-name") + delete_network_signal = pyqtSignal(str, name="delete-network") + + def __init__(self, parent: Optional[QtWidgets.QWidget] = None, /) -> None: + """Initialize the network control window.""" + super().__init__(parent) if parent else super().__init__() + + self._init_instance_variables() + self._setupUI() + self._init_timers() + self._init_model_view() + self._init_network_worker() + self._setup_navigation_signals() + self._setup_action_signals() + self._setup_toggle_signals() + self._setup_password_visibility_signals() + self._setup_icons() + self._setup_input_fields() + self._setup_keyboard() + self._setup_scrollbar_signals() + + self._network_list_worker.build() + self.request_network_scan.emit() + self.hide() - # Network Scan - self.network_list_widget = QtWidgets.QListWidget( - parent=self.panel.network_list_page - ) - self.build_network_list() + # Initialize UI state + self._init_ui_state() + + def _init_ui_state(self) -> None: + """Initialize UI to a clean disconnected state.""" + self.loadingwidget.setVisible(False) + self._hide_all_info_elements() + self._configure_info_box_centered() + self.mn_info_box.setVisible(True) + self.mn_info_box.setText( + "Network connection required.\n\nConnect to Wi-Fi\nor\nTurn on Hotspot" + ) + + def _hide_all_info_elements(self) -> None: + """Hide ALL elements in the info panel (details, loading, info box).""" + # Hide network details + self.netlist_ip.setVisible(False) + self.netlist_ssuid.setVisible(False) + self.mn_info_seperator.setVisible(False) + self.line_2.setVisible(False) + self.netlist_strength.setVisible(False) + self.netlist_strength_label.setVisible(False) + self.line_3.setVisible(False) + self.netlist_security.setVisible(False) + self.netlist_security_label.setVisible(False) + # Hide loading + self.loadingwidget.setVisible(False) + # Hide info box + self.mn_info_box.setVisible(False) + + def _init_instance_variables(self) -> None: + """Initialize all instance variables.""" + self._icon_provider = WifiIconProvider() + self._ongoing_update = False + self._is_first_run = True + self._networks: Dict[str, NetworkInfo] = {} + self._previous_panel: Optional[QtWidgets.QWidget] = None + self._current_field: Optional[QtWidgets.QLineEdit] = None + self._current_network_is_open = False + self._current_network_is_hidden = False + self._is_connecting = False + self._target_ssid: Optional[str] = None + self._last_displayed_ssid: Optional[str] = None + self._current_network_ssid: Optional[str] = ( + None # Track current network for priority + ) + + def _setupUI(self) -> None: + """Setup all UI elements programmatically.""" + self.setObjectName("wifi_stacked_page") + self.resize(800, 480) + + size_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum + ) + size_policy.setHorizontalStretch(0) + size_policy.setVerticalStretch(0) + size_policy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) + self.setSizePolicy(size_policy) + self.setMinimumSize(QtCore.QSize(0, 400)) + self.setMaximumSize(QtCore.QSize(16777215, 575)) + self.setStyleSheet( + "#wifi_stacked_page{\n" + " background-image: url(:/background/media/1st_background.png);\n" + "}\n" + ) + + self._sdbus_network = SdbusNetworkManagerAsync() + self._popup = Popup(self) + self._right_arrow_icon = QtGui.QPixmap( + ":/arrow_icons/media/btn_icons/right_arrow.svg" + ) + + # Create all pages + self._setup_main_network_page() + self._setup_network_list_page() + self._setup_add_network_page() + self._setup_saved_connection_page() + self._setup_saved_details_page() + self._setup_hotspot_page() + self._setup_hidden_network_page() + + self.setCurrentIndex(0) + + def _create_white_palette(self) -> QtGui.QPalette: + """Create a palette with white text.""" + palette = QtGui.QPalette() + white_brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + white_brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + grey_brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) + grey_brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + + for group in [ + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorGroup.Inactive, + ]: + palette.setBrush(group, QtGui.QPalette.ColorRole.WindowText, white_brush) + palette.setBrush(group, QtGui.QPalette.ColorRole.Text, white_brush) - self.network_list_worker = BuildNetworkList() - self.network_list_worker.finished_network_list_build.connect( - self.handle_network_list + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.WindowText, + grey_brush, ) - self.panel.rescan_button.clicked.connect( - lambda: QtCore.QTimer.singleShot(100, self.network_list_worker.build) + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.Text, + grey_brush, + ) + + return palette + + def _setup_main_network_page(self) -> None: + """Setup the main network page.""" + self.main_network_page = QtWidgets.QWidget() + size_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, ) + self.main_network_page.setSizePolicy(size_policy) + self.main_network_page.setObjectName("main_network_page") - self.sdbus_network.nm_state_change.connect(self.evaluate_network_state) - self.panel.wifi_button.clicked.connect( - partial( - self.setCurrentIndex, - self.indexOf(self.panel.network_list_page), + main_layout = QtWidgets.QVBoxLayout(self.main_network_page) + main_layout.setObjectName("verticalLayout_14") + + # Header layout + header_layout = QtWidgets.QHBoxLayout() + header_layout.setObjectName("main_network_header_layout") + + header_layout.addItem( + QtWidgets.QSpacerItem( + 60, + 60, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) ) - self.panel.hotspot_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.panel.hotspot_page)) + + self.network_main_title = QtWidgets.QLabel(parent=self.main_network_page) + title_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum + ) + self.network_main_title.setSizePolicy(title_policy) + self.network_main_title.setMinimumSize(QtCore.QSize(300, 0)) + self.network_main_title.setMaximumSize(QtCore.QSize(16777215, 60)) + font = QtGui.QFont() + font.setPointSize(20) + self.network_main_title.setFont(font) + self.network_main_title.setStyleSheet("color:white") + self.network_main_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.network_main_title.setText("Networks") + self.network_main_title.setObjectName("network_main_title") + header_layout.addWidget(self.network_main_title) + + self.network_backButton = IconButton(parent=self.main_network_page) + self.network_backButton.setMinimumSize(QtCore.QSize(60, 60)) + self.network_backButton.setMaximumSize(QtCore.QSize(60, 60)) + self.network_backButton.setText("") + self.network_backButton.setFlat(True) + self.network_backButton.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + ) + self.network_backButton.setObjectName("network_backButton") + header_layout.addWidget(self.network_backButton) + + main_layout.addLayout(header_layout) + + # Content layout + content_layout = QtWidgets.QHBoxLayout() + content_layout.setObjectName("main_network_content_layout") + + # Information frame + self.mn_information_layout = BlocksCustomFrame(parent=self.main_network_page) + info_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.mn_information_layout.setSizePolicy(info_policy) + self.mn_information_layout.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.mn_information_layout.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.mn_information_layout.setObjectName("mn_information_layout") + + info_layout = QtWidgets.QVBoxLayout(self.mn_information_layout) + info_layout.setObjectName("verticalLayout_3") + + # SSID label + self.netlist_ssuid = QtWidgets.QLabel(parent=self.mn_information_layout) + ssid_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum + ) + self.netlist_ssuid.setSizePolicy(ssid_policy) + font = QtGui.QFont() + font.setPointSize(17) + self.netlist_ssuid.setFont(font) + self.netlist_ssuid.setStyleSheet("color: rgb(255, 255, 255);") + self.netlist_ssuid.setText("") + self.netlist_ssuid.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_ssuid.setObjectName("netlist_ssuid") + info_layout.addWidget(self.netlist_ssuid) + + # Separator + self.mn_info_seperator = QtWidgets.QFrame(parent=self.mn_information_layout) + self.mn_info_seperator.setFrameShape(QtWidgets.QFrame.Shape.HLine) + self.mn_info_seperator.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + self.mn_info_seperator.setObjectName("mn_info_seperator") + info_layout.addWidget(self.mn_info_seperator) + + # IP label + self.netlist_ip = QtWidgets.QLabel(parent=self.mn_information_layout) + font = QtGui.QFont() + font.setPointSize(15) + self.netlist_ip.setFont(font) + self.netlist_ip.setStyleSheet("color: rgb(255, 255, 255);") + self.netlist_ip.setText("") + self.netlist_ip.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_ip.setObjectName("netlist_ip") + info_layout.addWidget(self.netlist_ip) + + # Connection info layout + conn_info_layout = QtWidgets.QHBoxLayout() + conn_info_layout.setObjectName("mn_conn_info") + + # Signal strength section + sg_info_layout = QtWidgets.QVBoxLayout() + sg_info_layout.setObjectName("mn_sg_info_layout") + + self.netlist_strength_label = QtWidgets.QLabel( + parent=self.mn_information_layout + ) + self.netlist_strength_label.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + self.netlist_strength_label.setFont(font) + self.netlist_strength_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_strength_label.setText("Signal\nStrength") + self.netlist_strength_label.setObjectName("netlist_strength_label") + sg_info_layout.addWidget(self.netlist_strength_label) + + self.line_2 = QtWidgets.QFrame(parent=self.mn_information_layout) + self.line_2.setFrameShape(QtWidgets.QFrame.Shape.HLine) + self.line_2.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + self.line_2.setObjectName("line_2") + sg_info_layout.addWidget(self.line_2) + + self.netlist_strength = QtWidgets.QLabel(parent=self.mn_information_layout) + font = QtGui.QFont() + font.setPointSize(11) + self.netlist_strength.setFont(font) + self.netlist_strength.setStyleSheet("color: rgb(255, 255, 255);") + self.netlist_strength.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_strength.setText("") + self.netlist_strength.setObjectName("netlist_strength") + sg_info_layout.addWidget(self.netlist_strength) + + conn_info_layout.addLayout(sg_info_layout) + + # Security section + sec_info_layout = QtWidgets.QVBoxLayout() + sec_info_layout.setObjectName("mn_sec_info_layout") + + self.netlist_security_label = QtWidgets.QLabel( + parent=self.mn_information_layout + ) + self.netlist_security_label.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + self.netlist_security_label.setFont(font) + self.netlist_security_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_security_label.setText("Security\nType") + self.netlist_security_label.setObjectName("netlist_security_label") + sec_info_layout.addWidget(self.netlist_security_label) + + self.line_3 = QtWidgets.QFrame(parent=self.mn_information_layout) + self.line_3.setFrameShape(QtWidgets.QFrame.Shape.HLine) + self.line_3.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + self.line_3.setObjectName("line_3") + sec_info_layout.addWidget(self.line_3) + + self.netlist_security = QtWidgets.QLabel(parent=self.mn_information_layout) + font = QtGui.QFont() + font.setPointSize(11) + self.netlist_security.setFont(font) + self.netlist_security.setStyleSheet("color: rgb(255, 255, 255);") + self.netlist_security.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_security.setText("") + self.netlist_security.setObjectName("netlist_security") + sec_info_layout.addWidget(self.netlist_security) + + conn_info_layout.addLayout(sec_info_layout) + info_layout.addLayout(conn_info_layout) + + # Info box + self.mn_info_box = QtWidgets.QLabel(parent=self.mn_information_layout) + self.mn_info_box.setEnabled(False) + font = QtGui.QFont() + font.setPointSize(17) + self.mn_info_box.setFont(font) + self.mn_info_box.setStyleSheet("color: white") + self.mn_info_box.setTextFormat(QtCore.Qt.TextFormat.PlainText) + self.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.mn_info_box.setText( + "No network connection.\n\n" + "Try connecting to Wi-Fi \n" + "or turn on the hotspot\n" + "using the buttons on the side." + ) + self.mn_info_box.setObjectName("mn_info_box") + info_layout.addWidget(self.mn_info_box) + + # Loading widget + self.loadingwidget = LoadingOverlayWidget(parent=self.mn_information_layout) + self.loadingwidget.setEnabled(True) + loading_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum + ) + self.loadingwidget.setSizePolicy(loading_policy) + self.loadingwidget.setText("") + self.loadingwidget.setObjectName("loadingwidget") + info_layout.addWidget(self.loadingwidget) + + content_layout.addWidget(self.mn_information_layout) + + # Option buttons layout + option_layout = QtWidgets.QVBoxLayout() + option_layout.setObjectName("mn_option_button_layout") + + self.wifi_button = NetworkWidgetbuttons(parent=self.main_network_page) + wifi_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.wifi_button.setSizePolicy(wifi_policy) + self.wifi_button.setMaximumSize(QtCore.QSize(400, 9999)) + font = QtGui.QFont() + font.setPointSize(20) + self.wifi_button.setFont(font) + self.wifi_button.setText("Wi-Fi") + self.wifi_button.setObjectName("wifi_button") + option_layout.addWidget(self.wifi_button) + + self.hotspot_button = NetworkWidgetbuttons(parent=self.main_network_page) + self.hotspot_button.setSizePolicy(wifi_policy) + self.hotspot_button.setMaximumSize(QtCore.QSize(400, 9999)) + font = QtGui.QFont() + font.setPointSize(20) + self.hotspot_button.setFont(font) + self.hotspot_button.setText("Hotspot") + self.hotspot_button.setObjectName("hotspot_button") + option_layout.addWidget(self.hotspot_button) + + content_layout.addLayout(option_layout) + main_layout.addLayout(content_layout) + + self.addWidget(self.main_network_page) + + def _setup_network_list_page(self) -> None: + """Setup the network list page.""" + self.network_list_page = QtWidgets.QWidget() + self.network_list_page.setObjectName("network_list_page") + + main_layout = QtWidgets.QVBoxLayout(self.network_list_page) + main_layout.setObjectName("verticalLayout_9") + + # Header layout + header_layout = QtWidgets.QHBoxLayout() + header_layout.setObjectName("nl_header_layout") + + self.rescan_button = IconButton(parent=self.network_list_page) + self.rescan_button.setMinimumSize(QtCore.QSize(60, 60)) + self.rescan_button.setMaximumSize(QtCore.QSize(60, 60)) + self.rescan_button.setText("Reload") + self.rescan_button.setFlat(True) + self.rescan_button.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/refresh.svg") + ) + self.rescan_button.setProperty("button_type", "icon") + self.rescan_button.setObjectName("rescan_button") + header_layout.addWidget(self.rescan_button) + + self.network_list_title = QtWidgets.QLabel(parent=self.network_list_page) + self.network_list_title.setMaximumSize(QtCore.QSize(16777215, 60)) + self.network_list_title.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(20) + self.network_list_title.setFont(font) + self.network_list_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.network_list_title.setText("Wi-Fi List") + self.network_list_title.setObjectName("network_list_title") + header_layout.addWidget(self.network_list_title) + + self.nl_back_button = IconButton(parent=self.network_list_page) + self.nl_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.nl_back_button.setMaximumSize(QtCore.QSize(60, 60)) + self.nl_back_button.setText("Back") + self.nl_back_button.setFlat(True) + self.nl_back_button.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + ) + self.nl_back_button.setProperty("class", "back_btn") + self.nl_back_button.setProperty("button_type", "icon") + self.nl_back_button.setObjectName("nl_back_button") + header_layout.addWidget(self.nl_back_button) + + main_layout.addLayout(header_layout) + + # List view layout + list_layout = QtWidgets.QHBoxLayout() + list_layout.setObjectName("horizontalLayout_2") + + self.listView = QtWidgets.QListView(self.network_list_page) + list_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + ) + list_policy.setHorizontalStretch(1) + list_policy.setVerticalStretch(1) + self.listView.setSizePolicy(list_policy) + self.listView.setMinimumSize(QtCore.QSize(0, 0)) + self.listView.setStyleSheet("background-color: rgba(255, 255, 255, 0);") + self.listView.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.listView.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) + self.listView.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.listView.setHorizontalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.listView.setSelectionBehavior( + QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems + ) + self.listView.setVerticalScrollMode( + QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel ) + self.listView.setEditTriggers( + QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers + ) + self.listView.setUniformItemSizes(True) + self.listView.setSpacing(5) - self.panel.hotspot_button.setPixmap( - QtGui.QPixmap(":/network/media/btn_icons/hotspot.svg") + # Setup touch scrolling + QtWidgets.QScroller.grabGesture( + self.listView, + QtWidgets.QScroller.ScrollerGestureType.TouchGesture, ) - self.panel.wifi_button.setPixmap( - QtGui.QPixmap(":/network/media/btn_icons/wifi_config.svg") + QtWidgets.QScroller.grabGesture( + self.listView, + QtWidgets.QScroller.ScrollerGestureType.LeftMouseButtonGesture, ) - self.panel.nl_back_button.clicked.connect( - partial( - self.setCurrentIndex, - self.indexOf(self.panel.main_network_page), + scroller_instance = QtWidgets.QScroller.scroller(self.listView) + scroller_props = scroller_instance.scrollerProperties() + scroller_props.setScrollMetric( + QtWidgets.QScrollerProperties.ScrollMetric.DragVelocitySmoothingFactor, + 0.05, + ) + scroller_props.setScrollMetric( + QtWidgets.QScrollerProperties.ScrollMetric.DecelerationFactor, + 0.4, + ) + QtWidgets.QScroller.scroller(self.listView).setScrollerProperties( + scroller_props + ) + + self.verticalScrollBar = CustomScrollBar(parent=self.network_list_page) + scrollbar_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Minimum + ) + self.verticalScrollBar.setSizePolicy(scrollbar_policy) + self.verticalScrollBar.setOrientation(QtCore.Qt.Orientation.Vertical) + self.verticalScrollBar.setObjectName("verticalScrollBar") + self.verticalScrollBar.setAttribute( + QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents, True + ) + + self.listView.setVerticalScrollBar(self.verticalScrollBar) + self.listView.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded + ) + + list_layout.addWidget(self.listView) + list_layout.addWidget(self.verticalScrollBar) + + main_layout.addLayout(list_layout) + + self.scroller = QtWidgets.QScroller.scroller(self.listView) + + self.addWidget(self.network_list_page) + + def _setup_add_network_page(self) -> None: + """Setup the add network page.""" + self.add_network_page = QtWidgets.QWidget() + self.add_network_page.setObjectName("add_network_page") + + main_layout = QtWidgets.QVBoxLayout(self.add_network_page) + main_layout.setObjectName("verticalLayout_10") + + # Header layout + header_layout = QtWidgets.QHBoxLayout() + header_layout.setObjectName("add_np_header_layout") + + header_layout.addItem( + QtWidgets.QSpacerItem( + 40, + 60, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) ) - self.panel.network_backButton.clicked.connect(self.hide) - self.panel.rescan_button.clicked.connect( - lambda: self.sdbus_network.rescan_networks() + self.add_network_network_label = QtWidgets.QLabel(parent=self.add_network_page) + label_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Preferred, + ) + self.add_network_network_label.setSizePolicy(label_policy) + self.add_network_network_label.setMinimumSize(QtCore.QSize(0, 60)) + self.add_network_network_label.setMaximumSize(QtCore.QSize(16777215, 60)) + font = QtGui.QFont() + font.setPointSize(20) + self.add_network_network_label.setFont(font) + self.add_network_network_label.setStyleSheet("color:white") + self.add_network_network_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.add_network_network_label.setText("TextLabel") + self.add_network_network_label.setObjectName("add_network_network_label") + header_layout.addWidget(self.add_network_network_label) + + self.add_network_page_backButton = IconButton(parent=self.add_network_page) + self.add_network_page_backButton.setMinimumSize(QtCore.QSize(60, 60)) + self.add_network_page_backButton.setMaximumSize(QtCore.QSize(60, 60)) + self.add_network_page_backButton.setText("Back") + self.add_network_page_backButton.setFlat(True) + self.add_network_page_backButton.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + ) + self.add_network_page_backButton.setProperty("class", "back_btn") + self.add_network_page_backButton.setProperty("button_type", "icon") + self.add_network_page_backButton.setObjectName("add_network_page_backButton") + header_layout.addWidget(self.add_network_page_backButton) + + main_layout.addLayout(header_layout) + + # Content layout + content_layout = QtWidgets.QVBoxLayout() + content_layout.setSizeConstraint( + QtWidgets.QLayout.SizeConstraint.SetMinimumSize + ) + content_layout.setObjectName("add_np_content_layout") + + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 50, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) ) - self.request_network_scan.connect(self.rescan_networks) - self.panel.add_network_validation_button.clicked.connect(self.add_network) - self.panel.add_network_page_backButton.clicked.connect( - partial( - self.setCurrentIndex, - self.indexOf(self.panel.network_list_page), + # Password frame + self.frame_2 = BlocksCustomFrame(parent=self.add_network_page) + frame_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum + ) + frame_policy.setVerticalStretch(2) + self.frame_2.setSizePolicy(frame_policy) + self.frame_2.setMinimumSize(QtCore.QSize(0, 80)) + self.frame_2.setMaximumSize(QtCore.QSize(16777215, 90)) + self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_2.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame_2.setObjectName("frame_2") + + frame_layout_widget = QtWidgets.QWidget(parent=self.frame_2) + frame_layout_widget.setGeometry(QtCore.QRect(10, 10, 761, 82)) + frame_layout_widget.setObjectName("layoutWidget_2") + + password_layout = QtWidgets.QHBoxLayout(frame_layout_widget) + password_layout.setSizeConstraint( + QtWidgets.QLayout.SizeConstraint.SetMaximumSize + ) + password_layout.setContentsMargins(0, 0, 0, 0) + password_layout.setObjectName("horizontalLayout_5") + + self.add_network_password_label = QtWidgets.QLabel(parent=frame_layout_widget) + self.add_network_password_label.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + self.add_network_password_label.setFont(font) + self.add_network_password_label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.add_network_password_label.setText("Password") + self.add_network_password_label.setObjectName("add_network_password_label") + password_layout.addWidget(self.add_network_password_label) + + self.add_network_password_field = BlocksCustomLinEdit( + parent=frame_layout_widget + ) + self.add_network_password_field.setHidden(True) + self.add_network_password_field.setMinimumSize(QtCore.QSize(500, 60)) + font = QtGui.QFont() + font.setPointSize(12) + self.add_network_password_field.setFont(font) + self.add_network_password_field.setObjectName("add_network_password_field") + password_layout.addWidget(self.add_network_password_field) + + self.add_network_password_view = IconButton(parent=frame_layout_widget) + self.add_network_password_view.setMinimumSize(QtCore.QSize(60, 60)) + self.add_network_password_view.setMaximumSize(QtCore.QSize(60, 60)) + self.add_network_password_view.setText("View") + self.add_network_password_view.setFlat(True) + self.add_network_password_view.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") + ) + self.add_network_password_view.setProperty("class", "back_btn") + self.add_network_password_view.setProperty("button_type", "icon") + self.add_network_password_view.setObjectName("add_network_password_view") + password_layout.addWidget(self.add_network_password_view) + + content_layout.addWidget(self.frame_2) + + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 150, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) ) - self.panel.add_network_password_view.pressed.connect( - partial( - self.panel.add_network_password_field.setEchoMode, - QtWidgets.QLineEdit.EchoMode.Normal, + + # Validation button layout + button_layout = QtWidgets.QHBoxLayout() + button_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize) + button_layout.setObjectName("horizontalLayout_6") + + self.add_network_validation_button = BlocksCustomButton( + parent=self.add_network_page + ) + self.add_network_validation_button.setEnabled(True) + btn_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + ) + btn_policy.setHorizontalStretch(1) + btn_policy.setVerticalStretch(1) + self.add_network_validation_button.setSizePolicy(btn_policy) + self.add_network_validation_button.setMinimumSize(QtCore.QSize(250, 80)) + self.add_network_validation_button.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setFamily("Momcake") + font.setPointSize(15) + self.add_network_validation_button.setFont(font) + self.add_network_validation_button.setIconSize(QtCore.QSize(16, 16)) + self.add_network_validation_button.setCheckable(False) + self.add_network_validation_button.setChecked(False) + self.add_network_validation_button.setFlat(True) + self.add_network_validation_button.setProperty( + "icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") + ) + self.add_network_validation_button.setText("Activate") + self.add_network_validation_button.setObjectName( + "add_network_validation_button" + ) + button_layout.addWidget( + self.add_network_validation_button, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignTop, + ) + + content_layout.addLayout(button_layout) + main_layout.addLayout(content_layout) + + self.addWidget(self.add_network_page) + + def _setup_hidden_network_page(self) -> None: + """Setup the hidden network page for connecting to networks with hidden SSID.""" + self.hidden_network_page = QtWidgets.QWidget() + self.hidden_network_page.setObjectName("hidden_network_page") + + main_layout = QtWidgets.QVBoxLayout(self.hidden_network_page) + main_layout.setObjectName("hidden_network_layout") + + # Header layout + header_layout = QtWidgets.QHBoxLayout() + header_layout.addItem( + QtWidgets.QSpacerItem( + 40, + 60, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) ) - self.panel.add_network_password_view.released.connect( - partial( - self.panel.add_network_password_field.setEchoMode, - QtWidgets.QLineEdit.EchoMode.Password, + + self.hidden_network_title = QtWidgets.QLabel(parent=self.hidden_network_page) + self.hidden_network_title.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(20) + self.hidden_network_title.setFont(font) + self.hidden_network_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.hidden_network_title.setText("Hidden Network") + header_layout.addWidget(self.hidden_network_title) + + self.hidden_network_back_button = IconButton(parent=self.hidden_network_page) + self.hidden_network_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.hidden_network_back_button.setMaximumSize(QtCore.QSize(60, 60)) + self.hidden_network_back_button.setFlat(True) + self.hidden_network_back_button.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + ) + self.hidden_network_back_button.setProperty("button_type", "icon") + header_layout.addWidget(self.hidden_network_back_button) + + main_layout.addLayout(header_layout) + + # Content + content_layout = QtWidgets.QVBoxLayout() + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 30, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) ) - self.panel.saved_connection_back_button.clicked.connect( - partial( - self.setCurrentIndex, - self.indexOf(self.panel.network_list_page), + # SSID Frame + ssid_frame = BlocksCustomFrame(parent=self.hidden_network_page) + ssid_frame.setMinimumSize(QtCore.QSize(0, 80)) + ssid_frame.setMaximumSize(QtCore.QSize(16777215, 90)) + ssid_frame_layout = QtWidgets.QHBoxLayout(ssid_frame) + + ssid_label = QtWidgets.QLabel("Network\nName", parent=ssid_frame) + ssid_label.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + ssid_label.setFont(font) + ssid_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + ssid_frame_layout.addWidget(ssid_label) + + self.hidden_network_ssid_field = BlocksCustomLinEdit(parent=ssid_frame) + self.hidden_network_ssid_field.setMinimumSize(QtCore.QSize(500, 60)) + font = QtGui.QFont() + font.setPointSize(12) + self.hidden_network_ssid_field.setFont(font) + self.hidden_network_ssid_field.setPlaceholderText("Enter network name") + ssid_frame_layout.addWidget(self.hidden_network_ssid_field) + + content_layout.addWidget(ssid_frame) + + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) ) - self.delete_network_signal.connect(self.delete_network) - self.panel.snd_back.clicked.connect( - lambda: self.update_network( - ssid=self.panel.saved_connection_network_name.text(), - password=self.panel.saved_connection_change_password_field.text(), - new_ssid=None, + + # Password Frame + password_frame = BlocksCustomFrame(parent=self.hidden_network_page) + password_frame.setMinimumSize(QtCore.QSize(0, 80)) + password_frame.setMaximumSize(QtCore.QSize(16777215, 90)) + password_frame_layout = QtWidgets.QHBoxLayout(password_frame) + + password_label = QtWidgets.QLabel("Password", parent=password_frame) + password_label.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + password_label.setFont(font) + password_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + password_frame_layout.addWidget(password_label) + + self.hidden_network_password_field = BlocksCustomLinEdit(parent=password_frame) + self.hidden_network_password_field.setHidden(True) + self.hidden_network_password_field.setMinimumSize(QtCore.QSize(500, 60)) + font = QtGui.QFont() + font.setPointSize(12) + self.hidden_network_password_field.setFont(font) + self.hidden_network_password_field.setPlaceholderText( + "Enter password (leave empty for open networks)" + ) + self.hidden_network_password_field.setEchoMode( + QtWidgets.QLineEdit.EchoMode.Password + ) + password_frame_layout.addWidget(self.hidden_network_password_field) + + self.hidden_network_password_view = IconButton(parent=password_frame) + self.hidden_network_password_view.setMinimumSize(QtCore.QSize(60, 60)) + self.hidden_network_password_view.setMaximumSize(QtCore.QSize(60, 60)) + self.hidden_network_password_view.setFlat(True) + self.hidden_network_password_view.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") + ) + self.hidden_network_password_view.setProperty("button_type", "icon") + password_frame_layout.addWidget(self.hidden_network_password_view) + + content_layout.addWidget(password_frame) + + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 50, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) ) - self.panel.saved_connection_change_password_view.pressed.connect( - partial( - self.panel.saved_connection_change_password_field.setEchoMode, - QtWidgets.QLineEdit.EchoMode.Normal, + + # Connect button + self.hidden_network_connect_button = BlocksCustomButton( + parent=self.hidden_network_page + ) + self.hidden_network_connect_button.setMinimumSize(QtCore.QSize(250, 80)) + self.hidden_network_connect_button.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.hidden_network_connect_button.setFont(font) + self.hidden_network_connect_button.setFlat(True) + self.hidden_network_connect_button.setProperty( + "icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") + ) + self.hidden_network_connect_button.setText("Connect") + content_layout.addWidget( + self.hidden_network_connect_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) + + main_layout.addLayout(content_layout) + self.addWidget(self.hidden_network_page) + + # Connect signals + self.hidden_network_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) + ) + self.hidden_network_connect_button.clicked.connect( + self._on_hidden_network_connect + ) + self.hidden_network_ssid_field.clicked.connect( + lambda: self._on_show_keyboard( + self.hidden_network_page, self.hidden_network_ssid_field ) ) - self.panel.saved_connection_change_password_view.released.connect( - partial( - self.panel.saved_connection_change_password_field.setEchoMode, - QtWidgets.QLineEdit.EchoMode.Password, + self.hidden_network_password_field.clicked.connect( + lambda: self._on_show_keyboard( + self.hidden_network_page, self.hidden_network_password_field ) ) - self.panel.hotspot_back_button.clicked.connect( - lambda: self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) + self._setup_password_visibility_toggle( + self.hidden_network_password_view, self.hidden_network_password_field ) - self.panel.hotspot_password_input_field.setPlaceholderText( - "Defaults to: 123456789" + def _on_hidden_network_connect(self) -> None: + """Handle connection to hidden network.""" + ssid = self.hidden_network_ssid_field.text().strip() + password = self.hidden_network_password_field.text() + + if not ssid: + self._show_error_popup("Please enter a network name.") + return + + self._current_network_is_hidden = True + self._current_network_is_open = not password + + result = self._sdbus_network.add_wifi_network(ssid=ssid, psk=password) + + if result is None: + self._handle_failed_network_add("Failed to add network") + return + + error_msg = result.get("error", "") if isinstance(result, dict) else "" + + if not error_msg: + self.hidden_network_ssid_field.clear() + self.hidden_network_password_field.clear() + self._handle_successful_network_add(ssid) + else: + self._handle_failed_network_add(error_msg) + + def _setup_saved_connection_page(self) -> None: + """Setup the saved connection page.""" + self.saved_connection_page = QtWidgets.QWidget() + self.saved_connection_page.setObjectName("saved_connection_page") + + main_layout = QtWidgets.QVBoxLayout(self.saved_connection_page) + main_layout.setObjectName("verticalLayout_11") + + # Header layout + header_layout = QtWidgets.QHBoxLayout() + header_layout.setObjectName("horizontalLayout_7") + + header_layout.addItem( + QtWidgets.QSpacerItem( + 60, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + + self.saved_connection_network_name = QtWidgets.QLabel( + parent=self.saved_connection_page ) - self.panel.hotspot_change_confirm.clicked.connect( - lambda: self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) + name_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, ) + self.saved_connection_network_name.setSizePolicy(name_policy) + self.saved_connection_network_name.setMaximumSize(QtCore.QSize(16777215, 60)) + font = QtGui.QFont() + font.setPointSize(20) + self.saved_connection_network_name.setFont(font) + self.saved_connection_network_name.setStyleSheet("color: rgb(255, 255, 255);") + self.saved_connection_network_name.setText("") + self.saved_connection_network_name.setAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.saved_connection_network_name.setObjectName( + "saved_connection_network_name" + ) + header_layout.addWidget(self.saved_connection_network_name) - self.panel.hotspot_password_input_field.setHidden(True) - self.panel.hotspot_password_view_button.pressed.connect( - partial(self.panel.hotspot_password_input_field.setHidden, False) + self.saved_connection_back_button = IconButton( + parent=self.saved_connection_page + ) + self.saved_connection_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.saved_connection_back_button.setMaximumSize(QtCore.QSize(60, 60)) + self.saved_connection_back_button.setText("Back") + self.saved_connection_back_button.setFlat(True) + self.saved_connection_back_button.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") ) - self.panel.hotspot_password_view_button.released.connect( - partial(self.panel.hotspot_password_input_field.setHidden, True) + self.saved_connection_back_button.setProperty("class", "back_btn") + self.saved_connection_back_button.setProperty("button_type", "icon") + self.saved_connection_back_button.setObjectName("saved_connection_back_button") + header_layout.addWidget( + self.saved_connection_back_button, 0, QtCore.Qt.AlignmentFlag.AlignRight ) - self.panel.hotspot_name_input_field.setText( - str(self.sdbus_network.get_hotspot_ssid()) + + main_layout.addLayout(header_layout) + + # Content layout + content_layout = QtWidgets.QVBoxLayout() + content_layout.setObjectName("verticalLayout_5") + + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) ) - self.panel.hotspot_password_input_field.setText( - str(self.sdbus_network.hotspot_password) + + # Main content horizontal layout + main_content_layout = QtWidgets.QHBoxLayout() + main_content_layout.setObjectName("horizontalLayout_9") + + # Info frame layout + info_layout = QtWidgets.QVBoxLayout() + info_layout.setObjectName("verticalLayout_2") + + self.frame = BlocksCustomFrame(parent=self.saved_connection_page) + frame_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, ) - self.panel.wifi_button.toggle_button.stateChange.connect(self.on_toggle_state) - self.panel.hotspot_button.toggle_button.stateChange.connect( - self.on_toggle_state + self.frame.setSizePolicy(frame_policy) + self.frame.setMaximumSize(QtCore.QSize(400, 16777215)) + self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame.setObjectName("frame") + + frame_inner_layout = QtWidgets.QVBoxLayout(self.frame) + frame_inner_layout.setObjectName("verticalLayout_6") + + # Signal strength row + signal_layout = QtWidgets.QHBoxLayout() + signal_layout.setObjectName("horizontalLayout") + + self.netlist_strength_label_2 = QtWidgets.QLabel(parent=self.frame) + self.netlist_strength_label_2.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + self.netlist_strength_label_2.setFont(font) + self.netlist_strength_label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_strength_label_2.setText("Signal\nStrength") + self.netlist_strength_label_2.setObjectName("netlist_strength_label_2") + signal_layout.addWidget(self.netlist_strength_label_2) + + self.saved_connection_signal_strength_info_frame = QtWidgets.QLabel( + parent=self.frame + ) + self.saved_connection_signal_strength_info_frame.setMinimumSize( + QtCore.QSize(250, 0) + ) + font = QtGui.QFont() + font.setPointSize(11) + self.saved_connection_signal_strength_info_frame.setFont(font) + self.saved_connection_signal_strength_info_frame.setStyleSheet( + "color: rgb(255, 255, 255);" + ) + self.saved_connection_signal_strength_info_frame.setAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.saved_connection_signal_strength_info_frame.setText("TextLabel") + self.saved_connection_signal_strength_info_frame.setObjectName( + "saved_connection_signal_strength_info_frame" + ) + signal_layout.addWidget(self.saved_connection_signal_strength_info_frame) + + frame_inner_layout.addLayout(signal_layout) + + self.line_4 = QtWidgets.QFrame(parent=self.frame) + self.line_4.setFrameShape(QtWidgets.QFrame.Shape.HLine) + self.line_4.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + self.line_4.setObjectName("line_4") + frame_inner_layout.addWidget(self.line_4) + + # Security type row + security_layout = QtWidgets.QHBoxLayout() + security_layout.setObjectName("horizontalLayout_2") + + self.netlist_security_label_2 = QtWidgets.QLabel(parent=self.frame) + self.netlist_security_label_2.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + self.netlist_security_label_2.setFont(font) + self.netlist_security_label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_security_label_2.setText("Security\nType") + self.netlist_security_label_2.setObjectName("netlist_security_label_2") + security_layout.addWidget(self.netlist_security_label_2) + + self.saved_connection_security_type_info_label = QtWidgets.QLabel( + parent=self.frame + ) + self.saved_connection_security_type_info_label.setMinimumSize( + QtCore.QSize(250, 0) + ) + font = QtGui.QFont() + font.setPointSize(11) + self.saved_connection_security_type_info_label.setFont(font) + self.saved_connection_security_type_info_label.setStyleSheet( + "color: rgb(255, 255, 255);" + ) + self.saved_connection_security_type_info_label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.saved_connection_security_type_info_label.setText("TextLabel") + self.saved_connection_security_type_info_label.setObjectName( + "saved_connection_security_type_info_label" + ) + security_layout.addWidget(self.saved_connection_security_type_info_label) + + frame_inner_layout.addLayout(security_layout) + + self.line_5 = QtWidgets.QFrame(parent=self.frame) + self.line_5.setFrameShape(QtWidgets.QFrame.Shape.HLine) + self.line_5.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + self.line_5.setObjectName("line_5") + frame_inner_layout.addWidget(self.line_5) + + # Status row + status_layout = QtWidgets.QHBoxLayout() + status_layout.setObjectName("horizontalLayout_8") + + self.netlist_security_label_4 = QtWidgets.QLabel(parent=self.frame) + self.netlist_security_label_4.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + self.netlist_security_label_4.setFont(font) + self.netlist_security_label_4.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_security_label_4.setText("Status") + self.netlist_security_label_4.setObjectName("netlist_security_label_4") + status_layout.addWidget(self.netlist_security_label_4) + + self.sn_info = QtWidgets.QLabel(parent=self.frame) + self.sn_info.setMinimumSize(QtCore.QSize(250, 0)) + font = QtGui.QFont() + font.setPointSize(11) + self.sn_info.setFont(font) + self.sn_info.setStyleSheet("color: rgb(255, 255, 255);") + self.sn_info.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.sn_info.setText("TextLabel") + self.sn_info.setObjectName("sn_info") + status_layout.addWidget(self.sn_info) + + frame_inner_layout.addLayout(status_layout) + info_layout.addWidget(self.frame) + main_content_layout.addLayout(info_layout) + + # Action buttons frame + self.frame_8 = BlocksCustomFrame(parent=self.saved_connection_page) + self.frame_8.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_8.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame_8.setObjectName("frame_8") + + buttons_layout = QtWidgets.QVBoxLayout(self.frame_8) + buttons_layout.setObjectName("verticalLayout_4") + + self.network_activate_btn = BlocksCustomButton(parent=self.frame_8) + self.network_activate_btn.setMinimumSize(QtCore.QSize(250, 80)) + self.network_activate_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.network_activate_btn.setFont(font) + self.network_activate_btn.setFlat(True) + self.network_activate_btn.setText("Connect") + self.network_activate_btn.setObjectName("network_activate_btn") + buttons_layout.addWidget( + self.network_activate_btn, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + + self.network_details_btn = BlocksCustomButton(parent=self.frame_8) + self.network_details_btn.setMinimumSize(QtCore.QSize(250, 80)) + self.network_details_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.network_details_btn.setFont(font) + self.network_details_btn.setFlat(True) + self.network_details_btn.setText("Details") + self.network_details_btn.setObjectName("network_details_btn") + buttons_layout.addWidget( + self.network_details_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) + + self.network_delete_btn = BlocksCustomButton(parent=self.frame_8) + self.network_delete_btn.setMinimumSize(QtCore.QSize(250, 80)) + self.network_delete_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.network_delete_btn.setFont(font) + self.network_delete_btn.setFlat(True) + self.network_delete_btn.setText("Forget") + self.network_delete_btn.setObjectName("network_delete_btn") + buttons_layout.addWidget( + self.network_delete_btn, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + + main_content_layout.addWidget(self.frame_8) + content_layout.addLayout(main_content_layout) + main_layout.addLayout(content_layout) + + self.addWidget(self.saved_connection_page) + + def _setup_saved_details_page(self) -> None: + """Setup the saved network details page.""" + self.saved_details_page = QtWidgets.QWidget() + self.saved_details_page.setObjectName("saved_details_page") + + main_layout = QtWidgets.QVBoxLayout(self.saved_details_page) + main_layout.setObjectName("verticalLayout_19") + + # Header layout + header_layout = QtWidgets.QHBoxLayout() + header_layout.setObjectName("horizontalLayout_14") + + header_layout.addItem( + QtWidgets.QSpacerItem( + 60, + 60, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) ) - self.panel.saved_connection_change_password_view.pressed.connect( - lambda: self.panel.saved_connection_change_password_view.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/see.svg") + + self.snd_name = QtWidgets.QLabel(parent=self.saved_details_page) + name_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.snd_name.setSizePolicy(name_policy) + self.snd_name.setMaximumSize(QtCore.QSize(16777215, 60)) + self.snd_name.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(20) + self.snd_name.setFont(font) + self.snd_name.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.snd_name.setText("SSID") + self.snd_name.setObjectName("snd_name") + header_layout.addWidget(self.snd_name) + + self.snd_back = IconButton(parent=self.saved_details_page) + self.snd_back.setMinimumSize(QtCore.QSize(60, 60)) + self.snd_back.setMaximumSize(QtCore.QSize(60, 60)) + self.snd_back.setText("Back") + self.snd_back.setFlat(True) + self.snd_back.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + ) + self.snd_back.setProperty("class", "back_btn") + self.snd_back.setProperty("button_type", "icon") + self.snd_back.setObjectName("snd_back") + header_layout.addWidget(self.snd_back) + + main_layout.addLayout(header_layout) + + # Content layout + content_layout = QtWidgets.QVBoxLayout() + content_layout.setObjectName("verticalLayout_8") + + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) ) - self.panel.saved_connection_change_password_view.released.connect( - lambda: self.panel.saved_connection_change_password_view.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") + + # Password change frame + self.frame_9 = BlocksCustomFrame(parent=self.saved_details_page) + frame_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum + ) + self.frame_9.setSizePolicy(frame_policy) + self.frame_9.setMinimumSize(QtCore.QSize(0, 70)) + self.frame_9.setMaximumSize(QtCore.QSize(16777215, 70)) + self.frame_9.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_9.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame_9.setObjectName("frame_9") + + frame_layout_widget = QtWidgets.QWidget(parent=self.frame_9) + frame_layout_widget.setGeometry(QtCore.QRect(0, 0, 776, 62)) + frame_layout_widget.setObjectName("layoutWidget_8") + + password_layout = QtWidgets.QHBoxLayout(frame_layout_widget) + password_layout.setContentsMargins(0, 0, 0, 0) + password_layout.setObjectName("horizontalLayout_10") + + self.saved_connection_change_password_label_3 = QtWidgets.QLabel( + parent=frame_layout_widget + ) + self.saved_connection_change_password_label_3.setPalette( + self._create_white_palette() + ) + font = QtGui.QFont() + font.setPointSize(15) + self.saved_connection_change_password_label_3.setFont(font) + self.saved_connection_change_password_label_3.setAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.saved_connection_change_password_label_3.setText("Change\nPassword") + self.saved_connection_change_password_label_3.setObjectName( + "saved_connection_change_password_label_3" + ) + password_layout.addWidget( + self.saved_connection_change_password_label_3, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + + self.saved_connection_change_password_field = BlocksCustomLinEdit( + parent=frame_layout_widget + ) + self.saved_connection_change_password_field.setHidden(True) + self.saved_connection_change_password_field.setMinimumSize( + QtCore.QSize(500, 60) + ) + self.saved_connection_change_password_field.setMaximumSize( + QtCore.QSize(500, 16777215) + ) + font = QtGui.QFont() + font.setPointSize(12) + self.saved_connection_change_password_field.setFont(font) + self.saved_connection_change_password_field.setObjectName( + "saved_connection_change_password_field" + ) + password_layout.addWidget( + self.saved_connection_change_password_field, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter, + ) + + self.saved_connection_change_password_view = IconButton( + parent=frame_layout_widget + ) + self.saved_connection_change_password_view.setMinimumSize(QtCore.QSize(60, 60)) + self.saved_connection_change_password_view.setMaximumSize(QtCore.QSize(60, 60)) + self.saved_connection_change_password_view.setText("View") + self.saved_connection_change_password_view.setFlat(True) + self.saved_connection_change_password_view.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") + ) + self.saved_connection_change_password_view.setProperty("class", "back_btn") + self.saved_connection_change_password_view.setProperty("button_type", "icon") + self.saved_connection_change_password_view.setObjectName( + "saved_connection_change_password_view" + ) + password_layout.addWidget( + self.saved_connection_change_password_view, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + + content_layout.addWidget(self.frame_9) + + # Priority buttons layout + priority_outer_layout = QtWidgets.QHBoxLayout() + priority_outer_layout.setObjectName("horizontalLayout_13") + + priority_inner_layout = QtWidgets.QVBoxLayout() + priority_inner_layout.setObjectName("verticalLayout_13") + + self.frame_12 = BlocksCustomFrame(parent=self.saved_details_page) + frame_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.frame_12.setSizePolicy(frame_policy) + self.frame_12.setMinimumSize(QtCore.QSize(400, 160)) + self.frame_12.setMaximumSize(QtCore.QSize(400, 99999)) + self.frame_12.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_12.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame_12.setProperty("text", "Network priority") + self.frame_12.setObjectName("frame_12") + + frame_inner_layout = QtWidgets.QVBoxLayout(self.frame_12) + frame_inner_layout.setObjectName("verticalLayout_17") + + frame_inner_layout.addItem( + QtWidgets.QSpacerItem( + 10, + 10, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) ) - self.panel.add_network_password_view.released.connect( - lambda: self.panel.add_network_password_view.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") + + # Priority buttons + buttons_layout = QtWidgets.QHBoxLayout() + buttons_layout.setObjectName("horizontalLayout_4") + + self.priority_btn_group = QtWidgets.QButtonGroup(self) + self.priority_btn_group.setObjectName("priority_btn_group") + + self.low_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) + self.low_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) + self.low_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) + self.low_priority_btn.setCheckable(True) + self.low_priority_btn.setAutoExclusive(True) + self.low_priority_btn.setFlat(True) + self.low_priority_btn.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg") + ) + self.low_priority_btn.setText("Low") + self.low_priority_btn.setProperty("class", "back_btn") + self.low_priority_btn.setProperty("button_type", "icon") + self.low_priority_btn.setObjectName("low_priority_btn") + self.priority_btn_group.addButton(self.low_priority_btn) + buttons_layout.addWidget(self.low_priority_btn) + + self.med_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) + self.med_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) + self.med_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) + self.med_priority_btn.setCheckable(True) + self.med_priority_btn.setChecked(False) # Don't set default checked + self.med_priority_btn.setAutoExclusive(True) + self.med_priority_btn.setFlat(True) + self.med_priority_btn.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg") + ) + self.med_priority_btn.setText("Medium") + self.med_priority_btn.setProperty("class", "back_btn") + self.med_priority_btn.setProperty("button_type", "icon") + self.med_priority_btn.setObjectName("med_priority_btn") + self.priority_btn_group.addButton(self.med_priority_btn) + buttons_layout.addWidget(self.med_priority_btn) + + self.high_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) + self.high_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) + self.high_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) + self.high_priority_btn.setCheckable(True) + self.high_priority_btn.setChecked(False) + self.high_priority_btn.setAutoExclusive(True) + self.high_priority_btn.setFlat(True) + self.high_priority_btn.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg") + ) + self.high_priority_btn.setText("High") + self.high_priority_btn.setProperty("class", "back_btn") + self.high_priority_btn.setProperty("button_type", "icon") + self.high_priority_btn.setObjectName("high_priority_btn") + self.priority_btn_group.addButton(self.high_priority_btn) + buttons_layout.addWidget(self.high_priority_btn) + + frame_inner_layout.addLayout(buttons_layout) + + priority_inner_layout.addWidget( + self.frame_12, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + + priority_outer_layout.addLayout(priority_inner_layout) + content_layout.addLayout(priority_outer_layout) + main_layout.addLayout(content_layout) + + self.addWidget(self.saved_details_page) + + def _setup_hotspot_page(self) -> None: + """Setup the hotspot configuration page.""" + self.hotspot_page = QtWidgets.QWidget() + self.hotspot_page.setObjectName("hotspot_page") + + main_layout = QtWidgets.QVBoxLayout(self.hotspot_page) + main_layout.setObjectName("verticalLayout_12") + + # Header layout + header_layout = QtWidgets.QHBoxLayout() + header_layout.setObjectName("hospot_page_header_layout") + + header_layout.addItem( + QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) ) - self.panel.add_network_password_view.pressed.connect( - lambda: self.panel.add_network_password_view.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/see.svg") + + self.hotspot_header_title = QtWidgets.QLabel(parent=self.hotspot_page) + self.hotspot_header_title.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(20) + self.hotspot_header_title.setFont(font) + self.hotspot_header_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.hotspot_header_title.setText("Hotspot") + self.hotspot_header_title.setObjectName("hotspot_header_title") + header_layout.addWidget(self.hotspot_header_title) + + self.hotspot_back_button = IconButton(parent=self.hotspot_page) + self.hotspot_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.hotspot_back_button.setMaximumSize(QtCore.QSize(60, 60)) + self.hotspot_back_button.setText("Back") + self.hotspot_back_button.setFlat(True) + self.hotspot_back_button.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + ) + self.hotspot_back_button.setProperty("class", "back_btn") + self.hotspot_back_button.setProperty("button_type", "icon") + self.hotspot_back_button.setObjectName("hotspot_back_button") + header_layout.addWidget(self.hotspot_back_button) + + main_layout.addLayout(header_layout) + + # Content layout + content_layout = QtWidgets.QVBoxLayout() + content_layout.setContentsMargins(-1, 5, -1, 5) + content_layout.setObjectName("hotspot_page_content_layout") + + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 50, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) ) - self.panel.hotspot_password_view_button.released.connect( - lambda: self.panel.hotspot_password_view_button.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") + + # Hotspot name frame + self.frame_6 = BlocksCustomFrame(parent=self.hotspot_page) + frame_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.frame_6.setSizePolicy(frame_policy) + self.frame_6.setMinimumSize(QtCore.QSize(70, 80)) + self.frame_6.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_6.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame_6.setObjectName("frame_6") + + frame_layout_widget = QtWidgets.QWidget(parent=self.frame_6) + frame_layout_widget.setGeometry(QtCore.QRect(0, 10, 776, 61)) + frame_layout_widget.setObjectName("layoutWidget_6") + + name_layout = QtWidgets.QHBoxLayout(frame_layout_widget) + name_layout.setContentsMargins(0, 0, 0, 0) + name_layout.setObjectName("horizontalLayout_11") + + self.hotspot_info_name_label = QtWidgets.QLabel(parent=frame_layout_widget) + label_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Maximum + ) + self.hotspot_info_name_label.setSizePolicy(label_policy) + self.hotspot_info_name_label.setMaximumSize(QtCore.QSize(150, 16777215)) + self.hotspot_info_name_label.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setFamily("Momcake") + font.setPointSize(10) + self.hotspot_info_name_label.setFont(font) + self.hotspot_info_name_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.hotspot_info_name_label.setText("Hotspot Name: ") + self.hotspot_info_name_label.setObjectName("hotspot_info_name_label") + name_layout.addWidget(self.hotspot_info_name_label) + + self.hotspot_name_input_field = BlocksCustomLinEdit(parent=frame_layout_widget) + field_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + ) + self.hotspot_name_input_field.setSizePolicy(field_policy) + self.hotspot_name_input_field.setMinimumSize(QtCore.QSize(500, 40)) + self.hotspot_name_input_field.setMaximumSize(QtCore.QSize(500, 60)) + font = QtGui.QFont() + font.setPointSize(12) + self.hotspot_name_input_field.setFont(font) + # Name should be visible, not masked + self.hotspot_name_input_field.setEchoMode(QtWidgets.QLineEdit.EchoMode.Normal) + self.hotspot_name_input_field.setObjectName("hotspot_name_input_field") + name_layout.addWidget( + self.hotspot_name_input_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) + + name_layout.addItem( + QtWidgets.QSpacerItem( + 60, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) ) - self.panel.hotspot_password_view_button.pressed.connect( - lambda: self.panel.hotspot_password_view_button.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/see.svg") + + content_layout.addWidget(self.frame_6) + + content_layout.addItem( + QtWidgets.QSpacerItem( + 773, + 128, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) ) - self.panel.add_network_password_field.setCursor( - QtCore.Qt.CursorShape.BlankCursor + # Hotspot password frame + self.frame_7 = BlocksCustomFrame(parent=self.hotspot_page) + frame_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum + ) + self.frame_7.setSizePolicy(frame_policy) + self.frame_7.setMinimumSize(QtCore.QSize(0, 80)) + self.frame_7.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_7.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame_7.setObjectName("frame_7") + + password_layout_widget = QtWidgets.QWidget(parent=self.frame_7) + password_layout_widget.setGeometry(QtCore.QRect(0, 10, 776, 62)) + password_layout_widget.setObjectName("layoutWidget_7") + + password_layout = QtWidgets.QHBoxLayout(password_layout_widget) + password_layout.setContentsMargins(0, 0, 0, 0) + password_layout.setObjectName("horizontalLayout_12") + + self.hotspot_info_password_label = QtWidgets.QLabel( + parent=password_layout_widget + ) + self.hotspot_info_password_label.setSizePolicy(label_policy) + self.hotspot_info_password_label.setMaximumSize(QtCore.QSize(150, 16777215)) + self.hotspot_info_password_label.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setFamily("Momcake") + font.setPointSize(10) + self.hotspot_info_password_label.setFont(font) + self.hotspot_info_password_label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.hotspot_info_password_label.setText("Hotspot Password:") + self.hotspot_info_password_label.setObjectName("hotspot_info_password_label") + password_layout.addWidget(self.hotspot_info_password_label) + + self.hotspot_password_input_field = BlocksCustomLinEdit( + parent=password_layout_widget + ) + self.hotspot_password_input_field.setHidden(True) + self.hotspot_password_input_field.setSizePolicy(field_policy) + self.hotspot_password_input_field.setMinimumSize(QtCore.QSize(500, 40)) + self.hotspot_password_input_field.setMaximumSize(QtCore.QSize(500, 60)) + font = QtGui.QFont() + font.setPointSize(12) + self.hotspot_password_input_field.setFont(font) + self.hotspot_password_input_field.setEchoMode( + QtWidgets.QLineEdit.EchoMode.Password + ) + self.hotspot_password_input_field.setObjectName("hotspot_password_input_field") + password_layout.addWidget( + self.hotspot_password_input_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) + + self.hotspot_password_view_button = IconButton(parent=password_layout_widget) + self.hotspot_password_view_button.setMinimumSize(QtCore.QSize(60, 60)) + self.hotspot_password_view_button.setMaximumSize(QtCore.QSize(60, 60)) + self.hotspot_password_view_button.setText("View") + self.hotspot_password_view_button.setFlat(True) + self.hotspot_password_view_button.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") + ) + self.hotspot_password_view_button.setProperty("class", "back_btn") + self.hotspot_password_view_button.setProperty("button_type", "icon") + self.hotspot_password_view_button.setObjectName("hotspot_password_view_button") + password_layout.addWidget(self.hotspot_password_view_button) + + content_layout.addWidget(self.frame_7) + + # Save button + self.hotspot_change_confirm = BlocksCustomButton(parent=self.hotspot_page) + self.hotspot_change_confirm.setMinimumSize(QtCore.QSize(200, 80)) + self.hotspot_change_confirm.setMaximumSize(QtCore.QSize(250, 100)) + font = QtGui.QFont() + font.setPointSize(18) + font.setBold(True) + font.setWeight(75) + self.hotspot_change_confirm.setFont(font) + self.hotspot_change_confirm.setProperty( + "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/save.svg") + ) + self.hotspot_change_confirm.setText("Save") + self.hotspot_change_confirm.setObjectName("hotspot_change_confirm") + content_layout.addWidget( + self.hotspot_change_confirm, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) + + main_layout.addLayout(content_layout) + + self.addWidget(self.hotspot_page) + + def _init_timers(self) -> None: + """Initialize all timers.""" + self._status_check_timer = QtCore.QTimer(self) + self._status_check_timer.setInterval(STATUS_CHECK_INTERVAL_MS) + + self._delayed_action_timer = QtCore.QTimer(self) + self._delayed_action_timer.setSingleShot(True) + + self._load_timer = QtCore.QTimer(self) + self._load_timer.setSingleShot(True) + self._load_timer.timeout.connect(self._handle_load_timeout) + + def _init_model_view(self) -> None: + """Initialize the model and view for network list.""" + self._model = EntryListModel() + self._model.setParent(self.listView) + self._entry_delegate = EntryDelegate() + self.listView.setModel(self._model) + self.listView.setItemDelegate(self._entry_delegate) + self._entry_delegate.item_selected.connect(self._on_ssid_item_clicked) + self._configure_list_view_palette() + + def _init_network_worker(self) -> None: + """Initialize the network list worker.""" + self._network_list_worker = BuildNetworkList( + nm=self._sdbus_network, poll_interval_ms=DEFAULT_POLL_INTERVAL_MS ) - self.panel.hotspot_name_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) - self.panel.hotspot_password_input_field.setCursor( - QtCore.Qt.CursorShape.BlankCursor + self._network_list_worker.finished_network_list_build.connect( + self._handle_network_list ) - self.panel.network_delete_btn.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/garbage-icon.svg") + self._network_list_worker.start_polling() + self.rescan_button.clicked.connect(self._network_list_worker.build) + + def _setup_navigation_signals(self) -> None: + """Setup navigation button signals.""" + self.wifi_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) + ) + self.hotspot_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.hotspot_page)) + ) + self.nl_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) ) + self.network_backButton.clicked.connect(self.hide) - self.panel.network_activate_btn.setPixmap( - QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") + self.add_network_page_backButton.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) ) - self.panel.network_details_btn.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/printer_settings.svg") + self.saved_connection_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) + ) + self.snd_back.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.saved_connection_page)) + ) + self.network_details_btn.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.saved_details_page)) ) - self.panel.snd_back.clicked.connect( - lambda: self.setCurrentIndex(self.indexOf(self.panel.saved_connection_page)) + self.hotspot_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) ) - self.panel.network_details_btn.clicked.connect( - lambda: self.setCurrentIndex(self.indexOf(self.panel.saved_details_page)) + self.hotspot_change_confirm.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) ) - self.panel.network_activate_btn.clicked.connect( - lambda: self.saved_wifi_option_selected() + def _setup_action_signals(self) -> None: + """Setup action button signals.""" + self._sdbus_network.nm_state_change.connect(self._evaluate_network_state) + self.request_network_scan.connect(self._rescan_networks) + self.delete_network_signal.connect(self._delete_network) + + self.add_network_validation_button.clicked.connect(self._add_network) + + self.snd_back.clicked.connect(self._on_save_network_settings) + self.network_activate_btn.clicked.connect(self._on_saved_wifi_option_selected) + self.network_delete_btn.clicked.connect(self._on_saved_wifi_option_selected) + + self._status_check_timer.timeout.connect(self._check_connection_status) + + def _setup_toggle_signals(self) -> None: + """Setup toggle button signals.""" + self.wifi_button.toggle_button.stateChange.connect(self._on_toggle_state) + self.hotspot_button.toggle_button.stateChange.connect(self._on_toggle_state) + + def _setup_password_visibility_signals(self) -> None: + """Setup password visibility toggle signals.""" + self._setup_password_visibility_toggle( + self.add_network_password_view, + self.add_network_password_field, + ) + self._setup_password_visibility_toggle( + self.saved_connection_change_password_view, + self.saved_connection_change_password_field, ) - self.panel.network_delete_btn.clicked.connect( - lambda: self.saved_wifi_option_selected() + self._setup_password_visibility_toggle( + self.hotspot_password_view_button, + self.hotspot_password_input_field, ) - self.network_list_worker.build() - self.request_network_scan.emit() - self.hide() - self.info_box_load() + def _setup_password_visibility_toggle( + self, view_button: QtWidgets.QWidget, password_field: QtWidgets.QLineEdit + ) -> None: + """Setup password visibility toggle for a button/field pair.""" + view_button.setCheckable(True) - self.qwerty = CustomQwertyKeyboard(self) - self.addWidget(self.qwerty) - self.qwerty.value_selected.connect(self.on_qwerty_value_selected) - self.qwerty.request_back.connect(self.on_qwerty_go_back) + see_icon = QtGui.QPixmap(":/ui/media/btn_icons/see.svg") + unsee_icon = QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") - self.panel.add_network_password_field.clicked.connect( - lambda: self.on_show_keyboard( - self.panel.add_network_page, self.panel.add_network_password_field + # Connect toggle signal + view_button.toggled.connect( + lambda checked: password_field.setHidden(not checked) + ) + + # Update icon based on toggle state + view_button.toggled.connect( + lambda checked: view_button.setPixmap( + unsee_icon if not checked else see_icon ) ) - self.panel.hotspot_password_input_field.clicked.connect( - lambda: self.on_show_keyboard( - self.panel.hotspot_page, self.panel.hotspot_password_input_field + + def _setup_icons(self) -> None: + """Setup button icons.""" + self.hotspot_button.setPixmap( + QtGui.QPixmap(":/network/media/btn_icons/hotspot.svg") + ) + self.wifi_button.setPixmap( + QtGui.QPixmap(":/network/media/btn_icons/wifi_config.svg") + ) + self.network_delete_btn.setPixmap( + QtGui.QPixmap(":/ui/media/btn_icons/garbage-icon.svg") + ) + self.network_activate_btn.setPixmap( + QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") + ) + self.network_details_btn.setPixmap( + QtGui.QPixmap(":/ui/media/btn_icons/printer_settings.svg") + ) + + def _setup_input_fields(self) -> None: + """Setup input field properties.""" + self.add_network_password_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) + self.hotspot_name_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) + self.hotspot_password_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) + + self.hotspot_password_input_field.setPlaceholderText("Defaults to: 123456789") + self.hotspot_name_input_field.setText( + str(self._sdbus_network.get_hotspot_ssid() or "PrinterHotspot") + ) + self.hotspot_password_input_field.setText( + str(self._sdbus_network.hotspot_password or "123456789") + ) + + def _setup_keyboard(self) -> None: + """Setup the on-screen keyboard.""" + self._qwerty = CustomQwertyKeyboard(self) + self.addWidget(self._qwerty) + self._qwerty.value_selected.connect(self._on_qwerty_value_selected) + self._qwerty.request_back.connect(self._on_qwerty_go_back) + + self.add_network_password_field.clicked.connect( + lambda: self._on_show_keyboard( + self.add_network_page, self.add_network_password_field ) ) - self.panel.hotspot_name_input_field.clicked.connect( - lambda: self.on_show_keyboard( - self.panel.hotspot_page, self.panel.hotspot_name_input_field + self.hotspot_password_input_field.clicked.connect( + lambda: self._on_show_keyboard( + self.hotspot_page, self.hotspot_password_input_field ) ) - self.panel.saved_connection_change_password_field.clicked.connect( - lambda: self.on_show_keyboard( - self.panel.saved_connection_page, - self.panel.saved_connection_change_password_field, + self.hotspot_name_input_field.clicked.connect( + lambda: self._on_show_keyboard( + self.hotspot_page, self.hotspot_name_input_field + ) + ) + self.saved_connection_change_password_field.clicked.connect( + lambda: self._on_show_keyboard( + self.saved_connection_page, + self.saved_connection_change_password_field, ) ) - def saved_wifi_option_selected(self): - """Handle connect/delete network button clicks""" - _sender = self.sender() - self.panel.wifi_button.toggle_button.state = ( - self.panel.wifi_button.toggle_button.State.ON + def _setup_scrollbar_signals(self) -> None: + """Setup scrollbar synchronization signals.""" + self.listView.verticalScrollBar().valueChanged.connect( + self._handle_scrollbar_change ) - self.panel.hotspot_button.toggle_button.state = ( - self.panel.hotspot_button.toggle_button.State.OFF + self.verticalScrollBar.valueChanged.connect(self._handle_scrollbar_change) + self.verticalScrollBar.valueChanged.connect( + lambda value: self.listView.verticalScrollBar().setValue(value) ) + self.verticalScrollBar.show() - if _sender == self.panel.network_delete_btn: - self.sdbus_network.delete_network( - self.panel.saved_connection_network_name.text() - ) - self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) + def _configure_list_view_palette(self) -> None: + """Configure the list view palette for transparency.""" + palette = QtGui.QPalette() - elif _sender == self.panel.network_activate_btn: - self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) - self.sdbus_network.connect_network( - self.panel.saved_connection_network_name.text() - ) - self.info_box_load(True) - - def on_show_keyboard(self, panel: QtWidgets.QWidget, field: QtWidgets.QLineEdit): - """Handle keyboard show""" - self.previousPanel = panel - self.currentField = field - self.qwerty.set_value(field.text()) - self.setCurrentIndex(self.indexOf(self.qwerty)) - - def on_qwerty_go_back(self): - """Hide keyboard""" - self.setCurrentIndex(self.indexOf(self.previousPanel)) - - def on_qwerty_value_selected(self, value: str): - """Handle keyboard value input""" - self.setCurrentIndex(self.indexOf(self.previousPanel)) - if hasattr(self, "currentField") and self.currentField: - self.currentField.setText(value) - - def info_box_load(self, toggle: bool = False) -> None: - """ - Shows or hides the loading screen. - Sets a 30-second timeout to handle loading failures. + for group in [ + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorGroup.Inactive, + QtGui.QPalette.ColorGroup.Disabled, + ]: + transparent = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + transparent.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(group, QtGui.QPalette.ColorRole.Button, transparent) + palette.setBrush(group, QtGui.QPalette.ColorRole.Window, transparent) + + no_brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + no_brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) + palette.setBrush(group, QtGui.QPalette.ColorRole.Base, no_brush) + + highlight = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) + highlight.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(group, QtGui.QPalette.ColorRole.Highlight, highlight) + + link = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) + link.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(group, QtGui.QPalette.ColorRole.Link, link) + + self.listView.setPalette(palette) + + def _show_error_popup(self, message: str, timeout: int = 6000) -> None: + """Show an error popup message.""" + self._popup.raise_() + self._popup.new_message( + message_type=Popup.MessageType.ERROR, + message=message, + timeout=timeout, + userInput=False, + ) + + def _show_info_popup(self, message: str, timeout: int = 4000) -> None: + """Show an info popup message.""" + self._popup.raise_() + self._popup.new_message( + message_type=Popup.MessageType.INFO, + message=message, + timeout=timeout, + userInput=False, + ) + + def _show_warning_popup(self, message: str, timeout: int = 5000) -> None: + """Show a warning popup message.""" + self._popup.raise_() + self._popup.new_message( + message_type=Popup.MessageType.WARNING, + message=message, + timeout=timeout, + userInput=False, + ) + + def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: + """Handle close event.""" + self._stop_all_timers() + self._network_list_worker.stop_polling() + super().closeEvent(event) + + def showEvent(self, event: Optional[QtGui.QShowEvent]) -> None: + """Handle show event.""" + if self._networks: + self._build_model_list() + self._evaluate_network_state() + super().showEvent(event) + + def _stop_all_timers(self) -> None: + """Stop all active timers.""" + timers = [ + self._load_timer, + self._status_check_timer, + self._delayed_action_timer, + ] + for timer in timers: + if timer.isActive(): + timer.stop() + + def _on_show_keyboard( + self, panel: QtWidgets.QWidget, field: QtWidgets.QLineEdit + ) -> None: + """Show the on-screen keyboard for a field.""" + self._previous_panel = panel + self._current_field = field + self._qwerty.set_value(field.text()) + self.setCurrentIndex(self.indexOf(self._qwerty)) + + def _on_qwerty_go_back(self) -> None: + """Handle keyboard back button.""" + if self._previous_panel: + self.setCurrentIndex(self.indexOf(self._previous_panel)) + + def _on_qwerty_value_selected(self, value: str) -> None: + """Handle keyboard value selection.""" + if self._previous_panel: + self.setCurrentIndex(self.indexOf(self._previous_panel)) + if self._current_field: + self._current_field.setText(value) + + def _set_loading_state(self, loading: bool) -> None: + """Set loading state - controls loading widget visibility. + + This method ensures mutual exclusivity between + loading widget, network details, and info box. """ - self._show_loadscreen(toggle) + self.wifi_button.setEnabled(not loading) + self.hotspot_button.setEnabled(not loading) + + if loading: + self._is_connecting = True + # + # Hide ALL other elements first before showing loading + # This prevents the dual panel visibility bug + self._hide_all_info_elements() + # Force UI update to ensure elements are hidden + self.repaint() + # Now show loading + self.loadingwidget.setVisible(True) - self.panel.wifi_button.setEnabled(not toggle) - self.panel.hotspot_button.setEnabled(not toggle) + if self._load_timer.isActive(): + self._load_timer.stop() + self._load_timer.start(LOAD_TIMEOUT_MS) + if not self._status_check_timer.isActive(): + self._status_check_timer.start() + else: + self._is_connecting = False + self._target_ssid = None + # Just hide loading - caller decides what to show next + self.loadingwidget.setVisible(False) - if toggle: if self._load_timer.isActive(): self._load_timer.stop() - self._load_timer.start(30000) + if self._status_check_timer.isActive(): + self._status_check_timer.stop() + + def _show_network_details(self) -> None: + """Show network details panel - HIDES everything else first.""" + # Hide everything else first to prevent dual panel + self.loadingwidget.setVisible(False) + self.mn_info_box.setVisible(False) + # Force UI update + self.repaint() - def _handle_load_timeout(self): - """ - Logic to execute if the loading screen is still visible after 30 seconds.< - """ - wifi_btn = self.panel.wifi_button - hotspot_btn = self.panel.hotspot_button + # Then show only the details + self.netlist_ip.setVisible(True) + self.netlist_ssuid.setVisible(True) + self.mn_info_seperator.setVisible(True) + self.line_2.setVisible(True) + self.netlist_strength.setVisible(True) + self.netlist_strength_label.setVisible(True) + self.line_3.setVisible(True) + self.netlist_security.setVisible(True) + self.netlist_security_label.setVisible(True) + + def _show_disconnected_message(self) -> None: + """Show the disconnected state message - HIDES everything else first.""" + # Hide everything else first to prevent dual panel + self.loadingwidget.setVisible(False) + self._hide_network_detail_labels() + # Force UI update + self.repaint() + + # Then show info box + self._configure_info_box_centered() + self.mn_info_box.setVisible(True) + self.mn_info_box.setText( + "Network connection required.\n\nConnect to Wi-Fi\nor\nTurn on Hotspot" + ) + + def _hide_network_detail_labels(self) -> None: + """Hide only the network detail labels (not loading or info box).""" + self.netlist_ip.setVisible(False) + self.netlist_ssuid.setVisible(False) + self.mn_info_seperator.setVisible(False) + self.line_2.setVisible(False) + self.netlist_strength.setVisible(False) + self.netlist_strength_label.setVisible(False) + self.line_3.setVisible(False) + self.netlist_security.setVisible(False) + self.netlist_security_label.setVisible(False) + + def _check_connection_status(self) -> None: + """Backup periodic check to detect successful connections.""" + if not self.loadingwidget.isVisible(): + if self._status_check_timer.isActive(): + self._status_check_timer.stop() + return + + connectivity = self._sdbus_network.check_connectivity() + is_connected = connectivity in ("FULL", "LIMITED") - if self.panel.loadingwidget.isVisible(): - if wifi_btn.toggle_button.state == wifi_btn.toggle_button.State.ON: - message = "Wi-Fi Connection Failed.\nThe connection attempt \ntimed out.\n Please check\nyour network stability \nor\n try reconnecting." + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button - elif hotspot_btn.toggle_button.state == hotspot_btn.toggle_button.State.ON: - message = "Hotspot Setup Failed.\nThe local network sharing\n timed out.\n\n restart the hotspot." + if hotspot_btn.state == hotspot_btn.State.ON: + hotspot_ip = self._sdbus_network.get_device_ip_by_interface("wlan0") + if hotspot_ip: + logger.debug("Hotspot connection detected via status check") + # Stop loading first, then show details + self._set_loading_state(False) + self._update_hotspot_display() + self._show_network_details() + return + + if wifi_btn.state == wifi_btn.State.ON: + current_ssid = self._sdbus_network.get_current_ssid() + + if self._target_ssid: + if current_ssid == self._target_ssid and is_connected: + logger.debug("Target Wi-Fi connection detected: %s", current_ssid) + # Stop loading first, then show details + self._set_loading_state(False) + self._update_wifi_display() + self._show_network_details() + return else: - message = "Loading timed out.\n Please check your connection \n and try again." + if current_ssid and is_connected: + logger.debug("Wi-Fi connection detected: %s", current_ssid) + # Stop loading first, then show details + self._set_loading_state(False) + self._update_wifi_display() + self._show_network_details() + return + + def _handle_load_timeout(self) -> None: + """Handle connection timeout.""" + if not self.loadingwidget.isVisible(): + return + + connectivity = self._sdbus_network.check_connectivity() + is_connected = connectivity in ("FULL", "LIMITED") - self.panel.mn_info_box.setText(message) - self._show_loadscreen(False) - self._expand_infobox(True) + wifi_btn = self.wifi_button + hotspot_btn = self.hotspot_button + + # Final check if connection succeeded + if wifi_btn.toggle_button.state == wifi_btn.toggle_button.State.ON: + current_ssid = self._sdbus_network.get_current_ssid() + + if self._target_ssid: + if current_ssid == self._target_ssid and is_connected: + logger.debug("Target connection succeeded on timeout check") + self._set_loading_state(False) + self._update_wifi_display() + self._show_network_details() + return + else: + if current_ssid and is_connected: + logger.debug("Connection succeeded on timeout check") + self._set_loading_state(False) + self._update_wifi_display() + self._show_network_details() + return + + elif hotspot_btn.toggle_button.state == hotspot_btn.toggle_button.State.ON: + hotspot_ip = self._sdbus_network.get_device_ip_by_interface("wlan0") + if hotspot_ip: + logger.debug("Hotspot succeeded on timeout check") + self._set_loading_state(False) + self._update_hotspot_display() + self._show_network_details() + return + + # Connection actually failed + self._is_connecting = False + self._target_ssid = None + self._set_loading_state(False) + + # Show error message + self._hide_all_info_elements() + self._configure_info_box_centered() + self.mn_info_box.setVisible(True) + self.mn_info_box.setText(self._get_timeout_message(wifi_btn, hotspot_btn)) hotspot_btn.setEnabled(True) wifi_btn.setEnabled(True) - def _show_loadscreen(self, toggle: bool = False): - """Expand LOAD box on the main network panel - - Args: - toggle (bool, optional): show or not (Defaults to False) - """ - self.panel.netlist_ip.setVisible(not toggle) - self.panel.netlist_ssuid.setVisible(not toggle) - self.panel.mn_info_seperator.setVisible(not toggle) - self.panel.line_2.setVisible(not toggle) - self.panel.netlist_strength.setVisible(not toggle) - self.panel.netlist_strength_label.setVisible(not toggle) + self._show_error_popup("Connection timed out. Please try again.") - self.panel.line_3.setVisible(not toggle) - self.panel.netlist_security.setVisible(not toggle) - self.panel.netlist_security_label.setVisible(not toggle) + def _get_timeout_message(self, wifi_btn, hotspot_btn) -> str: + """Get appropriate timeout message based on state.""" + if wifi_btn.toggle_button.state == wifi_btn.toggle_button.State.ON: + return "Wi-Fi Connection Failed.\nThe connection attempt\n timed out." + elif hotspot_btn.toggle_button.state == hotspot_btn.toggle_button.State.ON: + return "Hotspot Setup Failed.\nPlease restart the hotspot." + else: + return "Loading timed out.\nPlease check your connection\n and try again." - self.panel.mn_info_box.setVisible(not toggle) + def _configure_info_box_centered(self) -> None: + """Configure info box for centered text.""" + self.mn_info_box.setWordWrap(True) + self.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.panel.loadingwidget.setVisible(toggle) + def _clear_network_display(self) -> None: + """Clear all network display labels.""" + self.netlist_ssuid.setText("") + self.netlist_ip.setText("") + self.netlist_strength.setText("") + self.netlist_security.setText("") + self._last_displayed_ssid = None @QtCore.pyqtSlot(object, name="stateChange") - def on_toggle_state(self, new_state) -> None: - """Handle toggle button changes""" + def _on_toggle_state(self, new_state) -> None: + """Handle toggle button state change.""" sender_button = self.sender() - wifi_btn = self.panel.wifi_button.toggle_button - hotspot_btn = self.panel.hotspot_button.toggle_button + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button is_sender_now_on = new_state == sender_button.State.ON - _old_hotspot = None - saved_network = self.sdbus_network.get_saved_networks() + # Show loading IMMEDIATELY when turning something on + if is_sender_now_on: + self._set_loading_state(True) + self.repaint() - if sender_button is wifi_btn: - if is_sender_now_on: - hotspot_btn.state = hotspot_btn.State.OFF - self.sdbus_network.toggle_hotspot(False) - if saved_network: - try: - ssid = next( - ( - n["ssid"] - for n in saved_network - if "ap" not in n["mode"] and n["signal"] != 0 - ), - None, - ) - self.sdbus_network.connect_network(str(ssid)) - - except Exception as e: - logger.error(f"error when turning ON wifi on_toggle_state:{e}") + saved_networks = self._sdbus_network.get_saved_networks_with_for() + if sender_button is wifi_btn: + self._handle_wifi_toggle(is_sender_now_on, hotspot_btn, saved_networks) elif sender_button is hotspot_btn: - if is_sender_now_on: - wifi_btn.state = wifi_btn.State.OFF - - for n in saved_network: - if "ap" in n.get("mode", ""): - _old_hotspot = n - break - - if ( - _old_hotspot - and _old_hotspot["ssid"] - != self.panel.hotspot_name_input_field.text() - ): - self.sdbus_network.delete_network(_old_hotspot["ssid"]) - - self.sdbus_network.create_hotspot( - self.panel.hotspot_name_input_field.text(), - self.panel.hotspot_password_input_field.text(), - ) - self.sdbus_network.toggle_hotspot(True) - self.sdbus_network.connect_network( - self.panel.hotspot_name_input_field.text() - ) + self._handle_hotspot_toggle(is_sender_now_on, wifi_btn, saved_networks) - self.info_box_load(False) + # Handle both OFF if ( hotspot_btn.state == hotspot_btn.State.OFF and wifi_btn.state == wifi_btn.State.OFF ): - self.evaluate_network_state() - else: - self.info_box_load(True) + self._set_loading_state(False) + self._show_disconnected_message() - @QtCore.pyqtSlot(str, name="nm-state-changed") - def evaluate_network_state(self, nm_state: str = "") -> None: - """Handles or Reloads network state + def _handle_wifi_toggle( + self, is_on: bool, hotspot_btn, saved_networks: List[Dict] + ) -> None: + """Handle Wi-Fi toggle state change.""" + if not is_on: + self._target_ssid = None + return - Args: - nm_state (str, optional): Handles Network state depending on state - """ - # NM State Constants: UNKNOWN=0, ASLEEP=10, DISCONNECTED=20, DISCONNECTING=30, - # CONNECTING=40, CONNECTED_LOCAL=50, CONNECTED_SITE=60, GLOBAL=70 + hotspot_btn.state = hotspot_btn.State.OFF + self._sdbus_network.toggle_hotspot(False) - wifi_btn = self.panel.wifi_button.toggle_button - hotspot_btn = self.panel.hotspot_button.toggle_button - _nm_state = nm_state + # Check if already connected + current_ssid = self._sdbus_network.get_current_ssid() + connectivity = self._sdbus_network.check_connectivity() - if not _nm_state: - _nm_state = self.sdbus_network.check_nm_state() - if not _nm_state: - return + if current_ssid and connectivity == "FULL": + # Already connected - show immediately + self._target_ssid = current_ssid + self._set_loading_state(False) + self._update_wifi_display() + self._show_network_details() + return - if self.start: - self.start = False - saved_network = self.sdbus_network.get_saved_networks() - for n in saved_network: - if "ap" in n.get("mode", ""): - _old_hotspot = n - break - if _old_hotspot: - self.panel.hotspot_name_input_field.setText(_old_hotspot["ssid"]) - - connection = self.sdbus_network.check_connectivity() - if connection == "FULL": - self.panel.wifi_button.toggle_button.state = ( - self.panel.wifi_button.toggle_button.State.ON - ) - self.panel.hotspot_button.toggle_button.state = ( - self.panel.hotspot_button.toggle_button.State.OFF - ) - if connection == "LIMITED": - self.panel.wifi_button.toggle_button.state = ( - self.panel.wifi_button.toggle_button.State.OFF - ) - self.panel.hotspot_button.toggle_button.state = ( - self.panel.hotspot_button.toggle_button.State.ON - ) + # Filter wifi networks (not hotspots) + wifi_networks = [ + n for n in saved_networks if "ap" not in str(n.get("mode", "")) + ] - if not self.sdbus_network.check_wifi_interface(): + if not wifi_networks: + self._set_loading_state(False) + self._show_warning_popup( + "No saved Wi-Fi networks. Please add a network first." + ) + self._show_disconnected_message() return - if hotspot_btn.state == hotspot_btn.State.ON: - ipv4_addr = self.get_hotspot_ip_via_shell() + try: + ssid = wifi_networks[0]["ssid"] + self._target_ssid = ssid + self._sdbus_network.connect_network(str(ssid)) + except Exception as e: + logger.error("Error when turning ON wifi: %s", e) + self._set_loading_state(False) + self._show_error_popup("Failed to connect to Wi-Fi") + + def _handle_hotspot_toggle( + self, is_on: bool, wifi_btn, saved_networks: List[Dict] + ) -> None: + """Handle hotspot toggle state change.""" + if not is_on: + self._target_ssid = None + return - self.panel.netlist_ssuid.setText(self.panel.hotspot_name_input_field.text()) + wifi_btn.state = wifi_btn.State.OFF + self._target_ssid = None - self.panel.netlist_ip.setText(f"IP: {ipv4_addr or 'No IP Address'}") + new_hotspot_name = self.hotspot_name_input_field.text() or "PrinterHotspot" + new_hotspot_password = self.hotspot_password_input_field.text() or "123456789" - self.panel.netlist_strength.setText("--") + # Use QTimer to defer async operations + def setup_hotspot(): + try: + self._sdbus_network.create_hotspot( + new_hotspot_name, new_hotspot_password + ) + self._sdbus_network.toggle_hotspot(True) + except Exception as e: + logger.error("Error creating/activating hotspot: %s", e) + self._show_error_popup("Failed to start hotspot") + self._set_loading_state(False) - self.panel.netlist_security.setText("--") + QtCore.QTimer.singleShot(100, setup_hotspot) - self.panel.mn_info_box.setText("Hotspot On") + @QtCore.pyqtSlot(str, name="nm-state-changed") + def _evaluate_network_state(self, nm_state: str = "") -> None: + """Evaluate and update network state.""" + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button - if wifi_btn.state == wifi_btn.State.ON: - ipv4_addr = self.sdbus_network.get_current_ip_addr() - current_ssid = self.sdbus_network.get_current_ssid() - if current_ssid == "": - return - sec_type = self.sdbus_network.get_security_type_by_ssid(current_ssid) - signal_strength = self.sdbus_network.get_connection_signal_by_ssid( - current_ssid - ) - self.panel.netlist_ssuid.setText(current_ssid) - self.panel.netlist_ip.setText(f"IP: {ipv4_addr or 'No IP Address'}") - self.panel.netlist_security.setText(str(sec_type or "--").upper()) - self.panel.netlist_strength.setText( - str(signal_strength if signal_strength != -1 else "--") - ) - self.panel.mn_info_box.setText("Connected") + state = nm_state or self._sdbus_network.check_nm_state() + if not state: + return - self._expand_infobox(False) - self.info_box_load(False) - self.panel.wifi_button.setEnabled(True) - self.panel.hotspot_button.setEnabled(True) - self.repaint() + if self._is_first_run: + self._handle_first_run_state() + self._is_first_run = False + return + if not self._sdbus_network.check_wifi_interface(): + return + + # Handle both OFF first if ( wifi_btn.state == wifi_btn.State.OFF and hotspot_btn.state == hotspot_btn.State.OFF ): - self.sdbus_network.disconnect_network() - self._expand_infobox(True) - self.panel.mn_info_box.setText( - "Network connection required.\n\nConnect to Wi-Fi\nor\nTurn on Hotspot" - ) + self._sdbus_network.disconnect_network() + self._clear_network_display() + self._set_loading_state(False) + self._show_disconnected_message() + return - def get_hotspot_ip_via_shell(self): - """ - Executes a shell command to retrieve the IPv4 address for a specified interface. + connectivity = self._sdbus_network.check_connectivity() + is_connected = connectivity in ("FULL", "LIMITED") - Returns: - The IP address string (e.g., '10.42.0.1') or None if not found. - """ - command = ["ip", "-4", "addr", "show", "wlan0"] - try: - result = subprocess.run( # nosec: B603 - command, - capture_output=True, - text=True, - check=True, - timeout=5, - ) - except subprocess.CalledProcessError as e: - logging.error( - "Caught exception (exit code %d) failed to run command: %s \nStderr: %s", - e.returncode, - command, - e.stderr.strip(), - ) - return "" - except FileNotFoundError: - logging.error("Command not found") - return "" - except subprocess.TimeoutExpired as e: - logging.error("Caught exception, failed to run command %s", e) - return "" - - for line in result.stdout.splitlines(): - line = line.strip() - if line.startswith("inet "): - ip_address = line.split()[1].split("/")[0] - return ip_address - logging.error("No IPv4 address found in output for wlan0") - return "" + # Handle hotspot + if hotspot_btn.state == hotspot_btn.State.ON: + hotspot_ip = self._sdbus_network.get_device_ip_by_interface("wlan0") + if hotspot_ip or is_connected: + # Stop loading first, then update display, then show details + self._set_loading_state(False) + self._update_hotspot_display() + self._show_network_details() + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + return - def close(self) -> bool: - """Close class, close network module""" - self.sdbus_network.close() - return super().close() + # Handle wifi + if wifi_btn.state == wifi_btn.State.ON: + current_ssid = self._sdbus_network.get_current_ssid() + + if self._target_ssid: + if current_ssid == self._target_ssid and is_connected: + logger.debug("Connected to target: %s", current_ssid) + # Stop loading first, then update display, then show details + self._set_loading_state(False) + self._update_wifi_display() + self._show_network_details() + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + else: + if current_ssid and is_connected: + # Stop loading first, then update display, then show details + self._set_loading_state(False) + self._update_wifi_display() + self._show_network_details() + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + self.update() - def _expand_infobox(self, toggle: bool = False) -> None: - """Expand information box on the main network panel + def _handle_first_run_state(self) -> None: + """Handle initial state on first run.""" + saved_networks = self._sdbus_network.get_saved_networks_with_for() - Args: - toggle (bool, optional): Expand or not (Defaults to False) - """ - self.panel.netlist_ip.setVisible(not toggle) - self.panel.netlist_ssuid.setVisible(not toggle) - self.panel.mn_info_seperator.setVisible(not toggle) - self.panel.line_2.setVisible(not toggle) - self.panel.netlist_strength.setVisible(not toggle) - self.panel.netlist_strength_label.setVisible(not toggle) - - self.panel.line_3.setVisible(not toggle) - self.panel.netlist_security.setVisible(not toggle) - self.panel.netlist_security_label.setVisible(not toggle) - # Align text - self.panel.mn_info_box.setWordWrap(True) - self.panel.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + old_hotspot = next( + (n for n in saved_networks if "ap" in str(n.get("mode", ""))), None + ) + if old_hotspot: + self.hotspot_name_input_field.setText(old_hotspot["ssid"]) - @QtCore.pyqtSlot(str, name="delete-network") - def delete_network(self, ssid: str) -> None: - """Delete network""" - self.sdbus_network.delete_network(ssid=ssid) + connectivity = self._sdbus_network.check_connectivity() + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + current_ssid = self._sdbus_network.get_current_ssid() - @QtCore.pyqtSlot(name="rescan-networks") - def rescan_networks(self) -> None: - """Rescan for networks""" - self.sdbus_network.rescan_networks() + self._is_connecting = False + self.loadingwidget.setVisible(False) - @QtCore.pyqtSlot(name="handle-hotspot-back") - def handle_hotspot_back(self) -> None: - """Handle go back a page from hotspot page""" - if ( - self.panel.hotspot_password_input_field.text() - != self.sdbus_network.hotspot_password - ): - self.panel.hotspot_password_input_field.setText( - self.sdbus_network.hotspot_password + with QtCore.QSignalBlocker(wifi_btn), QtCore.QSignalBlocker(hotspot_btn): + if connectivity == "FULL" and current_ssid: + wifi_btn.state = wifi_btn.State.ON + hotspot_btn.state = hotspot_btn.State.OFF + self._update_wifi_display() + self._show_network_details() + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + elif connectivity == "LIMITED": + wifi_btn.state = wifi_btn.State.OFF + hotspot_btn.state = hotspot_btn.State.ON + self._update_hotspot_display() + self._show_network_details() + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + else: + wifi_btn.state = wifi_btn.State.OFF + hotspot_btn.state = hotspot_btn.State.OFF + self._clear_network_display() + self._show_disconnected_message() + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + + def _update_hotspot_display(self) -> None: + """Update display for hotspot mode.""" + ipv4_addr = self._sdbus_network.get_device_ip_by_interface("wlan0") + if not ipv4_addr: + ipv4_addr = self._sdbus_network.get_current_ip_addr() + + hotspot_name = self.hotspot_name_input_field.text() + if not hotspot_name: + hotspot_name = self._sdbus_network.hotspot_ssid or "Hotspot" + self.hotspot_name_input_field.setText(hotspot_name) + + self.netlist_ssuid.setText(hotspot_name) + # Handle empty IP properly + if ipv4_addr and ipv4_addr.strip(): + self.netlist_ip.setText(f"IP: {ipv4_addr}") + else: + self.netlist_ip.setText("IP: Obtaining...") + self.netlist_strength.setText("--") + self.netlist_security.setText("WPA2") + self._last_displayed_ssid = hotspot_name + + def _update_wifi_display(self) -> None: + """Update display for wifi connection.""" + current_ssid = self._sdbus_network.get_current_ssid() + + if current_ssid: + ipv4_addr = self._sdbus_network.get_current_ip_addr() + sec_type = self._sdbus_network.get_security_type_by_ssid(current_ssid) + signal_strength = self._sdbus_network.get_connection_signal_by_ssid( + current_ssid ) - if ( - self.panel.hotspot_name_input_field.text() - != self.sdbus_network.hotspot_ssid - ): - self.panel.hotspot_name_input_field.setText(self.sdbus_network.hotspot_ssid) - self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) - - @QtCore.pyqtSlot(name="add_network") - def add_network(self) -> None: - """Slot for adding a new network + self.netlist_ssuid.setText(current_ssid) + # Handle empty IP properly + if ipv4_addr and ipv4_addr.strip(): + self.netlist_ip.setText(f"IP: {ipv4_addr}") + else: + self.netlist_ip.setText("IP: Obtaining...") + self.netlist_security.setText(str(sec_type or "OPEN").upper()) + self.netlist_strength.setText( + f"{signal_strength}%" + if signal_strength and signal_strength != -1 + else "--" + ) + self._last_displayed_ssid = current_ssid + else: + self._clear_network_display() - Emitted Signals: - - add_network_confirmation(pyqtSignal): Signal with a dict that contains the result of adding a new network to the machine. + @QtCore.pyqtSlot(str, name="delete-network") + def _delete_network(self, ssid: str) -> None: + """Delete a network.""" + try: + self._sdbus_network.delete_network(ssid=ssid) + except Exception as e: + logger.error("Failed to delete network %s: %s", ssid, e) + self._show_error_popup("Failed to delete network") - """ - # Check if a password was inserted + @QtCore.pyqtSlot(name="rescan-networks") + def _rescan_networks(self) -> None: + """Trigger network rescan.""" + self._sdbus_network.rescan_networks() + + @QtCore.pyqtSlot(name="add-network") + def _add_network(self) -> None: + """Add a new network.""" + self.add_network_validation_button.setEnabled(False) + self.add_network_validation_button.update() + + password = self.add_network_password_field.text() + ssid = self.add_network_network_label.text() + + if not password and not self._current_network_is_open: + self._show_error_popup("Password field cannot be empty.") + self.add_network_validation_button.setEnabled(True) + return - self.panel.add_network_validation_button.setEnabled(False) - self.panel.add_network_validation_button.repaint() + result = self._sdbus_network.add_wifi_network(ssid=ssid, psk=password) + self.add_network_password_field.clear() - if not self.panel.add_network_password_field.text(): - self.popup.new_message( - message_type=Popup.MessageType.ERROR, - message="Password field cannot be empty.", - ) + if result is None: + self._handle_failed_network_add("Failed to add network") return - _network_psk = self.panel.add_network_password_field.text() - result = self.sdbus_network.add_wifi_network( - ssid=self.panel.add_network_network_label.text(), psk=_network_psk - ) + error_msg = result.get("error", "") if isinstance(result, dict) else "" - error_msg = result.get("error", "") - self.panel.add_network_password_field.clear() if not error_msg: - # Assume it was a success - QtCore.QTimer().singleShot(5000, self.network_list_worker.build) - QtCore.QTimer().singleShot( - 5000, - lambda: self.sdbus_network.connect_network( - self.panel.add_network_network_label.text() - ), + self._handle_successful_network_add(ssid) + else: + self._handle_failed_network_add(error_msg) + + def _handle_successful_network_add(self, ssid: str) -> None: + """Handle successful network addition.""" + self._target_ssid = ssid + self._set_loading_state(True) + self.setCurrentIndex(self.indexOf(self.main_network_page)) + + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn), QtCore.QSignalBlocker(hotspot_btn): + wifi_btn.state = wifi_btn.State.ON + hotspot_btn.state = hotspot_btn.State.OFF + + self._schedule_delayed_action( + self._network_list_worker.build, NETWORK_CONNECT_DELAY_MS + ) + + def connect_and_refresh(): + try: + self._sdbus_network.connect_network(ssid) + except Exception as e: + logger.error("Failed to connect to %s: %s", ssid, e) + self._show_error_popup(f"Failed to connect to {ssid}") + self._set_loading_state(False) + + QtCore.QTimer.singleShot(NETWORK_CONNECT_DELAY_MS, connect_and_refresh) + + self.add_network_validation_button.setEnabled(True) + self.wifi_button.setEnabled(False) + self.hotspot_button.setEnabled(False) + self.add_network_validation_button.update() + + def _handle_failed_network_add(self, error_msg: str) -> None: + """Handle failed network addition.""" + logging.error(error_msg) + error_messages = { + "Invalid password": "Invalid password. Please try again", + "Network connection properties error": ( + "Network connection properties error. Please try again" + ), + "Permission Denied": "Permission Denied. Please try again", + } + + message = error_messages.get( + error_msg, "Error while adding network. Please try again" + ) + + self.add_network_validation_button.setEnabled(True) + self.add_network_validation_button.update() + self._show_error_popup(message) + + def _on_save_network_settings(self) -> None: + """Save network settings.""" + self._update_network( + ssid=self.saved_connection_network_name.text(), + password=self.saved_connection_change_password_field.text(), + new_ssid=None, + ) + + def _update_network( + self, + ssid: str, + password: Optional[str], + new_ssid: Optional[str], + ) -> None: + """Update network settings.""" + if not self._sdbus_network.is_known(ssid): + return + + priority = self._get_selected_priority() + + try: + self._sdbus_network.update_connection_settings( + ssid=ssid, password=password, new_ssid=new_ssid, priority=priority ) - self.info_box_load(True) - self.setCurrentIndex(self.indexOf(self.panel.main_network_page)) - self.panel.add_network_validation_button.setEnabled(True) + except Exception as e: + logger.error("Failed to update network settings: %s", e) + self._show_error_popup("Failed to update network settings") - self.panel.wifi_button.setEnabled(False) - self.panel.hotspot_button.setEnabled(False) + self.setCurrentIndex(self.indexOf(self.network_list_page)) - self.panel.add_network_validation_button.repaint() - return + def _get_selected_priority(self) -> int: + """Get selected priority from radio buttons.""" + checked_btn = self.priority_btn_group.checkedButton() - if error_msg == "Invalid password": - message = "Invalid password. Please try again" - elif error_msg == "Network connection properties error": - message = "Network connection properties error. Please try again" - elif error_msg == "Permission Denied": - message = "Permission Denied. Please try again" + if checked_btn == self.high_priority_btn: + return PRIORITY_HIGH + elif checked_btn == self.low_priority_btn: + return PRIORITY_LOW else: - message = "Error while adding network. Please try again" - self.panel.add_network_validation_button.setEnabled(True) - self.panel.add_network_validation_button.repaint() - self.popup.new_message(message_type=Popup.MessageType.ERROR, message=message) + return PRIORITY_MEDIUM - @QtCore.pyqtSlot(QtWidgets.QListWidgetItem, name="ssid_item_clicked") - def ssid_item_clicked(self, item: QtWidgets.QListWidgetItem) -> None: - """Handles when a network is clicked on the QListWidget. + def _on_saved_wifi_option_selected(self) -> None: + """Handle saved wifi option selection.""" + sender = self.sender() - Args: - item (QListWidgetItem): The list entry that was clicked - """ - _current_item: QtWidgets.QWidget = ( - self.panel.network_list_widget.itemWidget(item) # type: ignore - ) - if _current_item: - _current_ssid_name = _current_item.findChild(QtWidgets.QLabel).text() - - if ( - _current_ssid_name in self.sdbus_network.get_saved_ssid_names() - ): # Network already saved go to the information page - self.setCurrentIndex(self.indexOf(self.panel.saved_connection_page)) - self.panel.saved_connection_network_name.setText( - str(_current_ssid_name) - ) - else: # Network not saved go to the add network page - self.setCurrentIndex(self.indexOf(self.panel.add_network_page)) - self.panel.add_network_network_label.setText( - str(_current_ssid_name) - ) # Add the network name to the title + wifi_toggle = self.wifi_button.toggle_button + hotspot_toggle = self.hotspot_button.toggle_button + + with QtCore.QSignalBlocker(wifi_toggle), QtCore.QSignalBlocker(hotspot_toggle): + wifi_toggle.state = wifi_toggle.State.ON + hotspot_toggle.state = hotspot_toggle.State.OFF - def update_network( + ssid = self.saved_connection_network_name.text() + + if sender == self.network_delete_btn: + self._handle_network_delete(ssid) + elif sender == self.network_activate_btn: + self._handle_network_activate(ssid) + + def _handle_network_delete(self, ssid: str) -> None: + """Handle network deletion.""" + try: + self._sdbus_network.delete_network(ssid) + if ssid in self._networks: + del self._networks[ssid] + self.setCurrentIndex(self.indexOf(self.network_list_page)) + self._build_model_list() + self._network_list_worker.build() + self._show_info_popup(f"Network '{ssid}' deleted") + except Exception as e: + logger.error("Failed to delete network %s: %s", ssid, e) + self._show_error_popup("Failed to delete network") + + def _handle_network_activate(self, ssid: str) -> None: + """Handle network activation.""" + self._target_ssid = ssid + # Show loading IMMEDIATELY + self._set_loading_state(True) + self.repaint() + + self.setCurrentIndex(self.indexOf(self.main_network_page)) + + try: + self._sdbus_network.connect_network(ssid) + except Exception as e: + logger.error("Failed to connect to %s: %s", ssid, e) + self._set_loading_state(False) + self._show_disconnected_message() + self._show_error_popup("Failed to connect to network") + + @QtCore.pyqtSlot(list, name="finished-network-list-build") + def _handle_network_list(self, data: List[tuple]) -> None: + """Handle network list build completion.""" + self._networks.clear() + hotspot_ssid = self._sdbus_network.hotspot_ssid + + for entry in data: + # Handle different tuple lengths + if len(entry) >= 6: + ssid, signal, status, is_open, is_saved, is_hidden = entry + elif len(entry) >= 5: + ssid, signal, status, is_open, is_saved = entry + is_hidden = self._is_hidden_ssid(ssid) + elif len(entry) >= 4: + ssid, signal, status, is_open = entry + is_saved = status in ("Active", "Saved") + is_hidden = self._is_hidden_ssid(ssid) + else: + ssid, signal, status = entry[0], entry[1], entry[2] + is_open = status == "Open" + is_saved = status in ("Active", "Saved") + is_hidden = self._is_hidden_ssid(ssid) + + if ssid == hotspot_ssid: + continue + + self._networks[ssid] = NetworkInfo( + signal=signal, + status=status, + is_open=is_open, + is_saved=is_saved, + is_hidden=is_hidden, + ) + + self._build_model_list() + + # Update main panel if connected + if self._last_displayed_ssid and self._last_displayed_ssid in self._networks: + network_info = self._networks[self._last_displayed_ssid] + self.netlist_strength.setText( + f"{network_info.signal}%" if network_info.signal != -1 else "--" + ) + + def _is_hidden_ssid(self, ssid: str) -> bool: + """Check if an SSID indicates a hidden network.""" + if ssid is None: + return True + ssid_stripped = ssid.strip() + ssid_lower = ssid_stripped.lower() + # Check for empty, unknown, or hidden indicators + return ( + ssid_stripped == "" + or ssid_lower == "unknown" + or ssid_lower == "" + or ssid_lower == "hidden" + or not ssid_stripped + ) + + def _build_model_list(self) -> None: + """Build the network list model.""" + self.listView.blockSignals(True) + self._reset_view_model() + + saved_networks = [] + unsaved_networks = [] + + for ssid, info in self._networks.items(): + if info.is_saved: + saved_networks.append((ssid, info)) + else: + unsaved_networks.append((ssid, info)) + + saved_networks.sort(key=lambda x: -x[1].signal) + unsaved_networks.sort(key=lambda x: -x[1].signal) + + for ssid, info in saved_networks: + self._add_network_entry( + ssid=ssid, + signal=info.signal, + status=info.status, + is_open=info.is_open, + is_hidden=info.is_hidden, + ) + + if saved_networks and unsaved_networks: + self._add_separator_entry() + + for ssid, info in unsaved_networks: + self._add_network_entry( + ssid=ssid, + signal=info.signal, + status=info.status, + is_open=info.is_open, + is_hidden=info.is_hidden, + ) + + # Add "Connect to Hidden Network" entry at the end + self._add_hidden_network_entry() + + self._sync_scrollbar() + self.listView.blockSignals(False) + self.listView.update() + + def _reset_view_model(self) -> None: + """Reset the view model.""" + self._model.clear() + self._entry_delegate.clear() + + def _add_separator_entry(self) -> None: + """Add a separator entry to the list.""" + item = ListItem( + text="", + left_icon=None, + right_text="", + right_icon=None, + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=20, + not_clickable=True, + ) + self._model.add_item(item) + + def _add_hidden_network_entry(self) -> None: + """Add a 'Connect to Hidden Network' entry at the end of the list.""" + wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/0bar_wifi_protected.svg") + item = ListItem( + text="Connect to Hidden Network...", + left_icon=wifi_pixmap, + right_text="", + right_icon=self._right_arrow_icon, + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=80, + not_clickable=False, + ) + self._model.add_item(item) + + def _add_network_entry( self, ssid: str, - password: typing.Union[str, None], - new_ssid: typing.Union[str, None], + signal: int, + status: str, + is_open: bool = False, + is_hidden: bool = False, ) -> None: - """Update network information""" - if not self.sdbus_network.is_known(ssid): + """Add a network entry to the list.""" + wifi_pixmap = self._icon_provider.get_pixmap(signal=signal, status=status) + + # Skipping hidden networks + # Check both the is_hidden flag AND the ssid content + if is_hidden or self._is_hidden_ssid(ssid): + return + display_ssid = ssid + + item = ListItem( + text=display_ssid, + left_icon=wifi_pixmap, + right_text=status, + right_icon=self._right_arrow_icon, + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=80, + not_clickable=False, # All entries are clickable + ) + self._model.add_item(item) + + @QtCore.pyqtSlot(ListItem, name="ssid-item-clicked") + def _on_ssid_item_clicked(self, item: ListItem) -> None: + """Handle network item click.""" + ssid = item.text + + # Handle hidden network entries - check for various hidden indicators + if ( + self._is_hidden_ssid(ssid) + or ssid == "Hidden Network" + or ssid == "Connect to Hidden Network..." + ): + self.setCurrentIndex(self.indexOf(self.hidden_network_page)) + return + + network_info = self._networks.get(ssid) + if network_info is None: + # Also check if it might be a hidden network in the _networks dict + # Hidden networks might have empty or UNKNOWN as key + for key, info in self._networks.items(): + if info.is_hidden: + self.setCurrentIndex(self.indexOf(self.hidden_network_page)) + return return - checked_btn = self.panel.priority_btn_group.checkedButton() - if checked_btn == self.panel.high_priority_btn: - priority = 90 - elif checked_btn == self.panel.low_priority_btn: - priority = 20 + if network_info.is_saved: + saved_networks = self._sdbus_network.get_saved_networks_with_for() + self._show_saved_network_page(ssid, saved_networks) else: - priority = 50 + self._show_add_network_page(ssid, is_open=network_info.is_open) - self.sdbus_network.update_connection_settings( - ssid=ssid, password=password, new_ssid=new_ssid, priority=priority - ) - QtCore.QTimer().singleShot(10000, lambda: self.network_list_worker.build()) - self.setCurrentIndex(self.indexOf(self.panel.network_list_page)) + def _show_saved_network_page(self, ssid: str, saved_networks: List[Dict]) -> None: + """Show the saved network page.""" + self.saved_connection_network_name.setText(str(ssid)) + self.snd_name.setText(str(ssid)) + self._current_network_ssid = ssid # Track for priority lookup - @QtCore.pyqtSlot(list, name="finished-network-list-build") - def handle_network_list(self, data: typing.List[typing.Tuple]) -> None: - """Handle available network list update""" - scroll_bar_position = self.network_list_widget.verticalScrollBar().value() - self.network_list_widget.blockSignals(True) - self.network_list_widget.clear() - self.network_list_widget.setSpacing(35) - for entry in data: - if entry == "separator": - self.separator_item() - continue - elif entry == "blank": - self.blank_space_item() - continue - if entry[0] == self.sdbus_network.hotspot_ssid: - continue - self.network_button_item(*entry) + # Fetch priority from get_saved_networks() which includes priority + # get_saved_networks_with_for() does NOT include priority field + priority = None + try: + full_saved_networks = self._sdbus_network.get_saved_networks() + if full_saved_networks: + for net in full_saved_networks: + if net.get("ssid") == ssid: + priority = net.get("priority") + logger.debug("Found priority %s for network %s", priority, ssid) + break + except Exception as e: + logger.error("Failed to get priority for %s: %s", ssid, e) - max_v = self.network_list_widget.verticalScrollBar().maximum() - if scroll_bar_position > max_v: - self.network_list_widget.verticalScrollBar().setValue(max_v) - else: - self.network_list_widget.verticalScrollBar().setValue(scroll_bar_position) - self.network_list_widget.verticalScrollBar().update() - self.evaluate_network_state() - QtCore.QTimer().singleShot(10000, lambda: self.network_list_worker.build()) - - def handle_button_click(self, ssid: str): - """Handles pressing a network""" - _saved_ssids = self.sdbus_network.get_saved_networks() - if any(item["ssid"] == ssid for item in _saved_ssids): - self.setCurrentIndex(self.indexOf(self.panel.saved_connection_page)) - self.panel.saved_connection_network_name.setText(str(ssid)) - self.panel.snd_name.setText(str(ssid)) - - # find the entry for this SSID - entry = next((item for item in _saved_ssids if item["ssid"] == ssid), None) - - logger.debug(_saved_ssids) - - if entry is not None: - priority = entry.get("priority") - - if priority == 90: - self.panel.high_priority_btn.setChecked(True) - elif priority == 20: - self.panel.low_priority_btn.setChecked(True) - else: - self.panel.med_priority_btn.setChecked(True) - _curr_ssid = self.sdbus_network.get_current_ssid() - if _curr_ssid != str(ssid): - self.panel.network_activate_btn.setDisabled(False) - self.panel.sn_info.setText("Saved Network") - else: - self.panel.network_activate_btn.setDisabled(True) - self.panel.sn_info.setText("Active Network") + self._set_priority_button(priority) - self.panel.frame.repaint() + network_info = self._networks.get(ssid) + if network_info: + signal_text = ( + f"{network_info.signal}%" if network_info.signal >= 0 else "--%" + ) + self.saved_connection_signal_strength_info_frame.setText(signal_text) + if network_info.is_open: + self.saved_connection_security_type_info_label.setText("OPEN") + else: + sec_type = self._sdbus_network.get_security_type_by_ssid(ssid) + self.saved_connection_security_type_info_label.setText( + str(sec_type or "WPA").upper() + ) else: - self.setCurrentIndex(self.indexOf(self.panel.add_network_page)) - self.panel.add_network_network_label.setText(str(ssid)) + self.saved_connection_signal_strength_info_frame.setText("--%") + self.saved_connection_security_type_info_label.setText("--") - def event(self, event: QtCore.QEvent) -> bool: - """Receives PyQt eEvents, this method is reimplemented from the QEvent class + current_ssid = self._sdbus_network.get_current_ssid() + if current_ssid != ssid: + self.network_activate_btn.setDisabled(False) + self.sn_info.setText("Saved Network") + else: + self.network_activate_btn.setDisabled(True) + self.sn_info.setText("Active Network") - Args: - event (QtCore.QEvent) + self.setCurrentIndex(self.indexOf(self.saved_connection_page)) + self.frame.repaint() - Returns: - bool: Event has been handled or not 1 - """ - if event.type() == QtCore.QEvent.Type.ApplicationActivated: - # Request a networks scan right at the start of the application - self.request_network_scan.emit() - return False - return super().event(event) - - def setCurrentIndex(self, index: int): - """Re-implementation of the QStackedWidget setCurrentIndex method - in order to clear and display text as needed for each panel on the StackedWidget - Args: - index (int): The index we want to change to + def _set_priority_button(self, priority: Optional[int]) -> None: + """Set the priority button based on value. + Block signals while setting to prevent unwanted triggers. """ + # Block signals to prevent any side effects + with ( + QtCore.QSignalBlocker(self.high_priority_btn), + QtCore.QSignalBlocker(self.med_priority_btn), + QtCore.QSignalBlocker(self.low_priority_btn), + ): + # Uncheck all first + self.high_priority_btn.setChecked(False) + self.med_priority_btn.setChecked(False) + self.low_priority_btn.setChecked(False) + + # Then check the correct one + if priority is not None: + if priority >= PRIORITY_HIGH: + self.high_priority_btn.setChecked(True) + elif priority <= PRIORITY_LOW: + self.low_priority_btn.setChecked(True) + else: + self.med_priority_btn.setChecked(True) + else: + # Default to medium if no priority set + self.med_priority_btn.setChecked(True) + + def _show_add_network_page(self, ssid: str, is_open: bool = False) -> None: + """Show the add network page.""" + self._current_network_is_open = is_open + self._current_network_is_hidden = False + self.add_network_network_label.setText(str(ssid)) + self.setCurrentIndex(self.indexOf(self.add_network_page)) + + def _handle_scrollbar_change(self, value: int) -> None: + """Handle scrollbar value change.""" + self.verticalScrollBar.blockSignals(True) + self.verticalScrollBar.setValue(value) + self.verticalScrollBar.blockSignals(False) + + def _sync_scrollbar(self) -> None: + """Synchronize scrollbar with list view.""" + list_scrollbar = self.listView.verticalScrollBar() + self.verticalScrollBar.setMinimum(list_scrollbar.minimum()) + self.verticalScrollBar.setMaximum(list_scrollbar.maximum()) + self.verticalScrollBar.setPageStep(list_scrollbar.pageStep()) + + def _schedule_delayed_action(self, callback: Callable, delay_ms: int) -> None: + """Schedule a delayed action.""" + try: + self._delayed_action_timer.timeout.disconnect() + except TypeError: + pass + + self._delayed_action_timer.timeout.connect(callback) + self._delayed_action_timer.start(delay_ms) + + def close(self) -> bool: + """Close the window.""" + self._network_list_worker.stop_polling() + self._sdbus_network.close() + return super().close() + + def setCurrentIndex(self, index: int) -> None: + """Set the current page index.""" if not self.isVisible(): return - _cur = self.currentIndex() - if index == self.indexOf(self.panel.add_network_page): # Add network page 2 - self.panel.add_network_password_field.clear() - self.panel.add_network_password_field.setPlaceholderText( - "Insert password here, press enter when finished." - ) - elif index == self.indexOf( - self.panel.saved_connection_page - ): # Network information page 3 - self.panel.saved_connection_change_password_field.clear() - self.panel.saved_connection_change_password_field.setPlaceholderText( - "Change network password" - ) - _security_type = self.sdbus_network.get_security_type_by_ssid( - ssid=self.panel.saved_connection_network_name.text() - ) - if not _security_type: - _security_type = "--" - self.panel.saved_connection_security_type_info_label.setText( - str(_security_type) - ) - _signal = self.sdbus_network.get_connection_signal_by_ssid( - self.panel.saved_connection_network_name.text() - ) - if _signal == -1: - _signal = "--" - _signal_string = f"{_signal}%" - self.panel.saved_connection_signal_strength_info_frame.setText( - _signal_string - ) - self.update() + + if index == self.indexOf(self.add_network_page): + self._setup_add_network_page_state() + elif index == self.indexOf(self.saved_connection_page): + self._setup_saved_connection_page_state() + + self.repaint() super().setCurrentIndex(index) - def setProperty(self, name: str, value: typing.Any) -> bool: - """setProperty-> Intercept the set property method + def _setup_add_network_page_state(self) -> None: + """Setup add network page state.""" + self.add_network_password_field.clear() - Args: - name (str): Name of the dynamic property - value (typing.Any): Value for the dynamic property + if self._current_network_is_open: + self.frame_2.setVisible(False) + self.add_network_validation_button.setText("Connect") + else: + self.frame_2.setVisible(True) + self.add_network_password_field.setPlaceholderText( + "Insert password here, press enter when finished." + ) + self.add_network_validation_button.setText("Activate") - Returns: - bool: Returns to the super class - """ + def _setup_saved_connection_page_state(self) -> None: + """Setup saved connection page state.""" + self.saved_connection_change_password_field.clear() + self.saved_connection_change_password_field.setPlaceholderText( + "Change network password" + ) + + def setProperty(self, name: str, value: Any) -> bool: + """Set a property value.""" if name == "backgroundPixmap": - self.background = value + self._background = value return super().setProperty(name, value) @QtCore.pyqtSlot(name="call-network-panel") - def show_network_panel( - self, - ) -> None: - """Slot for displaying networkWindow Panel""" + def show_network_panel(self) -> None: + """Show the network panel.""" if not self.parent(): return - self.setCurrentIndex(self.indexOf(self.panel.network_list_page)) - _parent_size = self.parent().size() # type: ignore - self.setGeometry(0, 0, _parent_size.width(), _parent_size.height()) + + self.setCurrentIndex(self.indexOf(self.network_list_page)) + parent_size = self.parent().size() + self.setGeometry(0, 0, parent_size.width(), parent_size.height()) self.updateGeometry() - self.update() + self.repaint() self.show() - - def build_network_list(self) -> None: - """Build available/saved network list""" - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorRole.Button, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) - palette.setBrush( - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorRole.Base, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorRole.Window, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorRole.Highlight, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorRole.Link, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Inactive, - QtGui.QPalette.ColorRole.Button, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) - palette.setBrush( - QtGui.QPalette.ColorGroup.Inactive, - QtGui.QPalette.ColorRole.Base, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Inactive, - QtGui.QPalette.ColorRole.Window, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Inactive, - QtGui.QPalette.ColorRole.Highlight, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Inactive, - QtGui.QPalette.ColorRole.Link, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Disabled, - QtGui.QPalette.ColorRole.Button, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) - palette.setBrush( - QtGui.QPalette.ColorGroup.Disabled, - QtGui.QPalette.ColorRole.Base, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Disabled, - QtGui.QPalette.ColorRole.Window, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Disabled, - QtGui.QPalette.ColorRole.Highlight, - brush, - ) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush( - QtGui.QPalette.ColorGroup.Disabled, - QtGui.QPalette.ColorRole.Link, - brush, - ) - self.network_list_widget.setPalette(palette) - self.network_list_widget.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.network_list_widget.setStyleSheet("background-color:transparent") - self.network_list_widget.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.network_list_widget.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.network_list_widget.setVerticalScrollBarPolicy( - QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) - self.network_list_widget.setHorizontalScrollBarPolicy( - QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) - self.network_list_widget.setSizeAdjustPolicy( - QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustToContents - ) - self.network_list_widget.setAutoScroll(False) - self.network_list_widget.setProperty("showDropIndicator", False) - self.network_list_widget.setDefaultDropAction(QtCore.Qt.DropAction.IgnoreAction) - self.network_list_widget.setAlternatingRowColors(False) - self.network_list_widget.setSelectionMode( - QtWidgets.QAbstractItemView.SelectionMode.NoSelection - ) - self.network_list_widget.setSelectionBehavior( - QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems - ) - self.network_list_widget.setVerticalScrollMode( - QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel - ) - self.network_list_widget.setHorizontalScrollMode( - QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel - ) - QtWidgets.QScroller.grabGesture( - self.network_list_widget, - QtWidgets.QScroller.ScrollerGestureType.TouchGesture, - ) - QtWidgets.QScroller.grabGesture( - self.network_list_widget, - QtWidgets.QScroller.ScrollerGestureType.LeftMouseButtonGesture, - ) - - self.network_list_widget.setObjectName("network_list_widget") - self.panel.nl_content_layout.addWidget(self.network_list_widget) - - def separator_item(self) -> None: - """Add separator item to network list""" - separator_item = QtWidgets.QListWidgetItem() - separator_widget = QtWidgets.QLabel() - separator_widget.setStyleSheet( - "background-color: gray; margin: 1px 1px; min-height: 1px; max-height: 1px;" - ) - separator_item.setSizeHint(QtCore.QSize(0, 2)) # Total vertical space: 2px - self.network_list_widget.addItem(separator_item) - self.network_list_widget.setItemWidget(separator_item, separator_widget) - - def blank_space_item(self) -> None: - """Add blank space item to network list""" - spacer_item = QtWidgets.QListWidgetItem() - spacer_widget = QtWidgets.QWidget() - spacer_widget.setFixedHeight(10) # Adjust height as needed - spacer_item.setSizeHint(spacer_widget.sizeHint()) - self.network_list_widget.addItem(spacer_item) - self.network_list_widget.setItemWidget(spacer_item, spacer_widget) - - def network_button_item(self, ssid, signal, right_text, /) -> None: - """Add a network entry to network list""" - wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/no_wifi.svg") - if 70 <= signal <= 100: - wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/3bar_wifi.svg") - elif signal >= 40: - wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/2bar_wifi.svg") - elif 1 < signal < 40: - wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/1bar_wifi.svg") - - button = ListCustomButton(parent=self.network_list_widget) - button.setText(ssid) - button.setRightText(right_text) - button.setPixmap(QtGui.QPixmap(":/arrow_icons/media/btn_icons/right_arrow.svg")) - button.setSecondPixmap(wifi_pixmap) - button.setFixedHeight(80) - button.setLeftFontSize(17) - button.setRightFontSize(12) - - button.clicked.connect(lambda checked, s=ssid: self.handle_button_click(s)) - item = QtWidgets.QListWidgetItem() - item.setSizeHint(button.sizeHint()) - self.network_list_widget.addItem(item) - self.network_list_widget.setItemWidget(item, button) diff --git a/BlocksScreen/lib/panels/widgets/sensorsPanel.py b/BlocksScreen/lib/panels/widgets/sensorsPanel.py index 29eea5e7..df63cfb5 100644 --- a/BlocksScreen/lib/panels/widgets/sensorsPanel.py +++ b/BlocksScreen/lib/panels/widgets/sensorsPanel.py @@ -1,8 +1,8 @@ import typing +from lib.panels.widgets.sensorWidget import SensorWidget from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.icon_button import IconButton -from lib.panels.widgets.sensorWidget import SensorWidget from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem from PyQt6 import QtCore, QtGui, QtWidgets @@ -44,16 +44,13 @@ def handle_available_fil_sensors(self, sensors: dict) -> None: if not isinstance(sensors, dict): return self.reset_view_model() - filtered_sensors = list( - filter( - lambda printer_obj: str(printer_obj).startswith( - "filament_switch_sensor" - ) - or str(printer_obj).startswith("filament_motion_sensor") - or str(printer_obj).startswith("cutter_sensor"), - sensors.keys(), + filtered_sensors = [ + sensor + for sensor in sensors.keys() + if sensor.startswith( + ("filament_switch_sensor", "filament_motion_sensor", "cutter_sensor") ) - ) + ] if filtered_sensors: self.sensor_list = [ self.create_sensor_widget(name=sensor) for sensor in filtered_sensors diff --git a/BlocksScreen/lib/ui/connectionWindow.ui b/BlocksScreen/lib/ui/connectionWindow.ui deleted file mode 100644 index f2bd4899..00000000 --- a/BlocksScreen/lib/ui/connectionWindow.ui +++ /dev/null @@ -1,938 +0,0 @@ - - - ConnectivityForm - - - Qt::WindowModal - - - - 0 - 0 - 800 - 480 - - - - - 0 - 0 - - - - - 800 - 480 - - - - - 800 - 480 - - - - Form - - - 1.000000000000000 - - - false - - - #ConnectivityForm{ -background-image: url(:/background/media/1st_background.png); -} - - - - - - - - 10 - 380 - 780 - 124 - - - - - 0 - 0 - - - - - 780 - 124 - - - - - 780 - 150 - - - - - 800 - 80 - - - - - - - - - - - - PreferAntialias - - - - true - - - false - - - - - - QFrame::NoFrame - - - QFrame::Plain - - - 0 - - - - 0 - - - 0 - - - 5 - - - 0 - - - 0 - - - - - - 0 - 0 - - - - - 100 - 80 - - - - - 100 - 80 - - - - - 160 - 80 - - - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - 66 - 66 - 66 - - - - - - - - - false - PreferAntialias - false - - - - BlankCursor - - - true - - - Qt::NoFocus - - - Qt::NoContextMenu - - - Qt::LeftToRight - - - false - - - - - - Restart Klipper - - - - :/system_icons/media/btn_icons/restart_klipper.svg - - - - - 46 - 42 - - - - false - - - false - - - false - - - 0 - - - 0 - - - false - - - true - - - :/system/media/btn_icons/restart_klipper.svg - - - true - - - bottom - - - - 255 - 255 - 255 - - - - - - - - - 0 - 0 - - - - - 100 - 80 - - - - - 100 - 80 - - - - - 80 - 80 - - - - BlankCursor - - - true - - - Qt::NoFocus - - - Qt::NoContextMenu - - - false - - - Reboot - - - - :/system_icons/media/btn_icons/firmware_restart.svg:/system_icons/media/btn_icons/firmware_restart.svg - - - false - - - false - - - true - - - :/system/media/btn_icons/reboot.svg - - - bottom - - - - 255 - 255 - 255 - - - - true - - - - - - - - 0 - 0 - - - - - 100 - 80 - - - - - 100 - 80 - - - - - 160 - 80 - - - - BlankCursor - - - true - - - Qt::NoFocus - - - Qt::NoContextMenu - - - false - - - Firmware Restart - - - - :/system_icons/media/btn_icons/firmware_restart.svg:/system_icons/media/btn_icons/firmware_restart.svg - - - false - - - false - - - true - - - :/system/media/btn_icons/restart_firmware.svg - - - true - - - bottom - - - - 255 - 255 - 255 - - - - - - - - - 0 - 0 - - - - - 100 - 80 - - - - - 100 - 80 - - - - - 80 - 80 - - - - - - - - - - - - 13 - - - - true - - - Qt::ClickFocus - - - false - - - - - - Retry - - - - :/system_icons/media/btn_icons/retry_connection.svg:/system_icons/media/btn_icons/retry_connection.svg - - - - 16 - 16 - - - - false - - - 0 - - - 0 - - - false - - - false - - - true - - - bottom - - - :/system/media/btn_icons/restart_printer.svg - - - - 255 - 255 - 255 - - - - true - - - - - - - - 0 - 0 - - - - - 100 - 80 - - - - - 100 - 80 - - - - - 80 - 80 - - - - BlankCursor - - - true - - - Qt::NoFocus - - - Qt::NoContextMenu - - - false - - - Update page - - - - :/system_icons/media/btn_icons/firmware_restart.svg:/system_icons/media/btn_icons/firmware_restart.svg - - - false - - - false - - - true - - - :/system/media/btn_icons/update-software-icon.svg - - - bottom - - - - 255 - 255 - 255 - - - - true - - - - - - - - 0 - 0 - - - - - 100 - 80 - - - - - 100 - 80 - - - - - 80 - 80 - - - - true - - - Qt::NoFocus - - - Qt::NoContextMenu - - - false - - - Wifi Settings - - - - :/system_icons/media/btn_icons/retry_connection.svg:/system_icons/media/btn_icons/retry_connection.svg - - - false - - - false - - - true - - - system_control_btn - - - :/network/media/btn_icons/wifi_config.svg - - - true - - - bottom - - - - - - - - - 0 - 0 - 800 - 380 - - - - - 0 - 0 - - - - - 800 - 380 - - - - - 800 - 380 - - - - false - - - - - - QFrame::NoFrame - - - QFrame::Raised - - - - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 775 - 70 - - - - - - - - - 0 - 0 - - - - - 800 - 380 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - Momcake - 17 - 75 - false - true - - - - color:white - - - - - - Qt::AutoText - - - false - - - Qt::AlignCenter - - - true - - - Qt::NoTextInteraction - - - - - - - - - IconButton - QPushButton -
lib.utils.icon_button
-
- - BlocksCustomFrame - QFrame -
lib.utils.blocks_frame
- 1 -
-
- - - - - - - -
diff --git a/BlocksScreen/lib/ui/connectionWindow_ui.py b/BlocksScreen/lib/ui/connectionWindow_ui.py deleted file mode 100644 index 772dc227..00000000 --- a/BlocksScreen/lib/ui/connectionWindow_ui.py +++ /dev/null @@ -1,338 +0,0 @@ -# Form implementation generated from reading ui file '/home/levi/BlocksScreen/BlocksScreen/lib/ui/connectionWindow.ui' -# -# Created by: PyQt6 UI code generator 6.7.1 -# -# WARNING: Any manual changes made to this file will be lost when pyuic6 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt6 import QtCore, QtGui, QtWidgets - - -class Ui_ConnectivityForm(object): - def setupUi(self, ConnectivityForm): - ConnectivityForm.setObjectName("ConnectivityForm") - ConnectivityForm.setWindowModality(QtCore.Qt.WindowModality.WindowModal) - ConnectivityForm.resize(800, 480) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(ConnectivityForm.sizePolicy().hasHeightForWidth()) - ConnectivityForm.setSizePolicy(sizePolicy) - ConnectivityForm.setMinimumSize(QtCore.QSize(800, 480)) - ConnectivityForm.setMaximumSize(QtCore.QSize(800, 480)) - ConnectivityForm.setWindowOpacity(1.0) - ConnectivityForm.setAutoFillBackground(False) - ConnectivityForm.setStyleSheet("#ConnectivityForm{\n" -"background-image: url(:/background/media/1st_background.png);\n" -"}") - ConnectivityForm.setProperty("class", "") - self.cw_buttonFrame = BlocksCustomFrame(parent=ConnectivityForm) - self.cw_buttonFrame.setGeometry(QtCore.QRect(10, 380, 780, 124)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cw_buttonFrame.sizePolicy().hasHeightForWidth()) - self.cw_buttonFrame.setSizePolicy(sizePolicy) - self.cw_buttonFrame.setMinimumSize(QtCore.QSize(780, 124)) - self.cw_buttonFrame.setMaximumSize(QtCore.QSize(780, 150)) - self.cw_buttonFrame.setBaseSize(QtCore.QSize(800, 80)) - palette = QtGui.QPalette() - self.cw_buttonFrame.setPalette(palette) - font = QtGui.QFont() - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.cw_buttonFrame.setFont(font) - self.cw_buttonFrame.setTabletTracking(True) - self.cw_buttonFrame.setAutoFillBackground(False) - self.cw_buttonFrame.setStyleSheet("") - self.cw_buttonFrame.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.cw_buttonFrame.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) - self.cw_buttonFrame.setLineWidth(0) - self.cw_buttonFrame.setObjectName("cw_buttonFrame") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.cw_buttonFrame) - self.horizontalLayout.setContentsMargins(0, 5, 0, 0) - self.horizontalLayout.setSpacing(0) - self.horizontalLayout.setObjectName("horizontalLayout") - self.RestartKlipperButton = IconButton(parent=self.cw_buttonFrame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.RestartKlipperButton.sizePolicy().hasHeightForWidth()) - self.RestartKlipperButton.setSizePolicy(sizePolicy) - self.RestartKlipperButton.setMinimumSize(QtCore.QSize(100, 80)) - self.RestartKlipperButton.setMaximumSize(QtCore.QSize(100, 80)) - self.RestartKlipperButton.setBaseSize(QtCore.QSize(160, 80)) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.ButtonText, brush) - self.RestartKlipperButton.setPalette(palette) - font = QtGui.QFont() - font.setStrikeOut(False) - font.setKerning(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) - self.RestartKlipperButton.setFont(font) - self.RestartKlipperButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) - self.RestartKlipperButton.setTabletTracking(True) - self.RestartKlipperButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.RestartKlipperButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.RestartKlipperButton.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.RestartKlipperButton.setAutoFillBackground(False) - self.RestartKlipperButton.setStyleSheet("") - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/restart_klipper.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On) - self.RestartKlipperButton.setIcon(icon) - self.RestartKlipperButton.setIconSize(QtCore.QSize(46, 42)) - self.RestartKlipperButton.setCheckable(False) - self.RestartKlipperButton.setAutoRepeat(False) - self.RestartKlipperButton.setAutoExclusive(False) - self.RestartKlipperButton.setAutoRepeatDelay(0) - self.RestartKlipperButton.setAutoRepeatInterval(0) - self.RestartKlipperButton.setAutoDefault(False) - self.RestartKlipperButton.setFlat(True) - self.RestartKlipperButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/restart_klipper.svg")) - self.RestartKlipperButton.setProperty("has_text", True) - self.RestartKlipperButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) - self.RestartKlipperButton.setObjectName("RestartKlipperButton") - self.horizontalLayout.addWidget(self.RestartKlipperButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.RebootSystemButton = IconButton(parent=self.cw_buttonFrame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.RebootSystemButton.sizePolicy().hasHeightForWidth()) - self.RebootSystemButton.setSizePolicy(sizePolicy) - self.RebootSystemButton.setMinimumSize(QtCore.QSize(100, 80)) - self.RebootSystemButton.setMaximumSize(QtCore.QSize(100, 80)) - self.RebootSystemButton.setBaseSize(QtCore.QSize(80, 80)) - self.RebootSystemButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) - self.RebootSystemButton.setTabletTracking(True) - self.RebootSystemButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.RebootSystemButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.RebootSystemButton.setAutoFillBackground(False) - icon1 = QtGui.QIcon() - icon1.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/firmware_restart.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) - self.RebootSystemButton.setIcon(icon1) - self.RebootSystemButton.setAutoDefault(False) - self.RebootSystemButton.setDefault(False) - self.RebootSystemButton.setFlat(True) - self.RebootSystemButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/reboot.svg")) - self.RebootSystemButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) - self.RebootSystemButton.setProperty("has_text", True) - self.RebootSystemButton.setObjectName("RebootSystemButton") - self.horizontalLayout.addWidget(self.RebootSystemButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.FirmwareRestartButton = IconButton(parent=self.cw_buttonFrame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.FirmwareRestartButton.sizePolicy().hasHeightForWidth()) - self.FirmwareRestartButton.setSizePolicy(sizePolicy) - self.FirmwareRestartButton.setMinimumSize(QtCore.QSize(100, 80)) - self.FirmwareRestartButton.setMaximumSize(QtCore.QSize(100, 80)) - self.FirmwareRestartButton.setBaseSize(QtCore.QSize(160, 80)) - self.FirmwareRestartButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) - self.FirmwareRestartButton.setTabletTracking(True) - self.FirmwareRestartButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.FirmwareRestartButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.FirmwareRestartButton.setAutoFillBackground(False) - self.FirmwareRestartButton.setIcon(icon1) - self.FirmwareRestartButton.setAutoDefault(False) - self.FirmwareRestartButton.setDefault(False) - self.FirmwareRestartButton.setFlat(True) - self.FirmwareRestartButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/restart_firmware.svg")) - self.FirmwareRestartButton.setProperty("has_text", True) - self.FirmwareRestartButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) - self.FirmwareRestartButton.setObjectName("FirmwareRestartButton") - self.horizontalLayout.addWidget(self.FirmwareRestartButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.RetryConnectionButton = IconButton(parent=self.cw_buttonFrame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.RetryConnectionButton.sizePolicy().hasHeightForWidth()) - self.RetryConnectionButton.setSizePolicy(sizePolicy) - self.RetryConnectionButton.setMinimumSize(QtCore.QSize(100, 80)) - self.RetryConnectionButton.setMaximumSize(QtCore.QSize(100, 80)) - self.RetryConnectionButton.setBaseSize(QtCore.QSize(80, 80)) - palette = QtGui.QPalette() - self.RetryConnectionButton.setPalette(palette) - font = QtGui.QFont() - font.setPointSize(13) - self.RetryConnectionButton.setFont(font) - self.RetryConnectionButton.setTabletTracking(True) - self.RetryConnectionButton.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) - self.RetryConnectionButton.setAutoFillBackground(False) - self.RetryConnectionButton.setStyleSheet("") - icon2 = QtGui.QIcon() - icon2.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/retry_connection.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) - self.RetryConnectionButton.setIcon(icon2) - self.RetryConnectionButton.setIconSize(QtCore.QSize(16, 16)) - self.RetryConnectionButton.setCheckable(False) - self.RetryConnectionButton.setAutoRepeatDelay(0) - self.RetryConnectionButton.setAutoRepeatInterval(0) - self.RetryConnectionButton.setAutoDefault(False) - self.RetryConnectionButton.setDefault(False) - self.RetryConnectionButton.setFlat(True) - self.RetryConnectionButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/restart_printer.svg")) - self.RetryConnectionButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) - self.RetryConnectionButton.setProperty("has_text", True) - self.RetryConnectionButton.setObjectName("RetryConnectionButton") - self.horizontalLayout.addWidget(self.RetryConnectionButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.updatepageButton = IconButton(parent=self.cw_buttonFrame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.updatepageButton.sizePolicy().hasHeightForWidth()) - self.updatepageButton.setSizePolicy(sizePolicy) - self.updatepageButton.setMinimumSize(QtCore.QSize(100, 80)) - self.updatepageButton.setMaximumSize(QtCore.QSize(100, 80)) - self.updatepageButton.setBaseSize(QtCore.QSize(80, 80)) - self.updatepageButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) - self.updatepageButton.setTabletTracking(True) - self.updatepageButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.updatepageButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.updatepageButton.setAutoFillBackground(False) - self.updatepageButton.setIcon(icon1) - self.updatepageButton.setAutoDefault(False) - self.updatepageButton.setDefault(False) - self.updatepageButton.setFlat(True) - self.updatepageButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/update-software-icon.svg")) - self.updatepageButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) - self.updatepageButton.setProperty("has_text", True) - self.updatepageButton.setObjectName("updatepageButton") - self.horizontalLayout.addWidget(self.updatepageButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.wifi_button = IconButton(parent=self.cw_buttonFrame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.wifi_button.sizePolicy().hasHeightForWidth()) - self.wifi_button.setSizePolicy(sizePolicy) - self.wifi_button.setMinimumSize(QtCore.QSize(100, 80)) - self.wifi_button.setMaximumSize(QtCore.QSize(100, 80)) - self.wifi_button.setBaseSize(QtCore.QSize(80, 80)) - self.wifi_button.setTabletTracking(True) - self.wifi_button.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.wifi_button.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) - self.wifi_button.setAutoFillBackground(False) - self.wifi_button.setIcon(icon2) - self.wifi_button.setAutoDefault(False) - self.wifi_button.setDefault(False) - self.wifi_button.setFlat(True) - self.wifi_button.setProperty("icon_pixmap", QtGui.QPixmap(":/network/media/btn_icons/wifi_config.svg")) - self.wifi_button.setProperty("has_text", True) - self.wifi_button.setObjectName("wifi_button") - self.horizontalLayout.addWidget(self.wifi_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.cw_Frame = QtWidgets.QFrame(parent=ConnectivityForm) - self.cw_Frame.setGeometry(QtCore.QRect(0, 0, 800, 380)) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.cw_Frame.sizePolicy().hasHeightForWidth()) - self.cw_Frame.setSizePolicy(sizePolicy) - self.cw_Frame.setMinimumSize(QtCore.QSize(800, 380)) - self.cw_Frame.setMaximumSize(QtCore.QSize(800, 380)) - self.cw_Frame.setAutoFillBackground(False) - self.cw_Frame.setStyleSheet("") - self.cw_Frame.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.cw_Frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.cw_Frame.setObjectName("cw_Frame") - self.verticalLayout = QtWidgets.QVBoxLayout(self.cw_Frame) - self.verticalLayout.setObjectName("verticalLayout") - spacerItem = QtWidgets.QSpacerItem(775, 70, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.verticalLayout.addItem(spacerItem) - self.connectionTextBox = QtWidgets.QLabel(parent=self.cw_Frame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.connectionTextBox.sizePolicy().hasHeightForWidth()) - self.connectionTextBox.setSizePolicy(sizePolicy) - self.connectionTextBox.setMaximumSize(QtCore.QSize(800, 380)) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.ButtonText, brush) - self.connectionTextBox.setPalette(palette) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(17) - font.setBold(True) - font.setItalic(False) - font.setWeight(75) - self.connectionTextBox.setFont(font) - self.connectionTextBox.setStyleSheet("color:white") - self.connectionTextBox.setText("") - self.connectionTextBox.setTextFormat(QtCore.Qt.TextFormat.AutoText) - self.connectionTextBox.setScaledContents(False) - self.connectionTextBox.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.connectionTextBox.setWordWrap(True) - self.connectionTextBox.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.NoTextInteraction) - self.connectionTextBox.setObjectName("connectionTextBox") - self.verticalLayout.addWidget(self.connectionTextBox) - - self.retranslateUi(ConnectivityForm) - QtCore.QMetaObject.connectSlotsByName(ConnectivityForm) - - def retranslateUi(self, ConnectivityForm): - _translate = QtCore.QCoreApplication.translate - ConnectivityForm.setWindowTitle(_translate("ConnectivityForm", "Form")) - self.RestartKlipperButton.setText(_translate("ConnectivityForm", "Restart Klipper")) - self.RestartKlipperButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) - self.RebootSystemButton.setText(_translate("ConnectivityForm", "Reboot")) - self.RebootSystemButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) - self.FirmwareRestartButton.setText(_translate("ConnectivityForm", "Firmware Restart")) - self.FirmwareRestartButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) - self.RetryConnectionButton.setText(_translate("ConnectivityForm", "Retry ")) - self.RetryConnectionButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) - self.updatepageButton.setText(_translate("ConnectivityForm", "Update page")) - self.updatepageButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) - self.wifi_button.setText(_translate("ConnectivityForm", "Wifi Settings")) - self.wifi_button.setProperty("class", _translate("ConnectivityForm", "system_control_btn")) - self.wifi_button.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) -from lib.utils.blocks_frame import BlocksCustomFrame -from lib.utils.icon_button import IconButton diff --git a/BlocksScreen/lib/ui/resources/icon_resources.qrc b/BlocksScreen/lib/ui/resources/icon_resources.qrc index a62dda06..5239a1fd 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources.qrc +++ b/BlocksScreen/lib/ui/resources/icon_resources.qrc @@ -1,11 +1,18 @@ - media/btn_icons/wifi_config.svg - media/btn_icons/wifi_locked.svg - media/btn_icons/wifi_unlocked.svg + media/btn_icons/0bar_wifi.svg + media/btn_icons/0bar_wifi_protected.svg media/btn_icons/1bar_wifi.svg + media/btn_icons/1bar_wifi_protected.svg media/btn_icons/2bar_wifi.svg + media/btn_icons/2bar_wifi_protected.svg media/btn_icons/3bar_wifi.svg + media/btn_icons/3bar_wifi_protected.svg + media/btn_icons/4bar_wifi.svg + media/btn_icons/4bar_wifi_protected.svg + media/btn_icons/wifi_config.svg + media/btn_icons/wifi_locked.svg + media/btn_icons/wifi_unlocked.svg media/btn_icons/hotspot.svg media/btn_icons/no_wifi.svg media/btn_icons/retry_wifi.svg diff --git a/BlocksScreen/lib/ui/resources/icon_resources_rc.py b/BlocksScreen/lib/ui/resources/icon_resources_rc.py index 9df24546..a7aca514 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/icon_resources_rc.py @@ -19325,7 +19325,7 @@ \x22\x32\x34\x36\x2e\x32\x38\x22\x20\x77\x69\x64\x74\x68\x3d\x22\ \x35\x32\x35\x2e\x39\x31\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\ \x31\x30\x37\x2e\x34\x35\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x02\xa8\ +\x00\x00\x05\x95\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ \x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ @@ -19334,42 +19334,190 @@ \x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ \x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ \x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x6f\x70\x61\x63\x69\x74\x79\x3a\x30\x2e\x37\x35\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x65\x30\x65\ -\x30\x64\x66\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ -\x65\x66\x73\x3e\x3c\x67\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x31\x38\x36\ -\x2e\x31\x38\x2c\x34\x30\x36\x2e\x32\x33\x63\x30\x2d\x39\x2e\x30\ -\x39\x2c\x33\x2e\x35\x32\x2d\x31\x36\x2e\x34\x31\x2c\x39\x2e\x34\ -\x37\x2d\x32\x32\x2e\x34\x38\x2c\x32\x33\x2e\x34\x38\x2d\x32\x34\ -\x2c\x35\x31\x2e\x30\x37\x2d\x33\x39\x2e\x32\x33\x2c\x38\x33\x2e\ -\x33\x39\x2d\x34\x33\x2e\x37\x38\x2c\x34\x37\x2e\x39\x34\x2d\x36\ -\x2e\x37\x36\x2c\x38\x39\x2e\x35\x2c\x38\x2e\x31\x33\x2c\x31\x32\ -\x34\x2e\x37\x37\x2c\x34\x33\x2e\x33\x31\x2c\x31\x30\x2c\x31\x30\ -\x2c\x31\x32\x2e\x36\x32\x2c\x32\x33\x2e\x36\x35\x2c\x37\x2e\x33\ -\x34\x2c\x33\x35\x2e\x35\x32\x2d\x38\x2e\x33\x32\x2c\x31\x38\x2e\ -\x37\x31\x2d\x33\x30\x2e\x35\x34\x2c\x32\x33\x2e\x31\x2d\x34\x34\ -\x2e\x36\x36\x2c\x38\x2e\x35\x34\x2d\x31\x33\x2e\x32\x2d\x31\x33\ -\x2e\x36\x33\x2d\x32\x38\x2e\x33\x38\x2d\x32\x33\x2e\x32\x39\x2d\ -\x34\x36\x2e\x34\x31\x2d\x32\x37\x2e\x31\x35\x2d\x33\x32\x2e\x37\ -\x34\x2d\x37\x2d\x36\x31\x2e\x34\x35\x2c\x31\x2e\x36\x33\x2d\x38\ -\x35\x2e\x36\x39\x2c\x32\x36\x2e\x33\x38\x2d\x31\x36\x2e\x34\x32\ -\x2c\x31\x36\x2e\x37\x36\x2d\x34\x31\x2e\x36\x39\x2c\x39\x2e\x38\ -\x36\x2d\x34\x37\x2e\x32\x33\x2d\x31\x33\x2e\x30\x37\x41\x36\x37\ -\x2e\x35\x36\x2c\x36\x37\x2e\x35\x36\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x31\x38\x36\x2e\x31\x38\x2c\x34\x30\x36\x2e\x32\x33\x5a\x22\x2f\ +\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ +\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\ +\x35\x34\x30\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ +\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ +\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ +\x31\x38\x2c\x37\x37\x43\x34\x30\x32\x2c\x37\x38\x2e\x37\x37\x2c\ +\x34\x39\x30\x2e\x31\x2c\x31\x31\x37\x2e\x30\x37\x2c\x35\x36\x33\ +\x2e\x39\x2c\x31\x39\x33\x2e\x30\x39\x63\x31\x34\x2e\x38\x2c\x31\ +\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\x2e\x39\x33\ +\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\x31\x2e\x31\x32\x2c\x31\ +\x33\x2e\x31\x37\x2d\x32\x38\x2e\x36\x32\x2c\x31\x33\x2d\x34\x31\ +\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\x30\x38\x2d\x31\x34\x2e\ +\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\x2e\x37\x31\x2d\x34\x36\ +\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\ +\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\x37\x35\x2d\x31\x32\x37\ +\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\x35\x34\x2d\x39\x2e\x31\ +\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\x2e\x33\x31\x2d\x31\x35\ +\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x2d\x34\x35\x2c\x31\x37\x2e\ +\x30\x35\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\x33\x2d\x31\ +\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x33\x2d\x38\x2e\x35\x37\ +\x2c\x39\x2e\x31\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\x32\x2e\x33\ +\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\x30\x39\x2d\ +\x37\x2e\x35\x37\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\x30\x39\x2d\ +\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\x33\x38\x30\x2c\x33\ +\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x37\x2e\x36\x32\x2d\x35\ +\x30\x2e\x33\x38\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\x38\x39\ +\x2e\x39\x33\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\x37\x2d\ +\x36\x30\x2e\x34\x36\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x39\x2e\ +\x38\x37\x2c\x32\x37\x37\x2e\x34\x33\x2c\x37\x38\x2e\x34\x38\x2c\ +\x32\x39\x30\x2e\x31\x38\x2c\x37\x37\x5a\x22\x2f\x3e\x3c\x70\x61\ +\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\ +\x20\x64\x3d\x22\x4d\x34\x36\x39\x2e\x38\x37\x2c\x33\x33\x32\x2e\ +\x32\x31\x63\x2d\x31\x31\x2c\x2e\x31\x38\x2d\x31\x37\x2e\x38\x33\ +\x2d\x33\x2e\x30\x37\x2d\x32\x33\x2e\x35\x32\x2d\x39\x2e\x31\x32\ +\x43\x34\x31\x34\x2c\x32\x38\x38\x2e\x36\x35\x2c\x33\x37\x35\x2e\ +\x32\x32\x2c\x32\x36\x37\x2e\x31\x36\x2c\x33\x33\x30\x2e\x31\x2c\ +\x32\x36\x30\x2e\x36\x38\x63\x2d\x36\x37\x2e\x33\x37\x2d\x39\x2e\ +\x36\x37\x2d\x31\x32\x36\x2e\x31\x37\x2c\x31\x30\x2e\x38\x33\x2d\ +\x31\x37\x35\x2e\x33\x39\x2c\x36\x31\x2e\x34\x31\x2d\x31\x36\x2e\ +\x33\x35\x2c\x31\x36\x2e\x38\x2d\x34\x30\x2e\x36\x37\x2c\x31\x32\ +\x2d\x34\x37\x2e\x39\x31\x2d\x31\x30\x2d\x33\x2e\x39\x2d\x31\x31\ +\x2e\x39\x2d\x31\x2e\x33\x38\x2d\x32\x32\x2e\x38\x2c\x36\x2e\x38\ +\x39\x2d\x33\x31\x2e\x35\x32\x2c\x34\x31\x2d\x34\x33\x2e\x32\x34\ +\x2c\x38\x39\x2e\x37\x35\x2d\x37\x30\x2e\x34\x38\x2c\x31\x34\x36\ +\x2e\x38\x34\x2d\x37\x39\x2e\x32\x38\x2c\x37\x35\x2e\x36\x2d\x31\ +\x31\x2e\x36\x36\x2c\x31\x34\x33\x2e\x36\x39\x2c\x38\x2e\x30\x35\ +\x2c\x32\x30\x33\x2e\x39\x31\x2c\x35\x38\x2e\x33\x36\x61\x32\x30\ +\x35\x2e\x37\x34\x2c\x32\x30\x35\x2e\x37\x34\x2c\x30\x2c\x30\x2c\ +\x31\x2c\x32\x33\x2e\x32\x35\x2c\x32\x32\x2e\x36\x39\x63\x38\x2e\ +\x30\x38\x2c\x39\x2e\x33\x2c\x39\x2e\x35\x2c\x32\x30\x2e\x36\x32\ +\x2c\x34\x2e\x35\x39\x2c\x33\x32\x2e\x33\x34\x53\x34\x37\x38\x2e\ +\x36\x32\x2c\x33\x33\x31\x2e\x36\x36\x2c\x34\x36\x39\x2e\x38\x37\ +\x2c\x33\x33\x32\x2e\x32\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ +\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\ +\x3d\x22\x4d\x31\x38\x34\x2e\x35\x32\x2c\x33\x38\x37\x2e\x34\x63\ +\x30\x2d\x39\x2e\x32\x32\x2c\x33\x2e\x35\x37\x2d\x31\x36\x2e\x36\ +\x35\x2c\x39\x2e\x36\x31\x2d\x32\x32\x2e\x38\x31\x43\x32\x31\x38\ +\x2c\x33\x34\x30\x2e\x32\x34\x2c\x32\x34\x36\x2c\x33\x32\x34\x2e\ +\x37\x38\x2c\x32\x37\x38\x2e\x37\x37\x2c\x33\x32\x30\x2e\x31\x35\ +\x63\x34\x38\x2e\x36\x36\x2d\x36\x2e\x38\x36\x2c\x39\x30\x2e\x38\ +\x33\x2c\x38\x2e\x32\x35\x2c\x31\x32\x36\x2e\x36\x33\x2c\x34\x34\ +\x2c\x31\x30\x2e\x31\x38\x2c\x31\x30\x2e\x31\x35\x2c\x31\x32\x2e\ +\x38\x31\x2c\x32\x34\x2c\x37\x2e\x34\x35\x2c\x33\x36\x2e\x30\x35\ +\x2d\x38\x2e\x34\x34\x2c\x31\x39\x2d\x33\x31\x2c\x32\x33\x2e\x34\ +\x35\x2d\x34\x35\x2e\x33\x32\x2c\x38\x2e\x36\x36\x2d\x31\x33\x2e\ +\x34\x2d\x31\x33\x2e\x38\x33\x2d\x32\x38\x2e\x38\x2d\x32\x33\x2e\ +\x36\x33\x2d\x34\x37\x2e\x31\x31\x2d\x32\x37\x2e\x35\x34\x2d\x33\ +\x33\x2e\x32\x32\x2d\x37\x2e\x31\x2d\x36\x32\x2e\x33\x36\x2c\x31\ +\x2e\x36\x35\x2d\x38\x37\x2c\x32\x36\x2e\x37\x37\x2d\x31\x36\x2e\ +\x36\x36\x2c\x31\x37\x2d\x34\x32\x2e\x33\x2c\x31\x30\x2d\x34\x37\ +\x2e\x39\x33\x2d\x31\x33\x2e\x32\x37\x41\x36\x39\x2e\x32\x38\x2c\ +\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x38\x34\x2e\ +\x35\x32\x2c\x33\x38\x37\x2e\x34\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ +\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\ +\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x32\x33\x63\x2d\x32\x32\x2c\ +\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x36\x2d\x33\x39\ +\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\x2e\x33\ +\x38\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\ +\x2e\x38\x36\x2d\x34\x31\x2e\x38\x2c\x32\x32\x2e\x32\x35\x2c\x30\ +\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x38\x2c\x33\x39\x2e\ +\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ +\x32\x33\x2c\x33\x30\x30\x2c\x35\x32\x33\x5a\x22\x2f\x3e\x3c\x2f\ +\x73\x76\x67\x3e\ +\x00\x00\x06\x23\ +\x3c\ +\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ +\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ +\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ +\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ +\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ +\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ +\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ +\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x35\x65\x36\ +\x30\x36\x31\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ +\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ +\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ +\x32\x36\x2c\x39\x34\x2e\x37\x63\x31\x31\x30\x2e\x39\x34\x2c\x31\ +\x2e\x37\x37\x2c\x31\x39\x38\x2e\x33\x39\x2c\x33\x39\x2e\x37\x37\ +\x2c\x32\x37\x31\x2e\x36\x32\x2c\x31\x31\x35\x2e\x32\x31\x2c\x31\ +\x34\x2e\x36\x39\x2c\x31\x35\x2e\x31\x33\x2c\x31\x31\x2e\x37\x32\ +\x2c\x33\x33\x2e\x36\x37\x2c\x33\x2e\x32\x34\x2c\x34\x33\x2e\x37\ +\x2d\x31\x31\x2c\x31\x33\x2e\x30\x37\x2d\x32\x38\x2e\x34\x2c\x31\ +\x32\x2e\x38\x39\x2d\x34\x31\x2e\x31\x35\x2e\x38\x33\x2d\x31\x35\ +\x2d\x31\x34\x2e\x31\x36\x2d\x33\x30\x2d\x32\x38\x2e\x35\x2d\x34\ +\x36\x2e\x32\x35\x2d\x34\x30\x2e\x37\x39\x2d\x33\x38\x2e\x31\x31\ +\x2d\x32\x38\x2e\x37\x38\x2d\x38\x30\x2e\x37\x31\x2d\x34\x36\x2e\ +\x33\x39\x2d\x31\x32\x36\x2e\x37\x38\x2d\x35\x34\x2e\x32\x33\x2d\ +\x35\x33\x2e\x36\x2d\x39\x2e\x31\x32\x2d\x31\x30\x36\x2e\x30\x39\ +\x2d\x34\x2e\x32\x38\x2d\x31\x35\x37\x2e\x32\x38\x2c\x31\x35\x2e\ +\x31\x31\x43\x31\x34\x39\x2c\x31\x39\x31\x2e\x34\x35\x2c\x31\x31\ +\x30\x2c\x32\x31\x38\x2e\x31\x32\x2c\x37\x36\x2e\x34\x2c\x32\x35\ +\x33\x2e\x38\x35\x63\x2d\x38\x2e\x35\x2c\x39\x2d\x31\x38\x2e\x35\ +\x2c\x31\x32\x2e\x32\x33\x2d\x33\x30\x2c\x37\x2e\x39\x31\x2d\x31\ +\x39\x2e\x39\x33\x2d\x37\x2e\x35\x2d\x32\x35\x2d\x33\x33\x2e\x38\ +\x32\x2d\x39\x2e\x36\x36\x2d\x35\x30\x2e\x32\x31\x61\x33\x37\x37\ +\x2e\x33\x2c\x33\x37\x37\x2e\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x35\ +\x37\x2e\x31\x38\x2d\x35\x30\x63\x34\x32\x2e\x37\x2d\x33\x30\x2e\ +\x33\x35\x2c\x38\x39\x2e\x32\x34\x2d\x35\x30\x2e\x37\x31\x2c\x31\ +\x33\x39\x2e\x36\x39\x2d\x36\x30\x43\x32\x35\x35\x2e\x34\x31\x2c\ +\x39\x37\x2e\x35\x35\x2c\x32\x37\x37\x2e\x36\x2c\x39\x36\x2e\x31\ +\x38\x2c\x32\x39\x30\x2e\x32\x36\x2c\x39\x34\x2e\x37\x5a\x22\x2f\ \x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x33\x39\ -\x2e\x38\x34\x63\x2d\x32\x31\x2e\x36\x37\x2c\x30\x2d\x33\x38\x2e\ -\x35\x37\x2d\x31\x38\x2e\x31\x38\x2d\x33\x38\x2e\x35\x33\x2d\x34\ -\x31\x2e\x34\x39\x2c\x30\x2d\x32\x33\x2c\x31\x36\x2e\x39\x34\x2d\ -\x34\x31\x2e\x32\x32\x2c\x33\x38\x2e\x32\x38\x2d\x34\x31\x2e\x31\ -\x38\x2c\x32\x31\x2e\x39\x33\x2c\x30\x2c\x33\x38\x2e\x37\x35\x2c\ -\x31\x37\x2e\x39\x31\x2c\x33\x38\x2e\x37\x36\x2c\x34\x31\x2e\x32\ -\x53\x33\x32\x31\x2e\x37\x36\x2c\x35\x33\x39\x2e\x38\x33\x2c\x33\ -\x30\x30\x2c\x35\x33\x39\x2e\x38\x34\x5a\x22\x2f\x3e\x3c\x2f\x67\ -\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x36\x38\x2e\x35\x31\x2c\ +\x33\x34\x38\x63\x2d\x31\x30\x2e\x39\x31\x2e\x31\x38\x2d\x31\x37\ +\x2e\x36\x39\x2d\x33\x2e\x30\x35\x2d\x32\x33\x2e\x33\x34\x2d\x39\ +\x2e\x30\x36\x2d\x33\x32\x2e\x31\x32\x2d\x33\x34\x2e\x31\x38\x2d\ +\x37\x30\x2e\x35\x39\x2d\x35\x35\x2e\x35\x2d\x31\x31\x35\x2e\x33\ +\x36\x2d\x36\x31\x2e\x39\x33\x2d\x36\x36\x2e\x38\x35\x2d\x39\x2e\ +\x35\x39\x2d\x31\x32\x35\x2e\x32\x2c\x31\x30\x2e\x37\x35\x2d\x31\ +\x37\x34\x2c\x36\x30\x2e\x39\x34\x2d\x31\x36\x2e\x32\x32\x2c\x31\ +\x36\x2e\x36\x37\x2d\x34\x30\x2e\x33\x36\x2c\x31\x31\x2e\x39\x31\ +\x2d\x34\x37\x2e\x35\x34\x2d\x31\x30\x2d\x33\x2e\x38\x38\x2d\x31\ +\x31\x2e\x38\x2d\x31\x2e\x33\x37\x2d\x32\x32\x2e\x36\x32\x2c\x36\ +\x2e\x38\x34\x2d\x33\x31\x2e\x32\x37\x2c\x34\x30\x2e\x36\x38\x2d\ +\x34\x32\x2e\x39\x31\x2c\x38\x39\x2e\x30\x36\x2d\x36\x39\x2e\x39\ +\x34\x2c\x31\x34\x35\x2e\x37\x31\x2d\x37\x38\x2e\x36\x38\x2c\x37\ +\x35\x2d\x31\x31\x2e\x35\x37\x2c\x31\x34\x32\x2e\x35\x39\x2c\x38\ +\x2c\x32\x30\x32\x2e\x33\x35\x2c\x35\x37\x2e\x39\x32\x61\x32\x30\ +\x33\x2e\x34\x37\x2c\x32\x30\x33\x2e\x34\x37\x2c\x30\x2c\x30\x2c\ +\x31\x2c\x32\x33\x2e\x30\x37\x2c\x32\x32\x2e\x35\x32\x63\x38\x2c\ +\x39\x2e\x32\x32\x2c\x39\x2e\x34\x33\x2c\x32\x30\x2e\x34\x36\x2c\ +\x34\x2e\x35\x36\x2c\x33\x32\x2e\x30\x38\x53\x34\x37\x37\x2e\x31\ +\x39\x2c\x33\x34\x37\x2e\x34\x31\x2c\x34\x36\x38\x2e\x35\x31\x2c\ +\x33\x34\x38\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ +\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x31\ +\x38\x35\x2e\x33\x36\x2c\x34\x30\x32\x2e\x37\x33\x63\x30\x2d\x39\ +\x2e\x31\x35\x2c\x33\x2e\x35\x35\x2d\x31\x36\x2e\x35\x32\x2c\x39\ +\x2e\x35\x34\x2d\x32\x32\x2e\x36\x34\x2c\x32\x33\x2e\x36\x36\x2d\ +\x32\x34\x2e\x31\x35\x2c\x35\x31\x2e\x34\x34\x2d\x33\x39\x2e\x35\ +\x2c\x38\x34\x2d\x34\x34\x2e\x30\x39\x2c\x34\x38\x2e\x32\x38\x2d\ +\x36\x2e\x38\x31\x2c\x39\x30\x2e\x31\x34\x2c\x38\x2e\x31\x39\x2c\ +\x31\x32\x35\x2e\x36\x37\x2c\x34\x33\x2e\x36\x32\x2c\x31\x30\x2e\ +\x30\x39\x2c\x31\x30\x2e\x30\x37\x2c\x31\x32\x2e\x37\x2c\x32\x33\ +\x2e\x38\x32\x2c\x37\x2e\x33\x39\x2c\x33\x35\x2e\x37\x37\x2d\x38\ +\x2e\x33\x39\x2c\x31\x38\x2e\x38\x35\x2d\x33\x30\x2e\x37\x37\x2c\ +\x32\x33\x2e\x32\x37\x2d\x34\x35\x2c\x38\x2e\x36\x2d\x31\x33\x2e\ +\x33\x2d\x31\x33\x2e\x37\x33\x2d\x32\x38\x2e\x35\x38\x2d\x32\x33\ +\x2e\x34\x35\x2d\x34\x36\x2e\x37\x35\x2d\x32\x37\x2e\x33\x33\x2d\ +\x33\x33\x2d\x37\x2e\x30\x35\x2d\x36\x31\x2e\x38\x38\x2c\x31\x2e\ +\x36\x34\x2d\x38\x36\x2e\x33\x2c\x32\x36\x2e\x35\x36\x2d\x31\x36\ +\x2e\x35\x34\x2c\x31\x36\x2e\x38\x38\x2d\x34\x32\x2c\x39\x2e\x39\ +\x33\x2d\x34\x37\x2e\x35\x37\x2d\x31\x33\x2e\x31\x37\x41\x37\x30\ +\x2e\x34\x31\x2c\x37\x30\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\ +\x31\x38\x35\x2e\x33\x36\x2c\x34\x30\x32\x2e\x37\x33\x5a\x22\x2f\ +\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ +\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x33\x37\ +\x2e\x33\x63\x2d\x32\x31\x2e\x38\x33\x2c\x30\x2d\x33\x38\x2e\x38\ +\x34\x2d\x31\x38\x2e\x33\x31\x2d\x33\x38\x2e\x38\x31\x2d\x34\x31\ +\x2e\x37\x39\x2c\x30\x2d\x32\x33\x2e\x31\x39\x2c\x31\x37\x2e\x30\ +\x36\x2d\x34\x31\x2e\x35\x31\x2c\x33\x38\x2e\x35\x36\x2d\x34\x31\ +\x2e\x34\x37\x2c\x32\x32\x2e\x30\x39\x2c\x30\x2c\x33\x39\x2c\x31\ +\x38\x2c\x33\x39\x2c\x34\x31\x2e\x34\x39\x53\x33\x32\x31\x2e\x39\ +\x31\x2c\x35\x33\x37\x2e\x32\x39\x2c\x33\x30\x30\x2c\x35\x33\x37\ +\x2e\x33\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ +\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x31\x35\ +\x36\x2e\x37\x36\x2c\x36\x31\x2c\x33\x30\x30\x2c\x32\x35\x39\x2e\ +\x38\x2c\x34\x34\x33\x2e\x32\x35\x2c\x36\x31\x68\x35\x37\x2e\x39\ +\x33\x4c\x33\x32\x39\x2c\x33\x30\x30\x2c\x35\x30\x31\x2e\x31\x39\ +\x2c\x35\x33\x39\x48\x34\x34\x33\x2e\x32\x35\x4c\x33\x30\x30\x2c\ +\x33\x34\x30\x2e\x31\x39\x2c\x31\x35\x36\x2e\x37\x36\x2c\x35\x33\ +\x39\x48\x39\x38\x2e\x38\x31\x4c\x32\x37\x31\x2c\x33\x30\x30\x2c\ +\x39\x38\x2e\x38\x33\x2c\x36\x31\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\ +\x67\x3e\ \x00\x00\x0b\x4d\ \x00\ \x00\x38\xfa\x78\x9c\xed\x9b\x49\x6f\x1d\xc7\x15\x85\xff\x0a\xc1\ @@ -20106,7 +20254,7 @@ \x61\x6e\x73\x6c\x61\x74\x65\x28\x31\x30\x39\x36\x2e\x38\x36\x20\ \x34\x34\x35\x2e\x35\x33\x29\x20\x72\x6f\x74\x61\x74\x65\x28\x31\ \x33\x35\x29\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x05\x99\ +\x00\x00\x05\x80\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ \x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ @@ -20118,86 +20266,176 @@ \x31\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\x35\x34\x30\x3b\x7d\ \x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\ \x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\x31\x38\x2c\x39\x33\ -\x2e\x38\x32\x43\x34\x30\x32\x2c\x39\x35\x2e\x36\x31\x2c\x34\x39\ -\x30\x2e\x31\x2c\x31\x33\x33\x2e\x39\x31\x2c\x35\x36\x33\x2e\x39\ -\x2c\x32\x30\x39\x2e\x39\x32\x63\x31\x34\x2e\x38\x2c\x31\x35\x2e\ -\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\x2e\x39\x33\x2c\x33\ -\x2e\x32\x36\x2c\x34\x34\x2e\x30\x35\x2d\x31\x31\x2e\x31\x32\x2c\ -\x31\x33\x2e\x31\x36\x2d\x32\x38\x2e\x36\x32\x2c\x31\x33\x2d\x34\ -\x31\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\x30\x38\x2d\x31\x34\ -\x2e\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\x2e\x37\x32\x2d\x34\ -\x36\x2e\x36\x31\x2d\x34\x31\x2e\x31\x31\x2d\x33\x38\x2e\x34\x2d\ -\x32\x39\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\x37\x35\x2d\x31\ -\x32\x37\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\x35\x34\x2d\x39\ -\x2e\x31\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\x2e\x33\x31\x2d\ -\x31\x35\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x2d\x34\x35\x2c\x31\ -\x37\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\x33\x2d\x31\x31\ -\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x34\x2d\x38\x2e\x35\x37\x2c\ -\x39\x2e\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\x32\x2e\x33\x32\x2d\ -\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\x30\x39\x2d\x37\x2e\ -\x35\x36\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\x30\x39\x2d\x39\x2e\ -\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\x33\x38\x30\x2e\x35\x31\x2c\ -\x33\x38\x30\x2e\x35\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x37\x2e\ -\x36\x32\x2d\x35\x30\x2e\x33\x38\x63\x34\x33\x2d\x33\x30\x2e\x35\ -\x38\x2c\x38\x39\x2e\x39\x33\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\ -\x2e\x37\x37\x2d\x36\x30\x2e\x34\x35\x43\x32\x35\x35\x2e\x30\x37\ -\x2c\x39\x36\x2e\x37\x2c\x32\x37\x37\x2e\x34\x33\x2c\x39\x35\x2e\ -\x33\x31\x2c\x32\x39\x30\x2e\x31\x38\x2c\x39\x33\x2e\x38\x32\x5a\ -\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x36\x39\x2e\x38\ -\x37\x2c\x33\x34\x39\x2e\x30\x35\x63\x2d\x31\x31\x2c\x2e\x31\x38\ -\x2d\x31\x37\x2e\x38\x33\x2d\x33\x2e\x30\x38\x2d\x32\x33\x2e\x35\ -\x32\x2d\x39\x2e\x31\x33\x43\x34\x31\x34\x2c\x33\x30\x35\x2e\x34\ -\x38\x2c\x33\x37\x35\x2e\x32\x32\x2c\x32\x38\x34\x2c\x33\x33\x30\ -\x2e\x31\x2c\x32\x37\x37\x2e\x35\x32\x63\x2d\x36\x37\x2e\x33\x37\ -\x2d\x39\x2e\x36\x37\x2d\x31\x32\x36\x2e\x31\x37\x2c\x31\x30\x2e\ -\x38\x32\x2d\x31\x37\x35\x2e\x33\x39\x2c\x36\x31\x2e\x34\x2d\x31\ -\x36\x2e\x33\x35\x2c\x31\x36\x2e\x38\x2d\x34\x30\x2e\x36\x37\x2c\ -\x31\x32\x2d\x34\x37\x2e\x39\x31\x2d\x31\x30\x2d\x33\x2e\x39\x2d\ -\x31\x31\x2e\x39\x2d\x31\x2e\x33\x38\x2d\x32\x32\x2e\x38\x2c\x36\ -\x2e\x38\x39\x2d\x33\x31\x2e\x35\x32\x2c\x34\x31\x2d\x34\x33\x2e\ -\x32\x34\x2c\x38\x39\x2e\x37\x35\x2d\x37\x30\x2e\x34\x38\x2c\x31\ -\x34\x36\x2e\x38\x34\x2d\x37\x39\x2e\x32\x38\x2c\x37\x35\x2e\x36\ -\x2d\x31\x31\x2e\x36\x36\x2c\x31\x34\x33\x2e\x36\x39\x2c\x38\x2c\ -\x32\x30\x33\x2e\x39\x31\x2c\x35\x38\x2e\x33\x36\x61\x32\x30\x35\ -\x2c\x32\x30\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x33\x2e\x32\x35\ -\x2c\x32\x32\x2e\x37\x63\x38\x2e\x30\x38\x2c\x39\x2e\x32\x39\x2c\ -\x39\x2e\x35\x2c\x32\x30\x2e\x36\x31\x2c\x34\x2e\x35\x39\x2c\x33\ -\x32\x2e\x33\x33\x43\x34\x38\x37\x2e\x34\x34\x2c\x33\x34\x33\x2e\ -\x30\x35\x2c\x34\x37\x38\x2e\x36\x32\x2c\x33\x34\x38\x2e\x34\x39\ -\x2c\x34\x36\x39\x2e\x38\x37\x2c\x33\x34\x39\x2e\x30\x35\x5a\x22\ -\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x31\x38\x34\x2e\x35\x32\ -\x2c\x34\x30\x34\x2e\x32\x33\x63\x30\x2d\x39\x2e\x32\x32\x2c\x33\ -\x2e\x35\x37\x2d\x31\x36\x2e\x36\x34\x2c\x39\x2e\x36\x31\x2d\x32\ -\x32\x2e\x38\x31\x43\x32\x31\x38\x2c\x33\x35\x37\x2e\x30\x38\x2c\ -\x32\x34\x36\x2c\x33\x34\x31\x2e\x36\x31\x2c\x32\x37\x38\x2e\x37\ -\x37\x2c\x33\x33\x37\x63\x34\x38\x2e\x36\x36\x2d\x36\x2e\x38\x36\ -\x2c\x39\x30\x2e\x38\x33\x2c\x38\x2e\x32\x35\x2c\x31\x32\x36\x2e\ -\x36\x33\x2c\x34\x33\x2e\x39\x35\x2c\x31\x30\x2e\x31\x38\x2c\x31\ -\x30\x2e\x31\x35\x2c\x31\x32\x2e\x38\x31\x2c\x32\x34\x2c\x37\x2e\ -\x34\x35\x2c\x33\x36\x2e\x30\x35\x2d\x38\x2e\x34\x34\x2c\x31\x39\ -\x2d\x33\x31\x2c\x32\x33\x2e\x34\x35\x2d\x34\x35\x2e\x33\x32\x2c\ -\x38\x2e\x36\x37\x2d\x31\x33\x2e\x34\x2d\x31\x33\x2e\x38\x34\x2d\ -\x32\x38\x2e\x38\x2d\x32\x33\x2e\x36\x34\x2d\x34\x37\x2e\x31\x31\ -\x2d\x32\x37\x2e\x35\x35\x2d\x33\x33\x2e\x32\x32\x2d\x37\x2e\x30\ -\x39\x2d\x36\x32\x2e\x33\x36\x2c\x31\x2e\x36\x35\x2d\x38\x37\x2c\ -\x32\x36\x2e\x37\x37\x2d\x31\x36\x2e\x36\x36\x2c\x31\x37\x2d\x34\ -\x32\x2e\x33\x2c\x31\x30\x2d\x34\x37\x2e\x39\x33\x2d\x31\x33\x2e\ -\x32\x37\x41\x36\x38\x2e\x39\x33\x2c\x36\x38\x2e\x39\x33\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x31\x38\x34\x2e\x35\x32\x2c\x34\x30\x34\x2e\ -\x32\x33\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x30\ -\x30\x2c\x35\x33\x39\x2e\x38\x34\x63\x2d\x32\x32\x2c\x30\x2d\x33\ -\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x35\x2d\x33\x39\x2e\x31\x31\ -\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\x2e\x33\x38\x2c\x31\ -\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\x2e\x38\x36\ -\x2d\x34\x31\x2e\x37\x39\x2c\x32\x32\x2e\x32\x35\x2c\x30\x2c\x33\ -\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x37\x2c\x33\x39\x2e\x33\x34\ -\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\x33\x39\ -\x2e\x38\x33\x2c\x33\x30\x30\x2c\x35\x33\x39\x2e\x38\x34\x5a\x22\ -\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\x31\x38\x2c\x37\x37\ +\x43\x34\x30\x32\x2c\x37\x38\x2e\x37\x37\x2c\x34\x39\x30\x2e\x31\ +\x2c\x31\x31\x37\x2e\x30\x37\x2c\x35\x36\x33\x2e\x39\x2c\x31\x39\ +\x33\x2e\x30\x39\x63\x31\x34\x2e\x38\x2c\x31\x35\x2e\x32\x35\x2c\ +\x31\x31\x2e\x38\x31\x2c\x33\x33\x2e\x39\x33\x2c\x33\x2e\x32\x36\ +\x2c\x34\x34\x2d\x31\x31\x2e\x31\x32\x2c\x31\x33\x2e\x31\x37\x2d\ +\x32\x38\x2e\x36\x32\x2c\x31\x33\x2d\x34\x31\x2e\x34\x36\x2e\x38\ +\x33\x2d\x31\x35\x2e\x30\x38\x2d\x31\x34\x2e\x32\x37\x2d\x33\x30\ +\x2e\x32\x2d\x32\x38\x2e\x37\x31\x2d\x34\x36\x2e\x36\x31\x2d\x34\ +\x31\x2e\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\x2d\x38\x31\x2e\x33\ +\x33\x2d\x34\x36\x2e\x37\x35\x2d\x31\x32\x37\x2e\x37\x35\x2d\x35\ +\x34\x2e\x36\x35\x2d\x35\x34\x2d\x39\x2e\x31\x39\x2d\x31\x30\x36\ +\x2e\x39\x32\x2d\x34\x2e\x33\x31\x2d\x31\x35\x38\x2e\x35\x2c\x31\ +\x35\x2e\x32\x33\x2d\x34\x35\x2c\x31\x37\x2e\x30\x35\x2d\x38\x34\ +\x2e\x32\x39\x2c\x34\x33\x2e\x39\x33\x2d\x31\x31\x38\x2e\x31\x36\ +\x2c\x37\x39\x2e\x39\x33\x2d\x38\x2e\x35\x37\x2c\x39\x2e\x31\x31\ +\x2d\x31\x38\x2e\x36\x35\x2c\x31\x32\x2e\x33\x32\x2d\x33\x30\x2e\ +\x31\x39\x2c\x38\x2d\x32\x30\x2e\x30\x39\x2d\x37\x2e\x35\x37\x2d\ +\x32\x35\x2e\x32\x2d\x33\x34\x2e\x30\x39\x2d\x39\x2e\x37\x34\x2d\ +\x35\x30\x2e\x36\x31\x61\x33\x38\x30\x2c\x33\x38\x30\x2c\x30\x2c\ +\x30\x2c\x31\x2c\x35\x37\x2e\x36\x32\x2d\x35\x30\x2e\x33\x38\x63\ +\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\x38\x39\x2e\x39\x33\x2d\x35\ +\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\x37\x2d\x36\x30\x2e\x34\x36\ +\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x39\x2e\x38\x37\x2c\x32\x37\ +\x37\x2e\x34\x33\x2c\x37\x38\x2e\x34\x38\x2c\x32\x39\x30\x2e\x31\ +\x38\x2c\x37\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ +\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\ +\x34\x36\x39\x2e\x38\x37\x2c\x33\x33\x32\x2e\x32\x31\x63\x2d\x31\ +\x31\x2c\x2e\x31\x38\x2d\x31\x37\x2e\x38\x33\x2d\x33\x2e\x30\x37\ +\x2d\x32\x33\x2e\x35\x32\x2d\x39\x2e\x31\x32\x43\x34\x31\x34\x2c\ +\x32\x38\x38\x2e\x36\x35\x2c\x33\x37\x35\x2e\x32\x32\x2c\x32\x36\ +\x37\x2e\x31\x36\x2c\x33\x33\x30\x2e\x31\x2c\x32\x36\x30\x2e\x36\ +\x38\x63\x2d\x36\x37\x2e\x33\x37\x2d\x39\x2e\x36\x37\x2d\x31\x32\ +\x36\x2e\x31\x37\x2c\x31\x30\x2e\x38\x33\x2d\x31\x37\x35\x2e\x33\ +\x39\x2c\x36\x31\x2e\x34\x31\x2d\x31\x36\x2e\x33\x35\x2c\x31\x36\ +\x2e\x38\x2d\x34\x30\x2e\x36\x37\x2c\x31\x32\x2d\x34\x37\x2e\x39\ +\x31\x2d\x31\x30\x2d\x33\x2e\x39\x2d\x31\x31\x2e\x39\x2d\x31\x2e\ +\x33\x38\x2d\x32\x32\x2e\x38\x2c\x36\x2e\x38\x39\x2d\x33\x31\x2e\ +\x35\x32\x2c\x34\x31\x2d\x34\x33\x2e\x32\x34\x2c\x38\x39\x2e\x37\ +\x35\x2d\x37\x30\x2e\x34\x38\x2c\x31\x34\x36\x2e\x38\x34\x2d\x37\ +\x39\x2e\x32\x38\x2c\x37\x35\x2e\x36\x2d\x31\x31\x2e\x36\x36\x2c\ +\x31\x34\x33\x2e\x36\x39\x2c\x38\x2e\x30\x35\x2c\x32\x30\x33\x2e\ +\x39\x31\x2c\x35\x38\x2e\x33\x36\x61\x32\x30\x35\x2e\x37\x34\x2c\ +\x32\x30\x35\x2e\x37\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x33\x2e\ +\x32\x35\x2c\x32\x32\x2e\x36\x39\x63\x38\x2e\x30\x38\x2c\x39\x2e\ +\x33\x2c\x39\x2e\x35\x2c\x32\x30\x2e\x36\x32\x2c\x34\x2e\x35\x39\ +\x2c\x33\x32\x2e\x33\x34\x53\x34\x37\x38\x2e\x36\x32\x2c\x33\x33\ +\x31\x2e\x36\x36\x2c\x34\x36\x39\x2e\x38\x37\x2c\x33\x33\x32\x2e\ +\x32\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ +\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x31\x38\ +\x34\x2e\x35\x32\x2c\x33\x38\x37\x2e\x34\x63\x30\x2d\x39\x2e\x32\ +\x32\x2c\x33\x2e\x35\x37\x2d\x31\x36\x2e\x36\x35\x2c\x39\x2e\x36\ +\x31\x2d\x32\x32\x2e\x38\x31\x43\x32\x31\x38\x2c\x33\x34\x30\x2e\ +\x32\x34\x2c\x32\x34\x36\x2c\x33\x32\x34\x2e\x37\x38\x2c\x32\x37\ +\x38\x2e\x37\x37\x2c\x33\x32\x30\x2e\x31\x35\x63\x34\x38\x2e\x36\ +\x36\x2d\x36\x2e\x38\x36\x2c\x39\x30\x2e\x38\x33\x2c\x38\x2e\x32\ +\x35\x2c\x31\x32\x36\x2e\x36\x33\x2c\x34\x34\x2c\x31\x30\x2e\x31\ +\x38\x2c\x31\x30\x2e\x31\x35\x2c\x31\x32\x2e\x38\x31\x2c\x32\x34\ +\x2c\x37\x2e\x34\x35\x2c\x33\x36\x2e\x30\x35\x2d\x38\x2e\x34\x34\ +\x2c\x31\x39\x2d\x33\x31\x2c\x32\x33\x2e\x34\x35\x2d\x34\x35\x2e\ +\x33\x32\x2c\x38\x2e\x36\x36\x2d\x31\x33\x2e\x34\x2d\x31\x33\x2e\ +\x38\x33\x2d\x32\x38\x2e\x38\x2d\x32\x33\x2e\x36\x33\x2d\x34\x37\ +\x2e\x31\x31\x2d\x32\x37\x2e\x35\x34\x2d\x33\x33\x2e\x32\x32\x2d\ +\x37\x2e\x31\x2d\x36\x32\x2e\x33\x36\x2c\x31\x2e\x36\x35\x2d\x38\ +\x37\x2c\x32\x36\x2e\x37\x37\x2d\x31\x36\x2e\x36\x36\x2c\x31\x37\ +\x2d\x34\x32\x2e\x33\x2c\x31\x30\x2d\x34\x37\x2e\x39\x33\x2d\x31\ +\x33\x2e\x32\x37\x41\x36\x39\x2e\x32\x38\x2c\x36\x39\x2e\x32\x38\ +\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x38\x34\x2e\x35\x32\x2c\x33\x38\ +\x37\x2e\x34\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ +\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\ +\x30\x30\x2c\x35\x32\x33\x63\x2d\x32\x32\x2c\x30\x2d\x33\x39\x2e\ +\x31\x35\x2d\x31\x38\x2e\x34\x36\x2d\x33\x39\x2e\x31\x31\x2d\x34\ +\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\x2e\x33\x38\x2c\x31\x37\x2e\ +\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\x2e\x38\x36\x2d\x34\ +\x31\x2e\x38\x2c\x32\x32\x2e\x32\x35\x2c\x30\x2c\x33\x39\x2e\x33\ +\x32\x2c\x31\x38\x2e\x31\x38\x2c\x33\x39\x2e\x33\x34\x2c\x34\x31\ +\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\x32\x33\x2c\x33\x30\ +\x30\x2c\x35\x32\x33\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x05\x95\ +\x3c\ +\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ +\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ +\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ +\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ +\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ +\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ +\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ +\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\ +\x35\x34\x30\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ +\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ +\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ +\x31\x38\x2c\x37\x37\x43\x34\x30\x32\x2c\x37\x38\x2e\x37\x37\x2c\ +\x34\x39\x30\x2e\x31\x2c\x31\x31\x37\x2e\x30\x37\x2c\x35\x36\x33\ +\x2e\x39\x2c\x31\x39\x33\x2e\x30\x39\x63\x31\x34\x2e\x38\x2c\x31\ +\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\x2e\x39\x33\ +\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\x31\x2e\x31\x32\x2c\x31\ +\x33\x2e\x31\x37\x2d\x32\x38\x2e\x36\x32\x2c\x31\x33\x2d\x34\x31\ +\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\x30\x38\x2d\x31\x34\x2e\ +\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\x2e\x37\x31\x2d\x34\x36\ +\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\ +\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\x37\x35\x2d\x31\x32\x37\ +\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\x35\x34\x2d\x39\x2e\x31\ +\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\x2e\x33\x31\x2d\x31\x35\ +\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x2d\x34\x35\x2c\x31\x37\x2e\ +\x30\x35\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\x33\x2d\x31\ +\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x33\x2d\x38\x2e\x35\x37\ +\x2c\x39\x2e\x31\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\x32\x2e\x33\ +\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\x30\x39\x2d\ +\x37\x2e\x35\x37\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\x30\x39\x2d\ +\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\x33\x38\x30\x2c\x33\ +\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x37\x2e\x36\x32\x2d\x35\ +\x30\x2e\x33\x38\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\x38\x39\ +\x2e\x39\x33\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\x37\x2d\ +\x36\x30\x2e\x34\x36\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x39\x2e\ +\x38\x37\x2c\x32\x37\x37\x2e\x34\x33\x2c\x37\x38\x2e\x34\x38\x2c\ +\x32\x39\x30\x2e\x31\x38\x2c\x37\x37\x5a\x22\x2f\x3e\x3c\x70\x61\ +\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\ +\x20\x64\x3d\x22\x4d\x34\x36\x39\x2e\x38\x37\x2c\x33\x33\x32\x2e\ +\x32\x31\x63\x2d\x31\x31\x2c\x2e\x31\x38\x2d\x31\x37\x2e\x38\x33\ +\x2d\x33\x2e\x30\x37\x2d\x32\x33\x2e\x35\x32\x2d\x39\x2e\x31\x32\ +\x43\x34\x31\x34\x2c\x32\x38\x38\x2e\x36\x35\x2c\x33\x37\x35\x2e\ +\x32\x32\x2c\x32\x36\x37\x2e\x31\x36\x2c\x33\x33\x30\x2e\x31\x2c\ +\x32\x36\x30\x2e\x36\x38\x63\x2d\x36\x37\x2e\x33\x37\x2d\x39\x2e\ +\x36\x37\x2d\x31\x32\x36\x2e\x31\x37\x2c\x31\x30\x2e\x38\x33\x2d\ +\x31\x37\x35\x2e\x33\x39\x2c\x36\x31\x2e\x34\x31\x2d\x31\x36\x2e\ +\x33\x35\x2c\x31\x36\x2e\x38\x2d\x34\x30\x2e\x36\x37\x2c\x31\x32\ +\x2d\x34\x37\x2e\x39\x31\x2d\x31\x30\x2d\x33\x2e\x39\x2d\x31\x31\ +\x2e\x39\x2d\x31\x2e\x33\x38\x2d\x32\x32\x2e\x38\x2c\x36\x2e\x38\ +\x39\x2d\x33\x31\x2e\x35\x32\x2c\x34\x31\x2d\x34\x33\x2e\x32\x34\ +\x2c\x38\x39\x2e\x37\x35\x2d\x37\x30\x2e\x34\x38\x2c\x31\x34\x36\ +\x2e\x38\x34\x2d\x37\x39\x2e\x32\x38\x2c\x37\x35\x2e\x36\x2d\x31\ +\x31\x2e\x36\x36\x2c\x31\x34\x33\x2e\x36\x39\x2c\x38\x2e\x30\x35\ +\x2c\x32\x30\x33\x2e\x39\x31\x2c\x35\x38\x2e\x33\x36\x61\x32\x30\ +\x35\x2e\x37\x34\x2c\x32\x30\x35\x2e\x37\x34\x2c\x30\x2c\x30\x2c\ +\x31\x2c\x32\x33\x2e\x32\x35\x2c\x32\x32\x2e\x36\x39\x63\x38\x2e\ +\x30\x38\x2c\x39\x2e\x33\x2c\x39\x2e\x35\x2c\x32\x30\x2e\x36\x32\ +\x2c\x34\x2e\x35\x39\x2c\x33\x32\x2e\x33\x34\x53\x34\x37\x38\x2e\ +\x36\x32\x2c\x33\x33\x31\x2e\x36\x36\x2c\x34\x36\x39\x2e\x38\x37\ +\x2c\x33\x33\x32\x2e\x32\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ +\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\ +\x3d\x22\x4d\x31\x38\x34\x2e\x35\x32\x2c\x33\x38\x37\x2e\x34\x63\ +\x30\x2d\x39\x2e\x32\x32\x2c\x33\x2e\x35\x37\x2d\x31\x36\x2e\x36\ +\x35\x2c\x39\x2e\x36\x31\x2d\x32\x32\x2e\x38\x31\x43\x32\x31\x38\ +\x2c\x33\x34\x30\x2e\x32\x34\x2c\x32\x34\x36\x2c\x33\x32\x34\x2e\ +\x37\x38\x2c\x32\x37\x38\x2e\x37\x37\x2c\x33\x32\x30\x2e\x31\x35\ +\x63\x34\x38\x2e\x36\x36\x2d\x36\x2e\x38\x36\x2c\x39\x30\x2e\x38\ +\x33\x2c\x38\x2e\x32\x35\x2c\x31\x32\x36\x2e\x36\x33\x2c\x34\x34\ +\x2c\x31\x30\x2e\x31\x38\x2c\x31\x30\x2e\x31\x35\x2c\x31\x32\x2e\ +\x38\x31\x2c\x32\x34\x2c\x37\x2e\x34\x35\x2c\x33\x36\x2e\x30\x35\ +\x2d\x38\x2e\x34\x34\x2c\x31\x39\x2d\x33\x31\x2c\x32\x33\x2e\x34\ +\x35\x2d\x34\x35\x2e\x33\x32\x2c\x38\x2e\x36\x36\x2d\x31\x33\x2e\ +\x34\x2d\x31\x33\x2e\x38\x33\x2d\x32\x38\x2e\x38\x2d\x32\x33\x2e\ +\x36\x33\x2d\x34\x37\x2e\x31\x31\x2d\x32\x37\x2e\x35\x34\x2d\x33\ +\x33\x2e\x32\x32\x2d\x37\x2e\x31\x2d\x36\x32\x2e\x33\x36\x2c\x31\ +\x2e\x36\x35\x2d\x38\x37\x2c\x32\x36\x2e\x37\x37\x2d\x31\x36\x2e\ +\x36\x36\x2c\x31\x37\x2d\x34\x32\x2e\x33\x2c\x31\x30\x2d\x34\x37\ +\x2e\x39\x33\x2d\x31\x33\x2e\x32\x37\x41\x36\x39\x2e\x32\x38\x2c\ +\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x38\x34\x2e\ +\x35\x32\x2c\x33\x38\x37\x2e\x34\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ +\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\ +\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x32\x33\x63\x2d\x32\x32\x2c\ +\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x36\x2d\x33\x39\ +\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\x2e\x33\ +\x38\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\ +\x2e\x38\x36\x2d\x34\x31\x2e\x38\x2c\x32\x32\x2e\x32\x35\x2c\x30\ +\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x38\x2c\x33\x39\x2e\ +\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ +\x32\x33\x2c\x33\x30\x30\x2c\x35\x32\x33\x5a\x22\x2f\x3e\x3c\x2f\ +\x73\x76\x67\x3e\ \x00\x00\x09\xc5\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -20357,7 +20595,7 @@ \x32\x37\x2e\x37\x35\x2c\x34\x31\x36\x2e\x31\x31\x2c\x35\x32\x38\ \x2e\x32\x39\x2c\x34\x31\x36\x2e\x36\x38\x5a\x22\x2f\x3e\x3c\x2f\ \x73\x76\x67\x3e\ -\x00\x00\x03\xdf\ +\x00\x00\x05\x95\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ \x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ @@ -20366,61 +20604,258 @@ \x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ \x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ \x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x33\x38\x62\x33\x34\x61\x3b\x7d\ -\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x31\x22\x20\x64\x3d\x22\x4d\x34\x36\x38\x2e\x35\x31\x2c\x33\x35\ -\x30\x2e\x35\x31\x63\x2d\x31\x30\x2e\x39\x31\x2e\x31\x38\x2d\x31\ -\x37\x2e\x36\x39\x2d\x33\x2e\x30\x35\x2d\x32\x33\x2e\x33\x34\x2d\ -\x39\x2e\x30\x36\x2d\x33\x32\x2e\x31\x32\x2d\x33\x34\x2e\x31\x38\ -\x2d\x37\x30\x2e\x35\x39\x2d\x35\x35\x2e\x35\x2d\x31\x31\x35\x2e\ -\x33\x36\x2d\x36\x31\x2e\x39\x33\x2d\x36\x36\x2e\x38\x35\x2d\x39\ -\x2e\x35\x39\x2d\x31\x32\x35\x2e\x32\x2c\x31\x30\x2e\x37\x35\x2d\ -\x31\x37\x34\x2c\x36\x30\x2e\x39\x34\x2d\x31\x36\x2e\x32\x32\x2c\ -\x31\x36\x2e\x36\x37\x2d\x34\x30\x2e\x33\x36\x2c\x31\x31\x2e\x39\ -\x31\x2d\x34\x37\x2e\x35\x34\x2d\x31\x30\x2d\x33\x2e\x38\x38\x2d\ -\x31\x31\x2e\x38\x2d\x31\x2e\x33\x37\x2d\x32\x32\x2e\x36\x32\x2c\ -\x36\x2e\x38\x34\x2d\x33\x31\x2e\x32\x37\x2c\x34\x30\x2e\x36\x38\ -\x2d\x34\x32\x2e\x39\x31\x2c\x38\x39\x2e\x30\x36\x2d\x36\x39\x2e\ -\x39\x34\x2c\x31\x34\x35\x2e\x37\x31\x2d\x37\x38\x2e\x36\x38\x2c\ -\x37\x35\x2d\x31\x31\x2e\x35\x37\x2c\x31\x34\x32\x2e\x35\x39\x2c\ -\x38\x2c\x32\x30\x32\x2e\x33\x35\x2c\x35\x37\x2e\x39\x32\x41\x32\ -\x30\x33\x2e\x34\x37\x2c\x32\x30\x33\x2e\x34\x37\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x34\x38\x36\x2e\x31\x39\x2c\x33\x30\x31\x63\x38\x2c\ -\x39\x2e\x32\x32\x2c\x39\x2e\x34\x33\x2c\x32\x30\x2e\x34\x36\x2c\ -\x34\x2e\x35\x36\x2c\x33\x32\x2e\x30\x38\x53\x34\x37\x37\x2e\x31\ -\x39\x2c\x33\x35\x30\x2c\x34\x36\x38\x2e\x35\x31\x2c\x33\x35\x30\ -\x2e\x35\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x31\ -\x38\x35\x2e\x33\x36\x2c\x34\x30\x35\x2e\x32\x37\x63\x30\x2d\x39\ -\x2e\x31\x35\x2c\x33\x2e\x35\x35\x2d\x31\x36\x2e\x35\x32\x2c\x39\ -\x2e\x35\x34\x2d\x32\x32\x2e\x36\x34\x2c\x32\x33\x2e\x36\x36\x2d\ -\x32\x34\x2e\x31\x35\x2c\x35\x31\x2e\x34\x34\x2d\x33\x39\x2e\x35\ -\x2c\x38\x34\x2d\x34\x34\x2e\x30\x39\x2c\x34\x38\x2e\x32\x38\x2d\ -\x36\x2e\x38\x31\x2c\x39\x30\x2e\x31\x34\x2c\x38\x2e\x31\x39\x2c\ -\x31\x32\x35\x2e\x36\x37\x2c\x34\x33\x2e\x36\x32\x2c\x31\x30\x2e\ -\x30\x39\x2c\x31\x30\x2e\x30\x37\x2c\x31\x32\x2e\x37\x2c\x32\x33\ -\x2e\x38\x32\x2c\x37\x2e\x33\x39\x2c\x33\x35\x2e\x37\x37\x2d\x38\ -\x2e\x33\x39\x2c\x31\x38\x2e\x38\x34\x2d\x33\x30\x2e\x37\x37\x2c\ -\x32\x33\x2e\x32\x37\x2d\x34\x35\x2c\x38\x2e\x36\x2d\x31\x33\x2e\ -\x33\x2d\x31\x33\x2e\x37\x33\x2d\x32\x38\x2e\x35\x38\x2d\x32\x33\ -\x2e\x34\x35\x2d\x34\x36\x2e\x37\x35\x2d\x32\x37\x2e\x33\x33\x2d\ -\x33\x33\x2d\x37\x2e\x30\x35\x2d\x36\x31\x2e\x38\x38\x2c\x31\x2e\ -\x36\x34\x2d\x38\x36\x2e\x33\x2c\x32\x36\x2e\x35\x36\x2d\x31\x36\ -\x2e\x35\x34\x2c\x31\x36\x2e\x38\x38\x2d\x34\x32\x2c\x39\x2e\x39\ -\x33\x2d\x34\x37\x2e\x35\x37\x2d\x31\x33\x2e\x31\x37\x41\x37\x30\ -\x2e\x34\x31\x2c\x37\x30\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x31\x38\x35\x2e\x33\x36\x2c\x34\x30\x35\x2e\x32\x37\x5a\x22\x2f\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x33\x39\ -\x2e\x38\x34\x63\x2d\x32\x31\x2e\x38\x33\x2c\x30\x2d\x33\x38\x2e\ -\x38\x34\x2d\x31\x38\x2e\x33\x31\x2d\x33\x38\x2e\x38\x31\x2d\x34\ -\x31\x2e\x37\x39\x2c\x30\x2d\x32\x33\x2e\x31\x39\x2c\x31\x37\x2e\ -\x30\x36\x2d\x34\x31\x2e\x35\x31\x2c\x33\x38\x2e\x35\x36\x2d\x34\ -\x31\x2e\x34\x37\x2c\x32\x32\x2e\x30\x39\x2c\x30\x2c\x33\x39\x2c\ -\x31\x38\x2c\x33\x39\x2c\x34\x31\x2e\x34\x39\x53\x33\x32\x31\x2e\ -\x39\x31\x2c\x35\x33\x39\x2e\x38\x33\x2c\x33\x30\x30\x2c\x35\x33\ -\x39\x2e\x38\x34\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ +\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\ +\x35\x34\x30\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ +\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ +\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ +\x31\x38\x2c\x37\x37\x43\x34\x30\x32\x2c\x37\x38\x2e\x37\x37\x2c\ +\x34\x39\x30\x2e\x31\x2c\x31\x31\x37\x2e\x30\x37\x2c\x35\x36\x33\ +\x2e\x39\x2c\x31\x39\x33\x2e\x30\x39\x63\x31\x34\x2e\x38\x2c\x31\ +\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\x2e\x39\x33\ +\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\x31\x2e\x31\x32\x2c\x31\ +\x33\x2e\x31\x37\x2d\x32\x38\x2e\x36\x32\x2c\x31\x33\x2d\x34\x31\ +\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\x30\x38\x2d\x31\x34\x2e\ +\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\x2e\x37\x31\x2d\x34\x36\ +\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\ +\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\x37\x35\x2d\x31\x32\x37\ +\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\x35\x34\x2d\x39\x2e\x31\ +\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\x2e\x33\x31\x2d\x31\x35\ +\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x2d\x34\x35\x2c\x31\x37\x2e\ +\x30\x35\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\x33\x2d\x31\ +\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x33\x2d\x38\x2e\x35\x37\ +\x2c\x39\x2e\x31\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\x32\x2e\x33\ +\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\x30\x39\x2d\ +\x37\x2e\x35\x37\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\x30\x39\x2d\ +\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\x33\x38\x30\x2c\x33\ +\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x37\x2e\x36\x32\x2d\x35\ +\x30\x2e\x33\x38\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\x38\x39\ +\x2e\x39\x33\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\x37\x2d\ +\x36\x30\x2e\x34\x36\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x39\x2e\ +\x38\x37\x2c\x32\x37\x37\x2e\x34\x33\x2c\x37\x38\x2e\x34\x38\x2c\ +\x32\x39\x30\x2e\x31\x38\x2c\x37\x37\x5a\x22\x2f\x3e\x3c\x70\x61\ +\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\ +\x20\x64\x3d\x22\x4d\x34\x36\x39\x2e\x38\x37\x2c\x33\x33\x32\x2e\ +\x32\x31\x63\x2d\x31\x31\x2c\x2e\x31\x38\x2d\x31\x37\x2e\x38\x33\ +\x2d\x33\x2e\x30\x37\x2d\x32\x33\x2e\x35\x32\x2d\x39\x2e\x31\x32\ +\x43\x34\x31\x34\x2c\x32\x38\x38\x2e\x36\x35\x2c\x33\x37\x35\x2e\ +\x32\x32\x2c\x32\x36\x37\x2e\x31\x36\x2c\x33\x33\x30\x2e\x31\x2c\ +\x32\x36\x30\x2e\x36\x38\x63\x2d\x36\x37\x2e\x33\x37\x2d\x39\x2e\ +\x36\x37\x2d\x31\x32\x36\x2e\x31\x37\x2c\x31\x30\x2e\x38\x33\x2d\ +\x31\x37\x35\x2e\x33\x39\x2c\x36\x31\x2e\x34\x31\x2d\x31\x36\x2e\ +\x33\x35\x2c\x31\x36\x2e\x38\x2d\x34\x30\x2e\x36\x37\x2c\x31\x32\ +\x2d\x34\x37\x2e\x39\x31\x2d\x31\x30\x2d\x33\x2e\x39\x2d\x31\x31\ +\x2e\x39\x2d\x31\x2e\x33\x38\x2d\x32\x32\x2e\x38\x2c\x36\x2e\x38\ +\x39\x2d\x33\x31\x2e\x35\x32\x2c\x34\x31\x2d\x34\x33\x2e\x32\x34\ +\x2c\x38\x39\x2e\x37\x35\x2d\x37\x30\x2e\x34\x38\x2c\x31\x34\x36\ +\x2e\x38\x34\x2d\x37\x39\x2e\x32\x38\x2c\x37\x35\x2e\x36\x2d\x31\ +\x31\x2e\x36\x36\x2c\x31\x34\x33\x2e\x36\x39\x2c\x38\x2e\x30\x35\ +\x2c\x32\x30\x33\x2e\x39\x31\x2c\x35\x38\x2e\x33\x36\x61\x32\x30\ +\x35\x2e\x37\x34\x2c\x32\x30\x35\x2e\x37\x34\x2c\x30\x2c\x30\x2c\ +\x31\x2c\x32\x33\x2e\x32\x35\x2c\x32\x32\x2e\x36\x39\x63\x38\x2e\ +\x30\x38\x2c\x39\x2e\x33\x2c\x39\x2e\x35\x2c\x32\x30\x2e\x36\x32\ +\x2c\x34\x2e\x35\x39\x2c\x33\x32\x2e\x33\x34\x53\x34\x37\x38\x2e\ +\x36\x32\x2c\x33\x33\x31\x2e\x36\x36\x2c\x34\x36\x39\x2e\x38\x37\ +\x2c\x33\x33\x32\x2e\x32\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ +\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\ +\x3d\x22\x4d\x31\x38\x34\x2e\x35\x32\x2c\x33\x38\x37\x2e\x34\x63\ +\x30\x2d\x39\x2e\x32\x32\x2c\x33\x2e\x35\x37\x2d\x31\x36\x2e\x36\ +\x35\x2c\x39\x2e\x36\x31\x2d\x32\x32\x2e\x38\x31\x43\x32\x31\x38\ +\x2c\x33\x34\x30\x2e\x32\x34\x2c\x32\x34\x36\x2c\x33\x32\x34\x2e\ +\x37\x38\x2c\x32\x37\x38\x2e\x37\x37\x2c\x33\x32\x30\x2e\x31\x35\ +\x63\x34\x38\x2e\x36\x36\x2d\x36\x2e\x38\x36\x2c\x39\x30\x2e\x38\ +\x33\x2c\x38\x2e\x32\x35\x2c\x31\x32\x36\x2e\x36\x33\x2c\x34\x34\ +\x2c\x31\x30\x2e\x31\x38\x2c\x31\x30\x2e\x31\x35\x2c\x31\x32\x2e\ +\x38\x31\x2c\x32\x34\x2c\x37\x2e\x34\x35\x2c\x33\x36\x2e\x30\x35\ +\x2d\x38\x2e\x34\x34\x2c\x31\x39\x2d\x33\x31\x2c\x32\x33\x2e\x34\ +\x35\x2d\x34\x35\x2e\x33\x32\x2c\x38\x2e\x36\x36\x2d\x31\x33\x2e\ +\x34\x2d\x31\x33\x2e\x38\x33\x2d\x32\x38\x2e\x38\x2d\x32\x33\x2e\ +\x36\x33\x2d\x34\x37\x2e\x31\x31\x2d\x32\x37\x2e\x35\x34\x2d\x33\ +\x33\x2e\x32\x32\x2d\x37\x2e\x31\x2d\x36\x32\x2e\x33\x36\x2c\x31\ +\x2e\x36\x35\x2d\x38\x37\x2c\x32\x36\x2e\x37\x37\x2d\x31\x36\x2e\ +\x36\x36\x2c\x31\x37\x2d\x34\x32\x2e\x33\x2c\x31\x30\x2d\x34\x37\ +\x2e\x39\x33\x2d\x31\x33\x2e\x32\x37\x41\x36\x39\x2e\x32\x38\x2c\ +\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x38\x34\x2e\ +\x35\x32\x2c\x33\x38\x37\x2e\x34\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ +\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\ +\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x32\x33\x63\x2d\x32\x32\x2c\ +\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x36\x2d\x33\x39\ +\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\x2e\x33\ +\x38\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\ +\x2e\x38\x36\x2d\x34\x31\x2e\x38\x2c\x32\x32\x2e\x32\x35\x2c\x30\ +\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x38\x2c\x33\x39\x2e\ +\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ +\x32\x33\x2c\x33\x30\x30\x2c\x35\x32\x33\x5a\x22\x2f\x3e\x3c\x2f\ +\x73\x76\x67\x3e\ +\x00\x00\x0a\x70\ +\x3c\ +\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ +\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ +\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ +\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ +\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ +\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ +\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ +\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\ +\x35\x34\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x66\x69\x6c\x6c\ +\x3a\x23\x39\x32\x39\x34\x39\x37\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\ +\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\ +\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\ +\x4d\x32\x39\x30\x2e\x31\x38\x2c\x37\x31\x2e\x33\x35\x43\x34\x30\ +\x32\x2c\x37\x33\x2e\x31\x34\x2c\x34\x39\x30\x2e\x31\x2c\x31\x31\ +\x31\x2e\x34\x34\x2c\x35\x36\x33\x2e\x39\x2c\x31\x38\x37\x2e\x34\ +\x35\x63\x31\x34\x2e\x38\x2c\x31\x35\x2e\x32\x35\x2c\x31\x31\x2e\ +\x38\x31\x2c\x33\x33\x2e\x39\x33\x2c\x33\x2e\x32\x36\x2c\x34\x34\ +\x2e\x30\x35\x2d\x31\x31\x2e\x31\x32\x2c\x31\x33\x2e\x31\x36\x2d\ +\x32\x38\x2e\x36\x32\x2c\x31\x33\x2d\x34\x31\x2e\x34\x36\x2e\x38\ +\x33\x2d\x31\x35\x2e\x30\x38\x2d\x31\x34\x2e\x32\x37\x2d\x33\x30\ +\x2e\x32\x2d\x32\x38\x2e\x37\x32\x2d\x34\x36\x2e\x36\x31\x2d\x34\ +\x31\x2e\x31\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\x2d\x38\x31\x2e\ +\x33\x33\x2d\x34\x36\x2e\x37\x34\x2d\x31\x32\x37\x2e\x37\x35\x2d\ +\x35\x34\x2e\x36\x34\x2d\x35\x34\x2d\x39\x2e\x32\x2d\x31\x30\x36\ +\x2e\x39\x32\x2d\x34\x2e\x33\x32\x2d\x31\x35\x38\x2e\x35\x2c\x31\ +\x35\x2e\x32\x32\x2d\x34\x35\x2c\x31\x37\x2d\x38\x34\x2e\x32\x39\ +\x2c\x34\x33\x2e\x39\x33\x2d\x31\x31\x38\x2e\x31\x36\x2c\x37\x39\ +\x2e\x39\x34\x2d\x38\x2e\x35\x37\x2c\x39\x2e\x31\x2d\x31\x38\x2e\ +\x36\x35\x2c\x31\x32\x2e\x33\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\ +\x2d\x32\x30\x2e\x30\x39\x2d\x37\x2e\x35\x36\x2d\x32\x35\x2e\x32\ +\x2d\x33\x34\x2e\x30\x38\x2d\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\ +\x31\x61\x33\x38\x30\x2e\x35\x31\x2c\x33\x38\x30\x2e\x35\x31\x2c\ +\x30\x2c\x30\x2c\x31\x2c\x35\x37\x2e\x36\x32\x2d\x35\x30\x2e\x33\ +\x38\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\x38\x39\x2e\x39\x33\ +\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\x37\x2d\x36\x30\x2e\ +\x34\x35\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x34\x2e\x32\x33\x2c\ +\x32\x37\x37\x2e\x34\x33\x2c\x37\x32\x2e\x38\x35\x2c\x32\x39\x30\ +\x2e\x31\x38\x2c\x37\x31\x2e\x33\x35\x5a\x22\x2f\x3e\x3c\x70\x61\ +\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\ +\x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x31\x37\x2e\x33\x37\x63\ +\x2d\x32\x32\x2c\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\ +\x35\x2d\x33\x39\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\ +\x32\x33\x2e\x33\x37\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\ +\x33\x2c\x33\x38\x2e\x38\x36\x2d\x34\x31\x2e\x37\x39\x2c\x32\x32\ +\x2e\x32\x35\x2c\x30\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\ +\x37\x2c\x33\x39\x2e\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\ +\x32\x2e\x31\x2c\x35\x31\x37\x2e\x33\x37\x2c\x33\x30\x30\x2c\x35\ +\x31\x37\x2e\x33\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\ +\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\ +\x4d\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x68\x31\ +\x34\x2e\x33\x35\x61\x31\x30\x2e\x36\x34\x2c\x31\x30\x2e\x36\x34\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x32\x2c\x31\x63\x32\x37\ +\x2e\x34\x37\x2c\x34\x2c\x34\x39\x2e\x36\x2c\x32\x36\x2e\x36\x37\ +\x2c\x35\x31\x2e\x36\x38\x2c\x35\x34\x2e\x32\x36\x2c\x31\x2e\x31\ +\x37\x2c\x31\x35\x2e\x35\x33\x2e\x35\x38\x2c\x33\x31\x2e\x32\x2e\ +\x37\x38\x2c\x34\x36\x2e\x38\x2c\x30\x2c\x32\x2e\x31\x32\x2c\x30\ +\x2c\x34\x2e\x32\x35\x2c\x30\x2c\x36\x2e\x38\x36\x68\x31\x31\x2e\ +\x37\x35\x63\x31\x33\x2e\x39\x34\x2c\x30\x2c\x31\x39\x2e\x33\x36\ +\x2c\x35\x2e\x33\x39\x2c\x31\x39\x2e\x33\x36\x2c\x31\x39\x2e\x32\ +\x32\x2c\x30\x2c\x33\x33\x2e\x32\x35\x2d\x2e\x31\x38\x2c\x36\x36\ +\x2e\x35\x2e\x31\x32\x2c\x39\x39\x2e\x37\x35\x2e\x30\x39\x2c\x39\ +\x2e\x36\x38\x2d\x32\x2e\x39\x33\x2c\x31\x36\x2e\x36\x37\x2d\x31\ +\x32\x2e\x31\x36\x2c\x32\x30\x2e\x33\x39\x48\x33\x37\x35\x2e\x31\ +\x32\x63\x2d\x37\x2e\x32\x34\x2d\x33\x2e\x31\x37\x2d\x31\x32\x2d\ +\x38\x2e\x31\x31\x2d\x31\x32\x2d\x31\x36\x2e\x35\x33\x2c\x30\x2d\ +\x33\x35\x2e\x34\x36\x2d\x2e\x30\x38\x2d\x37\x30\x2e\x39\x31\x2c\ +\x30\x2d\x31\x30\x36\x2e\x33\x37\x2c\x30\x2d\x31\x30\x2e\x32\x39\ +\x2c\x36\x2e\x32\x34\x2d\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\x35\ +\x36\x2d\x31\x36\x2e\x34\x34\x2c\x34\x2e\x36\x39\x2d\x2e\x30\x39\ +\x2c\x39\x2e\x33\x39\x2c\x30\x2c\x31\x34\x2e\x35\x31\x2c\x30\x2c\ +\x30\x2d\x31\x35\x2e\x37\x33\x2d\x2e\x30\x38\x2d\x33\x30\x2e\x35\ +\x39\x2c\x30\x2d\x34\x35\x2e\x34\x35\x2e\x31\x39\x2d\x32\x38\x2e\ +\x37\x39\x2c\x31\x37\x2e\x32\x38\x2d\x35\x32\x2e\x33\x36\x2c\x34\ +\x33\x2e\x38\x39\x2d\x36\x30\x2e\x35\x38\x43\x34\x34\x31\x2e\x37\ +\x31\x2c\x32\x38\x32\x2e\x31\x34\x2c\x34\x34\x35\x2e\x34\x31\x2c\ +\x32\x38\x31\x2e\x33\x33\x2c\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\ +\x30\x2e\x33\x37\x5a\x6d\x33\x38\x2e\x33\x31\x2c\x31\x30\x38\x2e\ +\x34\x39\x63\x30\x2d\x31\x36\x2e\x35\x34\x2e\x39\x34\x2d\x33\x32\ +\x2e\x37\x33\x2d\x2e\x32\x35\x2d\x34\x38\x2e\x37\x37\x2d\x31\x2e\ +\x33\x31\x2d\x31\x37\x2e\x38\x32\x2d\x31\x35\x2e\x39\x33\x2d\x32\ +\x39\x2e\x37\x35\x2d\x33\x32\x2e\x37\x37\x2d\x32\x38\x2e\x37\x37\ +\x2d\x31\x36\x2e\x36\x35\x2c\x31\x2d\x32\x39\x2e\x32\x31\x2c\x31\ +\x34\x2e\x37\x33\x2d\x32\x39\x2e\x34\x32\x2c\x33\x32\x2e\x35\x32\ +\x2d\x2e\x31\x36\x2c\x31\x33\x2e\x37\x37\x2c\x30\x2c\x32\x37\x2e\ +\x35\x34\x2c\x30\x2c\x34\x31\x2e\x33\x31\x61\x33\x33\x2e\x31\x39\ +\x2c\x33\x33\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x32\ +\x2c\x33\x2e\x37\x31\x5a\x6d\x2d\x33\x31\x2e\x33\x33\x2c\x34\x36\ +\x2e\x37\x31\x61\x31\x34\x2e\x39\x31\x2c\x31\x34\x2e\x39\x31\x2c\ +\x30\x2c\x30\x2c\x30\x2d\x31\x33\x2e\x36\x38\x2c\x38\x2e\x36\x36\ +\x63\x2d\x32\x2e\x38\x32\x2c\x35\x2e\x35\x37\x2d\x32\x2e\x38\x2c\ +\x31\x31\x2e\x35\x39\x2c\x31\x2e\x36\x2c\x31\x35\x2e\x38\x35\x2c\ +\x34\x2e\x31\x36\x2c\x34\x2c\x34\x2e\x34\x34\x2c\x38\x2e\x33\x35\ +\x2c\x34\x2e\x32\x32\x2c\x31\x33\x2e\x33\x32\x61\x36\x38\x2e\x33\ +\x38\x2c\x36\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\ +\x37\x2e\x37\x31\x63\x2e\x33\x37\x2c\x35\x2e\x33\x33\x2c\x33\x2e\ +\x36\x2c\x38\x2e\x38\x33\x2c\x38\x2c\x38\x2e\x38\x37\x73\x37\x2e\ +\x37\x35\x2d\x33\x2e\x34\x36\x2c\x38\x2e\x30\x37\x2d\x38\x2e\x37\ +\x36\x61\x31\x31\x31\x2e\x34\x34\x2c\x31\x31\x31\x2e\x34\x34\x2c\ +\x30\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x31\x2e\x35\x36\x2c\x39\x2e\ +\x36\x38\x2c\x39\x2e\x36\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x2e\ +\x32\x2d\x38\x2e\x31\x33\x63\x34\x2e\x37\x31\x2d\x34\x2e\x36\x34\ +\x2c\x35\x2e\x36\x31\x2d\x31\x30\x2e\x35\x33\x2c\x33\x2d\x31\x36\ +\x2e\x37\x32\x41\x31\x35\x2e\x34\x38\x2c\x31\x35\x2e\x34\x38\x2c\ +\x30\x2c\x30\x2c\x30\x2c\x34\x35\x36\x2e\x30\x35\x2c\x34\x33\x35\ +\x2e\x35\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ +\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x33\ +\x37\x39\x2e\x35\x38\x2c\x33\x38\x34\x2e\x33\x31\x63\x33\x2e\x31\ +\x38\x2d\x2e\x30\x36\x2c\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\ +\x2e\x36\x31\x2c\x30\x2c\x30\x2d\x33\x2e\x32\x34\x2c\x30\x2d\x36\ +\x2e\x34\x33\x2c\x30\x2d\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\ +\x35\x33\x2c\x30\x2d\x32\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\ +\x35\x38\x2d\x33\x32\x2e\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\ +\x38\x2e\x39\x33\x2d\x33\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\ +\x34\x2d\x32\x39\x2e\x35\x37\x43\x32\x34\x36\x2c\x33\x31\x39\x2e\ +\x31\x34\x2c\x32\x31\x38\x2c\x33\x33\x34\x2e\x36\x31\x2c\x31\x39\ +\x34\x2e\x31\x33\x2c\x33\x35\x39\x63\x2d\x36\x2c\x36\x2e\x31\x37\ +\x2d\x39\x2e\x36\x33\x2c\x31\x33\x2e\x35\x39\x2d\x39\x2e\x36\x31\ +\x2c\x32\x32\x2e\x38\x31\x61\x36\x38\x2e\x39\x33\x2c\x36\x38\x2e\ +\x39\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x33\x38\x63\ +\x35\x2e\x36\x33\x2c\x32\x33\x2e\x32\x38\x2c\x33\x31\x2e\x32\x37\ +\x2c\x33\x30\x2e\x32\x38\x2c\x34\x37\x2e\x39\x33\x2c\x31\x33\x2e\ +\x32\x37\x2c\x32\x34\x2e\x36\x31\x2d\x32\x35\x2e\x31\x31\x2c\x35\ +\x33\x2e\x37\x35\x2d\x33\x33\x2e\x38\x36\x2c\x38\x37\x2d\x32\x36\ +\x2e\x37\x37\x2c\x31\x35\x2c\x33\x2e\x32\x2c\x32\x38\x2c\x31\x30\ +\x2e\x33\x35\x2c\x33\x39\x2e\x36\x2c\x32\x30\x2e\x34\x32\x43\x33\ +\x36\x33\x2e\x31\x37\x2c\x33\x38\x38\x2e\x38\x32\x2c\x33\x37\x30\ +\x2e\x30\x37\x2c\x33\x38\x34\x2e\x34\x39\x2c\x33\x37\x39\x2e\x35\ +\x38\x2c\x33\x38\x34\x2e\x33\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ +\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\ +\x64\x3d\x22\x4d\x34\x35\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\ +\x31\x61\x32\x36\x2e\x31\x32\x2c\x32\x36\x2e\x31\x32\x2c\x30\x2c\ +\x30\x2c\x30\x2d\x37\x2e\x37\x39\x2c\x31\x2e\x36\x36\x63\x35\x2e\ +\x35\x39\x2c\x35\x2e\x37\x32\x2c\x31\x32\x2e\x33\x34\x2c\x38\x2e\ +\x37\x38\x2c\x32\x33\x2c\x38\x2e\x36\x31\x61\x32\x34\x2c\x32\x34\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x36\x2e\x32\x35\x2d\x31\x2e\x32\x37\ +\x41\x32\x35\x2e\x36\x38\x2c\x32\x35\x2e\x36\x38\x2c\x30\x2c\x30\ +\x2c\x30\x2c\x34\x35\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\x31\ +\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ +\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x33\x36\x2e\ +\x36\x32\x2c\x32\x37\x38\x2e\x34\x38\x63\x32\x2e\x36\x31\x2d\x2e\ +\x38\x2c\x35\x2e\x32\x33\x2d\x31\x2e\x34\x36\x2c\x37\x2e\x37\x37\ +\x2d\x32\x2e\x30\x39\x2c\x31\x2e\x31\x34\x2d\x2e\x32\x38\x2c\x32\ +\x2e\x32\x38\x2d\x2e\x35\x36\x2c\x33\x2e\x34\x32\x2d\x2e\x38\x36\ +\x6c\x2e\x36\x32\x2d\x2e\x31\x36\x68\x31\x36\x6c\x2e\x39\x33\x2e\ +\x33\x39\x63\x2e\x33\x36\x2e\x31\x35\x2e\x37\x32\x2e\x33\x33\x2c\ +\x31\x2e\x30\x37\x2e\x35\x6c\x2e\x33\x37\x2e\x31\x39\x61\x36\x36\ +\x2e\x31\x38\x2c\x36\x36\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x31\x2c\ +\x32\x36\x2e\x38\x33\x2c\x31\x30\x2e\x33\x38\x2c\x33\x30\x2e\x33\ +\x35\x2c\x33\x30\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\ +\x39\x34\x2d\x31\x30\x2e\x31\x35\x41\x32\x30\x35\x2c\x32\x30\x35\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x36\x34\x2e\x34\x34\x2c\x32\x35\ +\x34\x63\x2d\x36\x30\x2e\x32\x32\x2d\x35\x30\x2e\x33\x31\x2d\x31\ +\x32\x38\x2e\x33\x31\x2d\x37\x30\x2d\x32\x30\x33\x2e\x39\x31\x2d\ +\x35\x38\x2e\x33\x36\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\x2d\ +\x31\x30\x35\x2e\x38\x34\x2c\x33\x36\x2d\x31\x34\x36\x2e\x38\x34\ +\x2c\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\x37\x2c\x38\x2e\x37\x32\ +\x2d\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\x36\x32\x2d\x36\x2e\x38\ +\x39\x2c\x33\x31\x2e\x35\x32\x2c\x37\x2e\x32\x34\x2c\x32\x32\x2c\ +\x33\x31\x2e\x35\x36\x2c\x32\x36\x2e\x38\x33\x2c\x34\x37\x2e\x39\ +\x31\x2c\x31\x30\x2c\x34\x39\x2e\x32\x32\x2d\x35\x30\x2e\x35\x37\ +\x2c\x31\x30\x38\x2d\x37\x31\x2e\x30\x37\x2c\x31\x37\x35\x2e\x33\ +\x39\x2d\x36\x31\x2e\x34\x41\x31\x38\x37\x2e\x39\x32\x2c\x31\x38\ +\x37\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x31\x35\x2c\x32\ +\x38\x39\x2e\x36\x38\x2c\x36\x37\x2e\x34\x39\x2c\x36\x37\x2e\x34\ +\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x33\x36\x2e\x36\x32\x2c\x32\ +\x37\x38\x2e\x34\x38\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x06\x37\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -20523,6 +20958,696 @@ \x39\x38\x2e\x38\x31\x4c\x32\x37\x31\x2c\x33\x30\x30\x2e\x38\x38\ \x2c\x39\x38\x2e\x38\x33\x2c\x36\x31\x2e\x39\x31\x5a\x22\x2f\x3e\ \x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x0a\x76\ +\x3c\ +\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ +\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ +\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ +\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ +\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ +\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ +\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\x35\x34\x30\x3b\x7d\ +\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\ +\x32\x64\x33\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x66\x69\x6c\x6c\ +\x3a\x23\x39\x32\x39\x34\x39\x37\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\ +\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\ +\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\ +\x4d\x34\x34\x37\x2e\x31\x37\x2c\x33\x33\x34\x2e\x38\x35\x61\x32\ +\x36\x2e\x31\x32\x2c\x32\x36\x2e\x31\x32\x2c\x30\x2c\x30\x2c\x30\ +\x2d\x37\x2e\x37\x39\x2c\x31\x2e\x36\x36\x63\x35\x2e\x35\x39\x2c\ +\x35\x2e\x37\x32\x2c\x31\x32\x2e\x33\x34\x2c\x38\x2e\x37\x38\x2c\ +\x32\x33\x2c\x38\x2e\x36\x31\x61\x32\x33\x2e\x35\x35\x2c\x32\x33\ +\x2e\x35\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x36\x2e\x32\x35\x2d\x31\ +\x2e\x32\x37\x41\x32\x35\x2e\x37\x31\x2c\x32\x35\x2e\x37\x31\x2c\ +\x30\x2c\x30\x2c\x30\x2c\x34\x34\x37\x2e\x31\x37\x2c\x33\x33\x34\ +\x2e\x38\x35\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ +\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x32\ +\x39\x30\x2e\x31\x38\x2c\x37\x31\x2e\x33\x35\x43\x34\x30\x32\x2c\ +\x37\x33\x2e\x31\x34\x2c\x34\x39\x30\x2e\x31\x2c\x31\x31\x31\x2e\ +\x34\x34\x2c\x35\x36\x33\x2e\x39\x2c\x31\x38\x37\x2e\x34\x35\x63\ +\x31\x34\x2e\x38\x2c\x31\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\ +\x2c\x33\x33\x2e\x39\x33\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2e\x30\ +\x35\x2d\x31\x31\x2e\x31\x32\x2c\x31\x33\x2e\x31\x36\x2d\x32\x38\ +\x2e\x36\x32\x2c\x31\x33\x2d\x34\x31\x2e\x34\x36\x2e\x38\x33\x2d\ +\x31\x35\x2e\x30\x38\x2d\x31\x34\x2e\x32\x37\x2d\x33\x30\x2e\x32\ +\x2d\x32\x38\x2e\x37\x32\x2d\x34\x36\x2e\x36\x31\x2d\x34\x31\x2e\ +\x31\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\x2d\x38\x31\x2e\x33\x33\ +\x2d\x34\x36\x2e\x37\x34\x2d\x31\x32\x37\x2e\x37\x35\x2d\x35\x34\ +\x2e\x36\x34\x2d\x35\x34\x2d\x39\x2e\x32\x2d\x31\x30\x36\x2e\x39\ +\x32\x2d\x34\x2e\x33\x32\x2d\x31\x35\x38\x2e\x35\x2c\x31\x35\x2e\ +\x32\x32\x2d\x34\x35\x2c\x31\x37\x2d\x38\x34\x2e\x32\x39\x2c\x34\ +\x33\x2e\x39\x33\x2d\x31\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\ +\x34\x2d\x38\x2e\x35\x37\x2c\x39\x2e\x31\x2d\x31\x38\x2e\x36\x35\ +\x2c\x31\x32\x2e\x33\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\ +\x30\x2e\x30\x39\x2d\x37\x2e\x35\x36\x2d\x32\x35\x2e\x32\x2d\x33\ +\x34\x2e\x30\x38\x2d\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\ +\x33\x38\x30\x2e\x35\x31\x2c\x33\x38\x30\x2e\x35\x31\x2c\x30\x2c\ +\x30\x2c\x31\x2c\x35\x37\x2e\x36\x32\x2d\x35\x30\x2e\x33\x38\x63\ +\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\x38\x39\x2e\x39\x33\x2d\x35\ +\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\x37\x2d\x36\x30\x2e\x34\x35\ +\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x34\x2e\x32\x33\x2c\x32\x37\ +\x37\x2e\x34\x33\x2c\x37\x32\x2e\x38\x35\x2c\x32\x39\x30\x2e\x31\ +\x38\x2c\x37\x31\x2e\x33\x35\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ +\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\ +\x3d\x22\x4d\x33\x30\x30\x2c\x35\x31\x37\x2e\x33\x37\x63\x2d\x32\ +\x32\x2c\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x35\x2d\ +\x33\x39\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\ +\x2e\x33\x37\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\ +\x33\x38\x2e\x38\x36\x2d\x34\x31\x2e\x37\x39\x2c\x32\x32\x2e\x32\ +\x35\x2c\x30\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x37\x2c\ +\x33\x39\x2e\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\ +\x31\x2c\x35\x31\x37\x2e\x33\x37\x2c\x33\x30\x30\x2c\x35\x31\x37\ +\x2e\x33\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ +\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x34\ +\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x68\x31\x34\x2e\ +\x33\x35\x61\x31\x30\x2e\x36\x34\x2c\x31\x30\x2e\x36\x34\x2c\x30\ +\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x32\x2c\x31\x63\x32\x37\x2e\x34\ +\x37\x2c\x34\x2c\x34\x39\x2e\x36\x2c\x32\x36\x2e\x36\x37\x2c\x35\ +\x31\x2e\x36\x38\x2c\x35\x34\x2e\x32\x36\x2c\x31\x2e\x31\x37\x2c\ +\x31\x35\x2e\x35\x33\x2e\x35\x38\x2c\x33\x31\x2e\x32\x2e\x37\x38\ +\x2c\x34\x36\x2e\x38\x2c\x30\x2c\x32\x2e\x31\x32\x2c\x30\x2c\x34\ +\x2e\x32\x35\x2c\x30\x2c\x36\x2e\x38\x36\x68\x31\x31\x2e\x37\x35\ +\x63\x31\x33\x2e\x39\x34\x2c\x30\x2c\x31\x39\x2e\x33\x36\x2c\x35\ +\x2e\x33\x39\x2c\x31\x39\x2e\x33\x36\x2c\x31\x39\x2e\x32\x32\x2c\ +\x30\x2c\x33\x33\x2e\x32\x35\x2d\x2e\x31\x38\x2c\x36\x36\x2e\x35\ +\x2e\x31\x32\x2c\x39\x39\x2e\x37\x35\x2e\x30\x39\x2c\x39\x2e\x36\ +\x38\x2d\x32\x2e\x39\x33\x2c\x31\x36\x2e\x36\x37\x2d\x31\x32\x2e\ +\x31\x36\x2c\x32\x30\x2e\x33\x39\x48\x33\x37\x35\x2e\x31\x32\x63\ +\x2d\x37\x2e\x32\x34\x2d\x33\x2e\x31\x37\x2d\x31\x32\x2d\x38\x2e\ +\x31\x31\x2d\x31\x32\x2d\x31\x36\x2e\x35\x33\x2c\x30\x2d\x33\x35\ +\x2e\x34\x36\x2d\x2e\x30\x38\x2d\x37\x30\x2e\x39\x31\x2c\x30\x2d\ +\x31\x30\x36\x2e\x33\x37\x2c\x30\x2d\x31\x30\x2e\x32\x39\x2c\x36\ +\x2e\x32\x34\x2d\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\x35\x36\x2d\ +\x31\x36\x2e\x34\x34\x2c\x34\x2e\x36\x39\x2d\x2e\x30\x39\x2c\x39\ +\x2e\x33\x39\x2c\x30\x2c\x31\x34\x2e\x35\x31\x2c\x30\x2c\x30\x2d\ +\x31\x35\x2e\x37\x33\x2d\x2e\x30\x38\x2d\x33\x30\x2e\x35\x39\x2c\ +\x30\x2d\x34\x35\x2e\x34\x35\x2e\x31\x39\x2d\x32\x38\x2e\x37\x39\ +\x2c\x31\x37\x2e\x32\x38\x2d\x35\x32\x2e\x33\x36\x2c\x34\x33\x2e\ +\x38\x39\x2d\x36\x30\x2e\x35\x38\x43\x34\x34\x31\x2e\x37\x31\x2c\ +\x32\x38\x32\x2e\x31\x34\x2c\x34\x34\x35\x2e\x34\x31\x2c\x32\x38\ +\x31\x2e\x33\x33\x2c\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\ +\x33\x37\x5a\x6d\x33\x38\x2e\x33\x31\x2c\x31\x30\x38\x2e\x34\x39\ +\x63\x30\x2d\x31\x36\x2e\x35\x34\x2e\x39\x34\x2d\x33\x32\x2e\x37\ +\x33\x2d\x2e\x32\x35\x2d\x34\x38\x2e\x37\x37\x2d\x31\x2e\x33\x31\ +\x2d\x31\x37\x2e\x38\x32\x2d\x31\x35\x2e\x39\x33\x2d\x32\x39\x2e\ +\x37\x35\x2d\x33\x32\x2e\x37\x37\x2d\x32\x38\x2e\x37\x37\x2d\x31\ +\x36\x2e\x36\x35\x2c\x31\x2d\x32\x39\x2e\x32\x31\x2c\x31\x34\x2e\ +\x37\x33\x2d\x32\x39\x2e\x34\x32\x2c\x33\x32\x2e\x35\x32\x2d\x2e\ +\x31\x36\x2c\x31\x33\x2e\x37\x37\x2c\x30\x2c\x32\x37\x2e\x35\x34\ +\x2c\x30\x2c\x34\x31\x2e\x33\x31\x61\x33\x33\x2e\x31\x39\x2c\x33\ +\x33\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x32\x2c\x33\ +\x2e\x37\x31\x5a\x6d\x2d\x33\x31\x2e\x33\x33\x2c\x34\x36\x2e\x37\ +\x31\x61\x31\x34\x2e\x39\x31\x2c\x31\x34\x2e\x39\x31\x2c\x30\x2c\ +\x30\x2c\x30\x2d\x31\x33\x2e\x36\x38\x2c\x38\x2e\x36\x36\x63\x2d\ +\x32\x2e\x38\x32\x2c\x35\x2e\x35\x37\x2d\x32\x2e\x38\x2c\x31\x31\ +\x2e\x35\x39\x2c\x31\x2e\x36\x2c\x31\x35\x2e\x38\x35\x2c\x34\x2e\ +\x31\x36\x2c\x34\x2c\x34\x2e\x34\x34\x2c\x38\x2e\x33\x35\x2c\x34\ +\x2e\x32\x32\x2c\x31\x33\x2e\x33\x32\x61\x36\x38\x2e\x33\x38\x2c\ +\x36\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\x37\x2e\ +\x37\x31\x63\x2e\x33\x37\x2c\x35\x2e\x33\x33\x2c\x33\x2e\x36\x2c\ +\x38\x2e\x38\x33\x2c\x38\x2c\x38\x2e\x38\x37\x73\x37\x2e\x37\x35\ +\x2d\x33\x2e\x34\x36\x2c\x38\x2e\x30\x37\x2d\x38\x2e\x37\x36\x61\ +\x31\x31\x31\x2e\x34\x34\x2c\x31\x31\x31\x2e\x34\x34\x2c\x30\x2c\ +\x30\x2c\x30\x2c\x30\x2d\x31\x31\x2e\x35\x36\x2c\x39\x2e\x36\x38\ +\x2c\x39\x2e\x36\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x2e\x32\x2d\ +\x38\x2e\x31\x33\x63\x34\x2e\x37\x31\x2d\x34\x2e\x36\x34\x2c\x35\ +\x2e\x36\x31\x2d\x31\x30\x2e\x35\x33\x2c\x33\x2d\x31\x36\x2e\x37\ +\x32\x41\x31\x35\x2e\x34\x38\x2c\x31\x35\x2e\x34\x38\x2c\x30\x2c\ +\x30\x2c\x30\x2c\x34\x35\x36\x2e\x30\x35\x2c\x34\x33\x35\x2e\x35\ +\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ +\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x37\x39\ +\x2e\x35\x38\x2c\x33\x38\x34\x2e\x33\x31\x63\x33\x2e\x31\x38\x2d\ +\x2e\x30\x36\x2c\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\x2e\x36\ +\x31\x2c\x30\x2c\x30\x2d\x33\x2e\x32\x34\x2c\x30\x2d\x36\x2e\x34\ +\x33\x2c\x30\x2d\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\x35\x33\ +\x2c\x30\x2d\x32\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\x35\x38\ +\x2d\x33\x32\x2e\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\x38\x2e\ +\x39\x33\x2d\x33\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\x34\x2d\ +\x32\x39\x2e\x35\x37\x43\x32\x34\x36\x2c\x33\x31\x39\x2e\x31\x34\ +\x2c\x32\x31\x38\x2c\x33\x33\x34\x2e\x36\x31\x2c\x31\x39\x34\x2e\ +\x31\x33\x2c\x33\x35\x39\x63\x2d\x36\x2c\x36\x2e\x31\x37\x2d\x39\ +\x2e\x36\x33\x2c\x31\x33\x2e\x35\x39\x2d\x39\x2e\x36\x31\x2c\x32\ +\x32\x2e\x38\x31\x61\x36\x38\x2e\x39\x33\x2c\x36\x38\x2e\x39\x33\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x33\x38\x63\x35\x2e\ +\x36\x33\x2c\x32\x33\x2e\x32\x38\x2c\x33\x31\x2e\x32\x37\x2c\x33\ +\x30\x2e\x32\x38\x2c\x34\x37\x2e\x39\x33\x2c\x31\x33\x2e\x32\x37\ +\x2c\x32\x34\x2e\x36\x31\x2d\x32\x35\x2e\x31\x31\x2c\x35\x33\x2e\ +\x37\x35\x2d\x33\x33\x2e\x38\x36\x2c\x38\x37\x2d\x32\x36\x2e\x37\ +\x37\x2c\x31\x35\x2c\x33\x2e\x32\x2c\x32\x38\x2c\x31\x30\x2e\x33\ +\x35\x2c\x33\x39\x2e\x36\x2c\x32\x30\x2e\x34\x32\x43\x33\x36\x33\ +\x2e\x31\x37\x2c\x33\x38\x38\x2e\x38\x32\x2c\x33\x37\x30\x2e\x30\ +\x37\x2c\x33\x38\x34\x2e\x34\x39\x2c\x33\x37\x39\x2e\x35\x38\x2c\ +\x33\x38\x34\x2e\x33\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\ +\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\ +\x22\x4d\x34\x33\x36\x2e\x36\x32\x2c\x32\x37\x38\x2e\x34\x38\x63\ +\x32\x2e\x36\x31\x2d\x2e\x38\x2c\x35\x2e\x32\x33\x2d\x31\x2e\x34\ +\x36\x2c\x37\x2e\x37\x37\x2d\x32\x2e\x30\x39\x2c\x31\x2e\x31\x34\ +\x2d\x2e\x32\x38\x2c\x32\x2e\x32\x38\x2d\x2e\x35\x36\x2c\x33\x2e\ +\x34\x32\x2d\x2e\x38\x36\x6c\x2e\x36\x32\x2d\x2e\x31\x36\x68\x31\ +\x36\x6c\x2e\x39\x33\x2e\x33\x39\x63\x2e\x33\x36\x2e\x31\x35\x2e\ +\x37\x32\x2e\x33\x33\x2c\x31\x2e\x30\x37\x2e\x35\x6c\x2e\x33\x37\ +\x2e\x31\x39\x61\x36\x36\x2e\x31\x38\x2c\x36\x36\x2e\x31\x38\x2c\ +\x30\x2c\x30\x2c\x31\x2c\x32\x36\x2e\x38\x33\x2c\x31\x30\x2e\x33\ +\x38\x2c\x33\x30\x2e\x33\x35\x2c\x33\x30\x2e\x33\x35\x2c\x30\x2c\ +\x30\x2c\x30\x2d\x35\x2e\x39\x34\x2d\x31\x30\x2e\x31\x35\x41\x32\ +\x30\x35\x2c\x32\x30\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x36\x34\ +\x2e\x34\x34\x2c\x32\x35\x34\x63\x2d\x36\x30\x2e\x32\x32\x2d\x35\ +\x30\x2e\x33\x31\x2d\x31\x32\x38\x2e\x33\x31\x2d\x37\x30\x2d\x32\ +\x30\x33\x2e\x39\x31\x2d\x35\x38\x2e\x33\x36\x2d\x35\x37\x2e\x30\ +\x39\x2c\x38\x2e\x38\x2d\x31\x30\x35\x2e\x38\x34\x2c\x33\x36\x2d\ +\x31\x34\x36\x2e\x38\x34\x2c\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\ +\x37\x2c\x38\x2e\x37\x32\x2d\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\ +\x36\x32\x2d\x36\x2e\x38\x39\x2c\x33\x31\x2e\x35\x32\x2c\x37\x2e\ +\x32\x34\x2c\x32\x32\x2c\x33\x31\x2e\x35\x36\x2c\x32\x36\x2e\x38\ +\x33\x2c\x34\x37\x2e\x39\x31\x2c\x31\x30\x2c\x34\x39\x2e\x32\x32\ +\x2d\x35\x30\x2e\x35\x37\x2c\x31\x30\x38\x2d\x37\x31\x2e\x30\x37\ +\x2c\x31\x37\x35\x2e\x33\x39\x2d\x36\x31\x2e\x34\x41\x31\x38\x37\ +\x2e\x39\x32\x2c\x31\x38\x37\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x31\ +\x2c\x34\x31\x35\x2c\x32\x38\x39\x2e\x36\x38\x2c\x36\x37\x2e\x34\ +\x39\x2c\x36\x37\x2e\x34\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x33\ +\x36\x2e\x36\x32\x2c\x32\x37\x38\x2e\x34\x38\x5a\x22\x2f\x3e\x3c\ +\x2f\x73\x76\x67\x3e\ +\x00\x00\x0a\x5b\ +\x3c\ +\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ +\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ +\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ +\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ +\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ +\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ +\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\x35\x34\x30\x3b\x7d\ +\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x39\x32\x39\ +\x34\x39\x37\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ +\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ +\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ +\x31\x38\x2c\x37\x31\x2e\x33\x35\x43\x34\x30\x32\x2c\x37\x33\x2e\ +\x31\x34\x2c\x34\x39\x30\x2e\x31\x2c\x31\x31\x31\x2e\x34\x34\x2c\ +\x35\x36\x33\x2e\x39\x2c\x31\x38\x37\x2e\x34\x35\x63\x31\x34\x2e\ +\x38\x2c\x31\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\ +\x2e\x39\x33\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2e\x30\x35\x2d\x31\ +\x31\x2e\x31\x32\x2c\x31\x33\x2e\x31\x36\x2d\x32\x38\x2e\x36\x32\ +\x2c\x31\x33\x2d\x34\x31\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\ +\x30\x38\x2d\x31\x34\x2e\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\ +\x2e\x37\x32\x2d\x34\x36\x2e\x36\x31\x2d\x34\x31\x2e\x31\x31\x2d\ +\x33\x38\x2e\x34\x2d\x32\x39\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\ +\x2e\x37\x34\x2d\x31\x32\x37\x2e\x37\x35\x2d\x35\x34\x2e\x36\x34\ +\x2d\x35\x34\x2d\x39\x2e\x32\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\ +\x2e\x33\x32\x2d\x31\x35\x38\x2e\x35\x2c\x31\x35\x2e\x32\x32\x2d\ +\x34\x35\x2c\x31\x37\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\ +\x33\x2d\x31\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x34\x2d\x38\ +\x2e\x35\x37\x2c\x39\x2e\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\x32\ +\x2e\x33\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\x30\ +\x39\x2d\x37\x2e\x35\x36\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\x30\ +\x38\x2d\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\x33\x38\x30\ +\x2e\x35\x31\x2c\x33\x38\x30\x2e\x35\x31\x2c\x30\x2c\x30\x2c\x31\ +\x2c\x35\x37\x2e\x36\x32\x2d\x35\x30\x2e\x33\x38\x63\x34\x33\x2d\ +\x33\x30\x2e\x35\x38\x2c\x38\x39\x2e\x39\x33\x2d\x35\x31\x2e\x31\ +\x2c\x31\x34\x30\x2e\x37\x37\x2d\x36\x30\x2e\x34\x35\x43\x32\x35\ +\x35\x2e\x30\x37\x2c\x37\x34\x2e\x32\x33\x2c\x32\x37\x37\x2e\x34\ +\x33\x2c\x37\x32\x2e\x38\x35\x2c\x32\x39\x30\x2e\x31\x38\x2c\x37\ +\x31\x2e\x33\x35\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ +\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\ +\x33\x30\x30\x2c\x35\x31\x37\x2e\x33\x37\x63\x2d\x32\x32\x2c\x30\ +\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x35\x2d\x33\x39\x2e\ +\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\x2e\x33\x37\ +\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\x2e\ +\x38\x36\x2d\x34\x31\x2e\x37\x39\x2c\x32\x32\x2e\x32\x35\x2c\x30\ +\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x37\x2c\x33\x39\x2e\ +\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ +\x31\x37\x2e\x33\x37\x2c\x33\x30\x30\x2c\x35\x31\x37\x2e\x33\x37\ +\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ +\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x34\x34\x39\x2e\ +\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x68\x31\x34\x2e\x33\x35\x61\ +\x31\x30\x2e\x36\x34\x2c\x31\x30\x2e\x36\x34\x2c\x30\x2c\x30\x2c\ +\x30\x2c\x32\x2e\x34\x32\x2c\x31\x63\x32\x37\x2e\x34\x37\x2c\x34\ +\x2c\x34\x39\x2e\x36\x2c\x32\x36\x2e\x36\x37\x2c\x35\x31\x2e\x36\ +\x38\x2c\x35\x34\x2e\x32\x36\x2c\x31\x2e\x31\x37\x2c\x31\x35\x2e\ +\x35\x33\x2e\x35\x38\x2c\x33\x31\x2e\x32\x2e\x37\x38\x2c\x34\x36\ +\x2e\x38\x2c\x30\x2c\x32\x2e\x31\x32\x2c\x30\x2c\x34\x2e\x32\x35\ +\x2c\x30\x2c\x36\x2e\x38\x36\x68\x31\x31\x2e\x37\x35\x63\x31\x33\ +\x2e\x39\x34\x2c\x30\x2c\x31\x39\x2e\x33\x36\x2c\x35\x2e\x33\x39\ +\x2c\x31\x39\x2e\x33\x36\x2c\x31\x39\x2e\x32\x32\x2c\x30\x2c\x33\ +\x33\x2e\x32\x35\x2d\x2e\x31\x38\x2c\x36\x36\x2e\x35\x2e\x31\x32\ +\x2c\x39\x39\x2e\x37\x35\x2e\x30\x39\x2c\x39\x2e\x36\x38\x2d\x32\ +\x2e\x39\x33\x2c\x31\x36\x2e\x36\x37\x2d\x31\x32\x2e\x31\x36\x2c\ +\x32\x30\x2e\x33\x39\x48\x33\x37\x35\x2e\x31\x32\x63\x2d\x37\x2e\ +\x32\x34\x2d\x33\x2e\x31\x37\x2d\x31\x32\x2d\x38\x2e\x31\x31\x2d\ +\x31\x32\x2d\x31\x36\x2e\x35\x33\x2c\x30\x2d\x33\x35\x2e\x34\x36\ +\x2d\x2e\x30\x38\x2d\x37\x30\x2e\x39\x31\x2c\x30\x2d\x31\x30\x36\ +\x2e\x33\x37\x2c\x30\x2d\x31\x30\x2e\x32\x39\x2c\x36\x2e\x32\x34\ +\x2d\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\x35\x36\x2d\x31\x36\x2e\ +\x34\x34\x2c\x34\x2e\x36\x39\x2d\x2e\x30\x39\x2c\x39\x2e\x33\x39\ +\x2c\x30\x2c\x31\x34\x2e\x35\x31\x2c\x30\x2c\x30\x2d\x31\x35\x2e\ +\x37\x33\x2d\x2e\x30\x38\x2d\x33\x30\x2e\x35\x39\x2c\x30\x2d\x34\ +\x35\x2e\x34\x35\x2e\x31\x39\x2d\x32\x38\x2e\x37\x39\x2c\x31\x37\ +\x2e\x32\x38\x2d\x35\x32\x2e\x33\x36\x2c\x34\x33\x2e\x38\x39\x2d\ +\x36\x30\x2e\x35\x38\x43\x34\x34\x31\x2e\x37\x31\x2c\x32\x38\x32\ +\x2e\x31\x34\x2c\x34\x34\x35\x2e\x34\x31\x2c\x32\x38\x31\x2e\x33\ +\x33\x2c\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x5a\ +\x6d\x33\x38\x2e\x33\x31\x2c\x31\x30\x38\x2e\x34\x39\x63\x30\x2d\ +\x31\x36\x2e\x35\x34\x2e\x39\x34\x2d\x33\x32\x2e\x37\x33\x2d\x2e\ +\x32\x35\x2d\x34\x38\x2e\x37\x37\x2d\x31\x2e\x33\x31\x2d\x31\x37\ +\x2e\x38\x32\x2d\x31\x35\x2e\x39\x33\x2d\x32\x39\x2e\x37\x35\x2d\ +\x33\x32\x2e\x37\x37\x2d\x32\x38\x2e\x37\x37\x2d\x31\x36\x2e\x36\ +\x35\x2c\x31\x2d\x32\x39\x2e\x32\x31\x2c\x31\x34\x2e\x37\x33\x2d\ +\x32\x39\x2e\x34\x32\x2c\x33\x32\x2e\x35\x32\x2d\x2e\x31\x36\x2c\ +\x31\x33\x2e\x37\x37\x2c\x30\x2c\x32\x37\x2e\x35\x34\x2c\x30\x2c\ +\x34\x31\x2e\x33\x31\x61\x33\x33\x2e\x31\x39\x2c\x33\x33\x2e\x31\ +\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x32\x2c\x33\x2e\x37\x31\ +\x5a\x6d\x2d\x33\x31\x2e\x33\x33\x2c\x34\x36\x2e\x37\x31\x61\x31\ +\x34\x2e\x39\x31\x2c\x31\x34\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\ +\x2d\x31\x33\x2e\x36\x38\x2c\x38\x2e\x36\x36\x63\x2d\x32\x2e\x38\ +\x32\x2c\x35\x2e\x35\x37\x2d\x32\x2e\x38\x2c\x31\x31\x2e\x35\x39\ +\x2c\x31\x2e\x36\x2c\x31\x35\x2e\x38\x35\x2c\x34\x2e\x31\x36\x2c\ +\x34\x2c\x34\x2e\x34\x34\x2c\x38\x2e\x33\x35\x2c\x34\x2e\x32\x32\ +\x2c\x31\x33\x2e\x33\x32\x61\x36\x38\x2e\x33\x38\x2c\x36\x38\x2e\ +\x33\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\x37\x2e\x37\x31\x63\ +\x2e\x33\x37\x2c\x35\x2e\x33\x33\x2c\x33\x2e\x36\x2c\x38\x2e\x38\ +\x33\x2c\x38\x2c\x38\x2e\x38\x37\x73\x37\x2e\x37\x35\x2d\x33\x2e\ +\x34\x36\x2c\x38\x2e\x30\x37\x2d\x38\x2e\x37\x36\x61\x31\x31\x31\ +\x2e\x34\x34\x2c\x31\x31\x31\x2e\x34\x34\x2c\x30\x2c\x30\x2c\x30\ +\x2c\x30\x2d\x31\x31\x2e\x35\x36\x2c\x39\x2e\x36\x38\x2c\x39\x2e\ +\x36\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x2e\x32\x2d\x38\x2e\x31\ +\x33\x63\x34\x2e\x37\x31\x2d\x34\x2e\x36\x34\x2c\x35\x2e\x36\x31\ +\x2d\x31\x30\x2e\x35\x33\x2c\x33\x2d\x31\x36\x2e\x37\x32\x41\x31\ +\x35\x2e\x34\x38\x2c\x31\x35\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x30\ +\x2c\x34\x35\x36\x2e\x30\x35\x2c\x34\x33\x35\x2e\x35\x37\x5a\x22\ +\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ +\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x37\x39\x2e\x35\x38\ +\x2c\x33\x38\x34\x2e\x33\x31\x63\x33\x2e\x31\x38\x2d\x2e\x30\x36\ +\x2c\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\x2e\x36\x31\x2c\x30\ +\x2c\x30\x2d\x33\x2e\x32\x34\x2c\x30\x2d\x36\x2e\x34\x33\x2c\x30\ +\x2d\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\x35\x33\x2c\x30\x2d\ +\x32\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\x35\x38\x2d\x33\x32\ +\x2e\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\x38\x2e\x39\x33\x2d\ +\x33\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\x34\x2d\x32\x39\x2e\ +\x35\x37\x43\x32\x34\x36\x2c\x33\x31\x39\x2e\x31\x34\x2c\x32\x31\ +\x38\x2c\x33\x33\x34\x2e\x36\x31\x2c\x31\x39\x34\x2e\x31\x33\x2c\ +\x33\x35\x39\x63\x2d\x36\x2c\x36\x2e\x31\x37\x2d\x39\x2e\x36\x33\ +\x2c\x31\x33\x2e\x35\x39\x2d\x39\x2e\x36\x31\x2c\x32\x32\x2e\x38\ +\x31\x61\x36\x38\x2e\x39\x33\x2c\x36\x38\x2e\x39\x33\x2c\x30\x2c\ +\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x33\x38\x63\x35\x2e\x36\x33\x2c\ +\x32\x33\x2e\x32\x38\x2c\x33\x31\x2e\x32\x37\x2c\x33\x30\x2e\x32\ +\x38\x2c\x34\x37\x2e\x39\x33\x2c\x31\x33\x2e\x32\x37\x2c\x32\x34\ +\x2e\x36\x31\x2d\x32\x35\x2e\x31\x31\x2c\x35\x33\x2e\x37\x35\x2d\ +\x33\x33\x2e\x38\x36\x2c\x38\x37\x2d\x32\x36\x2e\x37\x37\x2c\x31\ +\x35\x2c\x33\x2e\x32\x2c\x32\x38\x2c\x31\x30\x2e\x33\x35\x2c\x33\ +\x39\x2e\x36\x2c\x32\x30\x2e\x34\x32\x43\x33\x36\x33\x2e\x31\x37\ +\x2c\x33\x38\x38\x2e\x38\x32\x2c\x33\x37\x30\x2e\x30\x37\x2c\x33\ +\x38\x34\x2e\x34\x39\x2c\x33\x37\x39\x2e\x35\x38\x2c\x33\x38\x34\ +\x2e\x33\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ +\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\ +\x35\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\x31\x61\x32\x36\x2e\ +\x31\x32\x2c\x32\x36\x2e\x31\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x37\ +\x2e\x37\x39\x2c\x31\x2e\x36\x36\x63\x35\x2e\x35\x39\x2c\x35\x2e\ +\x37\x32\x2c\x31\x32\x2e\x33\x34\x2c\x38\x2e\x37\x38\x2c\x32\x33\ +\x2c\x38\x2e\x36\x31\x61\x32\x34\x2c\x32\x34\x2c\x30\x2c\x30\x2c\ +\x30\x2c\x36\x2e\x32\x35\x2d\x31\x2e\x32\x37\x41\x32\x35\x2e\x36\ +\x38\x2c\x32\x35\x2e\x36\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x35\ +\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\x31\x5a\x22\x2f\x3e\x3c\ +\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ +\x31\x22\x20\x64\x3d\x22\x4d\x34\x33\x36\x2e\x36\x32\x2c\x32\x37\ +\x38\x2e\x34\x38\x63\x32\x2e\x36\x31\x2d\x2e\x38\x2c\x35\x2e\x32\ +\x33\x2d\x31\x2e\x34\x36\x2c\x37\x2e\x37\x37\x2d\x32\x2e\x30\x39\ +\x2c\x31\x2e\x31\x34\x2d\x2e\x32\x38\x2c\x32\x2e\x32\x38\x2d\x2e\ +\x35\x36\x2c\x33\x2e\x34\x32\x2d\x2e\x38\x36\x6c\x2e\x36\x32\x2d\ +\x2e\x31\x36\x68\x31\x36\x6c\x2e\x39\x33\x2e\x33\x39\x63\x2e\x33\ +\x36\x2e\x31\x35\x2e\x37\x32\x2e\x33\x33\x2c\x31\x2e\x30\x37\x2e\ +\x35\x6c\x2e\x33\x37\x2e\x31\x39\x61\x36\x36\x2e\x31\x38\x2c\x36\ +\x36\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x36\x2e\x38\x33\ +\x2c\x31\x30\x2e\x33\x38\x2c\x33\x30\x2e\x33\x35\x2c\x33\x30\x2e\ +\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\x39\x34\x2d\x31\x30\ +\x2e\x31\x35\x41\x32\x30\x35\x2c\x32\x30\x35\x2c\x30\x2c\x30\x2c\ +\x30\x2c\x34\x36\x34\x2e\x34\x34\x2c\x32\x35\x34\x63\x2d\x36\x30\ +\x2e\x32\x32\x2d\x35\x30\x2e\x33\x31\x2d\x31\x32\x38\x2e\x33\x31\ +\x2d\x37\x30\x2d\x32\x30\x33\x2e\x39\x31\x2d\x35\x38\x2e\x33\x36\ +\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\x2d\x31\x30\x35\x2e\x38\ +\x34\x2c\x33\x36\x2d\x31\x34\x36\x2e\x38\x34\x2c\x37\x39\x2e\x32\ +\x38\x2d\x38\x2e\x32\x37\x2c\x38\x2e\x37\x32\x2d\x31\x30\x2e\x37\ +\x39\x2c\x31\x39\x2e\x36\x32\x2d\x36\x2e\x38\x39\x2c\x33\x31\x2e\ +\x35\x32\x2c\x37\x2e\x32\x34\x2c\x32\x32\x2c\x33\x31\x2e\x35\x36\ +\x2c\x32\x36\x2e\x38\x33\x2c\x34\x37\x2e\x39\x31\x2c\x31\x30\x2c\ +\x34\x39\x2e\x32\x32\x2d\x35\x30\x2e\x35\x37\x2c\x31\x30\x38\x2d\ +\x37\x31\x2e\x30\x37\x2c\x31\x37\x35\x2e\x33\x39\x2d\x36\x31\x2e\ +\x34\x41\x31\x38\x37\x2e\x39\x32\x2c\x31\x38\x37\x2e\x39\x32\x2c\ +\x30\x2c\x30\x2c\x31\x2c\x34\x31\x35\x2c\x32\x38\x39\x2e\x36\x38\ +\x2c\x36\x37\x2e\x34\x39\x2c\x36\x37\x2e\x34\x39\x2c\x30\x2c\x30\ +\x2c\x31\x2c\x34\x33\x36\x2e\x36\x32\x2c\x32\x37\x38\x2e\x34\x38\ +\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x0b\x47\ +\x3c\ +\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ +\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ +\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ +\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ +\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ +\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ +\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ +\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x39\x32\x39\ +\x34\x39\x37\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x66\x69\x6c\x6c\ +\x3a\x23\x35\x65\x36\x30\x36\x31\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\ +\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\ +\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\ +\x4d\x32\x39\x30\x2e\x31\x38\x2c\x38\x32\x2e\x36\x32\x43\x34\x30\ +\x32\x2c\x38\x34\x2e\x34\x2c\x34\x39\x30\x2e\x31\x2c\x31\x32\x32\ +\x2e\x37\x2c\x35\x36\x33\x2e\x39\x2c\x31\x39\x38\x2e\x37\x31\x63\ +\x31\x34\x2e\x38\x2c\x31\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\ +\x2c\x33\x33\x2e\x39\x33\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\ +\x31\x2e\x31\x32\x2c\x31\x33\x2e\x31\x36\x2d\x32\x38\x2e\x36\x32\ +\x2c\x31\x33\x2d\x34\x31\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\ +\x30\x38\x2d\x31\x34\x2e\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\ +\x2e\x37\x31\x2d\x34\x36\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\ +\x38\x2e\x34\x2d\x32\x39\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\ +\x37\x35\x2d\x31\x32\x37\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\ +\x35\x34\x2d\x39\x2e\x31\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\ +\x2e\x33\x32\x2d\x31\x35\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x43\ +\x31\x34\x37\x2e\x38\x34\x2c\x31\x38\x30\x2e\x31\x32\x2c\x31\x30\ +\x38\x2e\x35\x35\x2c\x32\x30\x37\x2c\x37\x34\x2e\x36\x38\x2c\x32\ +\x34\x33\x63\x2d\x38\x2e\x35\x37\x2c\x39\x2e\x31\x2d\x31\x38\x2e\ +\x36\x35\x2c\x31\x32\x2e\x33\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\ +\x2d\x32\x30\x2e\x30\x39\x2d\x37\x2e\x35\x36\x2d\x32\x35\x2e\x32\ +\x2d\x33\x34\x2e\x30\x38\x2d\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\ +\x41\x33\x38\x30\x2c\x33\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x39\ +\x32\x2e\x33\x37\x2c\x31\x35\x30\x63\x34\x33\x2d\x33\x30\x2e\x35\ +\x37\x2c\x38\x39\x2e\x39\x33\x2d\x35\x31\x2e\x30\x39\x2c\x31\x34\ +\x30\x2e\x37\x37\x2d\x36\x30\x2e\x34\x35\x43\x32\x35\x35\x2e\x30\ +\x37\x2c\x38\x35\x2e\x34\x39\x2c\x32\x37\x37\x2e\x34\x33\x2c\x38\ +\x34\x2e\x31\x31\x2c\x32\x39\x30\x2e\x31\x38\x2c\x38\x32\x2e\x36\ +\x32\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ +\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x30\x30\ +\x2c\x35\x32\x38\x2e\x36\x34\x63\x2d\x32\x32\x2c\x30\x2d\x33\x39\ +\x2e\x31\x35\x2d\x31\x38\x2e\x34\x36\x2d\x33\x39\x2e\x31\x31\x2d\ +\x34\x32\x2e\x31\x32\x2c\x30\x2d\x32\x33\x2e\x33\x37\x2c\x31\x37\ +\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\x2e\x38\x36\x2d\ +\x34\x31\x2e\x37\x39\x2c\x32\x32\x2e\x32\x35\x2c\x30\x2c\x33\x39\ +\x2e\x33\x32\x2c\x31\x38\x2e\x31\x37\x2c\x33\x39\x2e\x33\x34\x2c\ +\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\x32\x38\x2e\ +\x36\x33\x2c\x33\x30\x30\x2c\x35\x32\x38\x2e\x36\x34\x5a\x22\x2f\ +\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ +\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x37\x39\x2e\x35\x38\x2c\ +\x33\x39\x35\x2e\x35\x37\x63\x33\x2e\x31\x38\x2d\x2e\x30\x36\x2c\ +\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\x2e\x36\x31\x2c\x30\x2c\ +\x30\x2d\x33\x2e\x32\x33\x2c\x30\x2d\x36\x2e\x34\x33\x2c\x30\x2d\ +\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\x35\x32\x2c\x30\x2d\x32\ +\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\x35\x38\x2d\x33\x32\x2e\ +\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\x38\x2e\x39\x33\x2d\x33\ +\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\x34\x2d\x32\x39\x2e\x35\ +\x37\x2d\x33\x32\x2e\x38\x2c\x34\x2e\x36\x32\x2d\x36\x30\x2e\x38\ +\x2c\x32\x30\x2e\x30\x39\x2d\x38\x34\x2e\x36\x34\x2c\x34\x34\x2e\ +\x34\x33\x2d\x36\x2c\x36\x2e\x31\x37\x2d\x39\x2e\x36\x33\x2c\x31\ +\x33\x2e\x36\x2d\x39\x2e\x36\x31\x2c\x32\x32\x2e\x38\x32\x61\x36\ +\x39\x2e\x32\x38\x2c\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x30\ +\x2c\x31\x2c\x37\x2e\x33\x38\x63\x35\x2e\x36\x33\x2c\x32\x33\x2e\ +\x32\x37\x2c\x33\x31\x2e\x32\x37\x2c\x33\x30\x2e\x32\x37\x2c\x34\ +\x37\x2e\x39\x33\x2c\x31\x33\x2e\x32\x36\x2c\x32\x34\x2e\x36\x31\ +\x2d\x32\x35\x2e\x31\x31\x2c\x35\x33\x2e\x37\x35\x2d\x33\x33\x2e\ +\x38\x36\x2c\x38\x37\x2d\x32\x36\x2e\x37\x36\x2c\x31\x35\x2c\x33\ +\x2e\x32\x2c\x32\x38\x2c\x31\x30\x2e\x33\x35\x2c\x33\x39\x2e\x36\ +\x2c\x32\x30\x2e\x34\x32\x43\x33\x36\x33\x2e\x31\x37\x2c\x34\x30\ +\x30\x2e\x30\x38\x2c\x33\x37\x30\x2e\x30\x37\x2c\x33\x39\x35\x2e\ +\x37\x35\x2c\x33\x37\x39\x2e\x35\x38\x2c\x33\x39\x35\x2e\x35\x37\ +\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ +\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x35\x34\x2e\ +\x36\x35\x2c\x33\x32\x37\x2e\x35\x37\x61\x32\x36\x2e\x31\x32\x2c\ +\x32\x36\x2e\x31\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x37\x2e\x37\x39\ +\x2c\x31\x2e\x36\x36\x63\x35\x2e\x35\x39\x2c\x35\x2e\x37\x32\x2c\ +\x31\x32\x2e\x33\x34\x2c\x38\x2e\x37\x39\x2c\x32\x33\x2c\x38\x2e\ +\x36\x31\x61\x32\x33\x2e\x35\x35\x2c\x32\x33\x2e\x35\x35\x2c\x30\ +\x2c\x30\x2c\x30\x2c\x36\x2e\x32\x35\x2d\x31\x2e\x32\x37\x41\x32\ +\x35\x2e\x37\x31\x2c\x32\x35\x2e\x37\x31\x2c\x30\x2c\x30\x2c\x30\ +\x2c\x34\x35\x34\x2e\x36\x35\x2c\x33\x32\x37\x2e\x35\x37\x5a\x22\ +\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ +\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x33\x36\x2e\x36\x32\ +\x2c\x32\x38\x39\x2e\x37\x34\x63\x32\x2e\x36\x31\x2d\x2e\x38\x2c\ +\x35\x2e\x32\x33\x2d\x31\x2e\x34\x35\x2c\x37\x2e\x37\x37\x2d\x32\ +\x2e\x30\x38\x6c\x33\x2e\x34\x32\x2d\x2e\x38\x37\x2e\x36\x32\x2d\ +\x2e\x31\x36\x68\x31\x36\x6c\x2e\x39\x33\x2e\x33\x39\x63\x2e\x33\ +\x36\x2e\x31\x36\x2e\x37\x32\x2e\x33\x33\x2c\x31\x2e\x30\x37\x2e\ +\x35\x31\x6c\x2e\x33\x37\x2e\x31\x38\x61\x36\x36\x2e\x31\x38\x2c\ +\x36\x36\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x36\x2e\x38\ +\x33\x2c\x31\x30\x2e\x33\x38\x2c\x33\x30\x2e\x34\x35\x2c\x33\x30\ +\x2e\x34\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\x39\x34\x2d\x31\ +\x30\x2e\x31\x35\x2c\x32\x30\x35\x2e\x38\x33\x2c\x32\x30\x35\x2e\ +\x38\x33\x2c\x30\x2c\x30\x2c\x30\x2d\x32\x33\x2e\x32\x35\x2d\x32\ +\x32\x2e\x37\x63\x2d\x36\x30\x2e\x32\x32\x2d\x35\x30\x2e\x33\x2d\ +\x31\x32\x38\x2e\x33\x31\x2d\x37\x30\x2d\x32\x30\x33\x2e\x39\x31\ +\x2d\x35\x38\x2e\x33\x36\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\ +\x31\x2d\x31\x30\x35\x2e\x38\x34\x2c\x33\x36\x2e\x30\x35\x2d\x31\ +\x34\x36\x2e\x38\x34\x2c\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\x37\ +\x2c\x38\x2e\x37\x33\x2d\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\x36\ +\x33\x2d\x36\x2e\x38\x39\x2c\x33\x31\x2e\x35\x32\x2c\x37\x2e\x32\ +\x34\x2c\x32\x32\x2c\x33\x31\x2e\x35\x36\x2c\x32\x36\x2e\x38\x33\ +\x2c\x34\x37\x2e\x39\x31\x2c\x31\x30\x2c\x34\x39\x2e\x32\x32\x2d\ +\x35\x30\x2e\x35\x37\x2c\x31\x30\x38\x2d\x37\x31\x2e\x30\x37\x2c\ +\x31\x37\x35\x2e\x33\x39\x2d\x36\x31\x2e\x34\x41\x31\x38\x38\x2e\ +\x31\x34\x2c\x31\x38\x38\x2e\x31\x34\x2c\x30\x2c\x30\x2c\x31\x2c\ +\x34\x31\x35\x2c\x33\x30\x30\x2e\x39\x34\x2c\x36\x37\x2e\x34\x39\ +\x2c\x36\x37\x2e\x34\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x33\x36\ +\x2e\x36\x32\x2c\x32\x38\x39\x2e\x37\x34\x5a\x22\x2f\x3e\x3c\x70\ +\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\ +\x22\x20\x64\x3d\x22\x4d\x34\x34\x39\x2e\x30\x37\x2c\x32\x39\x31\ +\x2e\x36\x33\x68\x31\x34\x2e\x33\x35\x61\x31\x30\x2e\x36\x34\x2c\ +\x31\x30\x2e\x36\x34\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x32\ +\x2c\x31\x63\x32\x37\x2e\x34\x37\x2c\x34\x2c\x34\x39\x2e\x36\x2c\ +\x32\x36\x2e\x36\x37\x2c\x35\x31\x2e\x36\x38\x2c\x35\x34\x2e\x32\ +\x36\x2c\x31\x2e\x31\x37\x2c\x31\x35\x2e\x35\x33\x2e\x35\x38\x2c\ +\x33\x31\x2e\x32\x2e\x37\x38\x2c\x34\x36\x2e\x38\x2c\x30\x2c\x32\ +\x2e\x31\x33\x2c\x30\x2c\x34\x2e\x32\x35\x2c\x30\x2c\x36\x2e\x38\ +\x37\x68\x31\x31\x2e\x37\x35\x63\x31\x33\x2e\x39\x34\x2c\x30\x2c\ +\x31\x39\x2e\x33\x36\x2c\x35\x2e\x33\x39\x2c\x31\x39\x2e\x33\x36\ +\x2c\x31\x39\x2e\x32\x32\x2c\x30\x2c\x33\x33\x2e\x32\x35\x2d\x2e\ +\x31\x38\x2c\x36\x36\x2e\x35\x2e\x31\x32\x2c\x39\x39\x2e\x37\x35\ +\x2e\x30\x39\x2c\x39\x2e\x36\x38\x2d\x32\x2e\x39\x33\x2c\x31\x36\ +\x2e\x36\x37\x2d\x31\x32\x2e\x31\x36\x2c\x32\x30\x2e\x33\x39\x48\ +\x33\x37\x35\x2e\x31\x32\x63\x2d\x37\x2e\x32\x34\x2d\x33\x2e\x31\ +\x37\x2d\x31\x32\x2d\x38\x2e\x31\x31\x2d\x31\x32\x2d\x31\x36\x2e\ +\x35\x33\x2c\x30\x2d\x33\x35\x2e\x34\x36\x2d\x2e\x30\x38\x2d\x37\ +\x30\x2e\x39\x31\x2c\x30\x2d\x31\x30\x36\x2e\x33\x37\x2c\x30\x2d\ +\x31\x30\x2e\x32\x39\x2c\x36\x2e\x32\x34\x2d\x31\x36\x2e\x32\x34\ +\x2c\x31\x36\x2e\x35\x36\x2d\x31\x36\x2e\x34\x34\x2c\x34\x2e\x36\ +\x39\x2d\x2e\x30\x39\x2c\x39\x2e\x33\x39\x2c\x30\x2c\x31\x34\x2e\ +\x35\x31\x2c\x30\x2c\x30\x2d\x31\x35\x2e\x37\x33\x2d\x2e\x30\x38\ +\x2d\x33\x30\x2e\x35\x39\x2c\x30\x2d\x34\x35\x2e\x34\x34\x2e\x31\ +\x39\x2d\x32\x38\x2e\x38\x2c\x31\x37\x2e\x32\x38\x2d\x35\x32\x2e\ +\x33\x37\x2c\x34\x33\x2e\x38\x39\x2d\x36\x30\x2e\x35\x39\x43\x34\ +\x34\x31\x2e\x37\x31\x2c\x32\x39\x33\x2e\x34\x31\x2c\x34\x34\x35\ +\x2e\x34\x31\x2c\x32\x39\x32\x2e\x35\x39\x2c\x34\x34\x39\x2e\x30\ +\x37\x2c\x32\x39\x31\x2e\x36\x33\x5a\x6d\x33\x38\x2e\x33\x31\x2c\ +\x31\x30\x38\x2e\x34\x39\x63\x30\x2d\x31\x36\x2e\x35\x34\x2e\x39\ +\x34\x2d\x33\x32\x2e\x37\x33\x2d\x2e\x32\x35\x2d\x34\x38\x2e\x37\ +\x37\x2d\x31\x2e\x33\x31\x2d\x31\x37\x2e\x38\x32\x2d\x31\x35\x2e\ +\x39\x33\x2d\x32\x39\x2e\x37\x35\x2d\x33\x32\x2e\x37\x37\x2d\x32\ +\x38\x2e\x37\x37\x2d\x31\x36\x2e\x36\x35\x2c\x31\x2d\x32\x39\x2e\ +\x32\x31\x2c\x31\x34\x2e\x37\x33\x2d\x32\x39\x2e\x34\x32\x2c\x33\ +\x32\x2e\x35\x32\x2d\x2e\x31\x36\x2c\x31\x33\x2e\x37\x37\x2c\x30\ +\x2c\x32\x37\x2e\x35\x34\x2c\x30\x2c\x34\x31\x2e\x33\x31\x61\x33\ +\x33\x2e\x31\x39\x2c\x33\x33\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x30\ +\x2c\x2e\x34\x32\x2c\x33\x2e\x37\x31\x5a\x6d\x2d\x33\x31\x2e\x33\ +\x33\x2c\x34\x36\x2e\x37\x31\x61\x31\x34\x2e\x39\x32\x2c\x31\x34\ +\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x33\x2e\x36\x38\x2c\ +\x38\x2e\x36\x36\x63\x2d\x32\x2e\x38\x32\x2c\x35\x2e\x35\x37\x2d\ +\x32\x2e\x38\x2c\x31\x31\x2e\x35\x39\x2c\x31\x2e\x36\x2c\x31\x35\ +\x2e\x38\x36\x2c\x34\x2e\x31\x36\x2c\x34\x2c\x34\x2e\x34\x34\x2c\ +\x38\x2e\x33\x34\x2c\x34\x2e\x32\x32\x2c\x31\x33\x2e\x33\x31\x61\ +\x36\x38\x2e\x33\x38\x2c\x36\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\ +\x30\x2c\x30\x2c\x37\x2e\x37\x31\x63\x2e\x33\x37\x2c\x35\x2e\x33\ +\x34\x2c\x33\x2e\x36\x2c\x38\x2e\x38\x33\x2c\x38\x2c\x38\x2e\x38\ +\x37\x73\x37\x2e\x37\x35\x2d\x33\x2e\x34\x36\x2c\x38\x2e\x30\x37\ +\x2d\x38\x2e\x37\x36\x61\x31\x31\x31\x2e\x34\x33\x2c\x31\x31\x31\ +\x2e\x34\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x31\x2e\x35\ +\x36\x2c\x39\x2e\x36\x39\x2c\x39\x2e\x36\x39\x2c\x30\x2c\x30\x2c\ +\x31\x2c\x33\x2e\x32\x2d\x38\x2e\x31\x33\x63\x34\x2e\x37\x31\x2d\ +\x34\x2e\x36\x34\x2c\x35\x2e\x36\x31\x2d\x31\x30\x2e\x35\x33\x2c\ +\x33\x2d\x31\x36\x2e\x37\x32\x41\x31\x35\x2e\x34\x38\x2c\x31\x35\ +\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x35\x36\x2e\x30\x35\ +\x2c\x34\x34\x36\x2e\x38\x33\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ +\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\ +\x3d\x22\x4d\x33\x35\x38\x2e\x32\x34\x2c\x34\x31\x35\x2e\x38\x38\ +\x63\x30\x2d\x31\x32\x2e\x30\x35\x2c\x37\x2e\x37\x2d\x31\x39\x2e\ +\x36\x39\x2c\x32\x30\x2d\x31\x39\x2e\x39\x33\x2c\x33\x2e\x32\x35\ +\x2d\x2e\x30\x36\x2c\x36\x2e\x34\x35\x2c\x30\x2c\x39\x2e\x38\x34\ +\x2c\x30\x68\x31\x2e\x32\x34\x63\x30\x2d\x32\x2e\x35\x32\x2c\x30\ +\x2d\x35\x2c\x30\x2d\x37\x2e\x34\x38\x6c\x2d\x36\x31\x2e\x37\x31\ +\x2d\x38\x35\x2e\x36\x32\x2c\x31\x37\x32\x2e\x32\x31\x2d\x32\x33\ +\x39\x48\x34\x34\x31\x2e\x38\x37\x4c\x32\x39\x38\x2e\x36\x32\x2c\ +\x32\x36\x32\x2e\x36\x33\x2c\x31\x35\x35\x2e\x33\x38\x2c\x36\x33\ +\x2e\x38\x36\x48\x39\x37\x2e\x34\x35\x6c\x31\x37\x32\x2e\x32\x31\ +\x2c\x32\x33\x39\x2d\x31\x37\x32\x2e\x32\x33\x2c\x32\x33\x39\x68\ +\x35\x37\x2e\x39\x35\x4c\x32\x39\x38\x2e\x36\x32\x2c\x33\x34\x33\ +\x6c\x35\x39\x2e\x35\x39\x2c\x38\x32\x2e\x36\x39\x5a\x22\x2f\x3e\ +\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x0a\x70\ +\x3c\ +\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ +\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ +\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ +\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ +\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ +\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ +\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ +\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\ +\x35\x34\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x66\x69\x6c\x6c\ +\x3a\x23\x39\x32\x39\x34\x39\x37\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\ +\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\ +\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\ +\x4d\x32\x39\x30\x2e\x31\x38\x2c\x37\x31\x2e\x33\x35\x43\x34\x30\ +\x32\x2c\x37\x33\x2e\x31\x34\x2c\x34\x39\x30\x2e\x31\x2c\x31\x31\ +\x31\x2e\x34\x34\x2c\x35\x36\x33\x2e\x39\x2c\x31\x38\x37\x2e\x34\ +\x35\x63\x31\x34\x2e\x38\x2c\x31\x35\x2e\x32\x35\x2c\x31\x31\x2e\ +\x38\x31\x2c\x33\x33\x2e\x39\x33\x2c\x33\x2e\x32\x36\x2c\x34\x34\ +\x2e\x30\x35\x2d\x31\x31\x2e\x31\x32\x2c\x31\x33\x2e\x31\x36\x2d\ +\x32\x38\x2e\x36\x32\x2c\x31\x33\x2d\x34\x31\x2e\x34\x36\x2e\x38\ +\x33\x2d\x31\x35\x2e\x30\x38\x2d\x31\x34\x2e\x32\x37\x2d\x33\x30\ +\x2e\x32\x2d\x32\x38\x2e\x37\x32\x2d\x34\x36\x2e\x36\x31\x2d\x34\ +\x31\x2e\x31\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\x2d\x38\x31\x2e\ +\x33\x33\x2d\x34\x36\x2e\x37\x34\x2d\x31\x32\x37\x2e\x37\x35\x2d\ +\x35\x34\x2e\x36\x34\x2d\x35\x34\x2d\x39\x2e\x32\x2d\x31\x30\x36\ +\x2e\x39\x32\x2d\x34\x2e\x33\x32\x2d\x31\x35\x38\x2e\x35\x2c\x31\ +\x35\x2e\x32\x32\x2d\x34\x35\x2c\x31\x37\x2d\x38\x34\x2e\x32\x39\ +\x2c\x34\x33\x2e\x39\x33\x2d\x31\x31\x38\x2e\x31\x36\x2c\x37\x39\ +\x2e\x39\x34\x2d\x38\x2e\x35\x37\x2c\x39\x2e\x31\x2d\x31\x38\x2e\ +\x36\x35\x2c\x31\x32\x2e\x33\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\ +\x2d\x32\x30\x2e\x30\x39\x2d\x37\x2e\x35\x36\x2d\x32\x35\x2e\x32\ +\x2d\x33\x34\x2e\x30\x38\x2d\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\ +\x31\x61\x33\x38\x30\x2e\x35\x31\x2c\x33\x38\x30\x2e\x35\x31\x2c\ +\x30\x2c\x30\x2c\x31\x2c\x35\x37\x2e\x36\x32\x2d\x35\x30\x2e\x33\ +\x38\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\x38\x39\x2e\x39\x33\ +\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\x37\x2d\x36\x30\x2e\ +\x34\x35\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x34\x2e\x32\x33\x2c\ +\x32\x37\x37\x2e\x34\x33\x2c\x37\x32\x2e\x38\x35\x2c\x32\x39\x30\ +\x2e\x31\x38\x2c\x37\x31\x2e\x33\x35\x5a\x22\x2f\x3e\x3c\x70\x61\ +\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\ +\x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x31\x37\x2e\x33\x37\x63\ +\x2d\x32\x32\x2c\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\ +\x35\x2d\x33\x39\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\ +\x32\x33\x2e\x33\x37\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\ +\x33\x2c\x33\x38\x2e\x38\x36\x2d\x34\x31\x2e\x37\x39\x2c\x32\x32\ +\x2e\x32\x35\x2c\x30\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\ +\x37\x2c\x33\x39\x2e\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\ +\x32\x2e\x31\x2c\x35\x31\x37\x2e\x33\x37\x2c\x33\x30\x30\x2c\x35\ +\x31\x37\x2e\x33\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\ +\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\ +\x4d\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x68\x31\ +\x34\x2e\x33\x35\x61\x31\x30\x2e\x36\x34\x2c\x31\x30\x2e\x36\x34\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x32\x2c\x31\x63\x32\x37\ +\x2e\x34\x37\x2c\x34\x2c\x34\x39\x2e\x36\x2c\x32\x36\x2e\x36\x37\ +\x2c\x35\x31\x2e\x36\x38\x2c\x35\x34\x2e\x32\x36\x2c\x31\x2e\x31\ +\x37\x2c\x31\x35\x2e\x35\x33\x2e\x35\x38\x2c\x33\x31\x2e\x32\x2e\ +\x37\x38\x2c\x34\x36\x2e\x38\x2c\x30\x2c\x32\x2e\x31\x32\x2c\x30\ +\x2c\x34\x2e\x32\x35\x2c\x30\x2c\x36\x2e\x38\x36\x68\x31\x31\x2e\ +\x37\x35\x63\x31\x33\x2e\x39\x34\x2c\x30\x2c\x31\x39\x2e\x33\x36\ +\x2c\x35\x2e\x33\x39\x2c\x31\x39\x2e\x33\x36\x2c\x31\x39\x2e\x32\ +\x32\x2c\x30\x2c\x33\x33\x2e\x32\x35\x2d\x2e\x31\x38\x2c\x36\x36\ +\x2e\x35\x2e\x31\x32\x2c\x39\x39\x2e\x37\x35\x2e\x30\x39\x2c\x39\ +\x2e\x36\x38\x2d\x32\x2e\x39\x33\x2c\x31\x36\x2e\x36\x37\x2d\x31\ +\x32\x2e\x31\x36\x2c\x32\x30\x2e\x33\x39\x48\x33\x37\x35\x2e\x31\ +\x32\x63\x2d\x37\x2e\x32\x34\x2d\x33\x2e\x31\x37\x2d\x31\x32\x2d\ +\x38\x2e\x31\x31\x2d\x31\x32\x2d\x31\x36\x2e\x35\x33\x2c\x30\x2d\ +\x33\x35\x2e\x34\x36\x2d\x2e\x30\x38\x2d\x37\x30\x2e\x39\x31\x2c\ +\x30\x2d\x31\x30\x36\x2e\x33\x37\x2c\x30\x2d\x31\x30\x2e\x32\x39\ +\x2c\x36\x2e\x32\x34\x2d\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\x35\ +\x36\x2d\x31\x36\x2e\x34\x34\x2c\x34\x2e\x36\x39\x2d\x2e\x30\x39\ +\x2c\x39\x2e\x33\x39\x2c\x30\x2c\x31\x34\x2e\x35\x31\x2c\x30\x2c\ +\x30\x2d\x31\x35\x2e\x37\x33\x2d\x2e\x30\x38\x2d\x33\x30\x2e\x35\ +\x39\x2c\x30\x2d\x34\x35\x2e\x34\x35\x2e\x31\x39\x2d\x32\x38\x2e\ +\x37\x39\x2c\x31\x37\x2e\x32\x38\x2d\x35\x32\x2e\x33\x36\x2c\x34\ +\x33\x2e\x38\x39\x2d\x36\x30\x2e\x35\x38\x43\x34\x34\x31\x2e\x37\ +\x31\x2c\x32\x38\x32\x2e\x31\x34\x2c\x34\x34\x35\x2e\x34\x31\x2c\ +\x32\x38\x31\x2e\x33\x33\x2c\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\ +\x30\x2e\x33\x37\x5a\x6d\x33\x38\x2e\x33\x31\x2c\x31\x30\x38\x2e\ +\x34\x39\x63\x30\x2d\x31\x36\x2e\x35\x34\x2e\x39\x34\x2d\x33\x32\ +\x2e\x37\x33\x2d\x2e\x32\x35\x2d\x34\x38\x2e\x37\x37\x2d\x31\x2e\ +\x33\x31\x2d\x31\x37\x2e\x38\x32\x2d\x31\x35\x2e\x39\x33\x2d\x32\ +\x39\x2e\x37\x35\x2d\x33\x32\x2e\x37\x37\x2d\x32\x38\x2e\x37\x37\ +\x2d\x31\x36\x2e\x36\x35\x2c\x31\x2d\x32\x39\x2e\x32\x31\x2c\x31\ +\x34\x2e\x37\x33\x2d\x32\x39\x2e\x34\x32\x2c\x33\x32\x2e\x35\x32\ +\x2d\x2e\x31\x36\x2c\x31\x33\x2e\x37\x37\x2c\x30\x2c\x32\x37\x2e\ +\x35\x34\x2c\x30\x2c\x34\x31\x2e\x33\x31\x61\x33\x33\x2e\x31\x39\ +\x2c\x33\x33\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x32\ +\x2c\x33\x2e\x37\x31\x5a\x6d\x2d\x33\x31\x2e\x33\x33\x2c\x34\x36\ +\x2e\x37\x31\x61\x31\x34\x2e\x39\x31\x2c\x31\x34\x2e\x39\x31\x2c\ +\x30\x2c\x30\x2c\x30\x2d\x31\x33\x2e\x36\x38\x2c\x38\x2e\x36\x36\ +\x63\x2d\x32\x2e\x38\x32\x2c\x35\x2e\x35\x37\x2d\x32\x2e\x38\x2c\ +\x31\x31\x2e\x35\x39\x2c\x31\x2e\x36\x2c\x31\x35\x2e\x38\x35\x2c\ +\x34\x2e\x31\x36\x2c\x34\x2c\x34\x2e\x34\x34\x2c\x38\x2e\x33\x35\ +\x2c\x34\x2e\x32\x32\x2c\x31\x33\x2e\x33\x32\x61\x36\x38\x2e\x33\ +\x38\x2c\x36\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\ +\x37\x2e\x37\x31\x63\x2e\x33\x37\x2c\x35\x2e\x33\x33\x2c\x33\x2e\ +\x36\x2c\x38\x2e\x38\x33\x2c\x38\x2c\x38\x2e\x38\x37\x73\x37\x2e\ +\x37\x35\x2d\x33\x2e\x34\x36\x2c\x38\x2e\x30\x37\x2d\x38\x2e\x37\ +\x36\x61\x31\x31\x31\x2e\x34\x34\x2c\x31\x31\x31\x2e\x34\x34\x2c\ +\x30\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x31\x2e\x35\x36\x2c\x39\x2e\ +\x36\x38\x2c\x39\x2e\x36\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x2e\ +\x32\x2d\x38\x2e\x31\x33\x63\x34\x2e\x37\x31\x2d\x34\x2e\x36\x34\ +\x2c\x35\x2e\x36\x31\x2d\x31\x30\x2e\x35\x33\x2c\x33\x2d\x31\x36\ +\x2e\x37\x32\x41\x31\x35\x2e\x34\x38\x2c\x31\x35\x2e\x34\x38\x2c\ +\x30\x2c\x30\x2c\x30\x2c\x34\x35\x36\x2e\x30\x35\x2c\x34\x33\x35\ +\x2e\x35\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ +\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\ +\x37\x39\x2e\x35\x38\x2c\x33\x38\x34\x2e\x33\x31\x63\x33\x2e\x31\ +\x38\x2d\x2e\x30\x36\x2c\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\ +\x2e\x36\x31\x2c\x30\x2c\x30\x2d\x33\x2e\x32\x34\x2c\x30\x2d\x36\ +\x2e\x34\x33\x2c\x30\x2d\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\ +\x35\x33\x2c\x30\x2d\x32\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\ +\x35\x38\x2d\x33\x32\x2e\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\ +\x38\x2e\x39\x33\x2d\x33\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\ +\x34\x2d\x32\x39\x2e\x35\x37\x43\x32\x34\x36\x2c\x33\x31\x39\x2e\ +\x31\x34\x2c\x32\x31\x38\x2c\x33\x33\x34\x2e\x36\x31\x2c\x31\x39\ +\x34\x2e\x31\x33\x2c\x33\x35\x39\x63\x2d\x36\x2c\x36\x2e\x31\x37\ +\x2d\x39\x2e\x36\x33\x2c\x31\x33\x2e\x35\x39\x2d\x39\x2e\x36\x31\ +\x2c\x32\x32\x2e\x38\x31\x61\x36\x38\x2e\x39\x33\x2c\x36\x38\x2e\ +\x39\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x33\x38\x63\ +\x35\x2e\x36\x33\x2c\x32\x33\x2e\x32\x38\x2c\x33\x31\x2e\x32\x37\ +\x2c\x33\x30\x2e\x32\x38\x2c\x34\x37\x2e\x39\x33\x2c\x31\x33\x2e\ +\x32\x37\x2c\x32\x34\x2e\x36\x31\x2d\x32\x35\x2e\x31\x31\x2c\x35\ +\x33\x2e\x37\x35\x2d\x33\x33\x2e\x38\x36\x2c\x38\x37\x2d\x32\x36\ +\x2e\x37\x37\x2c\x31\x35\x2c\x33\x2e\x32\x2c\x32\x38\x2c\x31\x30\ +\x2e\x33\x35\x2c\x33\x39\x2e\x36\x2c\x32\x30\x2e\x34\x32\x43\x33\ +\x36\x33\x2e\x31\x37\x2c\x33\x38\x38\x2e\x38\x32\x2c\x33\x37\x30\ +\x2e\x30\x37\x2c\x33\x38\x34\x2e\x34\x39\x2c\x33\x37\x39\x2e\x35\ +\x38\x2c\x33\x38\x34\x2e\x33\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ +\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\ +\x64\x3d\x22\x4d\x34\x35\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\ +\x31\x61\x32\x36\x2e\x31\x32\x2c\x32\x36\x2e\x31\x32\x2c\x30\x2c\ +\x30\x2c\x30\x2d\x37\x2e\x37\x39\x2c\x31\x2e\x36\x36\x63\x35\x2e\ +\x35\x39\x2c\x35\x2e\x37\x32\x2c\x31\x32\x2e\x33\x34\x2c\x38\x2e\ +\x37\x38\x2c\x32\x33\x2c\x38\x2e\x36\x31\x61\x32\x34\x2c\x32\x34\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x36\x2e\x32\x35\x2d\x31\x2e\x32\x37\ +\x41\x32\x35\x2e\x36\x38\x2c\x32\x35\x2e\x36\x38\x2c\x30\x2c\x30\ +\x2c\x30\x2c\x34\x35\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\x31\ +\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ +\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x33\x36\x2e\ +\x36\x32\x2c\x32\x37\x38\x2e\x34\x38\x63\x32\x2e\x36\x31\x2d\x2e\ +\x38\x2c\x35\x2e\x32\x33\x2d\x31\x2e\x34\x36\x2c\x37\x2e\x37\x37\ +\x2d\x32\x2e\x30\x39\x2c\x31\x2e\x31\x34\x2d\x2e\x32\x38\x2c\x32\ +\x2e\x32\x38\x2d\x2e\x35\x36\x2c\x33\x2e\x34\x32\x2d\x2e\x38\x36\ +\x6c\x2e\x36\x32\x2d\x2e\x31\x36\x68\x31\x36\x6c\x2e\x39\x33\x2e\ +\x33\x39\x63\x2e\x33\x36\x2e\x31\x35\x2e\x37\x32\x2e\x33\x33\x2c\ +\x31\x2e\x30\x37\x2e\x35\x6c\x2e\x33\x37\x2e\x31\x39\x61\x36\x36\ +\x2e\x31\x38\x2c\x36\x36\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x31\x2c\ +\x32\x36\x2e\x38\x33\x2c\x31\x30\x2e\x33\x38\x2c\x33\x30\x2e\x33\ +\x35\x2c\x33\x30\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\ +\x39\x34\x2d\x31\x30\x2e\x31\x35\x41\x32\x30\x35\x2c\x32\x30\x35\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x36\x34\x2e\x34\x34\x2c\x32\x35\ +\x34\x63\x2d\x36\x30\x2e\x32\x32\x2d\x35\x30\x2e\x33\x31\x2d\x31\ +\x32\x38\x2e\x33\x31\x2d\x37\x30\x2d\x32\x30\x33\x2e\x39\x31\x2d\ +\x35\x38\x2e\x33\x36\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\x2d\ +\x31\x30\x35\x2e\x38\x34\x2c\x33\x36\x2d\x31\x34\x36\x2e\x38\x34\ +\x2c\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\x37\x2c\x38\x2e\x37\x32\ +\x2d\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\x36\x32\x2d\x36\x2e\x38\ +\x39\x2c\x33\x31\x2e\x35\x32\x2c\x37\x2e\x32\x34\x2c\x32\x32\x2c\ +\x33\x31\x2e\x35\x36\x2c\x32\x36\x2e\x38\x33\x2c\x34\x37\x2e\x39\ +\x31\x2c\x31\x30\x2c\x34\x39\x2e\x32\x32\x2d\x35\x30\x2e\x35\x37\ +\x2c\x31\x30\x38\x2d\x37\x31\x2e\x30\x37\x2c\x31\x37\x35\x2e\x33\ +\x39\x2d\x36\x31\x2e\x34\x41\x31\x38\x37\x2e\x39\x32\x2c\x31\x38\ +\x37\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x31\x35\x2c\x32\ +\x38\x39\x2e\x36\x38\x2c\x36\x37\x2e\x34\x39\x2c\x36\x37\x2e\x34\ +\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x33\x36\x2e\x36\x32\x2c\x32\ +\x37\x38\x2e\x34\x38\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x09\x92\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -25810,6 +26935,10 @@ \x00\xdd\x57\xa7\ \x00\x31\ \x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0d\ +\x02\xdd\x57\xa7\ +\x00\x30\ +\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0f\ \x05\x15\x19\xa7\ \x00\x77\ @@ -25827,6 +26956,10 @@ \x00\x77\ \x00\x69\x00\x66\x00\x69\x00\x5f\x00\x6c\x00\x6f\x00\x63\x00\x6b\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0d\ +\x0a\xdd\x57\x87\ +\x00\x34\ +\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0d\ \x0c\xdd\x57\x87\ \x00\x33\ \x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ @@ -25839,10 +26972,35 @@ \x0e\xdd\x57\x87\ \x00\x32\ \x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x17\ +\x0f\x21\x74\x27\ +\x00\x32\ +\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x5f\x00\x70\x00\x72\x00\x6f\x00\x74\x00\x65\x00\x63\x00\x74\ +\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0b\ \x0f\x22\xf7\x67\ \x00\x6e\ \x00\x6f\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x17\ +\x0f\x29\x74\x27\ +\x00\x33\ +\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x5f\x00\x70\x00\x72\x00\x6f\x00\x74\x00\x65\x00\x63\x00\x74\ +\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x17\ +\x0f\x31\x74\x27\ +\x00\x34\ +\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x5f\x00\x70\x00\x72\x00\x6f\x00\x74\x00\x65\x00\x63\x00\x74\ +\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x17\ +\x0f\x51\x74\x27\ +\x00\x30\ +\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x5f\x00\x70\x00\x72\x00\x6f\x00\x74\x00\x65\x00\x63\x00\x74\ +\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x17\ +\x0f\x59\x74\x27\ +\x00\x31\ +\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x5f\x00\x70\x00\x72\x00\x6f\x00\x74\x00\x65\x00\x63\x00\x74\ +\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x15\ \x00\x03\x60\x47\ \x00\x74\ @@ -26057,9 +27215,9 @@ qt_resource_struct_v1 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x10\x00\x00\x00\x01\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9a\ -\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x97\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x8b\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa1\ +\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9e\ +\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ \x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x80\ \x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7c\ \x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x76\ @@ -26185,83 +27343,90 @@ \x00\x00\x0f\x00\x00\x00\x00\x00\x00\x01\x00\x04\x9a\xb0\ \x00\x00\x0f\x2c\x00\x00\x00\x00\x00\x01\x00\x04\xa1\x97\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x81\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x09\x00\x00\x00\x82\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x10\x00\x00\x00\x82\ \x00\x00\x0f\x50\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xfb\ -\x00\x00\x0f\x70\x00\x01\x00\x00\x00\x01\x00\x04\xad\xa7\ -\x00\x00\x0f\x94\x00\x00\x00\x00\x00\x01\x00\x04\xb8\xf8\ -\x00\x00\x0f\xb6\x00\x00\x00\x00\x00\x01\x00\x04\xc0\x9d\ -\x00\x00\x0f\xd2\x00\x00\x00\x00\x00\x01\x00\x04\xd1\x9d\ -\x00\x00\x0f\xf6\x00\x00\x00\x00\x00\x01\x00\x04\xdb\x1e\ -\x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe0\xbb\ -\x00\x00\x10\x3e\x00\x00\x00\x00\x00\x01\x00\x04\xea\x84\ -\x00\x00\x10\x5e\x00\x00\x00\x00\x00\x01\x00\x04\xee\x67\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x8c\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x8d\ -\x00\x00\x10\x7a\x00\x00\x00\x00\x00\x01\x00\x04\xf4\xa2\ -\x00\x00\x10\xaa\x00\x00\x00\x00\x00\x01\x00\x04\xfe\x38\ -\x00\x00\x10\xda\x00\x00\x00\x00\x00\x01\x00\x05\x0a\x14\ -\x00\x00\x10\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x10\x58\ -\x00\x00\x11\x28\x00\x00\x00\x00\x00\x01\x00\x05\x17\xe1\ -\x00\x00\x11\x54\x00\x00\x00\x00\x00\x01\x00\x05\x1e\x3f\ -\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x26\x2e\ -\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x33\xd1\ -\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x39\x06\ -\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x42\xdb\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x98\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x99\ -\x00\x00\x11\xd0\x00\x00\x00\x00\x00\x01\x00\x05\x4a\xa5\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9b\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\x9c\ -\x00\x00\x11\xf0\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6b\ -\x00\x00\x12\x06\x00\x00\x00\x00\x00\x01\x00\x05\x57\x1f\ -\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x59\x44\ -\x00\x00\x12\x56\x00\x00\x00\x00\x00\x01\x00\x05\x5a\xc4\ -\x00\x00\x12\x72\x00\x00\x00\x00\x00\x01\x00\x05\x62\x71\ -\x00\x00\x12\x92\x00\x00\x00\x00\x00\x01\x00\x05\x67\x46\ -\x00\x00\x12\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x68\x36\ -\x00\x00\x12\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x6c\x5f\ -\x00\x00\x12\xe4\x00\x00\x00\x00\x00\x01\x00\x05\x72\x96\ -\x00\x00\x12\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x87\xa3\ -\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x8c\x93\ -\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\x8f\x92\ -\x00\x00\x13\x4c\x00\x00\x00\x00\x00\x01\x00\x05\x95\x9c\ -\x00\x00\x13\x7e\x00\x00\x00\x00\x00\x01\x00\x05\x98\xeb\ -\x00\x00\x13\x96\x00\x00\x00\x00\x00\x01\x00\x05\x9d\x0c\ -\x00\x00\x13\xac\x00\x00\x00\x00\x00\x01\x00\x05\xa3\x03\ -\x00\x00\x13\xc0\x00\x00\x00\x00\x00\x01\x00\x05\xa5\x14\ -\x00\x00\x13\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xd7\ -\x00\x00\x13\xfe\x00\x00\x00\x00\x00\x01\x00\x05\xb2\x8b\ -\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb5\xd9\ -\x00\x00\x14\x4a\x00\x00\x00\x00\x00\x01\x00\x05\xb9\x67\ -\x00\x00\x14\x70\x00\x00\x00\x00\x00\x01\x00\x05\xbe\x08\ -\x00\x00\x14\x84\x00\x00\x00\x00\x00\x01\x00\x05\xc7\xda\ -\x00\x00\x14\xb0\x00\x00\x00\x00\x00\x01\x00\x05\xcd\x24\ -\x00\x00\x14\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x2b\ -\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xd4\x0f\ -\x00\x00\x15\x1a\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x58\ -\x00\x00\x15\x30\x00\x00\x00\x00\x00\x01\x00\x05\xdd\x08\ -\x00\x00\x15\x4c\x00\x00\x00\x00\x00\x01\x00\x05\xe0\x4c\ -\x00\x00\x15\x64\x00\x00\x00\x00\x00\x01\x00\x05\xe1\x78\ -\x00\x00\x15\x7e\x00\x00\x00\x00\x00\x01\x00\x05\xe7\x39\ -\x00\x00\x15\xa0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x5b\ -\x00\x00\x15\xbe\x00\x00\x00\x00\x00\x01\x00\x05\xee\x4e\ -\x00\x00\x15\xde\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x52\ -\x00\x00\x16\x00\x00\x00\x00\x00\x00\x01\x00\x05\xf2\x73\ -\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x05\xf5\x47\ -\x00\x00\x16\x4e\x00\x00\x00\x00\x00\x01\x00\x05\xfd\xb3\ -\x00\x00\x16\x72\x00\x00\x00\x00\x00\x01\x00\x06\x05\x73\ -\x00\x00\x16\x96\x00\x00\x00\x00\x00\x01\x00\x06\x0a\x8e\ -\x00\x00\x16\xbe\x00\x00\x00\x00\x00\x01\x00\x06\x0b\xe1\ +\x00\x00\x0f\x70\x00\x00\x00\x00\x00\x01\x00\x04\xb0\x94\ +\x00\x00\x0f\x90\x00\x01\x00\x00\x00\x01\x00\x04\xb6\xbb\ +\x00\x00\x0f\xb4\x00\x00\x00\x00\x00\x01\x00\x04\xc2\x0c\ +\x00\x00\x0f\xd6\x00\x00\x00\x00\x00\x01\x00\x04\xc9\xb1\ +\x00\x00\x0f\xf2\x00\x00\x00\x00\x00\x01\x00\x04\xda\xb1\ +\x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x32\ +\x00\x00\x10\x36\x00\x00\x00\x00\x00\x01\x00\x04\xe9\xb6\ +\x00\x00\x10\x56\x00\x00\x00\x00\x00\x01\x00\x04\xef\x4f\ +\x00\x00\x10\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xf9\x18\ +\x00\x00\x10\x9e\x00\x00\x00\x00\x00\x01\x00\x04\xfe\xb1\ +\x00\x00\x10\xd2\x00\x00\x00\x00\x00\x01\x00\x05\x09\x25\ +\x00\x00\x10\xee\x00\x00\x00\x00\x00\x01\x00\x05\x0f\x60\ +\x00\x00\x11\x22\x00\x00\x00\x00\x00\x01\x00\x05\x19\xda\ +\x00\x00\x11\x56\x00\x00\x00\x00\x00\x01\x00\x05\x24\x39\ +\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x84\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x94\ +\x00\x00\x11\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x39\xf8\ +\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x43\x8e\ +\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6a\ +\x00\x00\x12\x42\x00\x00\x00\x00\x00\x01\x00\x05\x55\xae\ +\x00\x00\x12\x6c\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x37\ +\x00\x00\x12\x98\x00\x00\x00\x00\x00\x01\x00\x05\x63\x95\ +\x00\x00\x12\xce\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x84\ +\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x79\x27\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x7e\x5c\ +\x00\x00\x12\xec\x00\x00\x00\x00\x00\x01\x00\x05\x88\x31\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9f\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ +\x00\x00\x13\x14\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xfb\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\xa3\ +\x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x94\xc1\ +\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x75\ +\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\x9a\ +\x00\x00\x13\x9a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\x1a\ +\x00\x00\x13\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xc7\ +\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xac\x9c\ +\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xad\x8c\ +\x00\x00\x14\x02\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xb5\ +\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb7\xec\ +\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xcc\xf9\ +\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xd1\xe9\ +\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xd4\xe8\ +\x00\x00\x14\x90\x00\x00\x00\x00\x00\x01\x00\x05\xda\xf2\ +\x00\x00\x14\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xde\x41\ +\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x62\ +\x00\x00\x14\xf0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x59\ +\x00\x00\x15\x04\x00\x00\x00\x00\x00\x01\x00\x05\xea\x6a\ +\x00\x00\x15\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xee\x2d\ +\x00\x00\x15\x42\x00\x00\x00\x00\x00\x01\x00\x05\xf7\xe1\ +\x00\x00\x15\x6c\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x2f\ +\x00\x00\x15\x8e\x00\x00\x00\x00\x00\x01\x00\x05\xfe\xbd\ +\x00\x00\x15\xb4\x00\x00\x00\x00\x00\x01\x00\x06\x03\x5e\ +\x00\x00\x15\xc8\x00\x00\x00\x00\x00\x01\x00\x06\x0d\x30\ +\x00\x00\x15\xf4\x00\x00\x00\x00\x00\x01\x00\x06\x12\x7a\ +\x00\x00\x16\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x18\x81\ +\x00\x00\x16\x32\x00\x00\x00\x00\x00\x01\x00\x06\x19\x65\ +\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x1b\xae\ +\x00\x00\x16\x74\x00\x00\x00\x00\x00\x01\x00\x06\x22\x5e\ +\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x25\xa2\ +\x00\x00\x16\xa8\x00\x00\x00\x00\x00\x01\x00\x06\x26\xce\ +\x00\x00\x16\xc2\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x8f\ +\x00\x00\x16\xe4\x00\x00\x00\x00\x00\x01\x00\x06\x2d\xb1\ +\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\xa4\ +\x00\x00\x17\x22\x00\x00\x00\x00\x00\x01\x00\x06\x36\xa8\ +\x00\x00\x17\x44\x00\x00\x00\x00\x00\x01\x00\x06\x37\xc9\ +\x00\x00\x17\x64\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x9d\ +\x00\x00\x17\x92\x00\x00\x00\x00\x00\x01\x00\x06\x43\x09\ +\x00\x00\x17\xb6\x00\x00\x00\x00\x00\x01\x00\x06\x4a\xc9\ +\x00\x00\x17\xda\x00\x00\x00\x00\x00\x01\x00\x06\x4f\xe4\ +\x00\x00\x18\x02\x00\x00\x00\x00\x00\x01\x00\x06\x51\x37\ " qt_resource_struct_v2 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x10\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9a\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa1\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x97\ +\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9e\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x8b\ +\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x80\ \x00\x00\x00\x00\x00\x00\x00\x00\ @@ -26368,9 +27533,9 @@ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x38\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x05\xfc\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ -\x00\x00\x01\x9b\xbc\x28\x2f\x35\ +\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ \x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x41\x71\ -\x00\x00\x01\x9b\xbc\x28\x2f\x35\ +\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3b\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x44\ @@ -26513,139 +27678,153 @@ \x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x81\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x09\x00\x00\x00\x82\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x10\x00\x00\x00\x82\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0f\x50\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xfb\ -\x00\x00\x01\x9b\x7f\x73\xe2\xad\ -\x00\x00\x0f\x70\x00\x01\x00\x00\x00\x01\x00\x04\xad\xa7\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x0f\x70\x00\x00\x00\x00\x00\x01\x00\x04\xb0\x94\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x0f\x90\x00\x01\x00\x00\x00\x01\x00\x04\xb6\xbb\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x0f\x94\x00\x00\x00\x00\x00\x01\x00\x04\xb8\xf8\ +\x00\x00\x0f\xb4\x00\x00\x00\x00\x00\x01\x00\x04\xc2\x0c\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x0f\xb6\x00\x00\x00\x00\x00\x01\x00\x04\xc0\x9d\ +\x00\x00\x0f\xd6\x00\x00\x00\x00\x00\x01\x00\x04\xc9\xb1\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x0f\xd2\x00\x00\x00\x00\x00\x01\x00\x04\xd1\x9d\ +\x00\x00\x0f\xf2\x00\x00\x00\x00\x00\x01\x00\x04\xda\xb1\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x0f\xf6\x00\x00\x00\x00\x00\x01\x00\x04\xdb\x1e\ -\x00\x00\x01\x9b\x7f\x73\xe2\xad\ -\x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe0\xbb\ +\x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x32\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x10\x36\x00\x00\x00\x00\x00\x01\x00\x04\xe9\xb6\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x10\x56\x00\x00\x00\x00\x00\x01\x00\x04\xef\x4f\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x10\x3e\x00\x00\x00\x00\x00\x01\x00\x04\xea\x84\ -\x00\x00\x01\x9b\x7f\x73\xe2\xad\ -\x00\x00\x10\x5e\x00\x00\x00\x00\x00\x01\x00\x04\xee\x67\ +\x00\x00\x10\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xf9\x18\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x10\x9e\x00\x00\x00\x00\x00\x01\x00\x04\xfe\xb1\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x10\xd2\x00\x00\x00\x00\x00\x01\x00\x05\x09\x25\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x8c\ +\x00\x00\x10\xee\x00\x00\x00\x00\x00\x01\x00\x05\x0f\x60\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x11\x22\x00\x00\x00\x00\x00\x01\x00\x05\x19\xda\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x11\x56\x00\x00\x00\x00\x00\x01\x00\x05\x24\x39\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x84\ +\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x8d\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x94\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x10\x7a\x00\x00\x00\x00\x00\x01\x00\x04\xf4\xa2\ +\x00\x00\x11\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x39\xf8\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x10\xaa\x00\x00\x00\x00\x00\x01\x00\x04\xfe\x38\ +\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x43\x8e\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x10\xda\x00\x00\x00\x00\x00\x01\x00\x05\x0a\x14\ +\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6a\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x10\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x10\x58\ +\x00\x00\x12\x42\x00\x00\x00\x00\x00\x01\x00\x05\x55\xae\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x11\x28\x00\x00\x00\x00\x00\x01\x00\x05\x17\xe1\ +\x00\x00\x12\x6c\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x37\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x11\x54\x00\x00\x00\x00\x00\x01\x00\x05\x1e\x3f\ +\x00\x00\x12\x98\x00\x00\x00\x00\x00\x01\x00\x05\x63\x95\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x26\x2e\ +\x00\x00\x12\xce\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x84\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x33\xd1\ +\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x79\x27\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x39\x06\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x7e\x5c\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x42\xdb\ +\x00\x00\x12\xec\x00\x00\x00\x00\x00\x01\x00\x05\x88\x31\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x98\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9f\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x99\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x11\xd0\x00\x00\x00\x00\x00\x01\x00\x05\x4a\xa5\ +\x00\x00\x13\x14\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xfb\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9b\ +\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\x9c\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\xa3\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x11\xf0\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6b\ +\x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x94\xc1\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x06\x00\x00\x00\x00\x00\x01\x00\x05\x57\x1f\ +\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x75\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x59\x44\ +\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\x9a\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x12\x56\x00\x00\x00\x00\x00\x01\x00\x05\x5a\xc4\ +\x00\x00\x13\x9a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\x1a\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x12\x72\x00\x00\x00\x00\x00\x01\x00\x05\x62\x71\ +\x00\x00\x13\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xc7\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x12\x92\x00\x00\x00\x00\x00\x01\x00\x05\x67\x46\ +\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xac\x9c\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x12\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x68\x36\ -\x00\x00\x01\x9b\xbc\x0f\x8a\x2e\ -\x00\x00\x12\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x6c\x5f\ +\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xad\x8c\ +\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ +\x00\x00\x14\x02\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xb5\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\xe4\x00\x00\x00\x00\x00\x01\x00\x05\x72\x96\ +\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb7\xec\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x12\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x87\xa3\ +\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xcc\xf9\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x13\x20\x00\x00\x00\x00\x00\x01\x00\x05\x8c\x93\ +\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xd1\xe9\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x13\x36\x00\x00\x00\x00\x00\x01\x00\x05\x8f\x92\ +\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xd4\xe8\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\x4c\x00\x00\x00\x00\x00\x01\x00\x05\x95\x9c\ +\x00\x00\x14\x90\x00\x00\x00\x00\x00\x01\x00\x05\xda\xf2\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x13\x7e\x00\x00\x00\x00\x00\x01\x00\x05\x98\xeb\ -\x00\x00\x01\x9b\xbc\x0f\x8a\x2e\ -\x00\x00\x13\x96\x00\x00\x00\x00\x00\x01\x00\x05\x9d\x0c\ +\x00\x00\x14\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xde\x41\ +\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ +\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x62\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x13\xac\x00\x00\x00\x00\x00\x01\x00\x05\xa3\x03\ +\x00\x00\x14\xf0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x59\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\xc0\x00\x00\x00\x00\x00\x01\x00\x05\xa5\x14\ +\x00\x00\x15\x04\x00\x00\x00\x00\x00\x01\x00\x05\xea\x6a\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x13\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xa8\xd7\ +\x00\x00\x15\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xee\x2d\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x13\xfe\x00\x00\x00\x00\x00\x01\x00\x05\xb2\x8b\ +\x00\x00\x15\x42\x00\x00\x00\x00\x00\x01\x00\x05\xf7\xe1\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb5\xd9\ +\x00\x00\x15\x6c\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x2f\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x4a\x00\x00\x00\x00\x00\x01\x00\x05\xb9\x67\ +\x00\x00\x15\x8e\x00\x00\x00\x00\x00\x01\x00\x05\xfe\xbd\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x14\x70\x00\x00\x00\x00\x00\x01\x00\x05\xbe\x08\ +\x00\x00\x15\xb4\x00\x00\x00\x00\x00\x01\x00\x06\x03\x5e\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x84\x00\x00\x00\x00\x00\x01\x00\x05\xc7\xda\ +\x00\x00\x15\xc8\x00\x00\x00\x00\x00\x01\x00\x06\x0d\x30\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x14\xb0\x00\x00\x00\x00\x00\x01\x00\x05\xcd\x24\ +\x00\x00\x15\xf4\x00\x00\x00\x00\x00\x01\x00\x06\x12\x7a\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\xd8\x00\x00\x00\x00\x00\x01\x00\x05\xd3\x2b\ +\x00\x00\x16\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x18\x81\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xd4\x0f\ +\x00\x00\x16\x32\x00\x00\x00\x00\x00\x01\x00\x06\x19\x65\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x15\x1a\x00\x00\x00\x00\x00\x01\x00\x05\xd6\x58\ +\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x1b\xae\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x15\x30\x00\x00\x00\x00\x00\x01\x00\x05\xdd\x08\ +\x00\x00\x16\x74\x00\x00\x00\x00\x00\x01\x00\x06\x22\x5e\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x4c\x00\x00\x00\x00\x00\x01\x00\x05\xe0\x4c\ +\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x25\xa2\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x15\x64\x00\x00\x00\x00\x00\x01\x00\x05\xe1\x78\ +\x00\x00\x16\xa8\x00\x00\x00\x00\x00\x01\x00\x06\x26\xce\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x15\x7e\x00\x00\x00\x00\x00\x01\x00\x05\xe7\x39\ +\x00\x00\x16\xc2\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x8f\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\xa0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x5b\ +\x00\x00\x16\xe4\x00\x00\x00\x00\x00\x01\x00\x06\x2d\xb1\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\xbe\x00\x00\x00\x00\x00\x01\x00\x05\xee\x4e\ +\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\xa4\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x15\xde\x00\x00\x00\x00\x00\x01\x00\x05\xf1\x52\ +\x00\x00\x17\x22\x00\x00\x00\x00\x00\x01\x00\x06\x36\xa8\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x00\x00\x00\x00\x00\x00\x01\x00\x05\xf2\x73\ +\x00\x00\x17\x44\x00\x00\x00\x00\x00\x01\x00\x06\x37\xc9\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x20\x00\x00\x00\x00\x00\x01\x00\x05\xf5\x47\ +\x00\x00\x17\x64\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x9d\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\x4e\x00\x00\x00\x00\x00\x01\x00\x05\xfd\xb3\ +\x00\x00\x17\x92\x00\x00\x00\x00\x00\x01\x00\x06\x43\x09\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x16\x72\x00\x00\x00\x00\x00\x01\x00\x06\x05\x73\ +\x00\x00\x17\xb6\x00\x00\x00\x00\x00\x01\x00\x06\x4a\xc9\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\x96\x00\x00\x00\x00\x00\x01\x00\x06\x0a\x8e\ +\x00\x00\x17\xda\x00\x00\x00\x00\x00\x01\x00\x06\x4f\xe4\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x16\xbe\x00\x00\x00\x00\x00\x01\x00\x06\x0b\xe1\ +\x00\x00\x18\x02\x00\x00\x00\x00\x00\x01\x00\x06\x51\x37\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ " diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/0bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/0bar_wifi.svg new file mode 100644 index 00000000..ceaff53d --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/0bar_wifi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/0bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/0bar_wifi_protected.svg new file mode 100644 index 00000000..a10ea388 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/0bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi.svg index debc48b2..3258893d 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi_protected.svg new file mode 100644 index 00000000..8793447e --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi.svg index d9ba78c1..203b70bb 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi_protected.svg new file mode 100644 index 00000000..a9f3233b --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi.svg index 7c694b76..8d98855c 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi_protected.svg new file mode 100644 index 00000000..458c1ac5 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi.svg new file mode 100644 index 00000000..9aadd8e7 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi_protected.svg new file mode 100644 index 00000000..639762e7 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/4bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/wifiConnectivityWindow.ui b/BlocksScreen/lib/ui/wifiConnectivityWindow.ui index 871ba2dd..688da18c 100644 --- a/BlocksScreen/lib/ui/wifiConnectivityWindow.ui +++ b/BlocksScreen/lib/ui/wifiConnectivityWindow.ui @@ -6,7 +6,7 @@ 0 0 - 800 + 852 480
@@ -697,7 +697,61 @@ using the buttons on the side.
- + + + + + + 1 + 1 + + + + + 0 + 0 + + + + BlankCursor + + + background-color: rgba(255, 255, 255, 0); + + + QFrame::NoFrame + + + QFrame::Plain + + + Qt::ScrollBarAlwaysOff + + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::ScrollPerPixel + + + QAbstractItemView::ScrollPerPixel + + + + + + + + 0 + 0 + + + + Qt::Vertical + + + +
@@ -3116,11 +3170,6 @@ Password QLabel
lib.panels.widgets.loadWidget
- - GroupButton - QPushButton -
lib.utils.group_button
-
diff --git a/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py b/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py index e66053a9..4a41aae5 100644 --- a/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py +++ b/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py @@ -300,14 +300,83 @@ def setupUi(self, wifi_stacked_page): self.nl_back_button.setObjectName("nl_back_button") self.nl_header_layout.addWidget(self.nl_back_button) self.verticalLayout_9.addLayout(self.nl_header_layout) - self.nl_content_layout = QtWidgets.QVBoxLayout() - self.nl_content_layout.setObjectName("nl_content_layout") - self.verticalLayout_9.addLayout(self.nl_content_layout) + self.horizontalLayout_2 = QtWidgets.QHBoxLayout() + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.listView = QtWidgets.QListView(self.network_list_page) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) + sizePolicy.setHorizontalStretch(1) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.listView.sizePolicy().hasHeightForWidth()) + self.listView.setSizePolicy(sizePolicy) + self.listView.setMinimumSize(QtCore.QSize(0, 0)) + self.listView.setStyleSheet("background-color: rgba(255, 255, 255, 0);") + self.listView.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.listView.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) + self.listView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.listView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + self.listView.setSelectionBehavior( + QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems + ) + self.listView.setHorizontalScrollBarPolicy( # No horizontal scroll + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.listView.setVerticalScrollMode( + QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel + ) + self.listView.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + QtWidgets.QScroller.grabGesture( + self.listView, + QtWidgets.QScroller.ScrollerGestureType.TouchGesture, + ) + QtWidgets.QScroller.grabGesture( + self.listView, + QtWidgets.QScroller.ScrollerGestureType.LeftMouseButtonGesture, + ) + self.listView.setEditTriggers( + QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers + ) + + scroller_instance = QtWidgets.QScroller.scroller(self.listView) + scroller_props = scroller_instance.scrollerProperties() + scroller_props.setScrollMetric( + QtWidgets.QScrollerProperties.ScrollMetric.DragVelocitySmoothingFactor, + 0.05, # Lower = more responsive + ) + scroller_props.setScrollMetric( + QtWidgets.QScrollerProperties.ScrollMetric.DecelerationFactor, + 0.4, # higher = less inertia + ) + QtWidgets.QScroller.scroller(self.listView).setScrollerProperties( + scroller_props + ) + self.verticalScrollBar = CustomScrollBar(parent=self.network_list_page) + self.listView.setVerticalScrollBar(self.verticalScrollBar) + self.listView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.horizontalLayout_2.addWidget(self.listView) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.verticalScrollBar.sizePolicy().hasHeightForWidth()) + self.verticalScrollBar.setSizePolicy(sizePolicy) + self.verticalScrollBar.setOrientation(QtCore.Qt.Orientation.Vertical) + self.verticalScrollBar.setObjectName("verticalScrollBar") + self.listView.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) + self.listView.setUniformItemSizes(True) + #self.listView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded) + self.listView.setSpacing(5) + self.horizontalLayout_2.addWidget(self.verticalScrollBar) + self.verticalLayout_9.addLayout(self.horizontalLayout_2) wifi_stacked_page.addWidget(self.network_list_page) self.add_network_page = QtWidgets.QWidget() self.add_network_page.setObjectName("add_network_page") self.verticalLayout_10 = QtWidgets.QVBoxLayout(self.add_network_page) self.verticalLayout_10.setObjectName("verticalLayout_10") + self.verticalScrollBar.setAttribute( + QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents, True + ) + self.scroller = QtWidgets.QScroller.scroller(self.listView) self.add_np_header_layout = QtWidgets.QHBoxLayout() self.add_np_header_layout.setObjectName("add_np_header_layout") spacerItem1 = QtWidgets.QSpacerItem(40, 60, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) @@ -1144,38 +1213,38 @@ def setupUi(self, wifi_stacked_page): wifi_stacked_page.addWidget(self.hotspot_page) self.retranslateUi(wifi_stacked_page) - wifi_stacked_page.setCurrentIndex(4) + wifi_stacked_page.setCurrentIndex(2) QtCore.QMetaObject.connectSlotsByName(wifi_stacked_page) def retranslateUi(self, wifi_stacked_page): _translate = QtCore.QCoreApplication.translate wifi_stacked_page.setWindowTitle(_translate("wifi_stacked_page", "StackedWidget")) - self.network_main_title.setText(_translate("wifi_stacked_page", "Networks")) - self.netlist_strength_label.setText(_translate("wifi_stacked_page", "Signal\n" -"Strength")) - self.netlist_strength.setText(_translate("wifi_stacked_page", "TextLabel")) - self.netlist_security_label.setText(_translate("wifi_stacked_page", "Security\n" -"Type")) - self.netlist_security.setText(_translate("wifi_stacked_page", "TextLabel")) - self.mn_info_box.setText(_translate("wifi_stacked_page", "No network connection.\n" + self.network_main_title.setText("Networks") + self.netlist_strength_label.setText( "Signal\n" +"Strength") + self.netlist_strength.setText( "TextLabel") + self.netlist_security_label.setText( "Security\n" +"Type") + self.netlist_security.setText("TextLabel") + self.mn_info_box.setText( "No network connection.\n" "\n" "Try connecting to Wi-Fi \n" "or turn on the hotspot\n" -"using the buttons on the side.")) - self.wifi_button.setText(_translate("wifi_stacked_page", "Wi-Fi")) - self.hotspot_button.setText(_translate("wifi_stacked_page", "Hotspot")) - self.rescan_button.setText(_translate("wifi_stacked_page", "Reload")) +"using the buttons on the side.") + self.wifi_button.setText( "Wi-Fi") + self.hotspot_button.setText( "Hotspot") + self.rescan_button.setText( "Reload") self.rescan_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.network_list_title.setText(_translate("wifi_stacked_page", "Wi-Fi List")) - self.nl_back_button.setText(_translate("wifi_stacked_page", "Back")) + self.network_list_title.setText("Wi-Fi List") + self.nl_back_button.setText("Back") self.nl_back_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.nl_back_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.add_network_network_label.setText(_translate("wifi_stacked_page", "TextLabel")) - self.add_network_page_backButton.setText(_translate("wifi_stacked_page", "Back")) + self.add_network_network_label.setText("TextLabel") + self.add_network_page_backButton.setText("Back") self.add_network_page_backButton.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.add_network_page_backButton.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.add_network_password_label.setText(_translate("wifi_stacked_page", "Password")) - self.add_network_password_view.setText(_translate("wifi_stacked_page", "View")) + self.add_network_password_label.setText("Password") + self.add_network_password_view.setText("View") self.add_network_password_view.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.add_network_password_view.setProperty("button_type", _translate("wifi_stacked_page", "icon")) self.add_network_validation_button.setText(_translate("wifi_stacked_page", "Activate")) @@ -1216,13 +1285,14 @@ def retranslateUi(self, wifi_stacked_page): self.hotspot_back_button.setText(_translate("wifi_stacked_page", "Back")) self.hotspot_back_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.hotspot_back_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.hotspot_info_name_label.setText(_translate("wifi_stacked_page", "Hotspot Name: ")) - self.hotspot_info_password_label.setText(_translate("wifi_stacked_page", "Hotspot Password:")) - self.hotspot_password_view_button.setText(_translate("wifi_stacked_page", "View")) + self.hotspot_info_name_label.setText("Hotspot Name: ") + self.hotspot_info_password_label.setText("Hotspot Password:") + self.hotspot_password_view_button.setText("View") self.hotspot_password_view_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) self.hotspot_password_view_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.hotspot_change_confirm.setText(_translate("wifi_stacked_page", "Save")) + self.hotspot_change_confirm.setText("Save") from lib.panels.widgets.loadWidget import LoadingOverlayWidget +from lib.utils.blocks_Scrollbar import CustomScrollBar from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_frame import BlocksCustomFrame from lib.utils.blocks_linedit import BlocksCustomLinEdit diff --git a/BlocksScreen/lib/utils/blocks_Scrollbar.py b/BlocksScreen/lib/utils/blocks_Scrollbar.py index 38e571e0..df2e436d 100644 --- a/BlocksScreen/lib/utils/blocks_Scrollbar.py +++ b/BlocksScreen/lib/utils/blocks_Scrollbar.py @@ -1,5 +1,5 @@ -from PyQt6 import QtCore, QtGui, QtWidgets import numpy as np +from PyQt6 import QtCore, QtGui, QtWidgets class CustomScrollBar(QtWidgets.QScrollBar): @@ -26,34 +26,16 @@ def paintEvent(self, event): handle_percentage = int((self.value() / max_val) * 100) - if handle_percentage < 15: - base_handle_length = int( - (groove.height() * page_step / (max_val - min_val + page_step)) - + np.interp(handle_percentage, [0, 15], [0, 40]) - ) - handle_pos = 0 - - elif handle_percentage > 85: - base_handle_length = int( - (groove.height() * page_step / (max_val - min_val + page_step)) - + np.interp(handle_percentage, [85, 100], [40, 0]) - ) - handle_pos = int( - (groove.height() - base_handle_length) - * (max_val - min_val) - / (max_val - min_val) - ) - else: - val = np.interp((handle_percentage), [15, 85], [0, 100]) / 100 * max_val + val = np.interp((handle_percentage), [15, 85], [0, 100]) / 100 * max_val - base_handle_length = int( - (groove.height() * page_step / (max_val - min_val + page_step)) + 40 - ) - handle_pos = int( - (groove.height() - base_handle_length) - * (val - min_val) - / (max_val - min_val) - ) + base_handle_length = int( + (groove.height() * page_step / (max_val - min_val + page_step)) + 40 + ) + handle_pos = int( + (groove.height() - base_handle_length) + * (val - min_val) + / (max_val - min_val) + ) handle_rect = QtCore.QRect( groove.x(), diff --git a/BlocksScreen/lib/utils/blocks_linedit.py b/BlocksScreen/lib/utils/blocks_linedit.py index 242e4b0d..a4342fe3 100644 --- a/BlocksScreen/lib/utils/blocks_linedit.py +++ b/BlocksScreen/lib/utils/blocks_linedit.py @@ -1,78 +1,139 @@ import typing + from PyQt6 import QtCore, QtGui, QtWidgets class BlocksCustomLinEdit(QtWidgets.QLineEdit): clicked = QtCore.pyqtSignal() - def __init__( - self, - parent: QtWidgets.QWidget, - ) -> None: - super(BlocksCustomLinEdit, self).__init__(parent) - - self.button_background = None - self.button_ellipse = None - self._text: str = "" - self.placeholder_str = "Type here" - self._name: str = "" - self.text_color: QtGui.QColor = QtGui.QColor(0, 0, 0) - self.secret: bool = False + # Layout constants + TEXT_MARGIN = 10 + CORNER_RADIUS = 8 + + def __init__(self, parent: typing.Optional[QtWidgets.QWidget] = None) -> None: + super().__init__(parent) + + # State + self._placeholder_str = "Type here" + self._name = "" + self._secret = False # True = show bullets, False = show text + self._show_toggle = False + self._is_password_visible = False + + # Pre-allocated colors (avoid allocation in paint) + self._bg_color = QtGui.QColor(223, 223, 223) + self._bg_pressed_color = QtGui.QColor(200, 200, 200) + self._text_color = QtGui.QColor(0, 0, 0) + self._placeholder_color = QtGui.QColor(130, 130, 130) + + # Touch support self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) + # Cursor + self.setCursor(QtCore.Qt.CursorShape.BlankCursor) + @property - def name(self): - """Widget name""" + def name(self) -> str: + """Widget name property.""" return self._name @name.setter - def name(self, new_name) -> None: - self._name = new_name - self.setObjectName(new_name) + def name(self, value: str) -> None: + self._name = value + self.setObjectName(value) + + def placeholderText(self) -> str: + """Get placeholder text.""" + return self._placeholder_str - def setText(self, text: str) -> None: - """Set widget text""" - super().setText(text) + def setPlaceholderText(self, text: str) -> None: + """Set placeholder text displayed when empty.""" + self._placeholder_str = text + self.update() + + def showToggleButton(self) -> bool: + """Check if toggle button is enabled.""" + return self._show_toggle def setHidden(self, hidden: bool) -> None: - """Hide widget text""" - self.secret = hidden + """ + Set whether text is hidden (password mode). + + Args: + hidden: True to show bullets, False to show actual text + """ + if self._secret == hidden: + return + + self._secret = hidden + self._is_password_visible = not hidden self.update() + def isPasswordVisible(self) -> bool: + """Check if password is currently visible.""" + return self._is_password_visible + + def _get_text_rect(self) -> QtCore.QRect: + """Calculate the rectangle available for text rendering.""" + left_margin = self.TEXT_MARGIN + right_margin = self.TEXT_MARGIN + + return self.rect().adjusted(left_margin, 0, -right_margin, 0) + def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: - """Re-implemented method, handle mouse press events""" + """Handle mouse press""" self.clicked.emit() super().mousePressEvent(event) - def paintEvent(self, e: typing.Optional[QtGui.QPaintEvent]): - """Re-implemented method, paint widget""" + def mouseReleaseEvent(self, event: QtGui.QMouseEvent) -> None: + """Handle mouse release""" + super().mouseReleaseEvent(event) + + def focusInEvent(self, event: QtGui.QFocusEvent) -> None: + """Handle focus in - emit clicked for virtual keyboard.""" + self.clicked.emit() + super().focusInEvent(event) + + def paintEvent(self, event: typing.Optional[QtGui.QPaintEvent]) -> None: + """Custom paint with embedded toggle button.""" painter = QtGui.QPainter(self) - painter.setRenderHint(painter.RenderHint.Antialiasing, True) + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) - # Draw background - bg_color = QtGui.QColor(223, 223, 223) - painter.setBrush(bg_color) + # Background + painter.setBrush(self._bg_color) painter.setPen(QtCore.Qt.PenStyle.NoPen) - painter.drawRoundedRect(self.rect(), 8, 8) + painter.drawRoundedRect(self.rect(), self.CORNER_RADIUS, self.CORNER_RADIUS) + + # Text + self._draw_text(painter) - margin = 5 + painter.end() + + def _draw_text(self, painter: QtGui.QPainter) -> None: + """Draw the text or placeholder.""" + text_rect = self._get_text_rect() display_text = self.text() - if self.secret and display_text: + + # Apply password masking + if self._secret and display_text: display_text = "*" * len(display_text) - if self.text(): - painter.setPen(self.text_color) + if display_text: + painter.setPen(self._text_color) + painter.setFont(self.font()) painter.drawText( - self.rect().adjusted(margin, 0, 0, 0), + text_rect, QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter, display_text, ) else: - painter.setPen(QtGui.QColor(150, 150, 150)) + # Placeholder text + painter.setPen(self._placeholder_color) + painter.setFont(self.font()) painter.drawText( - self.rect().adjusted(margin, 0, 0, 0), + text_rect, QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter, - self.placeholder_str, + self._placeholder_str, ) diff --git a/BlocksScreen/lib/utils/list_model.py b/BlocksScreen/lib/utils/list_model.py index 2f4cbc3a..a622f094 100644 --- a/BlocksScreen/lib/utils/list_model.py +++ b/BlocksScreen/lib/utils/list_model.py @@ -19,6 +19,7 @@ class ListItem: _rfontsize: int = 0 height: int = 60 # Change has needed notificate: bool = False # render red dot + not_clickable: bool = False class EntryListModel(QtCore.QAbstractListModel): @@ -59,6 +60,8 @@ def flags(self, index) -> QtCore.Qt.ItemFlag: """Models item flags, re-implemented method""" item = self.entries[index.row()] flags = QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable + if item.not_clickable: + return QtCore.Qt.ItemFlag.NoItemFlags if item.allow_check: flags |= QtCore.Qt.ItemFlag.ItemIsUserCheckable return flags @@ -152,6 +155,10 @@ def paint( path.addRoundedRect(QtCore.QRectF(rect), radius, radius) # Gradient background (left to right) + if item.not_clickable: + painter.restore() + return + if not item.selected: pressed_color = QtGui.QColor("#1A8FBF") pressed_color.setAlpha(20) @@ -327,6 +334,8 @@ def editorEvent( """Capture view model events, re-implemented method""" item = index.data(QtCore.Qt.ItemDataRole.UserRole) if event.type() == QtCore.QEvent.Type.MouseButtonPress: + if item and item.not_clickable: + return True if item.callback: if callable(item.callback): item.callback() From 3323adf7a44072c79b3d7b3dc216e147912ee116 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Wed, 4 Feb 2026 12:18:15 +0000 Subject: [PATCH 46/70] Fix incorrect file removal (#177) --- BlocksScreen/lib/ui/connectionWindow.ui | 938 +++++ BlocksScreen/lib/ui/connectionWindow_ui.py | 338 ++ BlocksScreen/lib/ui/wifiConnectivityWindow.ui | 3184 ----------------- .../lib/ui/wifiConnectivityWindow_ui.py | 1301 ------- 4 files changed, 1276 insertions(+), 4485 deletions(-) create mode 100644 BlocksScreen/lib/ui/connectionWindow.ui create mode 100644 BlocksScreen/lib/ui/connectionWindow_ui.py delete mode 100644 BlocksScreen/lib/ui/wifiConnectivityWindow.ui delete mode 100644 BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py diff --git a/BlocksScreen/lib/ui/connectionWindow.ui b/BlocksScreen/lib/ui/connectionWindow.ui new file mode 100644 index 00000000..f2bd4899 --- /dev/null +++ b/BlocksScreen/lib/ui/connectionWindow.ui @@ -0,0 +1,938 @@ + + + ConnectivityForm + + + Qt::WindowModal + + + + 0 + 0 + 800 + 480 + + + + + 0 + 0 + + + + + 800 + 480 + + + + + 800 + 480 + + + + Form + + + 1.000000000000000 + + + false + + + #ConnectivityForm{ +background-image: url(:/background/media/1st_background.png); +} + + + + + + + + 10 + 380 + 780 + 124 + + + + + 0 + 0 + + + + + 780 + 124 + + + + + 780 + 150 + + + + + 800 + 80 + + + + + + + + + + + + PreferAntialias + + + + true + + + false + + + + + + QFrame::NoFrame + + + QFrame::Plain + + + 0 + + + + 0 + + + 0 + + + 5 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + + 100 + 80 + + + + + 100 + 80 + + + + + 160 + 80 + + + + + + + + + 66 + 66 + 66 + + + + + + + 66 + 66 + 66 + + + + + + + 66 + 66 + 66 + + + + + + + + + 66 + 66 + 66 + + + + + + + 66 + 66 + 66 + + + + + + + 66 + 66 + 66 + + + + + + + + + 66 + 66 + 66 + + + + + + + 66 + 66 + 66 + + + + + + + 66 + 66 + 66 + + + + + + + + + false + PreferAntialias + false + + + + BlankCursor + + + true + + + Qt::NoFocus + + + Qt::NoContextMenu + + + Qt::LeftToRight + + + false + + + + + + Restart Klipper + + + + :/system_icons/media/btn_icons/restart_klipper.svg + + + + + 46 + 42 + + + + false + + + false + + + false + + + 0 + + + 0 + + + false + + + true + + + :/system/media/btn_icons/restart_klipper.svg + + + true + + + bottom + + + + 255 + 255 + 255 + + + + + + + + + 0 + 0 + + + + + 100 + 80 + + + + + 100 + 80 + + + + + 80 + 80 + + + + BlankCursor + + + true + + + Qt::NoFocus + + + Qt::NoContextMenu + + + false + + + Reboot + + + + :/system_icons/media/btn_icons/firmware_restart.svg:/system_icons/media/btn_icons/firmware_restart.svg + + + false + + + false + + + true + + + :/system/media/btn_icons/reboot.svg + + + bottom + + + + 255 + 255 + 255 + + + + true + + + + + + + + 0 + 0 + + + + + 100 + 80 + + + + + 100 + 80 + + + + + 160 + 80 + + + + BlankCursor + + + true + + + Qt::NoFocus + + + Qt::NoContextMenu + + + false + + + Firmware Restart + + + + :/system_icons/media/btn_icons/firmware_restart.svg:/system_icons/media/btn_icons/firmware_restart.svg + + + false + + + false + + + true + + + :/system/media/btn_icons/restart_firmware.svg + + + true + + + bottom + + + + 255 + 255 + 255 + + + + + + + + + 0 + 0 + + + + + 100 + 80 + + + + + 100 + 80 + + + + + 80 + 80 + + + + + + + + + + + + 13 + + + + true + + + Qt::ClickFocus + + + false + + + + + + Retry + + + + :/system_icons/media/btn_icons/retry_connection.svg:/system_icons/media/btn_icons/retry_connection.svg + + + + 16 + 16 + + + + false + + + 0 + + + 0 + + + false + + + false + + + true + + + bottom + + + :/system/media/btn_icons/restart_printer.svg + + + + 255 + 255 + 255 + + + + true + + + + + + + + 0 + 0 + + + + + 100 + 80 + + + + + 100 + 80 + + + + + 80 + 80 + + + + BlankCursor + + + true + + + Qt::NoFocus + + + Qt::NoContextMenu + + + false + + + Update page + + + + :/system_icons/media/btn_icons/firmware_restart.svg:/system_icons/media/btn_icons/firmware_restart.svg + + + false + + + false + + + true + + + :/system/media/btn_icons/update-software-icon.svg + + + bottom + + + + 255 + 255 + 255 + + + + true + + + + + + + + 0 + 0 + + + + + 100 + 80 + + + + + 100 + 80 + + + + + 80 + 80 + + + + true + + + Qt::NoFocus + + + Qt::NoContextMenu + + + false + + + Wifi Settings + + + + :/system_icons/media/btn_icons/retry_connection.svg:/system_icons/media/btn_icons/retry_connection.svg + + + false + + + false + + + true + + + system_control_btn + + + :/network/media/btn_icons/wifi_config.svg + + + true + + + bottom + + + + + + + + + 0 + 0 + 800 + 380 + + + + + 0 + 0 + + + + + 800 + 380 + + + + + 800 + 380 + + + + false + + + + + + QFrame::NoFrame + + + QFrame::Raised + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 775 + 70 + + + + + + + + + 0 + 0 + + + + + 800 + 380 + + + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + 255 + 255 + 255 + + + + + + + + + Momcake + 17 + 75 + false + true + + + + color:white + + + + + + Qt::AutoText + + + false + + + Qt::AlignCenter + + + true + + + Qt::NoTextInteraction + + + + + + + + + IconButton + QPushButton +
lib.utils.icon_button
+
+ + BlocksCustomFrame + QFrame +
lib.utils.blocks_frame
+ 1 +
+
+ + + + + + + +
diff --git a/BlocksScreen/lib/ui/connectionWindow_ui.py b/BlocksScreen/lib/ui/connectionWindow_ui.py new file mode 100644 index 00000000..772dc227 --- /dev/null +++ b/BlocksScreen/lib/ui/connectionWindow_ui.py @@ -0,0 +1,338 @@ +# Form implementation generated from reading ui file '/home/levi/BlocksScreen/BlocksScreen/lib/ui/connectionWindow.ui' +# +# Created by: PyQt6 UI code generator 6.7.1 +# +# WARNING: Any manual changes made to this file will be lost when pyuic6 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt6 import QtCore, QtGui, QtWidgets + + +class Ui_ConnectivityForm(object): + def setupUi(self, ConnectivityForm): + ConnectivityForm.setObjectName("ConnectivityForm") + ConnectivityForm.setWindowModality(QtCore.Qt.WindowModality.WindowModal) + ConnectivityForm.resize(800, 480) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(ConnectivityForm.sizePolicy().hasHeightForWidth()) + ConnectivityForm.setSizePolicy(sizePolicy) + ConnectivityForm.setMinimumSize(QtCore.QSize(800, 480)) + ConnectivityForm.setMaximumSize(QtCore.QSize(800, 480)) + ConnectivityForm.setWindowOpacity(1.0) + ConnectivityForm.setAutoFillBackground(False) + ConnectivityForm.setStyleSheet("#ConnectivityForm{\n" +"background-image: url(:/background/media/1st_background.png);\n" +"}") + ConnectivityForm.setProperty("class", "") + self.cw_buttonFrame = BlocksCustomFrame(parent=ConnectivityForm) + self.cw_buttonFrame.setGeometry(QtCore.QRect(10, 380, 780, 124)) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.cw_buttonFrame.sizePolicy().hasHeightForWidth()) + self.cw_buttonFrame.setSizePolicy(sizePolicy) + self.cw_buttonFrame.setMinimumSize(QtCore.QSize(780, 124)) + self.cw_buttonFrame.setMaximumSize(QtCore.QSize(780, 150)) + self.cw_buttonFrame.setBaseSize(QtCore.QSize(800, 80)) + palette = QtGui.QPalette() + self.cw_buttonFrame.setPalette(palette) + font = QtGui.QFont() + font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) + self.cw_buttonFrame.setFont(font) + self.cw_buttonFrame.setTabletTracking(True) + self.cw_buttonFrame.setAutoFillBackground(False) + self.cw_buttonFrame.setStyleSheet("") + self.cw_buttonFrame.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.cw_buttonFrame.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) + self.cw_buttonFrame.setLineWidth(0) + self.cw_buttonFrame.setObjectName("cw_buttonFrame") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.cw_buttonFrame) + self.horizontalLayout.setContentsMargins(0, 5, 0, 0) + self.horizontalLayout.setSpacing(0) + self.horizontalLayout.setObjectName("horizontalLayout") + self.RestartKlipperButton = IconButton(parent=self.cw_buttonFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.RestartKlipperButton.sizePolicy().hasHeightForWidth()) + self.RestartKlipperButton.setSizePolicy(sizePolicy) + self.RestartKlipperButton.setMinimumSize(QtCore.QSize(100, 80)) + self.RestartKlipperButton.setMaximumSize(QtCore.QSize(100, 80)) + self.RestartKlipperButton.setBaseSize(QtCore.QSize(160, 80)) + palette = QtGui.QPalette() + brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(66, 66, 66)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.ButtonText, brush) + self.RestartKlipperButton.setPalette(palette) + font = QtGui.QFont() + font.setStrikeOut(False) + font.setKerning(False) + font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) + self.RestartKlipperButton.setFont(font) + self.RestartKlipperButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) + self.RestartKlipperButton.setTabletTracking(True) + self.RestartKlipperButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.RestartKlipperButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.RestartKlipperButton.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.RestartKlipperButton.setAutoFillBackground(False) + self.RestartKlipperButton.setStyleSheet("") + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/restart_klipper.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On) + self.RestartKlipperButton.setIcon(icon) + self.RestartKlipperButton.setIconSize(QtCore.QSize(46, 42)) + self.RestartKlipperButton.setCheckable(False) + self.RestartKlipperButton.setAutoRepeat(False) + self.RestartKlipperButton.setAutoExclusive(False) + self.RestartKlipperButton.setAutoRepeatDelay(0) + self.RestartKlipperButton.setAutoRepeatInterval(0) + self.RestartKlipperButton.setAutoDefault(False) + self.RestartKlipperButton.setFlat(True) + self.RestartKlipperButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/restart_klipper.svg")) + self.RestartKlipperButton.setProperty("has_text", True) + self.RestartKlipperButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) + self.RestartKlipperButton.setObjectName("RestartKlipperButton") + self.horizontalLayout.addWidget(self.RestartKlipperButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) + self.RebootSystemButton = IconButton(parent=self.cw_buttonFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.RebootSystemButton.sizePolicy().hasHeightForWidth()) + self.RebootSystemButton.setSizePolicy(sizePolicy) + self.RebootSystemButton.setMinimumSize(QtCore.QSize(100, 80)) + self.RebootSystemButton.setMaximumSize(QtCore.QSize(100, 80)) + self.RebootSystemButton.setBaseSize(QtCore.QSize(80, 80)) + self.RebootSystemButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) + self.RebootSystemButton.setTabletTracking(True) + self.RebootSystemButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.RebootSystemButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.RebootSystemButton.setAutoFillBackground(False) + icon1 = QtGui.QIcon() + icon1.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/firmware_restart.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + self.RebootSystemButton.setIcon(icon1) + self.RebootSystemButton.setAutoDefault(False) + self.RebootSystemButton.setDefault(False) + self.RebootSystemButton.setFlat(True) + self.RebootSystemButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/reboot.svg")) + self.RebootSystemButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) + self.RebootSystemButton.setProperty("has_text", True) + self.RebootSystemButton.setObjectName("RebootSystemButton") + self.horizontalLayout.addWidget(self.RebootSystemButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) + self.FirmwareRestartButton = IconButton(parent=self.cw_buttonFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.FirmwareRestartButton.sizePolicy().hasHeightForWidth()) + self.FirmwareRestartButton.setSizePolicy(sizePolicy) + self.FirmwareRestartButton.setMinimumSize(QtCore.QSize(100, 80)) + self.FirmwareRestartButton.setMaximumSize(QtCore.QSize(100, 80)) + self.FirmwareRestartButton.setBaseSize(QtCore.QSize(160, 80)) + self.FirmwareRestartButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) + self.FirmwareRestartButton.setTabletTracking(True) + self.FirmwareRestartButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.FirmwareRestartButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.FirmwareRestartButton.setAutoFillBackground(False) + self.FirmwareRestartButton.setIcon(icon1) + self.FirmwareRestartButton.setAutoDefault(False) + self.FirmwareRestartButton.setDefault(False) + self.FirmwareRestartButton.setFlat(True) + self.FirmwareRestartButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/restart_firmware.svg")) + self.FirmwareRestartButton.setProperty("has_text", True) + self.FirmwareRestartButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) + self.FirmwareRestartButton.setObjectName("FirmwareRestartButton") + self.horizontalLayout.addWidget(self.FirmwareRestartButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) + self.RetryConnectionButton = IconButton(parent=self.cw_buttonFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.RetryConnectionButton.sizePolicy().hasHeightForWidth()) + self.RetryConnectionButton.setSizePolicy(sizePolicy) + self.RetryConnectionButton.setMinimumSize(QtCore.QSize(100, 80)) + self.RetryConnectionButton.setMaximumSize(QtCore.QSize(100, 80)) + self.RetryConnectionButton.setBaseSize(QtCore.QSize(80, 80)) + palette = QtGui.QPalette() + self.RetryConnectionButton.setPalette(palette) + font = QtGui.QFont() + font.setPointSize(13) + self.RetryConnectionButton.setFont(font) + self.RetryConnectionButton.setTabletTracking(True) + self.RetryConnectionButton.setFocusPolicy(QtCore.Qt.FocusPolicy.ClickFocus) + self.RetryConnectionButton.setAutoFillBackground(False) + self.RetryConnectionButton.setStyleSheet("") + icon2 = QtGui.QIcon() + icon2.addPixmap(QtGui.QPixmap(":/system_icons/media/btn_icons/retry_connection.svg"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + self.RetryConnectionButton.setIcon(icon2) + self.RetryConnectionButton.setIconSize(QtCore.QSize(16, 16)) + self.RetryConnectionButton.setCheckable(False) + self.RetryConnectionButton.setAutoRepeatDelay(0) + self.RetryConnectionButton.setAutoRepeatInterval(0) + self.RetryConnectionButton.setAutoDefault(False) + self.RetryConnectionButton.setDefault(False) + self.RetryConnectionButton.setFlat(True) + self.RetryConnectionButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/restart_printer.svg")) + self.RetryConnectionButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) + self.RetryConnectionButton.setProperty("has_text", True) + self.RetryConnectionButton.setObjectName("RetryConnectionButton") + self.horizontalLayout.addWidget(self.RetryConnectionButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) + self.updatepageButton = IconButton(parent=self.cw_buttonFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.updatepageButton.sizePolicy().hasHeightForWidth()) + self.updatepageButton.setSizePolicy(sizePolicy) + self.updatepageButton.setMinimumSize(QtCore.QSize(100, 80)) + self.updatepageButton.setMaximumSize(QtCore.QSize(100, 80)) + self.updatepageButton.setBaseSize(QtCore.QSize(80, 80)) + self.updatepageButton.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.BlankCursor)) + self.updatepageButton.setTabletTracking(True) + self.updatepageButton.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.updatepageButton.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.updatepageButton.setAutoFillBackground(False) + self.updatepageButton.setIcon(icon1) + self.updatepageButton.setAutoDefault(False) + self.updatepageButton.setDefault(False) + self.updatepageButton.setFlat(True) + self.updatepageButton.setProperty("icon_pixmap", QtGui.QPixmap(":/system/media/btn_icons/update-software-icon.svg")) + self.updatepageButton.setProperty("text_color", QtGui.QColor(255, 255, 255)) + self.updatepageButton.setProperty("has_text", True) + self.updatepageButton.setObjectName("updatepageButton") + self.horizontalLayout.addWidget(self.updatepageButton, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) + self.wifi_button = IconButton(parent=self.cw_buttonFrame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.wifi_button.sizePolicy().hasHeightForWidth()) + self.wifi_button.setSizePolicy(sizePolicy) + self.wifi_button.setMinimumSize(QtCore.QSize(100, 80)) + self.wifi_button.setMaximumSize(QtCore.QSize(100, 80)) + self.wifi_button.setBaseSize(QtCore.QSize(80, 80)) + self.wifi_button.setTabletTracking(True) + self.wifi_button.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.wifi_button.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.NoContextMenu) + self.wifi_button.setAutoFillBackground(False) + self.wifi_button.setIcon(icon2) + self.wifi_button.setAutoDefault(False) + self.wifi_button.setDefault(False) + self.wifi_button.setFlat(True) + self.wifi_button.setProperty("icon_pixmap", QtGui.QPixmap(":/network/media/btn_icons/wifi_config.svg")) + self.wifi_button.setProperty("has_text", True) + self.wifi_button.setObjectName("wifi_button") + self.horizontalLayout.addWidget(self.wifi_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) + self.cw_Frame = QtWidgets.QFrame(parent=ConnectivityForm) + self.cw_Frame.setGeometry(QtCore.QRect(0, 0, 800, 380)) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.cw_Frame.sizePolicy().hasHeightForWidth()) + self.cw_Frame.setSizePolicy(sizePolicy) + self.cw_Frame.setMinimumSize(QtCore.QSize(800, 380)) + self.cw_Frame.setMaximumSize(QtCore.QSize(800, 380)) + self.cw_Frame.setAutoFillBackground(False) + self.cw_Frame.setStyleSheet("") + self.cw_Frame.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.cw_Frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.cw_Frame.setObjectName("cw_Frame") + self.verticalLayout = QtWidgets.QVBoxLayout(self.cw_Frame) + self.verticalLayout.setObjectName("verticalLayout") + spacerItem = QtWidgets.QSpacerItem(775, 70, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) + self.verticalLayout.addItem(spacerItem) + self.connectionTextBox = QtWidgets.QLabel(parent=self.cw_Frame) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.connectionTextBox.sizePolicy().hasHeightForWidth()) + self.connectionTextBox.setSizePolicy(sizePolicy) + self.connectionTextBox.setMaximumSize(QtCore.QSize(800, 380)) + palette = QtGui.QPalette() + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.ButtonText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) + brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.ButtonText, brush) + self.connectionTextBox.setPalette(palette) + font = QtGui.QFont() + font.setFamily("Momcake") + font.setPointSize(17) + font.setBold(True) + font.setItalic(False) + font.setWeight(75) + self.connectionTextBox.setFont(font) + self.connectionTextBox.setStyleSheet("color:white") + self.connectionTextBox.setText("") + self.connectionTextBox.setTextFormat(QtCore.Qt.TextFormat.AutoText) + self.connectionTextBox.setScaledContents(False) + self.connectionTextBox.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.connectionTextBox.setWordWrap(True) + self.connectionTextBox.setTextInteractionFlags(QtCore.Qt.TextInteractionFlag.NoTextInteraction) + self.connectionTextBox.setObjectName("connectionTextBox") + self.verticalLayout.addWidget(self.connectionTextBox) + + self.retranslateUi(ConnectivityForm) + QtCore.QMetaObject.connectSlotsByName(ConnectivityForm) + + def retranslateUi(self, ConnectivityForm): + _translate = QtCore.QCoreApplication.translate + ConnectivityForm.setWindowTitle(_translate("ConnectivityForm", "Form")) + self.RestartKlipperButton.setText(_translate("ConnectivityForm", "Restart Klipper")) + self.RestartKlipperButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) + self.RebootSystemButton.setText(_translate("ConnectivityForm", "Reboot")) + self.RebootSystemButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) + self.FirmwareRestartButton.setText(_translate("ConnectivityForm", "Firmware Restart")) + self.FirmwareRestartButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) + self.RetryConnectionButton.setText(_translate("ConnectivityForm", "Retry ")) + self.RetryConnectionButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) + self.updatepageButton.setText(_translate("ConnectivityForm", "Update page")) + self.updatepageButton.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) + self.wifi_button.setText(_translate("ConnectivityForm", "Wifi Settings")) + self.wifi_button.setProperty("class", _translate("ConnectivityForm", "system_control_btn")) + self.wifi_button.setProperty("text_formatting", _translate("ConnectivityForm", "bottom")) +from lib.utils.blocks_frame import BlocksCustomFrame +from lib.utils.icon_button import IconButton diff --git a/BlocksScreen/lib/ui/wifiConnectivityWindow.ui b/BlocksScreen/lib/ui/wifiConnectivityWindow.ui deleted file mode 100644 index 688da18c..00000000 --- a/BlocksScreen/lib/ui/wifiConnectivityWindow.ui +++ /dev/null @@ -1,3184 +0,0 @@ - - - wifi_stacked_page - - - - 0 - 0 - 852 - 480 - - - - - 0 - 0 - - - - - 0 - 400 - - - - - 16777215 - 575 - - - - StackedWidget - - - #wifi_stacked_page{ - - - background-image: url(:/background/media/1st_background.png); -} - - - - 4 - - - - - 0 - 0 - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 60 - 60 - - - - - - - - - 0 - 0 - - - - - 300 - 0 - - - - - 16777215 - 60 - - - - - 20 - - - - color:white - - - Networks - - - Qt::AlignCenter - - - - - - - - 60 - 60 - - - - - 60 - 60 - - - - - - - true - - - :/ui/media/btn_icons/back.svg - - - - - - - - - - - - 0 - 0 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - 0 - 0 - - - - - 17 - - - - color: rgb(255, 255, 255); - - - - - - false - - - Qt::AlignCenter - - - - - - - Qt::Horizontal - - - - - - - - 15 - - - - color: rgb(255, 255, 255); - - - - - - Qt::AlignCenter - - - - - - - - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 120 - 120 - 120 - - - - - - - 120 - 120 - 120 - - - - - - - - - 15 - - - - Signal -Strength - - - Qt::AlignCenter - - - - - - - Qt::Horizontal - - - - - - - - 11 - - - - color: rgb(255, 255, 255); - - - TextLabel - - - Qt::AlignCenter - - - - - - - - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 120 - 120 - 120 - - - - - - - 120 - 120 - 120 - - - - - - - - - 15 - - - - Security -Type - - - Qt::AlignCenter - - - - - - - Qt::Horizontal - - - - - - - - 11 - - - - color: rgb(255, 255, 255); - - - TextLabel - - - Qt::AlignCenter - - - - - - - - - - - false - - - - 17 - - - - - - - - - - - - - - - - false - - - color: white - - - Qt::ImhNone - - - No network connection. - -Try connecting to Wi-Fi -or turn on the hotspot -using the buttons on the side. - - - Qt::PlainText - - - false - - - Qt::AlignCenter - - - - - - - true - - - - 0 - 0 - - - - - - - - - - - - - - - - - 0 - 0 - - - - - 400 - 9999 - - - - - 20 - - - - Wi-Fi - - - - - - - - 0 - 0 - - - - - 400 - 9999 - - - - - 20 - - - - Hotspot - - - - - - - - - - - - - - - - - - 60 - 60 - - - - - 60 - 60 - - - - Reload - - - true - - - icon - - - :/ui/media/btn_icons/refresh.svg - - - - - - - - 16777215 - 60 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 120 - 120 - 120 - - - - - - - 120 - 120 - 120 - - - - - - - - - 20 - - - - Wi-Fi List - - - Qt::AlignCenter - - - - - - - - 60 - 60 - - - - - 60 - 60 - - - - Back - - - true - - - back_btn - - - icon - - - :/ui/media/btn_icons/back.svg - - - - - - - - - - - - 1 - 1 - - - - - 0 - 0 - - - - BlankCursor - - - background-color: rgba(255, 255, 255, 0); - - - QFrame::NoFrame - - - QFrame::Plain - - - Qt::ScrollBarAlwaysOff - - - Qt::ScrollBarAlwaysOff - - - QAbstractItemView::ScrollPerPixel - - - QAbstractItemView::ScrollPerPixel - - - - - - - - 0 - 0 - - - - Qt::Vertical - - - - - - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 40 - 60 - - - - - - - - - 0 - 0 - - - - - 0 - 60 - - - - - 16777215 - 60 - - - - - 20 - - - - color:white - - - TextLabel - - - Qt::AlignCenter - - - - - - - - 60 - 60 - - - - - 60 - 60 - - - - Back - - - true - - - back_btn - - - icon - - - :/ui/media/btn_icons/back.svg - - - - - - - - - QLayout::SetMinimumSize - - - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 20 - 50 - - - - - - - - - 0 - 2 - - - - - 0 - 80 - - - - - 16777215 - 90 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 10 - 10 - 761 - 82 - - - - - QLayout::SetMaximumSize - - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 120 - 120 - 120 - - - - - - - 120 - 120 - 120 - - - - - - - - - 15 - - - - Password - - - Qt::AlignCenter - - - - - - - - 500 - 60 - - - - - 12 - - - - - - - - - 60 - 60 - - - - - 60 - 60 - - - - View - - - true - - - back_btn - - - icon - - - :/ui/media/btn_icons/unsee.svg - - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 20 - 150 - - - - - - - - QLayout::SetMinimumSize - - - - - true - - - - 1 - 1 - - - - - 250 - 80 - - - - - 250 - 80 - - - - - Momcake - 15 - - - - - - - Activate - - - - 16 - 16 - - - - false - - - false - - - true - - - :/dialog/media/btn_icons/yes.svg - - - - - - - - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 60 - 20 - - - - - - - - - 0 - 0 - - - - - 16777215 - 60 - - - - - 20 - - - - color: rgb(255, 255, 255); - - - - - - false - - - Qt::AlignCenter - - - - - - - - 60 - 60 - - - - - 60 - 60 - - - - Back - - - true - - - back_btn - - - icon - - - :/ui/media/btn_icons/back.svg - - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 20 - 20 - - - - - - - - - - - - - 0 - 0 - - - - - 400 - 16777215 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 120 - 120 - 120 - - - - - - - 120 - 120 - 120 - - - - - - - - - 15 - - - - Signal -Strength - - - Qt::AlignCenter - - - - - - - - 250 - 0 - - - - - 11 - - - - color: rgb(255, 255, 255); - - - TextLabel - - - Qt::AlignCenter - - - - - - - - - Qt::Horizontal - - - - - - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 120 - 120 - 120 - - - - - - - 120 - 120 - 120 - - - - - - - - - 15 - - - - Security -Type - - - Qt::AlignCenter - - - - - - - - 250 - 0 - - - - - 11 - - - - color: rgb(255, 255, 255); - - - TextLabel - - - Qt::AlignCenter - - - - - - - - - Qt::Horizontal - - - - - - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 120 - 120 - 120 - - - - - - - 120 - 120 - 120 - - - - - - - - - 15 - - - - Status - - - Qt::AlignCenter - - - - - - - - 250 - 0 - - - - - 11 - - - - color: rgb(255, 255, 255); - - - TextLabel - - - Qt::AlignCenter - - - - - - - - - - - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - - - 250 - 80 - - - - - 250 - 80 - - - - - 15 - - - - Connect - - - true - - - - - - - - 250 - 80 - - - - - 250 - 80 - - - - - 15 - - - - Details - - - true - - - - - - - - 250 - 80 - - - - - 250 - 80 - - - - - 15 - - - - Forget - - - true - - - - - - - - - - - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 60 - 60 - - - - - - - - - 0 - 0 - - - - - 16777215 - 60 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 120 - 120 - 120 - - - - - - - 120 - 120 - 120 - - - - - - - - - 20 - - - - SSID - - - Qt::AlignCenter - - - - - - - - 60 - 60 - - - - - 60 - 60 - - - - Back - - - true - - - back_btn - - - icon - - - :/ui/media/btn_icons/back.svg - - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 20 - 20 - - - - - - - - - 0 - 0 - - - - - 0 - 70 - - - - - 16777215 - 70 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 0 - 0 - 776 - 62 - - - - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 120 - 120 - 120 - - - - - - - 120 - 120 - 120 - - - - - - - - - 15 - - - - Change -Password - - - Qt::AlignCenter - - - - - - - - 500 - 60 - - - - - 500 - 16777215 - - - - - 12 - - - - - - - - - 60 - 60 - - - - - 60 - 60 - - - - View - - - true - - - back_btn - - - icon - - - :/ui/media/btn_icons/unsee.svg - - - - - - - - - - - - - - - - 0 - 0 - - - - - 400 - 160 - - - - - 400 - 99999 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - Network priority - - - - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 10 - 10 - - - - - - - - - - - 100 - 100 - - - - - 100 - 100 - - - - Low - - - true - - - true - - - true - - - back_btn - - - icon - - - :/ui/media/btn_icons/indf_svg.svg - - - priority_btn_group - - - - - - - - 100 - 100 - - - - - 100 - 100 - - - - Medium - - - true - - - true - - - true - - - true - - - back_btn - - - icon - - - :/ui/media/btn_icons/indf_svg.svg - - - priority_btn_group - - - - - - - - 100 - 100 - - - - - 100 - 100 - - - - High - - - true - - - false - - - true - - - true - - - back_btn - - - icon - - - :/ui/media/btn_icons/indf_svg.svg - - - priority_btn_group - - - - - - - - - - - - - - - - - - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 40 - 20 - - - - - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 120 - 120 - 120 - - - - - - - 120 - 120 - 120 - - - - - - - - - 20 - - - - Hotspot - - - Qt::AlignCenter - - - - - - - - 60 - 60 - - - - - 60 - 60 - - - - Back - - - true - - - back_btn - - - icon - - - :/ui/media/btn_icons/back.svg - - - - - - - - - 5 - - - 5 - - - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 20 - 50 - - - - - - - - - 0 - 0 - - - - - 70 - 80 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 0 - 10 - 776 - 61 - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 150 - 16777215 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - - - 120 - 120 - 120 - - - - - - - 120 - 120 - 120 - - - - - - - - - Momcake - 10 - - - - Hotspot Name: - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - - 500 - 40 - - - - - 500 - 60 - - - - - 12 - - - - QLineEdit::Password - - - - - - - Qt::Horizontal - - - QSizePolicy::Minimum - - - - 60 - 20 - - - - - - - - - - - - Qt::Vertical - - - QSizePolicy::Minimum - - - - 773 - 128 - - - - - - - - - 0 - 0 - - - - - 0 - 80 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - - 0 - 10 - 776 - 62 - - - - - - - - 0 - 0 - - - - - 0 - 0 - - - - - 150 - 16777215 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 127 - 127 - 127 - - - - - - - 170 - 170 - 170 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 127 - 127 - 127 - - - - - - - 170 - 170 - 170 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - - 127 - 127 - 127 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 127 - 127 - 127 - - - - - - - 170 - 170 - 170 - - - - - - - 127 - 127 - 127 - - - - - - - 255 - 255 - 255 - - - - - - - 127 - 127 - 127 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 255 - - - - - - - 0 - 0 - 0 - - - - - - - 255 - 255 - 255 - - - - - - - 255 - 255 - 220 - - - - - - - 0 - 0 - 0 - - - - - - - - - Momcake - 10 - - - - Hotspot Password: - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - - 500 - 40 - - - - - 500 - 60 - - - - - 12 - - - - QLineEdit::Password - - - - - - - - 60 - 60 - - - - - 60 - 60 - - - - View - - - true - - - back_btn - - - icon - - - :/ui/media/btn_icons/unsee.svg - - - - - - - - - - - - 200 - 80 - - - - - 250 - 100 - - - - - 18 - 75 - true - - - - Save - - - :/ui/media/btn_icons/save.svg - - - - - - - - - - - IconButton - QPushButton -
lib.utils.icon_button
-
- - BlocksCustomLinEdit - QLineEdit -
lib.utils.blocks_linedit
-
- - NetworkWidgetbuttons - QPushButton -
lib.utils.blocks_togglebutton
-
- - BlocksCustomFrame - QFrame -
lib.utils.blocks_frame
- 1 -
- - BlocksCustomButton - QPushButton -
lib.utils.blocks_button
-
- - LoadingOverlayWidget - QLabel -
lib.panels.widgets.loadWidget
-
-
- - - - - - - - - - -
diff --git a/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py b/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py deleted file mode 100644 index 4a41aae5..00000000 --- a/BlocksScreen/lib/ui/wifiConnectivityWindow_ui.py +++ /dev/null @@ -1,1301 +0,0 @@ -# Form implementation generated from reading ui file '/home/levi/BlocksScreen/BlocksScreen/lib/ui/wifiConnectivityWindow.ui' -# -# Created by: PyQt6 UI code generator 6.7.1 -# -# WARNING: Any manual changes made to this file will be lost when pyuic6 is -# run again. Do not edit this file unless you know what you are doing. - - -from PyQt6 import QtCore, QtGui, QtWidgets - - -class Ui_wifi_stacked_page(object): - def setupUi(self, wifi_stacked_page): - wifi_stacked_page.setObjectName("wifi_stacked_page") - wifi_stacked_page.resize(800, 480) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(wifi_stacked_page.sizePolicy().hasHeightForWidth()) - wifi_stacked_page.setSizePolicy(sizePolicy) - wifi_stacked_page.setMinimumSize(QtCore.QSize(0, 400)) - wifi_stacked_page.setMaximumSize(QtCore.QSize(16777215, 575)) - wifi_stacked_page.setStyleSheet("#wifi_stacked_page{\n" -" \n" -" \n" -" background-image: url(:/background/media/1st_background.png);\n" -"}\n" -"") - self.main_network_page = QtWidgets.QWidget() - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.main_network_page.sizePolicy().hasHeightForWidth()) - self.main_network_page.setSizePolicy(sizePolicy) - self.main_network_page.setObjectName("main_network_page") - self.verticalLayout_14 = QtWidgets.QVBoxLayout(self.main_network_page) - self.verticalLayout_14.setObjectName("verticalLayout_14") - self.main_network_header_layout = QtWidgets.QHBoxLayout() - self.main_network_header_layout.setObjectName("main_network_header_layout") - spacerItem = QtWidgets.QSpacerItem(60, 60, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.main_network_header_layout.addItem(spacerItem) - self.network_main_title = QtWidgets.QLabel(parent=self.main_network_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.network_main_title.sizePolicy().hasHeightForWidth()) - self.network_main_title.setSizePolicy(sizePolicy) - self.network_main_title.setMinimumSize(QtCore.QSize(300, 0)) - self.network_main_title.setMaximumSize(QtCore.QSize(16777215, 60)) - font = QtGui.QFont() - font.setPointSize(20) - self.network_main_title.setFont(font) - self.network_main_title.setStyleSheet("color:white") - self.network_main_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.network_main_title.setObjectName("network_main_title") - self.main_network_header_layout.addWidget(self.network_main_title) - self.network_backButton = IconButton(parent=self.main_network_page) - self.network_backButton.setMinimumSize(QtCore.QSize(60, 60)) - self.network_backButton.setMaximumSize(QtCore.QSize(60, 60)) - self.network_backButton.setText("") - self.network_backButton.setFlat(True) - self.network_backButton.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) - self.network_backButton.setObjectName("network_backButton") - self.main_network_header_layout.addWidget(self.network_backButton) - self.verticalLayout_14.addLayout(self.main_network_header_layout) - self.main_network_content_layout = QtWidgets.QHBoxLayout() - self.main_network_content_layout.setObjectName("main_network_content_layout") - self.mn_information_layout = BlocksCustomFrame(parent=self.main_network_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.mn_information_layout.sizePolicy().hasHeightForWidth()) - self.mn_information_layout.setSizePolicy(sizePolicy) - self.mn_information_layout.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.mn_information_layout.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.mn_information_layout.setObjectName("mn_information_layout") - self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.mn_information_layout) - self.verticalLayout_3.setObjectName("verticalLayout_3") - self.netlist_ssuid = QtWidgets.QLabel(parent=self.mn_information_layout) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.netlist_ssuid.sizePolicy().hasHeightForWidth()) - self.netlist_ssuid.setSizePolicy(sizePolicy) - font = QtGui.QFont() - font.setPointSize(17) - self.netlist_ssuid.setFont(font) - self.netlist_ssuid.setStyleSheet("color: rgb(255, 255, 255);") - self.netlist_ssuid.setText("") - self.netlist_ssuid.setScaledContents(False) - self.netlist_ssuid.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_ssuid.setObjectName("netlist_ssuid") - self.verticalLayout_3.addWidget(self.netlist_ssuid) - self.mn_info_seperator = QtWidgets.QFrame(parent=self.mn_information_layout) - self.mn_info_seperator.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.mn_info_seperator.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.mn_info_seperator.setObjectName("mn_info_seperator") - self.verticalLayout_3.addWidget(self.mn_info_seperator) - self.netlist_ip = QtWidgets.QLabel(parent=self.mn_information_layout) - font = QtGui.QFont() - font.setPointSize(15) - self.netlist_ip.setFont(font) - self.netlist_ip.setStyleSheet("color: rgb(255, 255, 255);") - self.netlist_ip.setText("") - self.netlist_ip.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_ip.setObjectName("netlist_ip") - self.verticalLayout_3.addWidget(self.netlist_ip) - self.mn_conn_info = QtWidgets.QHBoxLayout() - self.mn_conn_info.setObjectName("mn_conn_info") - self.mn_sg_info_layout = QtWidgets.QVBoxLayout() - self.mn_sg_info_layout.setObjectName("mn_sg_info_layout") - self.netlist_strength_label = QtWidgets.QLabel(parent=self.mn_information_layout) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.netlist_strength_label.setPalette(palette) - font = QtGui.QFont() - font.setPointSize(15) - self.netlist_strength_label.setFont(font) - self.netlist_strength_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_strength_label.setObjectName("netlist_strength_label") - self.mn_sg_info_layout.addWidget(self.netlist_strength_label) - self.line_2 = QtWidgets.QFrame(parent=self.mn_information_layout) - self.line_2.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.line_2.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line_2.setObjectName("line_2") - self.mn_sg_info_layout.addWidget(self.line_2) - self.netlist_strength = QtWidgets.QLabel(parent=self.mn_information_layout) - font = QtGui.QFont() - font.setPointSize(11) - self.netlist_strength.setFont(font) - self.netlist_strength.setStyleSheet("color: rgb(255, 255, 255);") - self.netlist_strength.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_strength.setObjectName("netlist_strength") - self.mn_sg_info_layout.addWidget(self.netlist_strength) - self.mn_conn_info.addLayout(self.mn_sg_info_layout) - self.mn_sec_info_layout = QtWidgets.QVBoxLayout() - self.mn_sec_info_layout.setObjectName("mn_sec_info_layout") - self.netlist_security_label = QtWidgets.QLabel(parent=self.mn_information_layout) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.netlist_security_label.setPalette(palette) - font = QtGui.QFont() - font.setPointSize(15) - self.netlist_security_label.setFont(font) - self.netlist_security_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_security_label.setObjectName("netlist_security_label") - self.mn_sec_info_layout.addWidget(self.netlist_security_label) - self.line_3 = QtWidgets.QFrame(parent=self.mn_information_layout) - self.line_3.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.line_3.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line_3.setObjectName("line_3") - self.mn_sec_info_layout.addWidget(self.line_3) - self.netlist_security = QtWidgets.QLabel(parent=self.mn_information_layout) - font = QtGui.QFont() - font.setPointSize(11) - self.netlist_security.setFont(font) - self.netlist_security.setStyleSheet("color: rgb(255, 255, 255);") - self.netlist_security.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_security.setObjectName("netlist_security") - self.mn_sec_info_layout.addWidget(self.netlist_security) - self.mn_conn_info.addLayout(self.mn_sec_info_layout) - self.verticalLayout_3.addLayout(self.mn_conn_info) - self.mn_info_box = QtWidgets.QLabel(parent=self.mn_information_layout) - self.mn_info_box.setEnabled(False) - font = QtGui.QFont() - font.setPointSize(17) - self.mn_info_box.setFont(font) - self.mn_info_box.setStatusTip("") - self.mn_info_box.setWhatsThis("") - self.mn_info_box.setAccessibleName("") - self.mn_info_box.setAccessibleDescription("") - self.mn_info_box.setAutoFillBackground(False) - self.mn_info_box.setStyleSheet("color: white") - self.mn_info_box.setInputMethodHints(QtCore.Qt.InputMethodHint.ImhNone) - self.mn_info_box.setTextFormat(QtCore.Qt.TextFormat.PlainText) - self.mn_info_box.setScaledContents(False) - self.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.mn_info_box.setObjectName("mn_info_box") - self.verticalLayout_3.addWidget(self.mn_info_box) - self.loadingwidget = LoadingOverlayWidget(parent=self.mn_information_layout) - self.loadingwidget.setEnabled(True) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.loadingwidget.sizePolicy().hasHeightForWidth()) - self.loadingwidget.setSizePolicy(sizePolicy) - self.loadingwidget.setText("") - self.loadingwidget.setObjectName("loadingwidget") - self.verticalLayout_3.addWidget(self.loadingwidget) - self.main_network_content_layout.addWidget(self.mn_information_layout) - self.mn_option_button_layout = QtWidgets.QVBoxLayout() - self.mn_option_button_layout.setObjectName("mn_option_button_layout") - self.wifi_button = NetworkWidgetbuttons(parent=self.main_network_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.wifi_button.sizePolicy().hasHeightForWidth()) - self.wifi_button.setSizePolicy(sizePolicy) - self.wifi_button.setMaximumSize(QtCore.QSize(400, 9999)) - font = QtGui.QFont() - font.setPointSize(20) - self.wifi_button.setFont(font) - self.wifi_button.setObjectName("wifi_button") - self.mn_option_button_layout.addWidget(self.wifi_button) - self.hotspot_button = NetworkWidgetbuttons(parent=self.main_network_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.hotspot_button.sizePolicy().hasHeightForWidth()) - self.hotspot_button.setSizePolicy(sizePolicy) - self.hotspot_button.setMaximumSize(QtCore.QSize(400, 9999)) - font = QtGui.QFont() - font.setPointSize(20) - self.hotspot_button.setFont(font) - self.hotspot_button.setObjectName("hotspot_button") - self.mn_option_button_layout.addWidget(self.hotspot_button) - self.main_network_content_layout.addLayout(self.mn_option_button_layout) - self.verticalLayout_14.addLayout(self.main_network_content_layout) - wifi_stacked_page.addWidget(self.main_network_page) - self.network_list_page = QtWidgets.QWidget() - self.network_list_page.setObjectName("network_list_page") - self.verticalLayout_9 = QtWidgets.QVBoxLayout(self.network_list_page) - self.verticalLayout_9.setObjectName("verticalLayout_9") - self.nl_header_layout = QtWidgets.QHBoxLayout() - self.nl_header_layout.setObjectName("nl_header_layout") - self.rescan_button = IconButton(parent=self.network_list_page) - self.rescan_button.setMinimumSize(QtCore.QSize(60, 60)) - self.rescan_button.setMaximumSize(QtCore.QSize(60, 60)) - self.rescan_button.setFlat(True) - self.rescan_button.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/refresh.svg")) - self.rescan_button.setObjectName("rescan_button") - self.nl_header_layout.addWidget(self.rescan_button) - self.network_list_title = QtWidgets.QLabel(parent=self.network_list_page) - self.network_list_title.setMaximumSize(QtCore.QSize(16777215, 60)) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.network_list_title.setPalette(palette) - font = QtGui.QFont() - font.setPointSize(20) - self.network_list_title.setFont(font) - self.network_list_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.network_list_title.setObjectName("network_list_title") - self.nl_header_layout.addWidget(self.network_list_title) - self.nl_back_button = IconButton(parent=self.network_list_page) - self.nl_back_button.setMinimumSize(QtCore.QSize(60, 60)) - self.nl_back_button.setMaximumSize(QtCore.QSize(60, 60)) - self.nl_back_button.setFlat(True) - self.nl_back_button.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) - self.nl_back_button.setObjectName("nl_back_button") - self.nl_header_layout.addWidget(self.nl_back_button) - self.verticalLayout_9.addLayout(self.nl_header_layout) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.listView = QtWidgets.QListView(self.network_list_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) - sizePolicy.setHorizontalStretch(1) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.listView.sizePolicy().hasHeightForWidth()) - self.listView.setSizePolicy(sizePolicy) - self.listView.setMinimumSize(QtCore.QSize(0, 0)) - self.listView.setStyleSheet("background-color: rgba(255, 255, 255, 0);") - self.listView.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) - self.listView.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) - self.listView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - self.listView.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff) - self.listView.setSelectionBehavior( - QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems - ) - self.listView.setHorizontalScrollBarPolicy( # No horizontal scroll - QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) - self.listView.setVerticalScrollMode( - QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel - ) - self.listView.setVerticalScrollBarPolicy( - QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff - ) - QtWidgets.QScroller.grabGesture( - self.listView, - QtWidgets.QScroller.ScrollerGestureType.TouchGesture, - ) - QtWidgets.QScroller.grabGesture( - self.listView, - QtWidgets.QScroller.ScrollerGestureType.LeftMouseButtonGesture, - ) - self.listView.setEditTriggers( - QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers - ) - - scroller_instance = QtWidgets.QScroller.scroller(self.listView) - scroller_props = scroller_instance.scrollerProperties() - scroller_props.setScrollMetric( - QtWidgets.QScrollerProperties.ScrollMetric.DragVelocitySmoothingFactor, - 0.05, # Lower = more responsive - ) - scroller_props.setScrollMetric( - QtWidgets.QScrollerProperties.ScrollMetric.DecelerationFactor, - 0.4, # higher = less inertia - ) - QtWidgets.QScroller.scroller(self.listView).setScrollerProperties( - scroller_props - ) - self.verticalScrollBar = CustomScrollBar(parent=self.network_list_page) - self.listView.setVerticalScrollBar(self.verticalScrollBar) - self.listView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded) - self.horizontalLayout_2.addWidget(self.listView) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Fixed, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.verticalScrollBar.sizePolicy().hasHeightForWidth()) - self.verticalScrollBar.setSizePolicy(sizePolicy) - self.verticalScrollBar.setOrientation(QtCore.Qt.Orientation.Vertical) - self.verticalScrollBar.setObjectName("verticalScrollBar") - self.listView.setVerticalScrollMode(QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel) - self.listView.setUniformItemSizes(True) - #self.listView.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarPolicy.ScrollBarAsNeeded) - self.listView.setSpacing(5) - self.horizontalLayout_2.addWidget(self.verticalScrollBar) - self.verticalLayout_9.addLayout(self.horizontalLayout_2) - wifi_stacked_page.addWidget(self.network_list_page) - self.add_network_page = QtWidgets.QWidget() - self.add_network_page.setObjectName("add_network_page") - self.verticalLayout_10 = QtWidgets.QVBoxLayout(self.add_network_page) - self.verticalLayout_10.setObjectName("verticalLayout_10") - self.verticalScrollBar.setAttribute( - QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents, True - ) - self.scroller = QtWidgets.QScroller.scroller(self.listView) - self.add_np_header_layout = QtWidgets.QHBoxLayout() - self.add_np_header_layout.setObjectName("add_np_header_layout") - spacerItem1 = QtWidgets.QSpacerItem(40, 60, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.add_np_header_layout.addItem(spacerItem1) - self.add_network_network_label = QtWidgets.QLabel(parent=self.add_network_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Preferred) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.add_network_network_label.sizePolicy().hasHeightForWidth()) - self.add_network_network_label.setSizePolicy(sizePolicy) - self.add_network_network_label.setMinimumSize(QtCore.QSize(0, 60)) - self.add_network_network_label.setMaximumSize(QtCore.QSize(16777215, 60)) - font = QtGui.QFont() - font.setPointSize(20) - self.add_network_network_label.setFont(font) - self.add_network_network_label.setStyleSheet("color:white") - self.add_network_network_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.add_network_network_label.setObjectName("add_network_network_label") - self.add_np_header_layout.addWidget(self.add_network_network_label) - self.add_network_page_backButton = IconButton(parent=self.add_network_page) - self.add_network_page_backButton.setMinimumSize(QtCore.QSize(60, 60)) - self.add_network_page_backButton.setMaximumSize(QtCore.QSize(60, 60)) - self.add_network_page_backButton.setFlat(True) - self.add_network_page_backButton.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) - self.add_network_page_backButton.setObjectName("add_network_page_backButton") - self.add_np_header_layout.addWidget(self.add_network_page_backButton) - self.verticalLayout_10.addLayout(self.add_np_header_layout) - self.add_np_content_layout = QtWidgets.QVBoxLayout() - self.add_np_content_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize) - self.add_np_content_layout.setObjectName("add_np_content_layout") - spacerItem2 = QtWidgets.QSpacerItem(20, 50, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.add_np_content_layout.addItem(spacerItem2) - self.frame_2 = BlocksCustomFrame(parent=self.add_network_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(2) - sizePolicy.setHeightForWidth(self.frame_2.sizePolicy().hasHeightForWidth()) - self.frame_2.setSizePolicy(sizePolicy) - self.frame_2.setMinimumSize(QtCore.QSize(0, 80)) - self.frame_2.setMaximumSize(QtCore.QSize(16777215, 90)) - self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_2.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_2.setObjectName("frame_2") - self.layoutWidget_2 = QtWidgets.QWidget(parent=self.frame_2) - self.layoutWidget_2.setGeometry(QtCore.QRect(10, 10, 761, 82)) - self.layoutWidget_2.setObjectName("layoutWidget_2") - self.horizontalLayout_5 = QtWidgets.QHBoxLayout(self.layoutWidget_2) - self.horizontalLayout_5.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMaximumSize) - self.horizontalLayout_5.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout_5.setObjectName("horizontalLayout_5") - self.add_network_password_label = QtWidgets.QLabel(parent=self.layoutWidget_2) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.add_network_password_label.setPalette(palette) - font = QtGui.QFont() - font.setPointSize(15) - self.add_network_password_label.setFont(font) - self.add_network_password_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.add_network_password_label.setObjectName("add_network_password_label") - self.horizontalLayout_5.addWidget(self.add_network_password_label) - self.add_network_password_field = BlocksCustomLinEdit(parent=self.layoutWidget_2) - self.add_network_password_field.setMinimumSize(QtCore.QSize(500, 60)) - font = QtGui.QFont() - font.setPointSize(12) - self.add_network_password_field.setFont(font) - self.add_network_password_field.setObjectName("add_network_password_field") - self.horizontalLayout_5.addWidget(self.add_network_password_field) - self.add_network_password_view = IconButton(parent=self.layoutWidget_2) - self.add_network_password_view.setMinimumSize(QtCore.QSize(60, 60)) - self.add_network_password_view.setMaximumSize(QtCore.QSize(60, 60)) - self.add_network_password_view.setFlat(True) - self.add_network_password_view.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg")) - self.add_network_password_view.setObjectName("add_network_password_view") - self.horizontalLayout_5.addWidget(self.add_network_password_view) - self.add_np_content_layout.addWidget(self.frame_2) - spacerItem3 = QtWidgets.QSpacerItem(20, 150, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.add_np_content_layout.addItem(spacerItem3) - self.horizontalLayout_6 = QtWidgets.QHBoxLayout() - self.horizontalLayout_6.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize) - self.horizontalLayout_6.setObjectName("horizontalLayout_6") - self.add_network_validation_button = BlocksCustomButton(parent=self.add_network_page) - self.add_network_validation_button.setEnabled(True) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) - sizePolicy.setHorizontalStretch(1) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.add_network_validation_button.sizePolicy().hasHeightForWidth()) - self.add_network_validation_button.setSizePolicy(sizePolicy) - self.add_network_validation_button.setMinimumSize(QtCore.QSize(250, 80)) - self.add_network_validation_button.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(15) - self.add_network_validation_button.setFont(font) - self.add_network_validation_button.setStyleSheet("") - self.add_network_validation_button.setIconSize(QtCore.QSize(16, 16)) - self.add_network_validation_button.setCheckable(False) - self.add_network_validation_button.setChecked(False) - self.add_network_validation_button.setFlat(True) - self.add_network_validation_button.setProperty("icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg")) - self.add_network_validation_button.setObjectName("add_network_validation_button") - self.horizontalLayout_6.addWidget(self.add_network_validation_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignTop) - self.add_np_content_layout.addLayout(self.horizontalLayout_6) - self.verticalLayout_10.addLayout(self.add_np_content_layout) - wifi_stacked_page.addWidget(self.add_network_page) - self.saved_connection_page = QtWidgets.QWidget() - self.saved_connection_page.setObjectName("saved_connection_page") - self.verticalLayout_11 = QtWidgets.QVBoxLayout(self.saved_connection_page) - self.verticalLayout_11.setObjectName("verticalLayout_11") - self.horizontalLayout_7 = QtWidgets.QHBoxLayout() - self.horizontalLayout_7.setObjectName("horizontalLayout_7") - spacerItem4 = QtWidgets.QSpacerItem(60, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.horizontalLayout_7.addItem(spacerItem4) - self.saved_connection_network_name = QtWidgets.QLabel(parent=self.saved_connection_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.saved_connection_network_name.sizePolicy().hasHeightForWidth()) - self.saved_connection_network_name.setSizePolicy(sizePolicy) - self.saved_connection_network_name.setMaximumSize(QtCore.QSize(16777215, 60)) - font = QtGui.QFont() - font.setPointSize(20) - self.saved_connection_network_name.setFont(font) - self.saved_connection_network_name.setStyleSheet("color: rgb(255, 255, 255);") - self.saved_connection_network_name.setText("") - self.saved_connection_network_name.setScaledContents(False) - self.saved_connection_network_name.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.saved_connection_network_name.setObjectName("saved_connection_network_name") - self.horizontalLayout_7.addWidget(self.saved_connection_network_name) - self.saved_connection_back_button = IconButton(parent=self.saved_connection_page) - self.saved_connection_back_button.setMinimumSize(QtCore.QSize(60, 60)) - self.saved_connection_back_button.setMaximumSize(QtCore.QSize(60, 60)) - self.saved_connection_back_button.setFlat(True) - self.saved_connection_back_button.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) - self.saved_connection_back_button.setObjectName("saved_connection_back_button") - self.horizontalLayout_7.addWidget(self.saved_connection_back_button, 0, QtCore.Qt.AlignmentFlag.AlignRight) - self.verticalLayout_11.addLayout(self.horizontalLayout_7) - self.verticalLayout_5 = QtWidgets.QVBoxLayout() - self.verticalLayout_5.setObjectName("verticalLayout_5") - spacerItem5 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.verticalLayout_5.addItem(spacerItem5) - self.horizontalLayout_9 = QtWidgets.QHBoxLayout() - self.horizontalLayout_9.setObjectName("horizontalLayout_9") - self.verticalLayout_2 = QtWidgets.QVBoxLayout() - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.frame = BlocksCustomFrame(parent=self.saved_connection_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.frame.sizePolicy().hasHeightForWidth()) - self.frame.setSizePolicy(sizePolicy) - self.frame.setMaximumSize(QtCore.QSize(400, 16777215)) - self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame.setObjectName("frame") - self.verticalLayout_6 = QtWidgets.QVBoxLayout(self.frame) - self.verticalLayout_6.setObjectName("verticalLayout_6") - self.horizontalLayout = QtWidgets.QHBoxLayout() - self.horizontalLayout.setObjectName("horizontalLayout") - self.netlist_strength_label_2 = QtWidgets.QLabel(parent=self.frame) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.netlist_strength_label_2.setPalette(palette) - font = QtGui.QFont() - font.setPointSize(15) - self.netlist_strength_label_2.setFont(font) - self.netlist_strength_label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_strength_label_2.setObjectName("netlist_strength_label_2") - self.horizontalLayout.addWidget(self.netlist_strength_label_2) - self.saved_connection_signal_strength_info_frame = QtWidgets.QLabel(parent=self.frame) - self.saved_connection_signal_strength_info_frame.setMinimumSize(QtCore.QSize(250, 0)) - font = QtGui.QFont() - font.setPointSize(11) - self.saved_connection_signal_strength_info_frame.setFont(font) - self.saved_connection_signal_strength_info_frame.setStyleSheet("color: rgb(255, 255, 255);") - self.saved_connection_signal_strength_info_frame.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.saved_connection_signal_strength_info_frame.setObjectName("saved_connection_signal_strength_info_frame") - self.horizontalLayout.addWidget(self.saved_connection_signal_strength_info_frame) - self.verticalLayout_6.addLayout(self.horizontalLayout) - self.line_4 = QtWidgets.QFrame(parent=self.frame) - self.line_4.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.line_4.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line_4.setObjectName("line_4") - self.verticalLayout_6.addWidget(self.line_4) - self.horizontalLayout_2 = QtWidgets.QHBoxLayout() - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.netlist_security_label_2 = QtWidgets.QLabel(parent=self.frame) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.netlist_security_label_2.setPalette(palette) - font = QtGui.QFont() - font.setPointSize(15) - self.netlist_security_label_2.setFont(font) - self.netlist_security_label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_security_label_2.setObjectName("netlist_security_label_2") - self.horizontalLayout_2.addWidget(self.netlist_security_label_2) - self.saved_connection_security_type_info_label = QtWidgets.QLabel(parent=self.frame) - self.saved_connection_security_type_info_label.setMinimumSize(QtCore.QSize(250, 0)) - font = QtGui.QFont() - font.setPointSize(11) - self.saved_connection_security_type_info_label.setFont(font) - self.saved_connection_security_type_info_label.setStyleSheet("color: rgb(255, 255, 255);") - self.saved_connection_security_type_info_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.saved_connection_security_type_info_label.setObjectName("saved_connection_security_type_info_label") - self.horizontalLayout_2.addWidget(self.saved_connection_security_type_info_label) - self.verticalLayout_6.addLayout(self.horizontalLayout_2) - self.line_5 = QtWidgets.QFrame(parent=self.frame) - self.line_5.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.line_5.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line_5.setObjectName("line_5") - self.verticalLayout_6.addWidget(self.line_5) - self.horizontalLayout_8 = QtWidgets.QHBoxLayout() - self.horizontalLayout_8.setObjectName("horizontalLayout_8") - self.netlist_security_label_4 = QtWidgets.QLabel(parent=self.frame) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.netlist_security_label_4.setPalette(palette) - font = QtGui.QFont() - font.setPointSize(15) - self.netlist_security_label_4.setFont(font) - self.netlist_security_label_4.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_security_label_4.setObjectName("netlist_security_label_4") - self.horizontalLayout_8.addWidget(self.netlist_security_label_4) - self.sn_info = QtWidgets.QLabel(parent=self.frame) - self.sn_info.setMinimumSize(QtCore.QSize(250, 0)) - font = QtGui.QFont() - font.setPointSize(11) - self.sn_info.setFont(font) - self.sn_info.setStyleSheet("color: rgb(255, 255, 255);") - self.sn_info.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.sn_info.setObjectName("sn_info") - self.horizontalLayout_8.addWidget(self.sn_info) - self.verticalLayout_6.addLayout(self.horizontalLayout_8) - self.verticalLayout_2.addWidget(self.frame) - self.horizontalLayout_9.addLayout(self.verticalLayout_2) - self.frame_8 = BlocksCustomFrame(parent=self.saved_connection_page) - self.frame_8.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_8.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_8.setObjectName("frame_8") - self.verticalLayout_4 = QtWidgets.QVBoxLayout(self.frame_8) - self.verticalLayout_4.setObjectName("verticalLayout_4") - self.network_activate_btn = BlocksCustomButton(parent=self.frame_8) - self.network_activate_btn.setMinimumSize(QtCore.QSize(250, 80)) - self.network_activate_btn.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setPointSize(15) - self.network_activate_btn.setFont(font) - self.network_activate_btn.setFlat(True) - self.network_activate_btn.setObjectName("network_activate_btn") - self.verticalLayout_4.addWidget(self.network_activate_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) - self.network_details_btn = BlocksCustomButton(parent=self.frame_8) - self.network_details_btn.setMinimumSize(QtCore.QSize(250, 80)) - self.network_details_btn.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setPointSize(15) - self.network_details_btn.setFont(font) - self.network_details_btn.setFlat(True) - self.network_details_btn.setObjectName("network_details_btn") - self.verticalLayout_4.addWidget(self.network_details_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.network_delete_btn = BlocksCustomButton(parent=self.frame_8) - self.network_delete_btn.setMinimumSize(QtCore.QSize(250, 80)) - self.network_delete_btn.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setPointSize(15) - self.network_delete_btn.setFont(font) - self.network_delete_btn.setFlat(True) - self.network_delete_btn.setObjectName("network_delete_btn") - self.verticalLayout_4.addWidget(self.network_delete_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) - self.horizontalLayout_9.addWidget(self.frame_8) - self.verticalLayout_5.addLayout(self.horizontalLayout_9) - self.verticalLayout_11.addLayout(self.verticalLayout_5) - wifi_stacked_page.addWidget(self.saved_connection_page) - self.saved_details_page = QtWidgets.QWidget() - self.saved_details_page.setObjectName("saved_details_page") - self.verticalLayout_19 = QtWidgets.QVBoxLayout(self.saved_details_page) - self.verticalLayout_19.setObjectName("verticalLayout_19") - self.horizontalLayout_14 = QtWidgets.QHBoxLayout() - self.horizontalLayout_14.setObjectName("horizontalLayout_14") - spacerItem6 = QtWidgets.QSpacerItem(60, 60, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.horizontalLayout_14.addItem(spacerItem6) - self.snd_name = QtWidgets.QLabel(parent=self.saved_details_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.snd_name.sizePolicy().hasHeightForWidth()) - self.snd_name.setSizePolicy(sizePolicy) - self.snd_name.setMaximumSize(QtCore.QSize(16777215, 60)) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.snd_name.setPalette(palette) - font = QtGui.QFont() - font.setPointSize(20) - self.snd_name.setFont(font) - self.snd_name.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.snd_name.setObjectName("snd_name") - self.horizontalLayout_14.addWidget(self.snd_name) - self.snd_back = IconButton(parent=self.saved_details_page) - self.snd_back.setMinimumSize(QtCore.QSize(60, 60)) - self.snd_back.setMaximumSize(QtCore.QSize(60, 60)) - self.snd_back.setFlat(True) - self.snd_back.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) - self.snd_back.setObjectName("snd_back") - self.horizontalLayout_14.addWidget(self.snd_back) - self.verticalLayout_19.addLayout(self.horizontalLayout_14) - self.verticalLayout_8 = QtWidgets.QVBoxLayout() - self.verticalLayout_8.setObjectName("verticalLayout_8") - spacerItem7 = QtWidgets.QSpacerItem(20, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.verticalLayout_8.addItem(spacerItem7) - self.frame_9 = BlocksCustomFrame(parent=self.saved_details_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.frame_9.sizePolicy().hasHeightForWidth()) - self.frame_9.setSizePolicy(sizePolicy) - self.frame_9.setMinimumSize(QtCore.QSize(0, 70)) - self.frame_9.setMaximumSize(QtCore.QSize(16777215, 70)) - self.frame_9.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_9.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_9.setObjectName("frame_9") - self.layoutWidget_8 = QtWidgets.QWidget(parent=self.frame_9) - self.layoutWidget_8.setGeometry(QtCore.QRect(0, 0, 776, 62)) - self.layoutWidget_8.setObjectName("layoutWidget_8") - self.horizontalLayout_10 = QtWidgets.QHBoxLayout(self.layoutWidget_8) - self.horizontalLayout_10.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout_10.setObjectName("horizontalLayout_10") - self.saved_connection_change_password_label_3 = QtWidgets.QLabel(parent=self.layoutWidget_8) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.saved_connection_change_password_label_3.setPalette(palette) - font = QtGui.QFont() - font.setPointSize(15) - self.saved_connection_change_password_label_3.setFont(font) - self.saved_connection_change_password_label_3.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.saved_connection_change_password_label_3.setObjectName("saved_connection_change_password_label_3") - self.horizontalLayout_10.addWidget(self.saved_connection_change_password_label_3, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) - self.saved_connection_change_password_field = BlocksCustomLinEdit(parent=self.layoutWidget_8) - self.saved_connection_change_password_field.setMinimumSize(QtCore.QSize(500, 60)) - self.saved_connection_change_password_field.setMaximumSize(QtCore.QSize(500, 16777215)) - font = QtGui.QFont() - font.setPointSize(12) - self.saved_connection_change_password_field.setFont(font) - self.saved_connection_change_password_field.setObjectName("saved_connection_change_password_field") - self.horizontalLayout_10.addWidget(self.saved_connection_change_password_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.saved_connection_change_password_view = IconButton(parent=self.layoutWidget_8) - self.saved_connection_change_password_view.setMinimumSize(QtCore.QSize(60, 60)) - self.saved_connection_change_password_view.setMaximumSize(QtCore.QSize(60, 60)) - self.saved_connection_change_password_view.setFlat(True) - self.saved_connection_change_password_view.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg")) - self.saved_connection_change_password_view.setObjectName("saved_connection_change_password_view") - self.horizontalLayout_10.addWidget(self.saved_connection_change_password_view, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) - self.verticalLayout_8.addWidget(self.frame_9) - self.horizontalLayout_13 = QtWidgets.QHBoxLayout() - self.horizontalLayout_13.setObjectName("horizontalLayout_13") - self.verticalLayout_13 = QtWidgets.QVBoxLayout() - self.verticalLayout_13.setObjectName("verticalLayout_13") - self.frame_12 = BlocksCustomFrame(parent=self.saved_details_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.frame_12.sizePolicy().hasHeightForWidth()) - self.frame_12.setSizePolicy(sizePolicy) - self.frame_12.setMinimumSize(QtCore.QSize(400, 160)) - self.frame_12.setMaximumSize(QtCore.QSize(400, 99999)) - self.frame_12.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_12.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_12.setObjectName("frame_12") - self.verticalLayout_17 = QtWidgets.QVBoxLayout(self.frame_12) - self.verticalLayout_17.setObjectName("verticalLayout_17") - spacerItem8 = QtWidgets.QSpacerItem(10, 10, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.verticalLayout_17.addItem(spacerItem8) - self.horizontalLayout_4 = QtWidgets.QHBoxLayout() - self.horizontalLayout_4.setObjectName("horizontalLayout_4") - self.low_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) - self.low_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) - self.low_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) - self.low_priority_btn.setCheckable(True) - self.low_priority_btn.setAutoExclusive(True) - self.low_priority_btn.setFlat(True) - self.low_priority_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg")) - self.low_priority_btn.setObjectName("low_priority_btn") - self.priority_btn_group = QtWidgets.QButtonGroup(wifi_stacked_page) - self.priority_btn_group.setObjectName("priority_btn_group") - self.priority_btn_group.addButton(self.low_priority_btn) - self.horizontalLayout_4.addWidget(self.low_priority_btn) - self.med_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) - self.med_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) - self.med_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) - self.med_priority_btn.setCheckable(True) - self.med_priority_btn.setChecked(True) - self.med_priority_btn.setAutoExclusive(True) - self.med_priority_btn.setFlat(True) - self.med_priority_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg")) - self.med_priority_btn.setObjectName("med_priority_btn") - self.priority_btn_group.addButton(self.med_priority_btn) - self.horizontalLayout_4.addWidget(self.med_priority_btn) - self.high_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) - self.high_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) - self.high_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) - self.high_priority_btn.setCheckable(True) - self.high_priority_btn.setChecked(False) - self.high_priority_btn.setAutoExclusive(True) - self.high_priority_btn.setFlat(True) - self.high_priority_btn.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg")) - self.high_priority_btn.setObjectName("high_priority_btn") - self.priority_btn_group.addButton(self.high_priority_btn) - self.horizontalLayout_4.addWidget(self.high_priority_btn) - self.verticalLayout_17.addLayout(self.horizontalLayout_4) - self.verticalLayout_13.addWidget(self.frame_12, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) - self.horizontalLayout_13.addLayout(self.verticalLayout_13) - self.verticalLayout_8.addLayout(self.horizontalLayout_13) - self.verticalLayout_19.addLayout(self.verticalLayout_8) - wifi_stacked_page.addWidget(self.saved_details_page) - self.hotspot_page = QtWidgets.QWidget() - self.hotspot_page.setObjectName("hotspot_page") - self.verticalLayout_12 = QtWidgets.QVBoxLayout(self.hotspot_page) - self.verticalLayout_12.setObjectName("verticalLayout_12") - self.hospot_page_header_layout = QtWidgets.QHBoxLayout() - self.hospot_page_header_layout.setObjectName("hospot_page_header_layout") - spacerItem9 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.hospot_page_header_layout.addItem(spacerItem9) - self.hotspot_header_title = QtWidgets.QLabel(parent=self.hotspot_page) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.hotspot_header_title.setPalette(palette) - font = QtGui.QFont() - font.setPointSize(20) - self.hotspot_header_title.setFont(font) - self.hotspot_header_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.hotspot_header_title.setObjectName("hotspot_header_title") - self.hospot_page_header_layout.addWidget(self.hotspot_header_title) - self.hotspot_back_button = IconButton(parent=self.hotspot_page) - self.hotspot_back_button.setMinimumSize(QtCore.QSize(60, 60)) - self.hotspot_back_button.setMaximumSize(QtCore.QSize(60, 60)) - self.hotspot_back_button.setFlat(True) - self.hotspot_back_button.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg")) - self.hotspot_back_button.setObjectName("hotspot_back_button") - self.hospot_page_header_layout.addWidget(self.hotspot_back_button) - self.verticalLayout_12.addLayout(self.hospot_page_header_layout) - self.hotspot_page_content_layout = QtWidgets.QVBoxLayout() - self.hotspot_page_content_layout.setContentsMargins(-1, 5, -1, 5) - self.hotspot_page_content_layout.setObjectName("hotspot_page_content_layout") - spacerItem10 = QtWidgets.QSpacerItem(20, 50, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.hotspot_page_content_layout.addItem(spacerItem10) - self.frame_6 = BlocksCustomFrame(parent=self.hotspot_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.frame_6.sizePolicy().hasHeightForWidth()) - self.frame_6.setSizePolicy(sizePolicy) - self.frame_6.setMinimumSize(QtCore.QSize(70, 80)) - self.frame_6.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_6.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_6.setObjectName("frame_6") - self.layoutWidget_6 = QtWidgets.QWidget(parent=self.frame_6) - self.layoutWidget_6.setGeometry(QtCore.QRect(0, 10, 776, 61)) - self.layoutWidget_6.setObjectName("layoutWidget_6") - self.horizontalLayout_11 = QtWidgets.QHBoxLayout(self.layoutWidget_6) - self.horizontalLayout_11.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout_11.setObjectName("horizontalLayout_11") - self.hotspot_info_name_label = QtWidgets.QLabel(parent=self.layoutWidget_6) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Maximum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.hotspot_info_name_label.sizePolicy().hasHeightForWidth()) - self.hotspot_info_name_label.setSizePolicy(sizePolicy) - self.hotspot_info_name_label.setMinimumSize(QtCore.QSize(0, 0)) - self.hotspot_info_name_label.setMaximumSize(QtCore.QSize(150, 16777215)) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - self.hotspot_info_name_label.setPalette(palette) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(10) - self.hotspot_info_name_label.setFont(font) - self.hotspot_info_name_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.hotspot_info_name_label.setObjectName("hotspot_info_name_label") - self.horizontalLayout_11.addWidget(self.hotspot_info_name_label) - self.hotspot_name_input_field = BlocksCustomLinEdit(parent=self.layoutWidget_6) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.hotspot_name_input_field.sizePolicy().hasHeightForWidth()) - self.hotspot_name_input_field.setSizePolicy(sizePolicy) - self.hotspot_name_input_field.setMinimumSize(QtCore.QSize(500, 40)) - self.hotspot_name_input_field.setMaximumSize(QtCore.QSize(500, 60)) - font = QtGui.QFont() - font.setPointSize(12) - self.hotspot_name_input_field.setFont(font) - self.hotspot_name_input_field.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password) - self.hotspot_name_input_field.setObjectName("hotspot_name_input_field") - self.horizontalLayout_11.addWidget(self.hotspot_name_input_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) - spacerItem11 = QtWidgets.QSpacerItem(60, 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.horizontalLayout_11.addItem(spacerItem11) - self.hotspot_page_content_layout.addWidget(self.frame_6) - spacerItem12 = QtWidgets.QSpacerItem(773, 128, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum) - self.hotspot_page_content_layout.addItem(spacerItem12) - self.frame_7 = BlocksCustomFrame(parent=self.hotspot_page) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.frame_7.sizePolicy().hasHeightForWidth()) - self.frame_7.setSizePolicy(sizePolicy) - self.frame_7.setMinimumSize(QtCore.QSize(0, 80)) - self.frame_7.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_7.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_7.setObjectName("frame_7") - self.layoutWidget_7 = QtWidgets.QWidget(parent=self.frame_7) - self.layoutWidget_7.setGeometry(QtCore.QRect(0, 10, 776, 62)) - self.layoutWidget_7.setObjectName("layoutWidget_7") - self.horizontalLayout_12 = QtWidgets.QHBoxLayout(self.layoutWidget_7) - self.horizontalLayout_12.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout_12.setObjectName("horizontalLayout_12") - self.hotspot_info_password_label = QtWidgets.QLabel(parent=self.layoutWidget_7) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Maximum) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.hotspot_info_password_label.sizePolicy().hasHeightForWidth()) - self.hotspot_info_password_label.setSizePolicy(sizePolicy) - self.hotspot_info_password_label.setMinimumSize(QtCore.QSize(0, 0)) - self.hotspot_info_password_label.setMaximumSize(QtCore.QSize(150, 16777215)) - palette = QtGui.QPalette() - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Light, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Midlight, brush) - brush = QtGui.QBrush(QtGui.QColor(127, 127, 127)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Dark, brush) - brush = QtGui.QBrush(QtGui.QColor(170, 170, 170)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Mid, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.BrightText, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.Shadow, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.AlternateBase, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.ToolTipBase, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Active, QtGui.QPalette.ColorRole.ToolTipText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Light, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Midlight, brush) - brush = QtGui.QBrush(QtGui.QColor(127, 127, 127)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Dark, brush) - brush = QtGui.QBrush(QtGui.QColor(170, 170, 170)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Mid, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.BrightText, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.Shadow, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.AlternateBase, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.ToolTipBase, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Inactive, QtGui.QPalette.ColorRole.ToolTipText, brush) - brush = QtGui.QBrush(QtGui.QColor(127, 127, 127)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.WindowText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Button, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Light, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Midlight, brush) - brush = QtGui.QBrush(QtGui.QColor(127, 127, 127)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Dark, brush) - brush = QtGui.QBrush(QtGui.QColor(170, 170, 170)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Mid, brush) - brush = QtGui.QBrush(QtGui.QColor(127, 127, 127)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Text, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.BrightText, brush) - brush = QtGui.QBrush(QtGui.QColor(127, 127, 127)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.ButtonText, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Base, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Window, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.Shadow, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.AlternateBase, brush) - brush = QtGui.QBrush(QtGui.QColor(255, 255, 220)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.ToolTipBase, brush) - brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(QtGui.QPalette.ColorGroup.Disabled, QtGui.QPalette.ColorRole.ToolTipText, brush) - self.hotspot_info_password_label.setPalette(palette) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(10) - self.hotspot_info_password_label.setFont(font) - self.hotspot_info_password_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.hotspot_info_password_label.setObjectName("hotspot_info_password_label") - self.horizontalLayout_12.addWidget(self.hotspot_info_password_label) - self.hotspot_password_input_field = BlocksCustomLinEdit(parent=self.layoutWidget_7) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.hotspot_password_input_field.sizePolicy().hasHeightForWidth()) - self.hotspot_password_input_field.setSizePolicy(sizePolicy) - self.hotspot_password_input_field.setMinimumSize(QtCore.QSize(500, 40)) - self.hotspot_password_input_field.setMaximumSize(QtCore.QSize(500, 60)) - font = QtGui.QFont() - font.setPointSize(12) - self.hotspot_password_input_field.setFont(font) - self.hotspot_password_input_field.setEchoMode(QtWidgets.QLineEdit.EchoMode.Password) - self.hotspot_password_input_field.setObjectName("hotspot_password_input_field") - self.horizontalLayout_12.addWidget(self.hotspot_password_input_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter) - self.hotspot_password_view_button = IconButton(parent=self.layoutWidget_7) - self.hotspot_password_view_button.setMinimumSize(QtCore.QSize(60, 60)) - self.hotspot_password_view_button.setMaximumSize(QtCore.QSize(60, 60)) - self.hotspot_password_view_button.setFlat(True) - self.hotspot_password_view_button.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg")) - self.hotspot_password_view_button.setObjectName("hotspot_password_view_button") - self.horizontalLayout_12.addWidget(self.hotspot_password_view_button) - self.hotspot_page_content_layout.addWidget(self.frame_7) - self.hotspot_change_confirm = BlocksCustomButton(parent=self.hotspot_page) - self.hotspot_change_confirm.setMinimumSize(QtCore.QSize(200, 80)) - self.hotspot_change_confirm.setMaximumSize(QtCore.QSize(250, 100)) - font = QtGui.QFont() - font.setPointSize(18) - font.setBold(True) - font.setWeight(75) - self.hotspot_change_confirm.setFont(font) - self.hotspot_change_confirm.setProperty("icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/save.svg")) - self.hotspot_change_confirm.setObjectName("hotspot_change_confirm") - self.hotspot_page_content_layout.addWidget(self.hotspot_change_confirm, 0, QtCore.Qt.AlignmentFlag.AlignHCenter|QtCore.Qt.AlignmentFlag.AlignVCenter) - self.verticalLayout_12.addLayout(self.hotspot_page_content_layout) - wifi_stacked_page.addWidget(self.hotspot_page) - - self.retranslateUi(wifi_stacked_page) - wifi_stacked_page.setCurrentIndex(2) - QtCore.QMetaObject.connectSlotsByName(wifi_stacked_page) - - def retranslateUi(self, wifi_stacked_page): - _translate = QtCore.QCoreApplication.translate - wifi_stacked_page.setWindowTitle(_translate("wifi_stacked_page", "StackedWidget")) - self.network_main_title.setText("Networks") - self.netlist_strength_label.setText( "Signal\n" -"Strength") - self.netlist_strength.setText( "TextLabel") - self.netlist_security_label.setText( "Security\n" -"Type") - self.netlist_security.setText("TextLabel") - self.mn_info_box.setText( "No network connection.\n" -"\n" -"Try connecting to Wi-Fi \n" -"or turn on the hotspot\n" -"using the buttons on the side.") - self.wifi_button.setText( "Wi-Fi") - self.hotspot_button.setText( "Hotspot") - self.rescan_button.setText( "Reload") - self.rescan_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.network_list_title.setText("Wi-Fi List") - self.nl_back_button.setText("Back") - self.nl_back_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) - self.nl_back_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.add_network_network_label.setText("TextLabel") - self.add_network_page_backButton.setText("Back") - self.add_network_page_backButton.setProperty("class", _translate("wifi_stacked_page", "back_btn")) - self.add_network_page_backButton.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.add_network_password_label.setText("Password") - self.add_network_password_view.setText("View") - self.add_network_password_view.setProperty("class", _translate("wifi_stacked_page", "back_btn")) - self.add_network_password_view.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.add_network_validation_button.setText(_translate("wifi_stacked_page", "Activate")) - self.saved_connection_back_button.setText(_translate("wifi_stacked_page", "Back")) - self.saved_connection_back_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) - self.saved_connection_back_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.netlist_strength_label_2.setText(_translate("wifi_stacked_page", "Signal\n" -"Strength")) - self.saved_connection_signal_strength_info_frame.setText(_translate("wifi_stacked_page", "TextLabel")) - self.netlist_security_label_2.setText(_translate("wifi_stacked_page", "Security\n" -"Type")) - self.saved_connection_security_type_info_label.setText(_translate("wifi_stacked_page", "TextLabel")) - self.netlist_security_label_4.setText(_translate("wifi_stacked_page", "Status")) - self.sn_info.setText(_translate("wifi_stacked_page", "TextLabel")) - self.network_activate_btn.setText(_translate("wifi_stacked_page", "Connect")) - self.network_details_btn.setText(_translate("wifi_stacked_page", "Details")) - self.network_delete_btn.setText(_translate("wifi_stacked_page", "Forget")) - self.snd_name.setText(_translate("wifi_stacked_page", "SSID")) - self.snd_back.setText(_translate("wifi_stacked_page", "Back")) - self.snd_back.setProperty("class", _translate("wifi_stacked_page", "back_btn")) - self.snd_back.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.saved_connection_change_password_label_3.setText(_translate("wifi_stacked_page", "Change\n" -"Password")) - self.saved_connection_change_password_view.setText(_translate("wifi_stacked_page", "View")) - self.saved_connection_change_password_view.setProperty("class", _translate("wifi_stacked_page", "back_btn")) - self.saved_connection_change_password_view.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.frame_12.setProperty("text", _translate("wifi_stacked_page", "Network priority")) - self.low_priority_btn.setText(_translate("wifi_stacked_page", "Low")) - self.low_priority_btn.setProperty("class", _translate("wifi_stacked_page", "back_btn")) - self.low_priority_btn.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.med_priority_btn.setText(_translate("wifi_stacked_page", "Medium")) - self.med_priority_btn.setProperty("class", _translate("wifi_stacked_page", "back_btn")) - self.med_priority_btn.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.high_priority_btn.setText(_translate("wifi_stacked_page", "High")) - self.high_priority_btn.setProperty("class", _translate("wifi_stacked_page", "back_btn")) - self.high_priority_btn.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.hotspot_header_title.setText(_translate("wifi_stacked_page", "Hotspot")) - self.hotspot_back_button.setText(_translate("wifi_stacked_page", "Back")) - self.hotspot_back_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) - self.hotspot_back_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.hotspot_info_name_label.setText("Hotspot Name: ") - self.hotspot_info_password_label.setText("Hotspot Password:") - self.hotspot_password_view_button.setText("View") - self.hotspot_password_view_button.setProperty("class", _translate("wifi_stacked_page", "back_btn")) - self.hotspot_password_view_button.setProperty("button_type", _translate("wifi_stacked_page", "icon")) - self.hotspot_change_confirm.setText("Save") -from lib.panels.widgets.loadWidget import LoadingOverlayWidget -from lib.utils.blocks_Scrollbar import CustomScrollBar -from lib.utils.blocks_button import BlocksCustomButton -from lib.utils.blocks_frame import BlocksCustomFrame -from lib.utils.blocks_linedit import BlocksCustomLinEdit -from lib.utils.blocks_togglebutton import NetworkWidgetbuttons -from lib.utils.check_button import BlocksCustomCheckButton -from lib.utils.icon_button import IconButton From 1a7e074d0fd07ee92ea5495efebd19439a576dcc Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Tue, 3 Mar 2026 10:37:22 +0000 Subject: [PATCH 47/70] uild: overhaul Makefile, and expand dev deps (#183) --- Makefile | 159 ++++++++++++++++++++++++++++++++++- scripts/requirements-dev.txt | 8 +- 2 files changed, 162 insertions(+), 5 deletions(-) diff --git a/Makefile b/Makefile index f7d3edae..cefd99d8 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,156 @@ -init: - pip3 install -r requirements.txt +.PHONY: all init init-dev venv run lint format-check security check \ + test test-all test-unit test-network test-ui test-integration test-fast \ + coverage coverage-all coverage-network clean clean-venv \ + docstrcov rcc rcc-all help -test: - py. test tests \ No newline at end of file +.DEFAULT_GOAL := help +SHELL := /bin/bash + +VENV := ~/.BlocksScreen-env +PYTHON := $(VENV)/bin/python +PIP := $(VENV)/bin/pip +SRC := BlocksScreen +TESTS := tests + +PYTEST_IGNORE := --ignore=$(TESTS)/network/test_sdbus_integration.py +PYTEST_FLAGS ?= -vvv +NM_INTEGRATION := NM_INTEGRATION_TESTS=1 + +PYRCC5 := /usr/bin/pyrcc5 +QRC_DIR := BlocksScreen/lib/ui/resources + +# ───────────────────────────────────────────────────────────────────────────── +##@ Help +# ───────────────────────────────────────────────────────────────────────────── + +help: ## Show this help message + @awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m\033[0m\n"} \ + /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-22s\033[0m %s\n", $$1, $$2 } \ + /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) }' \ + $(MAKEFILE_LIST) + +# ───────────────────────────────────────────────────────────────────────────── +##@ Environment +# ───────────────────────────────────────────────────────────────────────────── + +venv: ## Print venv activation command (source manually — subshells cannot export) + @echo "Run: source $(VENV)/bin/activate" + +init: ## Install production dependencies + $(PIP) install -r scripts/requirements.txt + +init-dev: ## Install dev + test dependencies + $(PIP) install -r scripts/requirements-dev.txt + +# ───────────────────────────────────────────────────────────────────────────── +##@ Run +# ───────────────────────────────────────────────────────────────────────────── + +run: ## Launch the BlocksScreen application + @echo "▶ Starting BlocksScreen..." + $(PYTHON) BlocksScreen/BlocksScreen.py + +# ───────────────────────────────────────────────────────────────────────────── +##@ Code Generation +# ───────────────────────────────────────────────────────────────────────────── + +rcc: ## Compile git-modified .qrc files to PyQt6 Python modules (_rc.py) + @files=$$(git diff --name-only HEAD -- '$(QRC_DIR)/*.qrc' 2>/dev/null; \ + git ls-files --others --exclude-standard -- '$(QRC_DIR)/*.qrc' 2>/dev/null); \ + if [ -z "$$files" ]; then echo " No modified .qrc files."; exit 0; fi; \ + for qrc in $$files; do \ + out="$${qrc%.qrc}_rc.py"; \ + echo " $$qrc → $$out"; \ + $(PYRCC5) "$$qrc" -o "$$out"; \ + sed -i 's/from PyQt5 import QtCore/from PyQt6 import QtCore/' "$$out"; \ + done + +rcc-all: ## Force recompile all .qrc files + @for qrc in $(QRC_DIR)/*.qrc; do \ + out="$${qrc%.qrc}_rc.py"; \ + echo " $$qrc → $$out"; \ + $(PYRCC5) "$$qrc" -o "$$out"; \ + sed -i 's/from PyQt5 import QtCore/from PyQt6 import QtCore/' "$$out"; \ + done + +# ───────────────────────────────────────────────────────────────────────────── +##@ Linting & Security +# ───────────────────────────────────────────────────────────────────────────── + +lint: ## Run pylint + $(PYTHON) -m pylint $(SRC) + +format-check: ## Verify formatting without modifying files (ruff-based) + $(PYTHON) -m ruff format --check $(SRC) $(TESTS) + $(PYTHON) -m ruff check $(SRC) $(TESTS) + +security: ## Run bandit security scan + $(PYTHON) -m bandit -c pyproject.toml -r $(SRC) + +check: format-check lint security test-fast ## Full pre-push gate (mirrors CI) + +# ───────────────────────────────────────────────────────────────────────────── +##@ Tests +# ───────────────────────────────────────────────────────────────────────────── + +test: ## Unit + UI tests (excludes real D-Bus integration) + $(PYTHON) -m pytest $(PYTEST_FLAGS) $(PYTEST_IGNORE) $(TESTS) + +test-fast: ## Stop on first failure, quiet output + $(PYTHON) -m pytest -x -q $(PYTEST_IGNORE) $(TESTS) + +test-unit: ## Unit tests only (*_unit.py) + $(PYTHON) -m pytest $(PYTEST_FLAGS) $(TESTS)/*/*_unit.py + +test-ui: ## UI tests only (*_ui.py) + $(PYTHON) -m pytest $(PYTEST_FLAGS) $(TESTS)/*/*_ui.py + +test-network: ## Network subsystem tests (unit + UI, no D-Bus) + $(PYTHON) -m pytest $(PYTEST_FLAGS) $(PYTEST_IGNORE) $(TESTS)/network/ + +test-integration: ## D-Bus integration tests (requires live NetworkManager) + $(NM_INTEGRATION) $(PYTHON) -m pytest $(PYTEST_FLAGS) $(TESTS)/*/*_integration.py + +test-all: ## All tests including D-Bus integration (requires NetworkManager) + $(NM_INTEGRATION) $(PYTHON) -m pytest $(PYTEST_FLAGS) -m "" $(TESTS) + +# ───────────────────────────────────────────────────────────────────────────── +##@ Coverage +# ───────────────────────────────────────────────────────────────────────────── + +COVERAGE_FLAGS := --cov-report=term-missing --cov-report=html:htmlcov --cov-fail-under=40 + +coverage: ## Coverage report — HTML + terminal (fail-under=40%) + $(PYTHON) -m pytest $(PYTEST_IGNORE) --cov=$(SRC) $(COVERAGE_FLAGS) $(TESTS) + @echo "Coverage report: htmlcov/index.html" + +coverage-all: ## Coverage including integration tests + $(NM_INTEGRATION) $(PYTHON) -m pytest -m "" --cov=$(SRC) $(COVERAGE_FLAGS) $(TESTS) + @echo "Coverage report: htmlcov/index.html" + +# ───────────────────────────────────────────────────────────────────────────── +##@ Documentation +# ───────────────────────────────────────────────────────────────────────────── + +docstrcov: ## Check docstring coverage (fail-under=80%, matches CI) + $(PYTHON) -m docstr_coverage $(SRC) \ + --exclude '.*/$(SRC)/lib/ui/.*?$$' \ + --fail-under 80 \ + --skip-magic --skip-init --skip-private --skip-property + + + +# ───────────────────────────────────────────────────────────────────────────── +##@ Cleanup +# ───────────────────────────────────────────────────────────────────────────── + +clean: ## Remove build artefacts, caches, and coverage data + rm -rf dist/ build/ *.egg-info src/*.egg-info site/ htmlcov .coverage + find . -depth \ + \( -type f -name '*.py[co]' \ + -o -type d -name __pycache__ \ + -o -type d -name .pytest_cache \) -exec rm -rf {} + + +clean-venv: ## Remove the virtual environment (destructive!) + @echo "Removing $(VENV)..." + rm -rf $(VENV) diff --git a/scripts/requirements-dev.txt b/scripts/requirements-dev.txt index 61706f13..8d6308d4 100644 --- a/scripts/requirements-dev.txt +++ b/scripts/requirements-dev.txt @@ -1,6 +1,12 @@ +# CI pipeline (mirrors .github/workflows/dev-ci.yml) ruff pylint pytest pytest-cov docstr_coverage -bandit \ No newline at end of file +bandit + +# Test suite plugins +pytest-qt +pytest-asyncio + From 89f892e5e442bac2f9f38fd756fc76d76453aa50 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Tue, 3 Mar 2026 10:39:41 +0000 Subject: [PATCH 48/70] bugfix: fixed missing home before ztilt (#180) Co-authored-by: Roberto --- BlocksScreen/lib/panels/controlTab.py | 1 + 1 file changed, 1 insertion(+) diff --git a/BlocksScreen/lib/panels/controlTab.py b/BlocksScreen/lib/panels/controlTab.py index 0f77cd24..f0a61a7b 100644 --- a/BlocksScreen/lib/panels/controlTab.py +++ b/BlocksScreen/lib/panels/controlTab.py @@ -460,6 +460,7 @@ def _handle_gcode_response(self, messages: list): def handle_ztilt(self): """Handle Z-Tilt Adjustment""" self.call_load_panel.emit(True, "Please wait, performing Z-axis calibration.") + self.run_gcode_signal.emit("G28\nM400") self.run_gcode_signal.emit("Z_TILT_ADJUST") @QtCore.pyqtSlot(str, name="on-klippy-status") From bb6ddea26b7723ed603559f2c6b57d2d00541d39 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Tue, 3 Mar 2026 11:48:17 +0000 Subject: [PATCH 49/70] Feat/cancel page (#170) * Feat: added cancel page * Rev: removed back button * add: hides cancel page * Refactor: removed cancel page connection to printtab * ADD: added logic to cancelPage * refactor: ran ruff formatter --------- Co-authored-by: Roberto Co-authored-by: Hugo Costa --- BlocksScreen/lib/panels/mainWindow.py | 39 +++ BlocksScreen/lib/panels/printTab.py | 3 + BlocksScreen/lib/panels/widgets/cancelPage.py | 267 ++++++++++++++++++ .../lib/panels/widgets/connectionPage.py | 2 + .../lib/panels/widgets/jobStatusPage.py | 8 +- 5 files changed, 316 insertions(+), 3 deletions(-) create mode 100644 BlocksScreen/lib/panels/widgets/cancelPage.py diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 32355803..66f792de 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -13,6 +13,7 @@ from lib.panels.printTab import PrintTab from lib.panels.utilitiesTab import UtilitiesTab from lib.panels.widgets.connectionPage import ConnectionPage +from lib.panels.widgets.cancelPage import CancelPage from lib.panels.widgets.popupDialogWidget import Popup from lib.printer import Printer from lib.ui.mainWindow_ui import Ui_MainWindow # With header @@ -65,6 +66,9 @@ class MainWindow(QtWidgets.QMainWindow): on_update_message: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( dict, name="on-update-message" ) + run_gcode_signal: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="run_gcode" + ) call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") def __init__(self): @@ -84,6 +88,8 @@ def __init__(self): self.conn_window = ConnectionPage(self, self.ws) self.up = UpdatePage(self) self.up.hide() + + self.conn_window.call_cancel_panel.connect(self.handle_cancel_print) self.installEventFilter(self.conn_window) self.printPanel = PrintTab( self.ui.printTab, self.file_data, self.ws, self.printer @@ -152,6 +158,8 @@ def __init__(self): self.query_object_list.connect(self.utilitiesPanel.on_object_list) self.printer.extruder_update.connect(self.on_extruder_update) self.printer.heater_bed_update.connect(self.on_heater_bed_update) + self.run_gcode_signal.connect(self.ws.api.run_gcode) + self.ui.main_content_widget.currentChanged.connect(slot=self.reset_tab_indexes) self.call_network_panel.connect(self.networkPanel.show_network_panel) self.conn_window.wifi_button_clicked.connect(self.call_network_panel.emit) @@ -191,11 +199,40 @@ def __init__(self): self, LoadingOverlayWidget.AnimationGIF.DEFAULT ) self.loadscreen.add_widget(self.loadwidget) + + self.cancelpage = CancelPage(self, ws=self.ws) + self.cancelpage.request_file_info.connect(self.file_data.on_request_fileinfo) + self.cancelpage.run_gcode.connect(self.ws.api.run_gcode) + self.printer.print_stats_update[str, str].connect( + self.cancelpage.on_print_stats_update + ) + self.printer.print_stats_update[str, dict].connect( + self.cancelpage.on_print_stats_update + ) + self.printer.print_stats_update[str, float].connect( + self.cancelpage.on_print_stats_update + ) + self.file_data.fileinfo.connect(self.cancelpage._show_screen_thumbnail) + self.printPanel.call_cancel_panel.connect(self.handle_cancel_print) + if self.config.has_section("server"): # @ Start websocket connection with moonraker self.bo_ws_startup.emit() self.reset_tab_indexes() + @QtCore.pyqtSlot(bool, name="show-cancel-page") + def handle_cancel_print(self, show: bool = True): + """Slot for displaying update Panel""" + if not show: + self.cancelpage.hide() + return + + self.cancelpage.setGeometry(0, 0, self.width(), self.height()) + self.cancelpage.raise_() + self.cancelpage.updateGeometry() + self.cancelpage.repaint() + self.cancelpage.show() + @QtCore.pyqtSlot(bool, str, name="show-load-page") def show_LoadScreen(self, show: bool = True, msg: str = ""): _sender = self.sender() @@ -734,6 +771,8 @@ def event(self, event: QtCore.QEvent) -> bool: events.PrintComplete.type(), events.PrintCancelled.type(), ): + if event.type() == events.PrintCancelled.type(): + self.handle_cancel_print() self.enable_tab_bar() self.ui.extruder_temp_display.clicked.disconnect() self.ui.bed_temp_display.clicked.disconnect() diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index 7431938e..b28e52bd 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -64,6 +64,7 @@ class PrintTab(QtWidgets.QStackedWidget): ) call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") + call_cancel_panel = QtCore.pyqtSignal(bool, name="call-load-panel") _z_offset: float = 0.0 _active_z_offset: float = 0.0 _finish_print_handled: bool = False @@ -124,6 +125,7 @@ def __init__( self.file_data.request_file_list ) self.file_data.on_dirs.connect(self.filesPage_widget.on_directories) + self.filesPage_widget.request_dir_info[str].connect( self.file_data.request_dir_info[str] ) @@ -137,6 +139,7 @@ def __init__( self.jobStatusPage_widget.show_request.connect( lambda: self.change_page(self.indexOf(self.jobStatusPage_widget)) ) + self.jobStatusPage_widget.call_cancel_panel.connect(self.call_cancel_panel) self.jobStatusPage_widget.hide_request.connect( lambda: self.change_page(self.indexOf(self.print_page)) ) diff --git a/BlocksScreen/lib/panels/widgets/cancelPage.py b/BlocksScreen/lib/panels/widgets/cancelPage.py new file mode 100644 index 00000000..e16fb2e6 --- /dev/null +++ b/BlocksScreen/lib/panels/widgets/cancelPage.py @@ -0,0 +1,267 @@ +from lib.utils.blocks_button import BlocksCustomButton +from lib.utils.blocks_frame import BlocksCustomFrame +from lib.utils.blocks_label import BlocksLabel +from PyQt6 import QtCore, QtGui, QtWidgets +import typing + +from lib.moonrakerComm import MoonWebSocket + + +class CancelPage(QtWidgets.QWidget): + """Update GUI Page, + retrieves from moonraker available clients and adds functionality + for updating or recovering them + """ + + request_file_info: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="request_file_info" + ) + reprint_start: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="reprint_start" + ) + run_gcode: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="run_gcode" + ) + + def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket) -> None: + super().__init__(parent) + self.ws: MoonWebSocket = ws + self._setupUI() + self.filename = "" + + self.reprint_start.connect(self.ws.api.start_print) + + self.confirm_button.clicked.connect(lambda: self._handle_accept()) + + self.refuse_button.clicked.connect(lambda: self._handle_refuse()) + + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_StyledBackground, True) + + def _handle_accept(self): + self.run_gcode.emit("SDCARD_RESET_FILE") + self.reprint_start.emit(self.filename) + self.close() + + def _handle_refuse(self): + self.close() + self.run_gcode.emit("SDCARD_RESET_FILE") + + @QtCore.pyqtSlot(str, dict, name="on_print_stats_update") + @QtCore.pyqtSlot(str, float, name="on_print_stats_update") + @QtCore.pyqtSlot(str, str, name="on_print_stats_update") + def on_print_stats_update(self, field: str, value: dict | float | str) -> None: + if isinstance(value, str): + if "filename" in field: + self.filename = value + if self.isVisible: + self.set_file_name(value) + + def show(self): + self.request_file_info.emit(self.filename) + + super().show() + + def set_pixmap(self, pixmap: QtGui.QPixmap) -> None: + if not hasattr(self, "_scene"): + self._scene = QtWidgets.QGraphicsScene(self) + self.cf_thumbnail.setScene(self._scene) + + # Scene rectangle (available display area) + graphics_rect = self.cf_thumbnail.rect().toRectF() + + # Scale pixmap preserving aspect ratio + pixmap = pixmap.scaled( + graphics_rect.size().toSize(), + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, + ) + + adjusted_x = (graphics_rect.width() - pixmap.width()) / 2.0 + adjusted_y = (graphics_rect.height() - pixmap.height()) / 2.0 + + if not hasattr(self, "_pixmap_item"): + self._pixmap_item = QtWidgets.QGraphicsPixmapItem(pixmap) + self._scene.addItem(self._pixmap_item) + else: + self._pixmap_item.setPixmap(pixmap) + + self._pixmap_item.setPos(adjusted_x, adjusted_y) + self._scene.setSceneRect(graphics_rect) + + def set_file_name(self, file_name: str) -> None: + self.cf_file_name.setText(file_name) + + def _show_screen_thumbnail(self, dict): + try: + thumbnails = dict["thumbnail_images"] + + last_thumb = QtGui.QPixmap.fromImage(thumbnails[-1]) + + if last_thumb.isNull(): + last_thumb = QtGui.QPixmap( + "BlocksScreen/lib/ui/resources/media/logoblocks400x300.png" + ) + except Exception as e: + print(e) + last_thumb = QtGui.QPixmap( + "BlocksScreen/lib/ui/resources/media/logoblocks400x300.png" + ) + self.set_pixmap(last_thumb) + + def _setupUI(self) -> None: + """Setup widget ui""" + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + ) + sizePolicy.setHorizontalStretch(1) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) + self.setSizePolicy(sizePolicy) + self.setObjectName("cancelPage") + self.setStyleSheet( + """#cancelPage { + background-image: url(:/background/media/1st_background.png); + }""" + ) + self.setMinimumSize(QtCore.QSize(800, 480)) + self.setMaximumSize(QtCore.QSize(800, 480)) + self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) + self.verticalLayout_4 = QtWidgets.QVBoxLayout(self) + self.verticalLayout_4.setObjectName("verticalLayout_4") + self.cf_header_title = QtWidgets.QHBoxLayout() + self.cf_header_title.setObjectName("cf_header_title") + + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.cf_file_name = BlocksLabel(parent=self) + self.cf_file_name.setMinimumSize(QtCore.QSize(0, 60)) + self.cf_file_name.setMaximumSize(QtCore.QSize(16777215, 60)) + font = QtGui.QFont() + font.setFamily("Momcake") + font.setPointSize(24) + self.cf_file_name.setFont(font) + self.cf_file_name.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) + self.cf_file_name.setSizePolicy(sizePolicy) + self.cf_file_name.setStyleSheet("background: transparent; color: white;") + self.cf_file_name.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.cf_file_name.setObjectName("cf_file_name") + self.cf_header_title.addWidget(self.cf_file_name) + + self.verticalLayout_4.addLayout(self.cf_header_title) + self.cf_content_vertical_layout = QtWidgets.QHBoxLayout() + self.cf_content_vertical_layout.setObjectName("cf_content_vertical_layout") + self.cf_content_horizontal_layout = QtWidgets.QVBoxLayout() + self.cf_content_horizontal_layout.setObjectName("cf_content_horizontal_layout") + self.info_frame = BlocksCustomFrame(parent=self) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.info_frame.setSizePolicy(sizePolicy) + + self.info_frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + + self.info_layout = QtWidgets.QVBoxLayout(self.info_frame) + + self.cf_info_tf = QtWidgets.QLabel(parent=self.info_frame) + self.cf_info_tf.setText("Print job was\ncancelled") + font = QtGui.QFont() + font.setFamily("Momcake") + font.setPointSize(20) + + self.cf_info_tf.setFont(font) + self.cf_info_tf.setStyleSheet("background: transparent; color: white;") + + self.cf_info_tf.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.info_layout.addWidget(self.cf_info_tf) + + self.cf_info_tr = QtWidgets.QLabel(parent=self.info_frame) + font = QtGui.QFont() + font.setPointSize(15) + self.cf_info_tr.setFont(font) + self.cf_info_tr.setStyleSheet("background: transparent; color: white;") + self.cf_info_tr.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.cf_info_tr.setText("Do you want to reprint?") + self.info_layout.addWidget(self.cf_info_tr) + + self.cf_confirm_layout = QtWidgets.QVBoxLayout() + self.cf_confirm_layout.setSpacing(15) + + self.confirm_button = BlocksCustomButton(parent=self.info_frame) + self.confirm_button.setMinimumSize(QtCore.QSize(250, 70)) + self.confirm_button.setMaximumSize(QtCore.QSize(250, 70)) + font = QtGui.QFont("Momcake", 18) + self.confirm_button.setFont(font) + self.confirm_button.setFlat(True) + self.confirm_button.setProperty( + "icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") + ) + self.confirm_button.setText("Reprint") + # 2. Align buttons to the right + self.cf_confirm_layout.addWidget( + self.confirm_button, 0, QtCore.Qt.AlignmentFlag.AlignCenter + ) + + self.refuse_button = BlocksCustomButton(parent=self.info_frame) + self.refuse_button.setMinimumSize(QtCore.QSize(250, 70)) + self.refuse_button.setMaximumSize(QtCore.QSize(250, 70)) + self.refuse_button.setFont(font) + self.refuse_button.setFlat(True) + self.refuse_button.setProperty( + "icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/no.svg") + ) + self.refuse_button.setText("Ignore") + # 2. Align buttons to the right + self.cf_confirm_layout.addWidget( + self.refuse_button, 0, QtCore.Qt.AlignmentFlag.AlignCenter + ) + + self.info_layout.addLayout(self.cf_confirm_layout) + + self.cf_content_horizontal_layout.addWidget(self.info_frame) + + self.cf_content_vertical_layout.addLayout(self.cf_content_horizontal_layout) + self.cf_thumbnail = QtWidgets.QGraphicsView(self) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + sizePolicy.setHorizontalStretch(1) + sizePolicy.setVerticalStretch(1) + sizePolicy.setHeightForWidth(self.cf_thumbnail.sizePolicy().hasHeightForWidth()) + self.cf_thumbnail.setSizePolicy(sizePolicy) + self.cf_thumbnail.setMinimumSize(QtCore.QSize(400, 300)) + self.cf_thumbnail.setMaximumSize(QtCore.QSize(400, 300)) + self.cf_thumbnail.setStyleSheet( + "QGraphicsView{\nbackground-color: transparent;\n}" + ) + self.cf_thumbnail.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) + self.cf_thumbnail.setFrameShadow(QtWidgets.QFrame.Shadow.Plain) + self.cf_thumbnail.setVerticalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.cf_thumbnail.setHorizontalScrollBarPolicy( + QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff + ) + self.cf_thumbnail.setSizeAdjustPolicy( + QtWidgets.QAbstractScrollArea.SizeAdjustPolicy.AdjustIgnored + ) + brush = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) + self.cf_thumbnail.setBackgroundBrush(brush) + self.cf_thumbnail.setRenderHints( + QtGui.QPainter.RenderHint.Antialiasing + | QtGui.QPainter.RenderHint.SmoothPixmapTransform + | QtGui.QPainter.RenderHint.TextAntialiasing + ) + self.cf_thumbnail.setViewportUpdateMode( + QtWidgets.QGraphicsView.ViewportUpdateMode.SmartViewportUpdate + ) + self.cf_thumbnail.setObjectName("cf_thumbnail") + self.cf_content_vertical_layout.addWidget( + self.cf_thumbnail, 0, QtCore.Qt.AlignmentFlag.AlignCenter + ) + self.verticalLayout_4.addLayout(self.cf_content_vertical_layout) diff --git a/BlocksScreen/lib/panels/widgets/connectionPage.py b/BlocksScreen/lib/panels/widgets/connectionPage.py index 9403d290..a03e42dd 100644 --- a/BlocksScreen/lib/panels/widgets/connectionPage.py +++ b/BlocksScreen/lib/panels/widgets/connectionPage.py @@ -15,6 +15,7 @@ class ConnectionPage(QtWidgets.QFrame): firmware_restart_clicked = QtCore.pyqtSignal(name="firmware_restart_clicked") update_button_clicked = QtCore.pyqtSignal(bool, name="show-update-page") call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") + call_cancel_panel = QtCore.pyqtSignal(bool, name="call-load-panel") def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket, /): super().__init__(parent) @@ -67,6 +68,7 @@ def showEvent(self, a0: QtCore.QEvent | None): """Handle show event""" self.ws.api.refresh_update_status() self.call_load_panel.emit(False, "") + self.call_cancel_panel.emit(False) return super().showEvent(a0) @QtCore.pyqtSlot(bool, name="on_klippy_connected") diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index bd5bbb8c..f868c222 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -53,6 +53,7 @@ class JobStatusWidget(QtWidgets.QWidget): request_file_info: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( str, name="request_file_info" ) + call_cancel_panel = QtCore.pyqtSignal(bool, name="call-load-panel") _internal_print_status: str = "" _current_file_name: str = "" @@ -225,18 +226,19 @@ def _handle_print_state(self, state: str) -> None: ) self.pause_printing_btn.setEnabled(True) self.request_query_print_stats.emit({"print_stats": ["filename"]}) + self.call_cancel_panel.emit(False) self.show_request.emit() lstate = "start" elif lstate in invalid_states: if lstate != "standby": self.print_finish.emit() - self._current_file_name = "" self._internal_print_status = "" + self._current_file_name = "" self.total_layers = "?" self.file_metadata.clear() self.hide_request.emit() - if hasattr(self, "thumbnail_view"): - getattr(self, "thumbnail_view").deleteLater() + # if hasattr(self, "thumbnail_view"): + # getattr(self, "thumbnail_view").deleteLater() # Send Event on Print state if hasattr(events, str("Print" + lstate.capitalize())): event_obj = getattr(events, str("Print" + lstate.capitalize())) From cc33f7b50c896e5586cbc16ff8cd5d810e9c2e51 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Tue, 3 Mar 2026 11:58:05 +0000 Subject: [PATCH 50/70] Refactor the Logging System (#172) * bugfix: logger works with each module and handles stdout/stderr * fix code formatation * Catch segmentation faults and log crash details --------- Signed-off-by: Hugo Costa Co-authored-by: Guilherme Costa Co-authored-by: Hugo Costa --- BlocksScreen/BlocksScreen.py | 21 +- BlocksScreen/lib/moonrakerComm.py | 2 +- BlocksScreen/lib/network.py | 2 +- BlocksScreen/lib/panels/mainWindow.py | 28 +- BlocksScreen/lib/panels/networkWindow.py | 2 +- BlocksScreen/lib/panels/printTab.py | 2 +- BlocksScreen/lib/panels/widgets/filesPage.py | 5 +- .../lib/panels/widgets/jobStatusPage.py | 2 +- BlocksScreen/lib/printer.py | 2 +- BlocksScreen/logger.py | 862 ++++++++++++++++-- 10 files changed, 840 insertions(+), 88 deletions(-) diff --git a/BlocksScreen/BlocksScreen.py b/BlocksScreen/BlocksScreen.py index a7a2098e..3ccceb74 100644 --- a/BlocksScreen/BlocksScreen.py +++ b/BlocksScreen/BlocksScreen.py @@ -2,11 +2,10 @@ import sys import typing -import logger from lib.panels.mainWindow import MainWindow +from logger import setup_logging from PyQt6 import QtCore, QtGui, QtWidgets -_logger = logging.getLogger(name="logs/BlocksScreen.log") QtGui.QGuiApplication.setAttribute( QtCore.Qt.ApplicationAttribute.AA_SynthesizeMouseForUnhandledTouchEvents, True, @@ -22,13 +21,6 @@ RESET = "\033[0m" -def setup_app_loggers(): - """Setup logger""" - _ = logger.create_logger(name="logs/BlocksScreen.log", level=logging.DEBUG) - _logger = logging.getLogger(name="logs/BlocksScreen.log") - _logger.info("============ BlocksScreen Initializing ============") - - def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): """Show splash screen on app initialization""" logo = QtGui.QPixmap("BlocksScreen/BlocksScreen/lib/ui/resources/logoblocks.png") @@ -39,7 +31,16 @@ def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): if __name__ == "__main__": - setup_app_loggers() + setup_logging( + filename="logs/BlocksScreen.log", + level=logging.DEBUG, # File gets DEBUG+ + console_output=True, # Print to terminal + console_level=logging.DEBUG, # Console gets DEBUG+ + capture_stderr=True, # Capture X11 errors + capture_stdout=False, # Don't capture print() + ) + _logger = logging.getLogger(__name__) + _logger.info("============ BlocksScreen Initializing ============") BlocksScreen = QtWidgets.QApplication([]) BlocksScreen.setApplicationName("BlocksScreen") BlocksScreen.setApplicationDisplayName("BlocksScreen") diff --git a/BlocksScreen/lib/moonrakerComm.py b/BlocksScreen/lib/moonrakerComm.py index ba298ba7..bf3a74cc 100644 --- a/BlocksScreen/lib/moonrakerComm.py +++ b/BlocksScreen/lib/moonrakerComm.py @@ -14,7 +14,7 @@ from lib.utils.RepeatedTimer import RepeatedTimer from PyQt6 import QtCore, QtWidgets -_logger = logging.getLogger(name="logs/BlocksScreen.log") +_logger = logging.getLogger(__name__) class OneShotTokenError(Exception): diff --git a/BlocksScreen/lib/network.py b/BlocksScreen/lib/network.py index 61ea4078..f6cadaa5 100644 --- a/BlocksScreen/lib/network.py +++ b/BlocksScreen/lib/network.py @@ -9,7 +9,7 @@ from PyQt6 import QtCore from sdbus_async import networkmanager as dbusNm -logger = logging.getLogger("logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class NetworkManagerRescanError(Exception): diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 66f792de..5d75e248 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -9,17 +9,16 @@ from lib.moonrakerComm import MoonWebSocket from lib.panels.controlTab import ControlTab from lib.panels.filamentTab import FilamentTab -from lib.panels.networkWindow import NetworkControlWindow from lib.panels.printTab import PrintTab from lib.panels.utilitiesTab import UtilitiesTab +from lib.panels.widgets.basePopup import BasePopup from lib.panels.widgets.connectionPage import ConnectionPage +from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.panels.widgets.cancelPage import CancelPage from lib.panels.widgets.popupDialogWidget import Popup +from lib.panels.widgets.updatePage import UpdatePage from lib.printer import Printer from lib.ui.mainWindow_ui import Ui_MainWindow # With header -from lib.panels.widgets.updatePage import UpdatePage -from lib.panels.widgets.basePopup import BasePopup -from lib.panels.widgets.loadWidget import LoadingOverlayWidget # from lib.ui.mainWindow_v2_ui import Ui_MainWindow # No header from lib.ui.resources.background_resources_rc import * @@ -29,10 +28,11 @@ from lib.ui.resources.main_menu_resources_rc import * from lib.ui.resources.system_resources_rc import * from lib.ui.resources.top_bar_resources_rc import * +from logger import LogManager from PyQt6 import QtCore, QtGui, QtWidgets from screensaver import ScreenSaver -_logger = logging.getLogger(name="logs/BlocksScreen.log") +_logger = logging.getLogger(__name__) def api_handler(func): @@ -99,7 +99,7 @@ def __init__(self): self.filamentPanel = FilamentTab(self.ui.filamentTab, self.printer, self.ws) self.controlPanel = ControlTab(self.ui.controlTab, self.ws, self.printer) self.utilitiesPanel = UtilitiesTab(self.ui.utilitiesTab, self.ws, self.printer) - self.networkPanel = NetworkControlWindow(self) + # self.networkPanel = NetworkControlWindow(self) self.bo_ws_startup.connect(slot=self.bo_start_websocket_connection) self.ws.connecting_signal.connect(self.conn_window.on_websocket_connecting) self.ws.connected_signal.connect( @@ -161,7 +161,7 @@ def __init__(self): self.run_gcode_signal.connect(self.ws.api.run_gcode) self.ui.main_content_widget.currentChanged.connect(slot=self.reset_tab_indexes) - self.call_network_panel.connect(self.networkPanel.show_network_panel) + # self.call_network_panel.connect(self.networkPanel.show_network_panel) self.conn_window.wifi_button_clicked.connect(self.call_network_panel.emit) self.ui.wifi_button.clicked.connect(self.call_network_panel.emit) self.handle_error_response.connect( @@ -389,7 +389,7 @@ def reset_tab_indexes(self): self.filamentPanel.setCurrentIndex(0) self.controlPanel.setCurrentIndex(0) self.utilitiesPanel.setCurrentIndex(0) - self.networkPanel.setCurrentIndex(0) + # self.networkPanel.setCurrentIndex(0) def current_panel_index(self) -> int: """Helper function to get the index of the current page in the current tab @@ -724,14 +724,10 @@ def set_header_nozzle_diameter(self, diam: str): def closeEvent(self, a0: typing.Optional[QtGui.QCloseEvent]) -> None: """Handles GUI closing""" - _loggers = [ - logging.getLogger(name) for name in logging.root.manager.loggerDict - ] # Get available logger handlers - for logger in _loggers: # noqa: F402 - if hasattr(logger, "cancel"): - _callback = getattr(logger, "cancel") - if callable(_callback): - _callback() + + # Shutdown logger (closes files, stops threads, restores streams) + LogManager.shutdown() + self.ws.wb_disconnect() self.close() if a0 is None: diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 19574cf5..37f61138 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -25,7 +25,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal -logger = logging.getLogger("logs/BlocksScreen.log") +logger = logging.getLogger(__name__) LOAD_TIMEOUT_MS = 30_000 diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index b28e52bd..8a0cb653 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -20,7 +20,7 @@ from lib.utils.display_button import DisplayButton from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger(name="logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class PrintTab(QtWidgets.QStackedWidget): diff --git a/BlocksScreen/lib/panels/widgets/filesPage.py b/BlocksScreen/lib/panels/widgets/filesPage.py index f8fa490f..77959e83 100644 --- a/BlocksScreen/lib/panels/widgets/filesPage.py +++ b/BlocksScreen/lib/panels/widgets/filesPage.py @@ -5,11 +5,10 @@ import helper_methods from lib.utils.blocks_Scrollbar import CustomScrollBar from lib.utils.icon_button import IconButton -from PyQt6 import QtCore, QtGui, QtWidgets - from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem +from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger("logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class FilesPage(QtWidgets.QWidget): diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index f868c222..67add6b9 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -10,7 +10,7 @@ from lib.utils.display_button import DisplayButton from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger("logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class JobStatusWidget(QtWidgets.QWidget): diff --git a/BlocksScreen/lib/printer.py b/BlocksScreen/lib/printer.py index 5889c19d..fd33f172 100644 --- a/BlocksScreen/lib/printer.py +++ b/BlocksScreen/lib/printer.py @@ -7,7 +7,7 @@ from lib.moonrakerComm import MoonWebSocket from PyQt6 import QtCore, QtWidgets -logger = logging.getLogger(name="logs/BlocksScreen.logs") +logger = logging.getLogger(__name__) class Printer(QtCore.QObject): diff --git a/BlocksScreen/logger.py b/BlocksScreen/logger.py index f63631e5..b4680ff1 100644 --- a/BlocksScreen/logger.py +++ b/BlocksScreen/logger.py @@ -1,95 +1,851 @@ +from __future__ import annotations + +import atexit import copy +import faulthandler import logging -import logging.config import logging.handlers +import os import pathlib import queue +import sys import threading +import traceback +from datetime import datetime +from typing import ClassVar, TextIO + +DEFAULT_FORMAT = ( + "[%(levelname)s] | %(asctime)s | %(name)s | " + "%(relativeCreated)6d | %(threadName)s : %(message)s" +) + +CRASH_LOG_PATH = "logs/blocksscreen_crash.log" +FAULT_LOG_PATH = "logs/blocksscreen_fault.log" + + +class StreamToLogger(TextIO): + """ + Redirects a stream (stdout/stderr) to a logger. + + Useful for capturing output from subprocesses, X11, or print statements. + """ + + def __init__( + self, + logger: logging.Logger, + level: int = logging.INFO, + original_stream: TextIO | None = None, + ) -> None: + self._logger = logger + self._level = level + self._original = original_stream + self._buffer = "" + + def write(self, message: str) -> int: + """Write message to logger.""" + if message: + if self._original: + try: + self._original.write(message) + self._original.flush() + except Exception: + pass + + self._buffer += message + + while "\n" in self._buffer: + line, self._buffer = self._buffer.split("\n", 1) + if line.strip(): + self._logger.log(self._level, line.rstrip()) + + return len(message) + + def flush(self) -> None: + """Flush remaining buffer.""" + if self._buffer.strip(): + self._logger.log(self._level, self._buffer.rstrip()) + self._buffer = "" + + if self._original: + try: + self._original.flush() + except Exception: + pass + + def fileno(self) -> int: + """Return file descriptor for compatibility.""" + if self._original: + return self._original.fileno() + raise OSError("No file descriptor available") + + def isatty(self) -> bool: + """Check if stream is a TTY.""" + if self._original: + return self._original.isatty() + return False + + # Required for TextIO interface + def read(self, n: int = -1) -> str: + return "" + + def readline(self, limit: int = -1) -> str: + return "" + + def readlines(self, hint: int = -1) -> list[str]: + return [] + + def seek(self, offset: int, whence: int = 0) -> int: + return 0 + + def tell(self) -> int: + return 0 + + def truncate(self, size: int | None = None) -> int: + return 0 + + def writable(self) -> bool: + return True + + def readable(self) -> bool: + return False + + def seekable(self) -> bool: + return False + + def close(self) -> None: + self.flush() + + @property + def closed(self) -> bool: + return False + + def __enter__(self) -> "StreamToLogger": + return self + + def __exit__(self, *args) -> None: + self.close() class QueueHandler(logging.Handler): - """Handler that sends events to a queue""" + """ + Logging handler that sends records to a queue. + + Records are formatted before being placed on the queue, + then consumed by a QueueListener in a background thread. + """ def __init__( self, - queue: queue.Queue, - format: str = "'[%(levelname)s] | %(asctime)s | %(name)s | %(relativeCreated)6d | %(threadName)s : %(message)s", - level=logging.DEBUG, - ): - super(QueueHandler, self).__init__() - self.log_queue = queue - self.setFormatter(logging.Formatter(format, validate=True)) - self.setLevel(level) - - def emit(self, record): - """Emit logging record""" + log_queue: queue.Queue, + fmt: str = DEFAULT_FORMAT, + level: int = logging.DEBUG, + ) -> None: + super().__init__(level) + self._queue = log_queue + self.setFormatter(logging.Formatter(fmt)) + + def emit(self, record: logging.LogRecord) -> None: + """Format and queue the log record.""" try: + # Format the message msg = self.format(record) + + # Copy record and update message record = copy.copy(record) - record.message = msg - record.name = record.name record.msg = msg - self.log_queue.put_nowait(record) + record.args = None # Already formatted + record.message = msg + + self._queue.put_nowait(record) except Exception: self.handleError(record) - def setFormatter(self, fmt: logging.Formatter | None) -> None: - """Set logging formatter""" - return super().setFormatter(fmt) +class AsyncFileHandler(logging.handlers.TimedRotatingFileHandler): + """ + Async file handler using a background thread. + + Wraps TimedRotatingFileHandler with a queue and worker thread + for non-blocking log writes. Automatically recreates log file + if deleted during runtime. + """ -class QueueListener(logging.handlers.TimedRotatingFileHandler): - """Threaded listener watching for log records on the queue handler queue, passes them for processing""" + def __init__( + self, + filename: str, + when: str = "midnight", + backup_count: int = 10, + encoding: str = "utf-8", + ) -> None: + self._log_path = pathlib.Path(filename) + + # Create log directory if needed + if self._log_path.parent != pathlib.Path("."): + self._log_path.parent.mkdir(parents=True, exist_ok=True) - def __init__(self, filename, encoding="utf-8"): - log_path = pathlib.Path(filename) - if log_path.parent != pathlib.Path("."): - log_path.parent.mkdir(parents=True, exist_ok=True) - super(QueueListener, self).__init__( + super().__init__( filename=filename, - when="MIDNIGHT", - backupCount=10, + when=when, + backupCount=backup_count, encoding=encoding, delay=True, ) - self.queue = queue.Queue() + + self._queue: queue.Queue[logging.LogRecord | None] = queue.Queue() + self._stop_event = threading.Event() + self._lock = threading.Lock() self._thread = threading.Thread( - name=f"log.{filename}", target=self._run, daemon=True + name=f"logger-{self._log_path.stem}", + target=self._worker, + daemon=True, ) self._thread.start() - def _run(self): - while True: + def _ensure_file_exists(self) -> None: + """Ensure log file and directory exist, recreate if deleted.""" + try: + # Check if directory exists + if not self._log_path.parent.exists(): + self._log_path.parent.mkdir(parents=True, exist_ok=True) + + # Check if file was deleted (stream is open but file gone) + if self.stream is not None and not self._log_path.exists(): + # Close old stream + try: + self.stream.close() + except Exception: + pass + self.stream = None + + # Reopen stream if needed + if self.stream is None: + self.stream = self._open() + + except Exception: + pass + + def emit(self, record: logging.LogRecord) -> None: + """Emit a record with file existence check.""" + with self._lock: + self._ensure_file_exists() + super().emit(record) + + def _worker(self) -> None: + """Background worker that processes queued log records.""" + while not self._stop_event.is_set(): try: - record = self.queue.get(True) + record = self._queue.get(timeout=0.5) if record is None: break self.handle(record) except queue.Empty: - break + continue + except Exception: + # Don't crash the worker thread + pass - def close(self): - """Close logger listener""" - if self._thread is None: + @property + def queue(self) -> queue.Queue: + """Get the log queue for QueueHandler.""" + return self._queue + + def close(self) -> None: + """Stop worker thread and close file handler.""" + if self._thread is None or not self._thread.is_alive(): + super().close() return - self.queue.put_nowait(None) - self._thread.join() + + # Signal worker to stop + self._stop_event.set() + self._queue.put_nowait(None) + + # Wait for worker to finish + self._thread.join(timeout=2.0) self._thread = None + # Close the file handler + super().close() + + +class _ExcludeStreamLoggers(logging.Filter): + """Filter to exclude stdout/stderr loggers from console output.""" + + def filter(self, record: logging.LogRecord) -> bool: + # Exclude to avoid double printing (already goes to console via StreamToLogger) + return record.name not in ("stdout", "stderr") + + +class CrashHandler: + """ + Handles unhandled exceptions and C-level crashes. + + Writes detailed crash information to log files including: + - Full traceback with line numbers + - Local variables at each frame + - Thread information + - Timestamp + """ + + _instance: ClassVar[CrashHandler | None] = None + _installed: ClassVar[bool] = False + + def __init__( + self, + crash_log_path: str = CRASH_LOG_PATH, + fault_log_path: str = FAULT_LOG_PATH, + include_locals: bool = True, + exit_on_crash: bool = True, + ) -> None: + self._crash_log_path = pathlib.Path(crash_log_path) + self._fault_log_path = pathlib.Path(fault_log_path) + self._include_locals = include_locals + self._exit_on_crash = exit_on_crash + self._original_excepthook = sys.excepthook + self._original_threading_excepthook = getattr(threading, "excepthook", None) + self._fault_file: TextIO | None = None + + @classmethod + def install( + cls, + crash_log_path: str = CRASH_LOG_PATH, + fault_log_path: str = FAULT_LOG_PATH, + include_locals: bool = True, + exit_on_crash: bool = True, + ) -> CrashHandler: + """ + Install the crash handler. + + Should be called as early as possible in the application startup. + + Args: + crash_log_path: Path to write Python exception logs + fault_log_path: Path to write C-level fault logs (segfaults) + include_locals: Include local variables in traceback + exit_on_crash: Force exit after logging (for systemd restart) + + Returns: + The CrashHandler instance + """ + if cls._installed and cls._instance: + return cls._instance + + handler = cls(crash_log_path, fault_log_path, include_locals, exit_on_crash) + handler._install() + cls._instance = handler + cls._installed = True + + return handler + + def _install(self) -> None: + """Install exception hooks.""" + # Setup faulthandler for C-level crashes (segfaults, etc.) + try: + self._fault_file = open(self._fault_log_path, "w") + faulthandler.enable(file=self._fault_file, all_threads=True) + + # Also dump traceback on SIGUSR1 (useful for debugging hangs) + try: + import signal + + faulthandler.register( + signal.SIGUSR1, + file=self._fault_file, + all_threads=True, + ) + except (AttributeError, OSError): + pass # Not available on all platforms + + except Exception as e: + # Fall back to stderr + faulthandler.enable() + sys.stderr.write(f"Warning: Could not setup fault log file: {e}\n") + + # Install Python exception hook + sys.excepthook = self._exception_hook + + # Install threading exception hook (Python 3.8+) + if hasattr(threading, "excepthook"): + threading.excepthook = self._threading_exception_hook + + def _format_exception_detailed( + self, + exc_type: type[BaseException], + exc_value: BaseException, + exc_tb: traceback, + ) -> str: + """Format exception with detailed information.""" + lines: list[str] = [] + + # Header + lines.append("=" * 80) + lines.append("UNHANDLED EXCEPTION") + lines.append("=" * 80) + lines.append(f"Time: {datetime.now().isoformat()}") + lines.append(f"Thread: {threading.current_thread().name}") + lines.append(f"Exception Type: {exc_type.__module__}.{exc_type.__name__}") + lines.append(f"Exception Value: {exc_value}") + lines.append("") + + # Full traceback with context + lines.append("-" * 80) + lines.append("TRACEBACK (most recent call last):") + lines.append("-" * 80) + + # Extract frames for detailed info + tb_frames = traceback.extract_tb(exc_tb) + + for i, frame in enumerate(tb_frames): + lines.append("") + lines.append(f" Frame {i + 1}: {frame.filename}") + lines.append(f" Line {frame.lineno} in {frame.name}()") + lines.append(f" Code: {frame.line}") + + # Try to get local variables if enabled + if self._include_locals and exc_tb: + try: + # Navigate to the correct frame + current_tb = exc_tb + for _ in range(i): + if current_tb.tb_next: + current_tb = current_tb.tb_next + + frame_locals = current_tb.tb_frame.f_locals + if frame_locals: + lines.append(" Locals:") + for name, value in frame_locals.items(): + # Skip private/dunder and limit value length + if name.startswith("__"): + continue + try: + value_str = repr(value) + if len(value_str) > 200: + value_str = value_str[:200] + "..." + except Exception: + value_str = "" + lines.append(f" {name} = {value_str}") + except Exception: + pass + + # Standard traceback + lines.append("") + lines.append("-" * 80) + lines.append("STANDARD TRACEBACK:") + lines.append("-" * 80) + lines.append("".join(traceback.format_exception(exc_type, exc_value, exc_tb))) + + # Thread info + lines.append("-" * 80) + lines.append("ACTIVE THREADS:") + lines.append("-" * 80) + for thread in threading.enumerate(): + daemon_str = " (daemon)" if thread.daemon else "" + lines.append( + f" - {thread.name}{daemon_str}: {'alive' if thread.is_alive() else 'dead'}" + ) + + lines.append("") + lines.append("=" * 80) + + return "\n".join(lines) + + def _write_crash_log(self, content: str) -> None: + """Write crash information to log file.""" + try: + # Ensure directory exists + self._crash_log_path.parent.mkdir(parents=True, exist_ok=True) + + # Write to crash log + with open(self._crash_log_path, "w") as f: + f.write(content) + + # Also append to a history file + history_path = self._crash_log_path.with_suffix(".history.log") + with open(history_path, "a") as f: + f.write(content) + f.write("\n\n") + + except Exception as e: + # Last resort: write to stderr + sys.stderr.write(f"Failed to write crash log: {e}\n") + sys.stderr.write(content) + + def _exception_hook( + self, + exc_type: type[BaseException], + exc_value: BaseException, + exc_tb, + ) -> None: + """Handle uncaught exceptions.""" + # Don't handle keyboard interrupt + if issubclass(exc_type, KeyboardInterrupt): + self._original_excepthook(exc_type, exc_value, exc_tb) + return + + # Format detailed crash info + crash_info = self._format_exception_detailed(exc_type, exc_value, exc_tb) + + # Write to crash log + self._write_crash_log(crash_info) + + # Also log via logging if available + try: + logger = logging.getLogger("crash") + logger.critical( + "Unhandled exception - see %s for details", self._crash_log_path + ) + logger.critical(crash_info) + except Exception: + pass + + # Call original hook (prints traceback) + self._original_excepthook(exc_type, exc_value, exc_tb) + + # Force exit if configured (for systemd restart) + if self._exit_on_crash: + os._exit(1) + + def _threading_exception_hook(self, args: threading.ExceptHookArgs) -> None: + """Handle uncaught exceptions in threads.""" + # Format detailed crash info + crash_info = self._format_exception_detailed( + args.exc_type, args.exc_value, args.exc_traceback + ) + + # Add thread context + thread_info = ( + f"\nThread that crashed: {args.thread.name if args.thread else 'Unknown'}\n" + ) + crash_info = crash_info.replace( + "UNHANDLED EXCEPTION", f"UNHANDLED THREAD EXCEPTION{thread_info}" + ) + + # Write to crash log + self._write_crash_log(crash_info) + + # Log via logging + try: + logger = logging.getLogger("crash") + logger.critical("Unhandled thread exception - see %s", self._crash_log_path) + except Exception: + pass + + # Call original hook if available + if self._original_threading_excepthook: + self._original_threading_excepthook(args) + + # Force exit if configured (for systemd restart) + # Thread crashes might want different behavior + if self._exit_on_crash: + os._exit(1) + + def uninstall(self) -> None: + """Restore original exception hooks.""" + sys.excepthook = self._original_excepthook + + if self._original_threading_excepthook and hasattr(threading, "excepthook"): + threading.excepthook = self._original_threading_excepthook + + if self._fault_file: + try: + self._fault_file.close() + except Exception: + pass + + CrashHandler._installed = False + CrashHandler._instance = None + + +class LogManager: + """ + Manages application logging. + + Creates async file loggers with queue-based handlers. + Ensures proper cleanup on application exit. + """ + + _handlers: ClassVar[dict[str, AsyncFileHandler]] = {} + _initialized: ClassVar[bool] = False + _original_stdout: ClassVar[TextIO | None] = None + _original_stderr: ClassVar[TextIO | None] = None + _crash_handler: ClassVar[CrashHandler | None] = None + + @classmethod + def _ensure_initialized(cls) -> None: + """Register cleanup handler on first use.""" + if not cls._initialized: + atexit.register(cls.shutdown) + cls._initialized = True + + @classmethod + def setup( + cls, + filename: str = "logs/BlocksScreen.log", + level: int = logging.DEBUG, + fmt: str = DEFAULT_FORMAT, + capture_stdout: bool = False, + capture_stderr: bool = True, + console_output: bool = True, + console_level: int | None = None, + enable_crash_handler: bool = True, + crash_log_path: str = CRASH_LOG_PATH, + include_locals_in_crash: bool = True, + ) -> None: + """ + Setup root logger for entire application. + + Call once at startup. After this, all modules can use: + logger = logging.getLogger(__name__) + + Args: + filename: Log file path + level: Logging level for all loggers + fmt: Log format string + capture_stdout: Redirect stdout to logger + capture_stderr: Redirect stderr to logger + console_output: Also print logs to console + console_level: Console log level (defaults to same as level) + enable_crash_handler: Enable crash handler for unhandled exceptions + crash_log_path: Path to write crash logs + include_locals_in_crash: Include local variables in crash logs + """ + # Install crash handler FIRST (before anything else can fail) + if enable_crash_handler: + cls._crash_handler = CrashHandler.install( + crash_log_path=crash_log_path, + include_locals=include_locals_in_crash, + ) + + cls._ensure_initialized() + + # Store original streams before any redirection + if cls._original_stdout is None: + cls._original_stdout = sys.stdout + if cls._original_stderr is None: + cls._original_stderr = sys.stderr + + # Get root logger + root = logging.getLogger() + + # Don't add duplicate handlers + if root.handlers: + return + + root.setLevel(level) + + # Create async file handler + file_handler = AsyncFileHandler(filename) + cls._handlers["root"] = file_handler + + # Create queue handler that feeds the file handler + queue_handler = QueueHandler(file_handler.queue, fmt, level) + root.addHandler(queue_handler) + + # Add console handler + if console_output: + cls._add_console_handler(root, console_level or level, fmt) + + # Capture stdout/stderr (after console handler is set up) + if capture_stdout: + cls.redirect_stdout() + if capture_stderr: + cls.redirect_stderr() + + # Log startup + logging.info("Logging initialized - crash logs: %s", crash_log_path) + + @classmethod + def _add_console_handler(cls, logger: logging.Logger, level: int, fmt: str) -> None: + """Add a console handler that prints to original stdout.""" + # Use original stdout to avoid recursion if stdout is redirected + stream = cls._original_stdout or sys.stdout + + console_handler = logging.StreamHandler(stream) + console_handler.setLevel(level) + console_handler.setFormatter(logging.Formatter(fmt)) + + # Filter out stderr logger to avoid double printing + console_handler.addFilter(_ExcludeStreamLoggers()) + + logger.addHandler(console_handler) + + @classmethod + def get_logger( + cls, + name: str, + filename: str | None = None, + level: int = logging.INFO, + fmt: str = DEFAULT_FORMAT, + ) -> logging.Logger: + """ + Get or create a named logger with its own file output. + + Args: + name: Logger name + filename: Log file path (defaults to "logs/{name}.log") + level: Logging level + fmt: Log format string + + Returns: + Configured Logger instance + """ + cls._ensure_initialized() + + logger = logging.getLogger(name) + + # Don't add duplicate handlers + if logger.handlers: + return logger + + logger.setLevel(level) + + # Create async file handler + if filename is None: + filename = f"logs/{name}.log" + + file_handler = AsyncFileHandler(filename) + cls._handlers[name] = file_handler + + # Create queue handler that feeds the file handler + queue_handler = QueueHandler(file_handler.queue, fmt, level) + logger.addHandler(queue_handler) + + # Don't propagate to root (has its own file) + logger.propagate = False + + return logger + + @classmethod + def redirect_stdout(cls, logger_name: str = "stdout") -> None: + """ + Redirect stdout to logger. + + Captures print() statements and subprocess output. + """ + logger = logging.getLogger(logger_name) + sys.stdout = StreamToLogger(logger, logging.INFO, cls._original_stdout) + + @classmethod + def redirect_stderr(cls, logger_name: str = "stderr") -> None: + """ + Redirect stderr to logger. + + Captures X11 errors, warnings, and subprocess errors. + """ + logger = logging.getLogger(logger_name) + sys.stderr = StreamToLogger(logger, logging.WARNING, cls._original_stderr) + + @classmethod + def restore_streams(cls) -> None: + """Restore original stdout/stderr.""" + if cls._original_stdout: + sys.stdout = cls._original_stdout + if cls._original_stderr: + sys.stderr = cls._original_stderr + + @classmethod + def shutdown(cls) -> None: + """Close all handlers. Called automatically on exit.""" + # Restore original streams + cls.restore_streams() + + # Close handlers + for handler in cls._handlers.values(): + handler.close() + cls._handlers.clear() + + # Uninstall crash handler + if cls._crash_handler: + cls._crash_handler.uninstall() + cls._crash_handler = None + + +def setup_logging( + filename: str = "logs/app.log", + level: int = logging.DEBUG, + fmt: str = DEFAULT_FORMAT, + capture_stdout: bool = False, + capture_stderr: bool = True, + console_output: bool = True, + console_level: int | None = None, + enable_crash_handler: bool = True, + crash_log_path: str = CRASH_LOG_PATH, + include_locals_in_crash: bool = True, +) -> None: + """ + Setup logging for entire application. + + Call once at startup. After this, all modules can use: + import logging + logger = logging.getLogger(__name__) + + Args: + filename: Log file path + level: Logging level + fmt: Log format string + capture_stdout: Redirect stdout (print statements) to logger + capture_stderr: Redirect stderr (X11 errors, warnings) to logger + console_output: Also print logs to console/terminal + console_level: Console log level (defaults to same as level) + enable_crash_handler: Enable crash handler for unhandled exceptions + crash_log_path: Path to write crash logs + include_locals_in_crash: Include local variables in crash logs + """ + LogManager.setup( + filename, + level, + fmt, + capture_stdout, + capture_stderr, + console_output, + console_level, + enable_crash_handler, + crash_log_path, + include_locals_in_crash, + ) + + +def get_logger( + name: str, + filename: str | None = None, + level: int = logging.INFO, + fmt: str = DEFAULT_FORMAT, +) -> logging.Logger: + """ + Get or create a logger with its own file output. + + Args: + name: Logger name + filename: Log file path (defaults to "logs/{name}.log") + level: Logging level + fmt: Log format string + + Returns: + Configured Logger instance + """ + return LogManager.get_logger(name, filename, level, fmt) + + +def install_crash_handler( + crash_log_path: str = CRASH_LOG_PATH, + fault_log_path: str = FAULT_LOG_PATH, + include_locals: bool = True, + exit_on_crash: bool = True, +) -> CrashHandler: + """ + Install crash handler without full logging setup. -global MainLoggingHandler + Use this if you want crash handling before logging is configured. + Call at the very beginning of your main.py. + Args: + crash_log_path: Path to write Python exception logs + fault_log_path: Path to write C-level fault logs + include_locals: Include local variables in traceback + exit_on_crash: Force process exit after logging crash (for systemd restart) -def create_logger( - name: str = "log", - level=logging.INFO, - format: str = "'[%(levelname)s] | %(asctime)s | %(name)s | %(relativeCreated)6d | %(threadName)s : %(message)s", -): - """Create amd return logger""" - global MainLoggingHandler - logger = logging.getLogger(name) - logger.setLevel(level) - ql = QueueListener(filename=name) - MainLoggingHandler = QueueHandler(ql.queue, format, level) - logger.addHandler(MainLoggingHandler) - return ql + Returns: + CrashHandler instance + """ + return CrashHandler.install( + crash_log_path, fault_log_path, include_locals, exit_on_crash + ) From 9b9d5fe0f2ad42e254171b7d061977ae0fedf0af Mon Sep 17 00:00:00 2001 From: HugoCLSC Date: Tue, 3 Mar 2026 12:16:16 +0000 Subject: [PATCH 51/70] Revert "Refactor the Logging System (#172)" This reverts commit cc33f7b50c896e5586cbc16ff8cd5d810e9c2e51. --- BlocksScreen/BlocksScreen.py | 21 +- BlocksScreen/lib/moonrakerComm.py | 2 +- BlocksScreen/lib/network.py | 2 +- BlocksScreen/lib/panels/mainWindow.py | 28 +- BlocksScreen/lib/panels/networkWindow.py | 2 +- BlocksScreen/lib/panels/printTab.py | 2 +- BlocksScreen/lib/panels/widgets/filesPage.py | 5 +- .../lib/panels/widgets/jobStatusPage.py | 2 +- BlocksScreen/lib/printer.py | 2 +- BlocksScreen/logger.py | 862 ++---------------- 10 files changed, 88 insertions(+), 840 deletions(-) diff --git a/BlocksScreen/BlocksScreen.py b/BlocksScreen/BlocksScreen.py index 3ccceb74..a7a2098e 100644 --- a/BlocksScreen/BlocksScreen.py +++ b/BlocksScreen/BlocksScreen.py @@ -2,10 +2,11 @@ import sys import typing +import logger from lib.panels.mainWindow import MainWindow -from logger import setup_logging from PyQt6 import QtCore, QtGui, QtWidgets +_logger = logging.getLogger(name="logs/BlocksScreen.log") QtGui.QGuiApplication.setAttribute( QtCore.Qt.ApplicationAttribute.AA_SynthesizeMouseForUnhandledTouchEvents, True, @@ -21,6 +22,13 @@ RESET = "\033[0m" +def setup_app_loggers(): + """Setup logger""" + _ = logger.create_logger(name="logs/BlocksScreen.log", level=logging.DEBUG) + _logger = logging.getLogger(name="logs/BlocksScreen.log") + _logger.info("============ BlocksScreen Initializing ============") + + def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): """Show splash screen on app initialization""" logo = QtGui.QPixmap("BlocksScreen/BlocksScreen/lib/ui/resources/logoblocks.png") @@ -31,16 +39,7 @@ def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): if __name__ == "__main__": - setup_logging( - filename="logs/BlocksScreen.log", - level=logging.DEBUG, # File gets DEBUG+ - console_output=True, # Print to terminal - console_level=logging.DEBUG, # Console gets DEBUG+ - capture_stderr=True, # Capture X11 errors - capture_stdout=False, # Don't capture print() - ) - _logger = logging.getLogger(__name__) - _logger.info("============ BlocksScreen Initializing ============") + setup_app_loggers() BlocksScreen = QtWidgets.QApplication([]) BlocksScreen.setApplicationName("BlocksScreen") BlocksScreen.setApplicationDisplayName("BlocksScreen") diff --git a/BlocksScreen/lib/moonrakerComm.py b/BlocksScreen/lib/moonrakerComm.py index bf3a74cc..ba298ba7 100644 --- a/BlocksScreen/lib/moonrakerComm.py +++ b/BlocksScreen/lib/moonrakerComm.py @@ -14,7 +14,7 @@ from lib.utils.RepeatedTimer import RepeatedTimer from PyQt6 import QtCore, QtWidgets -_logger = logging.getLogger(__name__) +_logger = logging.getLogger(name="logs/BlocksScreen.log") class OneShotTokenError(Exception): diff --git a/BlocksScreen/lib/network.py b/BlocksScreen/lib/network.py index f6cadaa5..61ea4078 100644 --- a/BlocksScreen/lib/network.py +++ b/BlocksScreen/lib/network.py @@ -9,7 +9,7 @@ from PyQt6 import QtCore from sdbus_async import networkmanager as dbusNm -logger = logging.getLogger(__name__) +logger = logging.getLogger("logs/BlocksScreen.log") class NetworkManagerRescanError(Exception): diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 5d75e248..66f792de 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -9,16 +9,17 @@ from lib.moonrakerComm import MoonWebSocket from lib.panels.controlTab import ControlTab from lib.panels.filamentTab import FilamentTab +from lib.panels.networkWindow import NetworkControlWindow from lib.panels.printTab import PrintTab from lib.panels.utilitiesTab import UtilitiesTab -from lib.panels.widgets.basePopup import BasePopup from lib.panels.widgets.connectionPage import ConnectionPage -from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.panels.widgets.cancelPage import CancelPage from lib.panels.widgets.popupDialogWidget import Popup -from lib.panels.widgets.updatePage import UpdatePage from lib.printer import Printer from lib.ui.mainWindow_ui import Ui_MainWindow # With header +from lib.panels.widgets.updatePage import UpdatePage +from lib.panels.widgets.basePopup import BasePopup +from lib.panels.widgets.loadWidget import LoadingOverlayWidget # from lib.ui.mainWindow_v2_ui import Ui_MainWindow # No header from lib.ui.resources.background_resources_rc import * @@ -28,11 +29,10 @@ from lib.ui.resources.main_menu_resources_rc import * from lib.ui.resources.system_resources_rc import * from lib.ui.resources.top_bar_resources_rc import * -from logger import LogManager from PyQt6 import QtCore, QtGui, QtWidgets from screensaver import ScreenSaver -_logger = logging.getLogger(__name__) +_logger = logging.getLogger(name="logs/BlocksScreen.log") def api_handler(func): @@ -99,7 +99,7 @@ def __init__(self): self.filamentPanel = FilamentTab(self.ui.filamentTab, self.printer, self.ws) self.controlPanel = ControlTab(self.ui.controlTab, self.ws, self.printer) self.utilitiesPanel = UtilitiesTab(self.ui.utilitiesTab, self.ws, self.printer) - # self.networkPanel = NetworkControlWindow(self) + self.networkPanel = NetworkControlWindow(self) self.bo_ws_startup.connect(slot=self.bo_start_websocket_connection) self.ws.connecting_signal.connect(self.conn_window.on_websocket_connecting) self.ws.connected_signal.connect( @@ -161,7 +161,7 @@ def __init__(self): self.run_gcode_signal.connect(self.ws.api.run_gcode) self.ui.main_content_widget.currentChanged.connect(slot=self.reset_tab_indexes) - # self.call_network_panel.connect(self.networkPanel.show_network_panel) + self.call_network_panel.connect(self.networkPanel.show_network_panel) self.conn_window.wifi_button_clicked.connect(self.call_network_panel.emit) self.ui.wifi_button.clicked.connect(self.call_network_panel.emit) self.handle_error_response.connect( @@ -389,7 +389,7 @@ def reset_tab_indexes(self): self.filamentPanel.setCurrentIndex(0) self.controlPanel.setCurrentIndex(0) self.utilitiesPanel.setCurrentIndex(0) - # self.networkPanel.setCurrentIndex(0) + self.networkPanel.setCurrentIndex(0) def current_panel_index(self) -> int: """Helper function to get the index of the current page in the current tab @@ -724,10 +724,14 @@ def set_header_nozzle_diameter(self, diam: str): def closeEvent(self, a0: typing.Optional[QtGui.QCloseEvent]) -> None: """Handles GUI closing""" - - # Shutdown logger (closes files, stops threads, restores streams) - LogManager.shutdown() - + _loggers = [ + logging.getLogger(name) for name in logging.root.manager.loggerDict + ] # Get available logger handlers + for logger in _loggers: # noqa: F402 + if hasattr(logger, "cancel"): + _callback = getattr(logger, "cancel") + if callable(_callback): + _callback() self.ws.wb_disconnect() self.close() if a0 is None: diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 37f61138..19574cf5 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -25,7 +25,7 @@ from PyQt6 import QtCore, QtGui, QtWidgets from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal -logger = logging.getLogger(__name__) +logger = logging.getLogger("logs/BlocksScreen.log") LOAD_TIMEOUT_MS = 30_000 diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index 8a0cb653..b28e52bd 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -20,7 +20,7 @@ from lib.utils.display_button import DisplayButton from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger(__name__) +logger = logging.getLogger(name="logs/BlocksScreen.log") class PrintTab(QtWidgets.QStackedWidget): diff --git a/BlocksScreen/lib/panels/widgets/filesPage.py b/BlocksScreen/lib/panels/widgets/filesPage.py index 77959e83..f8fa490f 100644 --- a/BlocksScreen/lib/panels/widgets/filesPage.py +++ b/BlocksScreen/lib/panels/widgets/filesPage.py @@ -5,10 +5,11 @@ import helper_methods from lib.utils.blocks_Scrollbar import CustomScrollBar from lib.utils.icon_button import IconButton -from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger(__name__) +from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem + +logger = logging.getLogger("logs/BlocksScreen.log") class FilesPage(QtWidgets.QWidget): diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index 67add6b9..f868c222 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -10,7 +10,7 @@ from lib.utils.display_button import DisplayButton from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger(__name__) +logger = logging.getLogger("logs/BlocksScreen.log") class JobStatusWidget(QtWidgets.QWidget): diff --git a/BlocksScreen/lib/printer.py b/BlocksScreen/lib/printer.py index fd33f172..5889c19d 100644 --- a/BlocksScreen/lib/printer.py +++ b/BlocksScreen/lib/printer.py @@ -7,7 +7,7 @@ from lib.moonrakerComm import MoonWebSocket from PyQt6 import QtCore, QtWidgets -logger = logging.getLogger(__name__) +logger = logging.getLogger(name="logs/BlocksScreen.logs") class Printer(QtCore.QObject): diff --git a/BlocksScreen/logger.py b/BlocksScreen/logger.py index b4680ff1..f63631e5 100644 --- a/BlocksScreen/logger.py +++ b/BlocksScreen/logger.py @@ -1,851 +1,95 @@ -from __future__ import annotations - -import atexit import copy -import faulthandler import logging +import logging.config import logging.handlers -import os import pathlib import queue -import sys import threading -import traceback -from datetime import datetime -from typing import ClassVar, TextIO - -DEFAULT_FORMAT = ( - "[%(levelname)s] | %(asctime)s | %(name)s | " - "%(relativeCreated)6d | %(threadName)s : %(message)s" -) - -CRASH_LOG_PATH = "logs/blocksscreen_crash.log" -FAULT_LOG_PATH = "logs/blocksscreen_fault.log" - - -class StreamToLogger(TextIO): - """ - Redirects a stream (stdout/stderr) to a logger. - - Useful for capturing output from subprocesses, X11, or print statements. - """ - - def __init__( - self, - logger: logging.Logger, - level: int = logging.INFO, - original_stream: TextIO | None = None, - ) -> None: - self._logger = logger - self._level = level - self._original = original_stream - self._buffer = "" - - def write(self, message: str) -> int: - """Write message to logger.""" - if message: - if self._original: - try: - self._original.write(message) - self._original.flush() - except Exception: - pass - - self._buffer += message - - while "\n" in self._buffer: - line, self._buffer = self._buffer.split("\n", 1) - if line.strip(): - self._logger.log(self._level, line.rstrip()) - - return len(message) - - def flush(self) -> None: - """Flush remaining buffer.""" - if self._buffer.strip(): - self._logger.log(self._level, self._buffer.rstrip()) - self._buffer = "" - - if self._original: - try: - self._original.flush() - except Exception: - pass - - def fileno(self) -> int: - """Return file descriptor for compatibility.""" - if self._original: - return self._original.fileno() - raise OSError("No file descriptor available") - - def isatty(self) -> bool: - """Check if stream is a TTY.""" - if self._original: - return self._original.isatty() - return False - - # Required for TextIO interface - def read(self, n: int = -1) -> str: - return "" - - def readline(self, limit: int = -1) -> str: - return "" - - def readlines(self, hint: int = -1) -> list[str]: - return [] - - def seek(self, offset: int, whence: int = 0) -> int: - return 0 - - def tell(self) -> int: - return 0 - - def truncate(self, size: int | None = None) -> int: - return 0 - - def writable(self) -> bool: - return True - - def readable(self) -> bool: - return False - - def seekable(self) -> bool: - return False - - def close(self) -> None: - self.flush() - - @property - def closed(self) -> bool: - return False - - def __enter__(self) -> "StreamToLogger": - return self - - def __exit__(self, *args) -> None: - self.close() class QueueHandler(logging.Handler): - """ - Logging handler that sends records to a queue. - - Records are formatted before being placed on the queue, - then consumed by a QueueListener in a background thread. - """ + """Handler that sends events to a queue""" def __init__( self, - log_queue: queue.Queue, - fmt: str = DEFAULT_FORMAT, - level: int = logging.DEBUG, - ) -> None: - super().__init__(level) - self._queue = log_queue - self.setFormatter(logging.Formatter(fmt)) - - def emit(self, record: logging.LogRecord) -> None: - """Format and queue the log record.""" + queue: queue.Queue, + format: str = "'[%(levelname)s] | %(asctime)s | %(name)s | %(relativeCreated)6d | %(threadName)s : %(message)s", + level=logging.DEBUG, + ): + super(QueueHandler, self).__init__() + self.log_queue = queue + self.setFormatter(logging.Formatter(format, validate=True)) + self.setLevel(level) + + def emit(self, record): + """Emit logging record""" try: - # Format the message msg = self.format(record) - - # Copy record and update message record = copy.copy(record) - record.msg = msg - record.args = None # Already formatted record.message = msg - - self._queue.put_nowait(record) + record.name = record.name + record.msg = msg + self.log_queue.put_nowait(record) except Exception: self.handleError(record) + def setFormatter(self, fmt: logging.Formatter | None) -> None: + """Set logging formatter""" + return super().setFormatter(fmt) -class AsyncFileHandler(logging.handlers.TimedRotatingFileHandler): - """ - Async file handler using a background thread. - - Wraps TimedRotatingFileHandler with a queue and worker thread - for non-blocking log writes. Automatically recreates log file - if deleted during runtime. - """ - def __init__( - self, - filename: str, - when: str = "midnight", - backup_count: int = 10, - encoding: str = "utf-8", - ) -> None: - self._log_path = pathlib.Path(filename) - - # Create log directory if needed - if self._log_path.parent != pathlib.Path("."): - self._log_path.parent.mkdir(parents=True, exist_ok=True) +class QueueListener(logging.handlers.TimedRotatingFileHandler): + """Threaded listener watching for log records on the queue handler queue, passes them for processing""" - super().__init__( + def __init__(self, filename, encoding="utf-8"): + log_path = pathlib.Path(filename) + if log_path.parent != pathlib.Path("."): + log_path.parent.mkdir(parents=True, exist_ok=True) + super(QueueListener, self).__init__( filename=filename, - when=when, - backupCount=backup_count, + when="MIDNIGHT", + backupCount=10, encoding=encoding, delay=True, ) - - self._queue: queue.Queue[logging.LogRecord | None] = queue.Queue() - self._stop_event = threading.Event() - self._lock = threading.Lock() + self.queue = queue.Queue() self._thread = threading.Thread( - name=f"logger-{self._log_path.stem}", - target=self._worker, - daemon=True, + name=f"log.{filename}", target=self._run, daemon=True ) self._thread.start() - def _ensure_file_exists(self) -> None: - """Ensure log file and directory exist, recreate if deleted.""" - try: - # Check if directory exists - if not self._log_path.parent.exists(): - self._log_path.parent.mkdir(parents=True, exist_ok=True) - - # Check if file was deleted (stream is open but file gone) - if self.stream is not None and not self._log_path.exists(): - # Close old stream - try: - self.stream.close() - except Exception: - pass - self.stream = None - - # Reopen stream if needed - if self.stream is None: - self.stream = self._open() - - except Exception: - pass - - def emit(self, record: logging.LogRecord) -> None: - """Emit a record with file existence check.""" - with self._lock: - self._ensure_file_exists() - super().emit(record) - - def _worker(self) -> None: - """Background worker that processes queued log records.""" - while not self._stop_event.is_set(): + def _run(self): + while True: try: - record = self._queue.get(timeout=0.5) + record = self.queue.get(True) if record is None: break self.handle(record) except queue.Empty: - continue - except Exception: - # Don't crash the worker thread - pass + break - @property - def queue(self) -> queue.Queue: - """Get the log queue for QueueHandler.""" - return self._queue - - def close(self) -> None: - """Stop worker thread and close file handler.""" - if self._thread is None or not self._thread.is_alive(): - super().close() + def close(self): + """Close logger listener""" + if self._thread is None: return - - # Signal worker to stop - self._stop_event.set() - self._queue.put_nowait(None) - - # Wait for worker to finish - self._thread.join(timeout=2.0) + self.queue.put_nowait(None) + self._thread.join() self._thread = None - # Close the file handler - super().close() - - -class _ExcludeStreamLoggers(logging.Filter): - """Filter to exclude stdout/stderr loggers from console output.""" - - def filter(self, record: logging.LogRecord) -> bool: - # Exclude to avoid double printing (already goes to console via StreamToLogger) - return record.name not in ("stdout", "stderr") - - -class CrashHandler: - """ - Handles unhandled exceptions and C-level crashes. - - Writes detailed crash information to log files including: - - Full traceback with line numbers - - Local variables at each frame - - Thread information - - Timestamp - """ - - _instance: ClassVar[CrashHandler | None] = None - _installed: ClassVar[bool] = False - - def __init__( - self, - crash_log_path: str = CRASH_LOG_PATH, - fault_log_path: str = FAULT_LOG_PATH, - include_locals: bool = True, - exit_on_crash: bool = True, - ) -> None: - self._crash_log_path = pathlib.Path(crash_log_path) - self._fault_log_path = pathlib.Path(fault_log_path) - self._include_locals = include_locals - self._exit_on_crash = exit_on_crash - self._original_excepthook = sys.excepthook - self._original_threading_excepthook = getattr(threading, "excepthook", None) - self._fault_file: TextIO | None = None - - @classmethod - def install( - cls, - crash_log_path: str = CRASH_LOG_PATH, - fault_log_path: str = FAULT_LOG_PATH, - include_locals: bool = True, - exit_on_crash: bool = True, - ) -> CrashHandler: - """ - Install the crash handler. - - Should be called as early as possible in the application startup. - - Args: - crash_log_path: Path to write Python exception logs - fault_log_path: Path to write C-level fault logs (segfaults) - include_locals: Include local variables in traceback - exit_on_crash: Force exit after logging (for systemd restart) - - Returns: - The CrashHandler instance - """ - if cls._installed and cls._instance: - return cls._instance - - handler = cls(crash_log_path, fault_log_path, include_locals, exit_on_crash) - handler._install() - cls._instance = handler - cls._installed = True - - return handler - - def _install(self) -> None: - """Install exception hooks.""" - # Setup faulthandler for C-level crashes (segfaults, etc.) - try: - self._fault_file = open(self._fault_log_path, "w") - faulthandler.enable(file=self._fault_file, all_threads=True) - - # Also dump traceback on SIGUSR1 (useful for debugging hangs) - try: - import signal - - faulthandler.register( - signal.SIGUSR1, - file=self._fault_file, - all_threads=True, - ) - except (AttributeError, OSError): - pass # Not available on all platforms - - except Exception as e: - # Fall back to stderr - faulthandler.enable() - sys.stderr.write(f"Warning: Could not setup fault log file: {e}\n") - - # Install Python exception hook - sys.excepthook = self._exception_hook - - # Install threading exception hook (Python 3.8+) - if hasattr(threading, "excepthook"): - threading.excepthook = self._threading_exception_hook - - def _format_exception_detailed( - self, - exc_type: type[BaseException], - exc_value: BaseException, - exc_tb: traceback, - ) -> str: - """Format exception with detailed information.""" - lines: list[str] = [] - - # Header - lines.append("=" * 80) - lines.append("UNHANDLED EXCEPTION") - lines.append("=" * 80) - lines.append(f"Time: {datetime.now().isoformat()}") - lines.append(f"Thread: {threading.current_thread().name}") - lines.append(f"Exception Type: {exc_type.__module__}.{exc_type.__name__}") - lines.append(f"Exception Value: {exc_value}") - lines.append("") - - # Full traceback with context - lines.append("-" * 80) - lines.append("TRACEBACK (most recent call last):") - lines.append("-" * 80) - - # Extract frames for detailed info - tb_frames = traceback.extract_tb(exc_tb) - - for i, frame in enumerate(tb_frames): - lines.append("") - lines.append(f" Frame {i + 1}: {frame.filename}") - lines.append(f" Line {frame.lineno} in {frame.name}()") - lines.append(f" Code: {frame.line}") - - # Try to get local variables if enabled - if self._include_locals and exc_tb: - try: - # Navigate to the correct frame - current_tb = exc_tb - for _ in range(i): - if current_tb.tb_next: - current_tb = current_tb.tb_next - - frame_locals = current_tb.tb_frame.f_locals - if frame_locals: - lines.append(" Locals:") - for name, value in frame_locals.items(): - # Skip private/dunder and limit value length - if name.startswith("__"): - continue - try: - value_str = repr(value) - if len(value_str) > 200: - value_str = value_str[:200] + "..." - except Exception: - value_str = "" - lines.append(f" {name} = {value_str}") - except Exception: - pass - - # Standard traceback - lines.append("") - lines.append("-" * 80) - lines.append("STANDARD TRACEBACK:") - lines.append("-" * 80) - lines.append("".join(traceback.format_exception(exc_type, exc_value, exc_tb))) - - # Thread info - lines.append("-" * 80) - lines.append("ACTIVE THREADS:") - lines.append("-" * 80) - for thread in threading.enumerate(): - daemon_str = " (daemon)" if thread.daemon else "" - lines.append( - f" - {thread.name}{daemon_str}: {'alive' if thread.is_alive() else 'dead'}" - ) - - lines.append("") - lines.append("=" * 80) - - return "\n".join(lines) - - def _write_crash_log(self, content: str) -> None: - """Write crash information to log file.""" - try: - # Ensure directory exists - self._crash_log_path.parent.mkdir(parents=True, exist_ok=True) - - # Write to crash log - with open(self._crash_log_path, "w") as f: - f.write(content) - - # Also append to a history file - history_path = self._crash_log_path.with_suffix(".history.log") - with open(history_path, "a") as f: - f.write(content) - f.write("\n\n") - - except Exception as e: - # Last resort: write to stderr - sys.stderr.write(f"Failed to write crash log: {e}\n") - sys.stderr.write(content) - - def _exception_hook( - self, - exc_type: type[BaseException], - exc_value: BaseException, - exc_tb, - ) -> None: - """Handle uncaught exceptions.""" - # Don't handle keyboard interrupt - if issubclass(exc_type, KeyboardInterrupt): - self._original_excepthook(exc_type, exc_value, exc_tb) - return - - # Format detailed crash info - crash_info = self._format_exception_detailed(exc_type, exc_value, exc_tb) - - # Write to crash log - self._write_crash_log(crash_info) - - # Also log via logging if available - try: - logger = logging.getLogger("crash") - logger.critical( - "Unhandled exception - see %s for details", self._crash_log_path - ) - logger.critical(crash_info) - except Exception: - pass - - # Call original hook (prints traceback) - self._original_excepthook(exc_type, exc_value, exc_tb) - - # Force exit if configured (for systemd restart) - if self._exit_on_crash: - os._exit(1) - - def _threading_exception_hook(self, args: threading.ExceptHookArgs) -> None: - """Handle uncaught exceptions in threads.""" - # Format detailed crash info - crash_info = self._format_exception_detailed( - args.exc_type, args.exc_value, args.exc_traceback - ) - - # Add thread context - thread_info = ( - f"\nThread that crashed: {args.thread.name if args.thread else 'Unknown'}\n" - ) - crash_info = crash_info.replace( - "UNHANDLED EXCEPTION", f"UNHANDLED THREAD EXCEPTION{thread_info}" - ) - - # Write to crash log - self._write_crash_log(crash_info) - - # Log via logging - try: - logger = logging.getLogger("crash") - logger.critical("Unhandled thread exception - see %s", self._crash_log_path) - except Exception: - pass - - # Call original hook if available - if self._original_threading_excepthook: - self._original_threading_excepthook(args) - - # Force exit if configured (for systemd restart) - # Thread crashes might want different behavior - if self._exit_on_crash: - os._exit(1) - - def uninstall(self) -> None: - """Restore original exception hooks.""" - sys.excepthook = self._original_excepthook - - if self._original_threading_excepthook and hasattr(threading, "excepthook"): - threading.excepthook = self._original_threading_excepthook - - if self._fault_file: - try: - self._fault_file.close() - except Exception: - pass - - CrashHandler._installed = False - CrashHandler._instance = None - - -class LogManager: - """ - Manages application logging. - - Creates async file loggers with queue-based handlers. - Ensures proper cleanup on application exit. - """ - - _handlers: ClassVar[dict[str, AsyncFileHandler]] = {} - _initialized: ClassVar[bool] = False - _original_stdout: ClassVar[TextIO | None] = None - _original_stderr: ClassVar[TextIO | None] = None - _crash_handler: ClassVar[CrashHandler | None] = None - - @classmethod - def _ensure_initialized(cls) -> None: - """Register cleanup handler on first use.""" - if not cls._initialized: - atexit.register(cls.shutdown) - cls._initialized = True - - @classmethod - def setup( - cls, - filename: str = "logs/BlocksScreen.log", - level: int = logging.DEBUG, - fmt: str = DEFAULT_FORMAT, - capture_stdout: bool = False, - capture_stderr: bool = True, - console_output: bool = True, - console_level: int | None = None, - enable_crash_handler: bool = True, - crash_log_path: str = CRASH_LOG_PATH, - include_locals_in_crash: bool = True, - ) -> None: - """ - Setup root logger for entire application. - - Call once at startup. After this, all modules can use: - logger = logging.getLogger(__name__) - - Args: - filename: Log file path - level: Logging level for all loggers - fmt: Log format string - capture_stdout: Redirect stdout to logger - capture_stderr: Redirect stderr to logger - console_output: Also print logs to console - console_level: Console log level (defaults to same as level) - enable_crash_handler: Enable crash handler for unhandled exceptions - crash_log_path: Path to write crash logs - include_locals_in_crash: Include local variables in crash logs - """ - # Install crash handler FIRST (before anything else can fail) - if enable_crash_handler: - cls._crash_handler = CrashHandler.install( - crash_log_path=crash_log_path, - include_locals=include_locals_in_crash, - ) - - cls._ensure_initialized() - - # Store original streams before any redirection - if cls._original_stdout is None: - cls._original_stdout = sys.stdout - if cls._original_stderr is None: - cls._original_stderr = sys.stderr - - # Get root logger - root = logging.getLogger() - - # Don't add duplicate handlers - if root.handlers: - return - - root.setLevel(level) - - # Create async file handler - file_handler = AsyncFileHandler(filename) - cls._handlers["root"] = file_handler - - # Create queue handler that feeds the file handler - queue_handler = QueueHandler(file_handler.queue, fmt, level) - root.addHandler(queue_handler) - - # Add console handler - if console_output: - cls._add_console_handler(root, console_level or level, fmt) - - # Capture stdout/stderr (after console handler is set up) - if capture_stdout: - cls.redirect_stdout() - if capture_stderr: - cls.redirect_stderr() - - # Log startup - logging.info("Logging initialized - crash logs: %s", crash_log_path) - - @classmethod - def _add_console_handler(cls, logger: logging.Logger, level: int, fmt: str) -> None: - """Add a console handler that prints to original stdout.""" - # Use original stdout to avoid recursion if stdout is redirected - stream = cls._original_stdout or sys.stdout - - console_handler = logging.StreamHandler(stream) - console_handler.setLevel(level) - console_handler.setFormatter(logging.Formatter(fmt)) - - # Filter out stderr logger to avoid double printing - console_handler.addFilter(_ExcludeStreamLoggers()) - - logger.addHandler(console_handler) - - @classmethod - def get_logger( - cls, - name: str, - filename: str | None = None, - level: int = logging.INFO, - fmt: str = DEFAULT_FORMAT, - ) -> logging.Logger: - """ - Get or create a named logger with its own file output. - - Args: - name: Logger name - filename: Log file path (defaults to "logs/{name}.log") - level: Logging level - fmt: Log format string - - Returns: - Configured Logger instance - """ - cls._ensure_initialized() - - logger = logging.getLogger(name) - - # Don't add duplicate handlers - if logger.handlers: - return logger - - logger.setLevel(level) - - # Create async file handler - if filename is None: - filename = f"logs/{name}.log" - - file_handler = AsyncFileHandler(filename) - cls._handlers[name] = file_handler - - # Create queue handler that feeds the file handler - queue_handler = QueueHandler(file_handler.queue, fmt, level) - logger.addHandler(queue_handler) - - # Don't propagate to root (has its own file) - logger.propagate = False - - return logger - - @classmethod - def redirect_stdout(cls, logger_name: str = "stdout") -> None: - """ - Redirect stdout to logger. - - Captures print() statements and subprocess output. - """ - logger = logging.getLogger(logger_name) - sys.stdout = StreamToLogger(logger, logging.INFO, cls._original_stdout) - - @classmethod - def redirect_stderr(cls, logger_name: str = "stderr") -> None: - """ - Redirect stderr to logger. - - Captures X11 errors, warnings, and subprocess errors. - """ - logger = logging.getLogger(logger_name) - sys.stderr = StreamToLogger(logger, logging.WARNING, cls._original_stderr) - - @classmethod - def restore_streams(cls) -> None: - """Restore original stdout/stderr.""" - if cls._original_stdout: - sys.stdout = cls._original_stdout - if cls._original_stderr: - sys.stderr = cls._original_stderr - - @classmethod - def shutdown(cls) -> None: - """Close all handlers. Called automatically on exit.""" - # Restore original streams - cls.restore_streams() - - # Close handlers - for handler in cls._handlers.values(): - handler.close() - cls._handlers.clear() - - # Uninstall crash handler - if cls._crash_handler: - cls._crash_handler.uninstall() - cls._crash_handler = None - - -def setup_logging( - filename: str = "logs/app.log", - level: int = logging.DEBUG, - fmt: str = DEFAULT_FORMAT, - capture_stdout: bool = False, - capture_stderr: bool = True, - console_output: bool = True, - console_level: int | None = None, - enable_crash_handler: bool = True, - crash_log_path: str = CRASH_LOG_PATH, - include_locals_in_crash: bool = True, -) -> None: - """ - Setup logging for entire application. - - Call once at startup. After this, all modules can use: - import logging - logger = logging.getLogger(__name__) - - Args: - filename: Log file path - level: Logging level - fmt: Log format string - capture_stdout: Redirect stdout (print statements) to logger - capture_stderr: Redirect stderr (X11 errors, warnings) to logger - console_output: Also print logs to console/terminal - console_level: Console log level (defaults to same as level) - enable_crash_handler: Enable crash handler for unhandled exceptions - crash_log_path: Path to write crash logs - include_locals_in_crash: Include local variables in crash logs - """ - LogManager.setup( - filename, - level, - fmt, - capture_stdout, - capture_stderr, - console_output, - console_level, - enable_crash_handler, - crash_log_path, - include_locals_in_crash, - ) - - -def get_logger( - name: str, - filename: str | None = None, - level: int = logging.INFO, - fmt: str = DEFAULT_FORMAT, -) -> logging.Logger: - """ - Get or create a logger with its own file output. - - Args: - name: Logger name - filename: Log file path (defaults to "logs/{name}.log") - level: Logging level - fmt: Log format string - - Returns: - Configured Logger instance - """ - return LogManager.get_logger(name, filename, level, fmt) - - -def install_crash_handler( - crash_log_path: str = CRASH_LOG_PATH, - fault_log_path: str = FAULT_LOG_PATH, - include_locals: bool = True, - exit_on_crash: bool = True, -) -> CrashHandler: - """ - Install crash handler without full logging setup. - Use this if you want crash handling before logging is configured. - Call at the very beginning of your main.py. +global MainLoggingHandler - Args: - crash_log_path: Path to write Python exception logs - fault_log_path: Path to write C-level fault logs - include_locals: Include local variables in traceback - exit_on_crash: Force process exit after logging crash (for systemd restart) - Returns: - CrashHandler instance - """ - return CrashHandler.install( - crash_log_path, fault_log_path, include_locals, exit_on_crash - ) +def create_logger( + name: str = "log", + level=logging.INFO, + format: str = "'[%(levelname)s] | %(asctime)s | %(name)s | %(relativeCreated)6d | %(threadName)s : %(message)s", +): + """Create amd return logger""" + global MainLoggingHandler + logger = logging.getLogger(name) + logger.setLevel(level) + ql = QueueListener(filename=name) + MainLoggingHandler = QueueHandler(ql.queue, format, level) + logger.addHandler(MainLoggingHandler) + return ql From 66ef05dd3612df36aadc443c7a6037333b57b21b Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Tue, 3 Mar 2026 13:46:07 +0000 Subject: [PATCH 52/70] Refactor the File Management System (#171) * refactor files list behaviour, added USB icons and handling when the current_dir is deleted * bugfix: generation of new .code files and handling of new files introduced, in case of not loading the thumbnail of a file always shows the blocksthumbnail file * fix bug about the scrollbar not showing when screen started on the filespage * fix: correct USB dir detection, logger name, time unit, and error coupling - files.py: replace fragile single-string USB preload tracker with a FIFO deque, fixing "No files found" shown on USB-prefixed dir creation; replace os.path with pathlib.Path - filesPage.py: fix _format_print_time returning label as seconds instead of minutes for durations under 60s; fix logger using file path as name instead of __name__; clarify retry debug log labels - mainWindow.py: replace back_btn.click() with on_directory_error() to avoid reaching through abstraction layers into nested widget internals --------- Co-authored-by: Guilherme Costa --- BlocksScreen/lib/files.py | 795 ++++++++-- BlocksScreen/lib/panels/mainWindow.py | 42 +- BlocksScreen/lib/panels/printTab.py | 15 + .../lib/panels/widgets/confirmPage.py | 17 +- BlocksScreen/lib/panels/widgets/filesPage.py | 1290 +++++++++++++---- .../lib/ui/resources/icon_resources.qrc | 1 + .../lib/ui/resources/icon_resources_rc.py | 220 ++- .../ui/resources/media/btn_icons/usb_icon.svg | 13 + BlocksScreen/lib/utils/list_model.py | 28 + 9 files changed, 1903 insertions(+), 518 deletions(-) create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/usb_icon.svg diff --git a/BlocksScreen/lib/files.py b/BlocksScreen/lib/files.py index 0eda561d..412f0648 100644 --- a/BlocksScreen/lib/files.py +++ b/BlocksScreen/lib/files.py @@ -1,180 +1,687 @@ -# -# Gcode File manager -# from __future__ import annotations -import os +import logging import typing +from collections import deque +from dataclasses import dataclass, field +from enum import Enum, auto +from pathlib import Path import events from events import ReceivedFileData from lib.moonrakerComm import MoonWebSocket from PyQt6 import QtCore, QtGui, QtWidgets +logger = logging.getLogger(__name__) + + +class FileAction(Enum): + """Enumeration of possible file actions from Moonraker notifications.""" + + CREATE_FILE = auto() + DELETE_FILE = auto() + MOVE_FILE = auto() + MODIFY_FILE = auto() + CREATE_DIR = auto() + DELETE_DIR = auto() + MOVE_DIR = auto() + ROOT_UPDATE = auto() + UNKNOWN = auto() + + @classmethod + def from_string(cls, action: str) -> "FileAction": + """Convert Moonraker action string to enum.""" + mapping = { + "create_file": cls.CREATE_FILE, + "delete_file": cls.DELETE_FILE, + "move_file": cls.MOVE_FILE, + "modify_file": cls.MODIFY_FILE, + "create_dir": cls.CREATE_DIR, + "delete_dir": cls.DELETE_DIR, + "move_dir": cls.MOVE_DIR, + "root_update": cls.ROOT_UPDATE, + } + return mapping.get(action.lower(), cls.UNKNOWN) + + +@dataclass +class FileMetadata: + """ + Data class for file metadata. + + Thumbnails are stored as QImage objects when available. + """ + + filename: str = "" + thumbnail_images: list[QtGui.QImage] = field(default_factory=list) + filament_total: typing.Union[dict, str, float] = field(default_factory=dict) + estimated_time: int = 0 + layer_count: int = -1 + total_layer: int = -1 + object_height: float = -1.0 + size: int = 0 + modified: float = 0.0 + filament_type: str = "Unknown" + filament_weight_total: float = -1.0 + layer_height: float = -1.0 + first_layer_height: float = -1.0 + first_layer_extruder_temp: float = -1.0 + first_layer_bed_temp: float = -1.0 + chamber_temp: float = -1.0 + filament_name: str = "Unknown" + nozzle_diameter: float = -1.0 + slicer: str = "Unknown" + slicer_version: str = "Unknown" + gcode_start_byte: int = 0 + gcode_end_byte: int = 0 + print_start_time: typing.Optional[float] = None + job_id: typing.Optional[str] = None + + def to_dict(self) -> dict: + """Convert to dictionary for signal emission.""" + return { + "filename": self.filename, + "thumbnail_images": self.thumbnail_images, + "filament_total": self.filament_total, + "estimated_time": self.estimated_time, + "layer_count": self.layer_count, + "total_layer": self.total_layer, + "object_height": self.object_height, + "size": self.size, + "modified": self.modified, + "filament_type": self.filament_type, + "filament_weight_total": self.filament_weight_total, + "layer_height": self.layer_height, + "first_layer_height": self.first_layer_height, + "first_layer_extruder_temp": self.first_layer_extruder_temp, + "first_layer_bed_temp": self.first_layer_bed_temp, + "chamber_temp": self.chamber_temp, + "filament_name": self.filament_name, + "nozzle_diameter": self.nozzle_diameter, + "slicer": self.slicer, + "slicer_version": self.slicer_version, + "gcode_start_byte": self.gcode_start_byte, + "gcode_end_byte": self.gcode_end_byte, + "print_start_time": self.print_start_time, + "job_id": self.job_id, + } + + @classmethod + def from_dict( + cls, data: dict, thumbnail_images: list[QtGui.QImage] + ) -> "FileMetadata": + """ + `Create FileMetadata from Moonraker API response.` + + All data comes directly from Moonraker - no local filesystem access. + """ + filename = data.get("filename", "") + + # Helper to safely get values with fallback + def safe_get(key: str, default: typing.Any) -> typing.Any: + value = data.get(key, default) + if value is None or value == -1.0: + return default + return value + + return cls( + filename=filename, + thumbnail_images=thumbnail_images, + filament_total=safe_get("filament_total", {}), + estimated_time=int(safe_get("estimated_time", 0)), + layer_count=safe_get("layer_count", -1), + total_layer=safe_get("total_layer", -1), + object_height=safe_get("object_height", -1.0), + size=safe_get("size", 0), + modified=safe_get("modified", 0.0), + filament_type=safe_get("filament_type", "Unknown") or "Unknown", + filament_weight_total=safe_get("filament_weight_total", -1.0), + layer_height=safe_get("layer_height", -1.0), + first_layer_height=safe_get("first_layer_height", -1.0), + first_layer_extruder_temp=safe_get("first_layer_extruder_temp", -1.0), + first_layer_bed_temp=safe_get("first_layer_bed_temp", -1.0), + chamber_temp=safe_get("chamber_temp", -1.0), + filament_name=safe_get("filament_name", "Unknown") or "Unknown", + nozzle_diameter=safe_get("nozzle_diameter", -1.0), + slicer=safe_get("slicer", "Unknown") or "Unknown", + slicer_version=safe_get("slicer_version", "Unknown") or "Unknown", + gcode_start_byte=safe_get("gcode_start_byte", 0), + gcode_end_byte=safe_get("gcode_end_byte", 0), + print_start_time=data.get("print_start_time"), + job_id=data.get("job_id"), + ) + class Files(QtCore.QObject): - request_file_list = QtCore.pyqtSignal([], [str], name="api-get-files-list") + """ + Manages gcode files with event-driven updates. + E + Signals emitted: + - on_dirs: Full directory list + - on_file_list: Full file list + - fileinfo: Single file metadata update + - file_added/removed/modified: Incremental updates + - dir_added/removed: Directory updates + - full_refresh_needed: Root changed + """ + + # Signals for API requests + request_file_list = QtCore.pyqtSignal([], [str], name="api_get_files_list") request_dir_info = QtCore.pyqtSignal( - [], [str], [str, bool], name="api-get-dir-info" - ) - request_file_metadata = QtCore.pyqtSignal([str], name="get_file_metadata") - request_files_thumbnails = QtCore.pyqtSignal([str], name="request_files_thumbnail") - request_file_download = QtCore.pyqtSignal([str, str], name="file_download") - on_dirs: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - list, name="on-dirs" - ) - on_file_list: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - list, name="on_file_list" - ) - fileinfo: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - dict, name="fileinfo" + [], [str], [str, bool], name="api_get_dir_info" ) + request_file_metadata = QtCore.pyqtSignal(str, name="get_file_metadata") - def __init__( - self, - parent: QtCore.QObject, - ws: MoonWebSocket, - update_interval: int = 5000, - ) -> None: - super(Files, self).__init__(parent) + # Signals for UI updates + on_dirs = QtCore.pyqtSignal(list, name="on_dirs") + on_file_list = QtCore.pyqtSignal(list, name="on_file_list") + fileinfo = QtCore.pyqtSignal(dict, name="fileinfo") + metadata_error = QtCore.pyqtSignal( + str, name="metadata_error" + ) # filename when metadata fails + + # Signals for incremental updates + file_added = QtCore.pyqtSignal(dict, name="file_added") + file_removed = QtCore.pyqtSignal(str, name="file_removed") + file_modified = QtCore.pyqtSignal(dict, name="file_modified") + dir_added = QtCore.pyqtSignal(dict, name="dir_added") + dir_removed = QtCore.pyqtSignal(str, name="dir_removed") + full_refresh_needed = QtCore.pyqtSignal(name="full_refresh_needed") + + # Signal for preloaded USB files + usb_files_loaded = QtCore.pyqtSignal( + str, list, name="usb_files_loaded" + ) # (usb_path, files) + GCODE_EXTENSION = ".gcode" + GCODE_PATH = "~/printer_data/gcodes" + + def __init__(self, parent: QtCore.QObject, ws: MoonWebSocket) -> None: + super().__init__(parent) self.ws = ws - self.gcode_path = os.path.expanduser("~/printer_data/gcodes") - self.files: list = [] - self.directories: list = [] - self.files_metadata: dict = {} - self.request_file_list.connect(slot=self.ws.api.get_file_list) - self.request_file_list[str].connect(slot=self.ws.api.get_file_list) - self.request_dir_info.connect(slot=self.ws.api.get_dir_information) + + # Internal state + self._files: dict[str, dict] = {} + self._directories: dict[str, dict] = {} + self._files_metadata: dict[str, FileMetadata] = {} + self._current_directory: str = "" + self._initial_load_complete: bool = False + self.gcode_path = Path(self.GCODE_PATH).expanduser() + # USB preloaded files cache: usb_path -> list of files + self._usb_files_cache: dict[str, list[dict]] = {} + # Track pending USB preload requests (ordered FIFO queue) + self._pending_usb_preloads: set[str] = set() + self._usb_preload_queue: deque[str] = deque() + + self._connect_signals() + self._install_event_filter() + + def _connect_signals(self) -> None: + """Connect internal signals to websocket API.""" + self.request_file_list.connect(self.ws.api.get_file_list) + self.request_file_list[str].connect(self.ws.api.get_file_list) + self.request_dir_info.connect(self.ws.api.get_dir_information) self.request_dir_info[str, bool].connect(self.ws.api.get_dir_information) - self.request_dir_info[str].connect(slot=self.ws.api.get_dir_information) - self.request_file_metadata.connect(slot=self.ws.api.get_gcode_metadata) - self.request_files_thumbnails.connect(slot=self.ws.api.get_gcode_thumbnail) - self.request_file_download.connect(slot=self.ws.api.download_file) - QtWidgets.QApplication.instance().installEventFilter(self) # type: ignore + self.request_dir_info[str].connect(self.ws.api.get_dir_information) + self.request_file_metadata.connect(self.ws.api.get_gcode_metadata) + + def _install_event_filter(self) -> None: + """Install event filter on application instance.""" + app = QtWidgets.QApplication.instance() + if app: + app.installEventFilter(self) + + @property + def file_list(self) -> list[dict]: + """Get list of files in current directory.""" + return list(self._files.values()) + + @property + def directories(self) -> list[dict]: + """Get list of directories in current directory.""" + return list(self._directories.values()) @property - def file_list(self): - """Available files list""" - return self.files + def current_directory(self) -> str: + """Get current directory path.""" + return self._current_directory + + @current_directory.setter + def current_directory(self, value: str) -> None: + """Set current directory path.""" + self._current_directory = value + + @property + def is_loaded(self) -> bool: + """Check if initial load is complete.""" + return self._initial_load_complete + + def get_file_metadata(self, filename: str) -> typing.Optional[FileMetadata]: + """Get cached metadata for a file.""" + return self._files_metadata.get(filename.removeprefix("/")) + + def get_file_data(self, filename: str) -> dict: + """Get cached file data dict for a file.""" + clean_name = filename.removeprefix("/") + metadata = self._files_metadata.get(clean_name) + if metadata: + return metadata.to_dict() + return {} + + def refresh_directory(self, directory: str = "") -> None: + """Force refresh of a specific directory.""" + logger.debug(f"Refreshing directory: {directory or 'root'}") + self._current_directory = directory + self.request_dir_info[str, bool].emit(directory, True) + + def initial_load(self) -> None: + """Perform initial load of file list.""" + logger.info("Performing initial file list load") + self._initial_load_complete = False + self.request_dir_info[str, bool].emit("", True) + + def handle_filelist_changed(self, data: typing.Union[dict, list]) -> None: + """Handle notify_filelist_changed from Moonraker.""" + if isinstance(data, dict) and "params" in data: + data = data.get("params", []) + + if isinstance(data, list): + if len(data) > 0: + data = data[0] + else: + return + + if not isinstance(data, dict): + return + + action_str = data.get("action", "") + action = FileAction.from_string(action_str) + item = data.get("item", {}) + source_item = data.get("source_item", {}) + + logger.debug(f"File list changed: action={action_str}, item={item}") + + handlers = { + FileAction.CREATE_FILE: self._handle_file_created, + FileAction.DELETE_FILE: self._handle_file_deleted, + FileAction.MODIFY_FILE: self._handle_file_modified, + FileAction.MOVE_FILE: self._handle_file_moved, + FileAction.CREATE_DIR: self._handle_dir_created, + FileAction.DELETE_DIR: self._handle_dir_deleted, + FileAction.MOVE_DIR: self._handle_dir_moved, + FileAction.ROOT_UPDATE: self._handle_root_update, + } + + handler = handlers.get(action) + if handler: + handler(item, source_item) + + def _handle_file_created(self, item: dict, _: dict) -> None: + """Handle new file creation.""" + path = item.get("path", "") + if not path: + return + + if self._is_usb_mount(path): + item["dirname"] = path + self._handle_dir_created(item, {}) + return + + if not path.lower().endswith(self.GCODE_EXTENSION): + return + + self._files[path] = item + self.file_added.emit(item) + + # Request metadata (will update later) + self.request_file_metadata.emit(path.removeprefix("/")) + logger.info(f"File created: {path}") + + def _handle_file_deleted(self, item: dict, _: dict) -> None: + """Handle file deletion.""" + path = item.get("path", "") + if not path: + return + + if self._is_usb_mount(path): + item["dirname"] = path + self._handle_dir_deleted(item, {}) + return + + self._files.pop(path, None) + self._files_metadata.pop(path.removeprefix("/"), None) + + self.file_removed.emit(path) + logger.info(f"File deleted: {path}") + + def _handle_file_modified(self, item: dict, _: dict) -> None: + """Handle file modification.""" + path = item.get("path", "") + if not path or not path.lower().endswith(self.GCODE_EXTENSION): + return + + self._files[path] = item + self._files_metadata.pop(path.removeprefix("/"), None) + + self.request_file_metadata.emit(path.removeprefix("/")) + self.file_modified.emit(item) + logger.info(f"File modified: {path}") + + def _handle_file_moved(self, item: dict, source_item: dict) -> None: + """Handle file move/rename.""" + old_path = source_item.get("path", "") + new_path = item.get("path", "") + + if old_path: + self._handle_file_deleted(source_item, {}) + if new_path: + self._handle_file_created(item, {}) + + def _handle_dir_created(self, item: dict, _: dict) -> None: + """Handle directory creation.""" + path = item.get("path", "") + dirname = item.get("dirname", "") + + if not dirname and path: + dirname = path.rstrip("/").split("/")[-1] + + if not dirname or dirname.startswith("."): + return + + item["dirname"] = dirname + self._directories[dirname] = item + self.dir_added.emit(item) + logger.info(f"Directory created: {dirname}") + + if self._is_usb_mount(dirname): + self._preload_usb_contents(dirname) + + def _handle_dir_deleted(self, item: dict, _: dict) -> None: + """Handle directory deletion.""" + path = item.get("path", "") + dirname = item.get("dirname", "") + + if not dirname and path: + dirname = path.rstrip("/").split("/")[-1] - def handle_message_received(self, method: str, data, params: dict) -> None: - """Handle file related messages received by moonraker""" + if not dirname: + return + + self._directories.pop(dirname, None) + + # Clear USB cache if this was a USB mount + if self._is_usb_mount(dirname): + self._usb_files_cache.pop(dirname, None) + self._pending_usb_preloads.discard(dirname) + if dirname in self._usb_preload_queue: + self._usb_preload_queue.remove(dirname) + logger.info(f"Cleared USB cache for: {dirname}") + + self.dir_removed.emit(dirname) + logger.info(f"Directory deleted: {dirname}") + + def _handle_dir_moved(self, item: dict, source_item: dict) -> None: + """Handle directory move/rename.""" + self._handle_dir_deleted(source_item, {}) + self._handle_dir_created(item, {}) + + def _handle_root_update(self, _: dict, __: dict) -> None: + """Handle root update.""" + logger.info("Root update detected, requesting full refresh") + self.full_refresh_needed.emit() + self.initial_load() + + @staticmethod + def _is_usb_mount(path: str) -> bool: + """Check if a path is a USB mount point.""" + path = path.removeprefix("/") + return "/" not in path and path.startswith("USB-") + + def handle_message_received( + self, method: str, data: typing.Any, params: dict + ) -> None: + """Handle file-related messages received from Moonraker.""" if "server.files.list" in method: - self.files.clear() - self.files = data - [self.request_file_metadata.emit(item["path"]) for item in self.files] + self._process_file_list(data) elif "server.files.metadata" in method: - if data["filename"] in self.files_metadata.keys(): - if not data.get("filename", None): - return - self.files_metadata.update({data["filename"]: data}) - else: - self.files_metadata[data["filename"]] = data + self._process_metadata(data) elif "server.files.get_directory" in method: - self.directories = data.get("dirs", {}) - self.files.clear() - self.files = data.get("files", []) - self.on_file_list[list].emit(self.files) - self.on_dirs[list].emit(self.directories) + self._process_directory_info(data) - @QtCore.pyqtSlot(str, str, name="on_request_delete_file") - def on_request_delete_file(self, filename: str, directory: str = "gcodes") -> None: - """Requests deletion of a file + def _process_file_list(self, data: list) -> None: + """Process full file list response.""" + self._files.clear() + + for item in data: + path = item.get("path", item.get("filename", "")) + if path: + self._files[path] = item + + self._initial_load_complete = True + self.on_file_list.emit(self.file_list) + logger.info(f"Loaded {len(self._files)} files") + # Request metadata only for gcode files (async update) + for path in self._files: + if path.lower().endswith(self.GCODE_EXTENSION): + self.request_file_metadata.emit(path.removeprefix("/")) + + def _process_metadata(self, data: dict) -> None: + """Process file metadata response.""" + filename = data.get("filename") + if not filename: + return + + thumbnails = data.get("thumbnails", []) + base_dir = (self.gcode_path / filename).parent + thumbnail_paths = [ + str(base_dir / t.get("relative_path", "")) + for t in thumbnails + if isinstance(t.get("relative_path", None), str) and t["relative_path"] + ] + + # Load images, filtering out invalid files + thumbnail_images = [] + for path in thumbnail_paths: + image = QtGui.QImage(path) + if not image.isNull(): # skip loading errors + thumbnail_images.append(image) + + metadata = FileMetadata.from_dict(data, thumbnail_images) + self._files_metadata[filename] = metadata + + # Emit updated fileinfo + self.fileinfo.emit(metadata.to_dict()) + logger.debug(f"Metadata loaded for: {filename}") + + def handle_metadata_error(self, error_data: typing.Union[str, dict]) -> None: + """ + Handle metadata request error from Moonraker. + + Parses the filename from the error message and emits metadata_error signal. + Called directly from MainWindow error handler. Args: - filename (str): file to delete - directory (str): root directory where the file is located + error_data: The error message string or dict from Moonraker """ - if not directory: - self.ws.api.delete_file(filename) + if not error_data: return - self.ws.api.delete_file(filename, directory) # Use the root directory 'gcodes' - @QtCore.pyqtSlot(str, name="on_request_fileinfo") - def on_request_fileinfo(self, filename: str) -> None: - """Requests metadata for a file + if isinstance(error_data, dict): + text = error_data.get("message", str(error_data)) + else: + text = str(error_data) + + if "metadata" not in text.lower(): + return + + # Parse filename from error message (format: ) + start = text.find("<") + 1 + end = text.find(">", start) + + if start > 0 and end > start: + filename = text[start:end] + clean_filename = filename.removeprefix("/") + self.metadata_error.emit(clean_filename) + logger.debug(f"Metadata error for: {clean_filename}") + + def _preload_usb_contents(self, usb_path: str) -> None: + """ + Preload USB contents when USB is inserted. + + Requests directory info for the USB mount so files are ready + when user navigates to it. Args: - filename (str): file to get metadata from + usb_path: The USB mount path (e.g., "USB-sda1") """ - _data: dict = { - "thumbnail_images": list, - "filament_total": dict, - "estimated_time": int, - "layer_count": int, - "object_height": float, - "size": int, - "filament_type": str, - "filament_weight_total": float, - "layer_height": float, - "first_layer_height": float, - "first_layer_extruder_temp": float, - "first_layer_bed_temp": float, - "chamber_temp": float, - "filament_name": str, - "nozzle_diameter": float, - "slicer": str, - "filename": str, - } - _file_metadata = self.files_metadata.get(str(filename), {}) - _data.update({"filename": filename}) - _thumbnails = _file_metadata.get("thumbnails", {}) - _thumbnail_paths = list( - map( - lambda thumbnail_path: os.path.join( - os.path.dirname(os.path.join(self.gcode_path, filename)), - thumbnail_path.get("relative_path", "?"), - ), - _thumbnails, - ) - ) - _thumbnail_images = list(map(lambda path: QtGui.QImage(path), _thumbnail_paths)) - _data.update({"thumbnail_images": _thumbnail_images}) - _data.update({"filament_total": _file_metadata.get("filament_total", "?")}) - _data.update({"estimated_time": _file_metadata.get("estimated_time", 0)}) - _data.update({"layer_count": _file_metadata.get("layer_count", -1.0)}) - _data.update({"total_layer": _file_metadata.get("total_layer", -1.0)}) - _data.update({"object_height": _file_metadata.get("object_height", -1.0)}) - _data.update({"nozzle_diameter": _file_metadata.get("nozzle_diameter", -1.0)}) - _data.update({"layer_height": _file_metadata.get("layer_height", -1.0)}) - _data.update( - {"first_layer_height": _file_metadata.get("first_layer_height", -1.0)} - ) - _data.update( - { - "first_layer_extruder_temp": _file_metadata.get( - "first_layer_extruder_temp", -1.0 - ) - } - ) - _data.update( - {"first_layer_bed_temp": _file_metadata.get("first_layer_bed_temp", -1.0)} - ) - _data.update({"chamber_temp": _file_metadata.get("chamber_temp", -1.0)}) - _data.update({"filament_name": _file_metadata.get("filament_name", -1.0)}) - _data.update({"filament_type": _file_metadata.get("filament_type", -1.0)}) - _data.update( - {"filament_weight_total": _file_metadata.get("filament_weight_total", -1.0)} + logger.info(f"Preloading USB contents: {usb_path}") + self._pending_usb_preloads.add(usb_path) + self._usb_preload_queue.append(usb_path) + self.ws.api.get_dir_information(usb_path, True) + + def get_cached_usb_files(self, usb_path: str) -> typing.Optional[list[dict]]: + """ + Get cached files for a USB path if available. + + Args: + usb_path: The USB mount path + + Returns: + List of file dicts if cached, None otherwise + """ + return self._usb_files_cache.get(usb_path.removeprefix("/")) + + def _process_usb_directory_info(self, usb_path: str, data: dict) -> None: + """ + Process preloaded USB directory info. + + Caches the files and requests metadata for gcode files. + + Args: + usb_path: The USB mount path + data: Directory info response from Moonraker + """ + files = [] + for file_data in data.get("files", []): + filename = file_data.get("filename", file_data.get("path", "")) + if filename: + files.append(file_data) + + full_path = f"{usb_path}/{filename}" + if filename.lower().endswith(self.GCODE_EXTENSION): + self.request_file_metadata.emit(full_path) + + # Cache the files + self._usb_files_cache[usb_path] = files + self.usb_files_loaded.emit(usb_path, files) + logger.info(f"Preloaded {len(files)} files from USB: {usb_path}") + + def _process_directory_info(self, data: dict) -> None: + """Process directory info response.""" + # Check if this is a USB preload response. + # Match by FIFO queue — Moonraker responds to get_dir_information in order. + matched_usb = None + + if self._usb_preload_queue: + candidate = self._usb_preload_queue.popleft() + if candidate in self._pending_usb_preloads: + matched_usb = candidate + + if matched_usb: + self._pending_usb_preloads.discard(matched_usb) + self._process_usb_directory_info(matched_usb, data) + return + + self._directories.clear() + self._files.clear() + + for dir_data in data.get("dirs", []): + dirname = dir_data.get("dirname", "") + if dirname and not dirname.startswith("."): + self._directories[dirname] = dir_data + + for file_data in data.get("files", []): + filename = file_data.get("filename", file_data.get("path", "")) + if filename: + self._files[filename] = file_data + + self.on_file_list.emit(self.file_list) + self.on_dirs.emit(self.directories) + self._initial_load_complete = True + + logger.info( + f"Directory loaded: {len(self._directories)} dirs, {len(self._files)} files" ) - _data.update({"slicer": _file_metadata.get("slicer", -1.0)}) - self.fileinfo.emit(_data) - - def eventFilter(self, a0: QtCore.QObject, a1: QtCore.QEvent) -> bool: - """Handle Websocket and Klippy events""" - if a1.type() == events.WebSocketOpen.type(): - self.request_file_list.emit() - self.request_dir_info[str, bool].emit("", False) + + # Request metadata only for gcode files (async update) + for filename in self._files: + if filename.lower().endswith(self.GCODE_EXTENSION): + self.request_file_metadata.emit(filename.removeprefix("/")) + + @QtCore.pyqtSlot(str, str, name="on_request_delete_file") + def on_request_delete_file(self, filename: str, directory: str = "gcodes") -> None: + """Request deletion of a file.""" + if not filename: + return + + if directory: + self.ws.api.delete_file(filename, directory) + else: + self.ws.api.delete_file(filename) + + logger.info(f"Requested deletion of: {filename}") + + @QtCore.pyqtSlot(str, name="on_request_fileinfo") + def on_request_fileinfo(self, filename: str) -> None: + """Request and emit metadata for a file.""" + clean_filename = filename.removeprefix("/") + cached = self._files_metadata.get(clean_filename) + + if cached: + self.fileinfo.emit(cached.to_dict()) + else: + self.request_file_metadata.emit(clean_filename) + + @QtCore.pyqtSlot(name="get_dir_info") + @QtCore.pyqtSlot(str, name="get_dir_info") + @QtCore.pyqtSlot(str, bool, name="get_dir_info") + def get_dir_information( + self, directory: str = "", extended: bool = True + ) -> typing.Optional[list]: + """Get directory information.""" + self._current_directory = directory + + if not extended and self._initial_load_complete: + return self.directories + + return self.ws.api.get_dir_information(directory, extended) + + def eventFilter(self, obj: QtCore.QObject, event: QtCore.QEvent) -> bool: + """Handle application-level events.""" + if event.type() == events.WebSocketOpen.type(): + self.initial_load() return False - if a1.type() == events.KlippyDisconnected.type(): - self.files_metadata.clear() - self.files.clear() + + if event.type() == events.KlippyDisconnected.type(): + self._clear_all_data() return False - return super().eventFilter(a0, a1) - def event(self, a0: QtCore.QEvent) -> bool: - """Filter ReceivedFileData event""" - if a0.type() == ReceivedFileData.type(): - if isinstance(a0, ReceivedFileData): - self.handle_message_received(a0.method, a0.data, a0.params) + return super().eventFilter(obj, event) + + def event(self, event: QtCore.QEvent) -> bool: + """Handle object-level events.""" + if event.type() == ReceivedFileData.type(): + if isinstance(event, ReceivedFileData): + self.handle_message_received(event.method, event.data, event.params) return True - return super().event(a0) + return super().event(event) + + def _clear_all_data(self) -> None: + """Clear all cached data.""" + self._files.clear() + self._directories.clear() + self._files_metadata.clear() + self._usb_files_cache.clear() + self._pending_usb_preloads.clear() + self._usb_preload_queue.clear() + self._initial_load_complete = False + logger.info("All file data cleared") diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 66f792de..7b0578fe 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -12,14 +12,14 @@ from lib.panels.networkWindow import NetworkControlWindow from lib.panels.printTab import PrintTab from lib.panels.utilitiesTab import UtilitiesTab -from lib.panels.widgets.connectionPage import ConnectionPage +from lib.panels.widgets.basePopup import BasePopup from lib.panels.widgets.cancelPage import CancelPage +from lib.panels.widgets.connectionPage import ConnectionPage +from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.panels.widgets.popupDialogWidget import Popup +from lib.panels.widgets.updatePage import UpdatePage from lib.printer import Printer from lib.ui.mainWindow_ui import Ui_MainWindow # With header -from lib.panels.widgets.updatePage import UpdatePage -from lib.panels.widgets.basePopup import BasePopup -from lib.panels.widgets.loadWidget import LoadingOverlayWidget # from lib.ui.mainWindow_v2_ui import Ui_MainWindow # No header from lib.ui.resources.background_resources_rc import * @@ -615,7 +615,7 @@ def _handle_notify_klippy_message(self, method, data, metadata) -> None: @api_handler def _handle_notify_filelist_changed_message(self, method, data, metadata) -> None: """Handle websocket file list messages""" - ... + self.file_data.handle_filelist_changed(data) @api_handler def _handle_notify_service_state_changed_message( @@ -654,24 +654,34 @@ def _handle_notify_gcode_response_message(self, method, data, metadata) -> None: @api_handler def _handle_error_message(self, method, data, metadata) -> None: - """Handle error messages""" + """Handle error messages from Moonraker API.""" self.handle_error_response[list].emit([data, metadata]) - if "metadata" in data.get("message", "").lower(): - # Quick fix, don't care about no metadata errors - return if self._popup_toggle: return - text = data - if isinstance(data, dict): - if "message" in data: - text = f"{data['message']}" - else: - text = data + + text = data.get("message", str(data)) if isinstance(data, dict) else str(data) + lower_text = text.lower() + + # Metadata errors - silent, handled by files_manager + if "metadata" in lower_text: + self.file_data.handle_metadata_error(text) + return + + # File not found - silent + if "file" in lower_text and "does not exist" in lower_text: + return + + # Directory not found - navigate back + show popup + if "does not exist" in lower_text: + self.printPanel.filesPage_widget.on_directory_error() + + # Show popup for all other errors (including directory errors) self.popup.new_message( message_type=Popup.MessageType.ERROR, - message=str(text), + message=text, userInput=True, ) + _logger.error(text) @api_handler def _handle_notify_cpu_throttled_message(self, method, data, metadata) -> None: diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index b28e52bd..ddc9c91a 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -129,8 +129,23 @@ def __init__( self.filesPage_widget.request_dir_info[str].connect( self.file_data.request_dir_info[str] ) + self.filesPage_widget.request_scan_metadata.connect( + self.ws.api.scan_gcode_metadata + ) + self.file_data.metadata_error.connect(self.filesPage_widget.on_metadata_error) self.filesPage_widget.request_dir_info.connect(self.file_data.request_dir_info) self.file_data.on_file_list.connect(self.filesPage_widget.on_file_list) + self.file_data.file_added.connect(self.filesPage_widget.on_file_added) + self.file_data.file_removed.connect(self.filesPage_widget.on_file_removed) + self.file_data.file_modified.connect(self.filesPage_widget.on_file_modified) + self.file_data.dir_added.connect(self.filesPage_widget.on_dir_added) + self.file_data.dir_removed.connect(self.filesPage_widget.on_dir_removed) + self.file_data.full_refresh_needed.connect( + self.filesPage_widget.on_full_refresh_needed + ) + self.file_data.usb_files_loaded.connect( + self.filesPage_widget.on_usb_files_loaded + ) self.jobStatusPage_widget = JobStatusWidget(self) self.addWidget(self.jobStatusPage_widget) self.confirmPage_widget.on_accept.connect( diff --git a/BlocksScreen/lib/panels/widgets/confirmPage.py b/BlocksScreen/lib/panels/widgets/confirmPage.py index 12432449..0f35ba39 100644 --- a/BlocksScreen/lib/panels/widgets/confirmPage.py +++ b/BlocksScreen/lib/panels/widgets/confirmPage.py @@ -25,7 +25,7 @@ def __init__(self, parent) -> None: self._setupUI() self.setMouseTracking(True) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) - self.thumbnail: QtGui.QImage = QtGui.QImage() + self.thumbnail: QtGui.QImage = self._blocksthumbnail self._thumbnails: typing.List = [] self.directory = "gcodes" self.filename = "" @@ -42,17 +42,19 @@ def __init__(self, parent) -> None: @QtCore.pyqtSlot(str, dict, name="on_show_widget") def on_show_widget(self, text: str, filedata: dict | None = None) -> None: """Handle widget show""" + if not filedata: + return directory = os.path.dirname(text) filename = os.path.basename(text) self.directory = directory self.filename = filename self.cf_file_name.setText(self.filename) - if not filedata: - return self._thumbnails = filedata.get("thumbnail_images", []) if self._thumbnails: _biggest_thumbnail = self._thumbnails[-1] # Show last which is biggest self.thumbnail = QtGui.QImage(_biggest_thumbnail) + else: + self.thumbnail = self._blocksthumbnail _total_filament = filedata.get("filament_weight_total") _estimated_time = filedata.get("estimated_time") if isinstance(_estimated_time, str): @@ -114,12 +116,6 @@ def paintEvent(self, event: QtGui.QPaintEvent) -> None: self._scene = QtWidgets.QGraphicsScene(self) self.cf_thumbnail.setScene(self._scene) - # Pick thumbnail or fallback logo - if self.thumbnail.isNull(): - self.thumbnail = QtGui.QImage( - "BlocksScreen/lib/ui/resources/media/logoblocks400x300.png" - ) - # Scene rectangle (available display area) graphics_rect = self.cf_thumbnail.rect().toRectF() @@ -323,3 +319,6 @@ def _setupUI(self) -> None: QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter, ) self.verticalLayout_4.addLayout(self.cf_content_vertical_layout) + self._blocksthumbnail = QtGui.QImage( + "BlocksScreen/lib/ui/resources/media/logoblocks400x300.png" + ) diff --git a/BlocksScreen/lib/panels/widgets/filesPage.py b/BlocksScreen/lib/panels/widgets/filesPage.py index f8fa490f..969399ac 100644 --- a/BlocksScreen/lib/panels/widgets/filesPage.py +++ b/BlocksScreen/lib/panels/widgets/filesPage.py @@ -1,411 +1,1121 @@ +import json import logging -import os import typing import helper_methods from lib.utils.blocks_Scrollbar import CustomScrollBar from lib.utils.icon_button import IconButton -from PyQt6 import QtCore, QtGui, QtWidgets - from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem +from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger("logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class FilesPage(QtWidgets.QWidget): - request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - name="request-back" - ) - file_selected: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, dict, name="file-selected" - ) - request_file_info: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, name="request-file-info" + # Signals + request_back = QtCore.pyqtSignal(name="request_back") + file_selected = QtCore.pyqtSignal(str, dict, name="file_selected") + request_file_info = QtCore.pyqtSignal(str, name="request_file_info") + request_dir_info = QtCore.pyqtSignal( + [], [str], [str, bool], name="api_get_dir_info" ) - request_dir_info: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - [], [str], [str, bool], name="api-get-dir-info" - ) - request_file_list: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - [], [str], name="api-get-files-list" - ) - request_file_metadata: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, name="api-get-gcode-metadata" - ) - file_list: list = [] - files_data: dict = {} - directories: list = [] - - def __init__(self, parent) -> None: - super().__init__() - self.model = EntryListModel() - self.entry_delegate = EntryDelegate() - self._setupUI() + request_file_list = QtCore.pyqtSignal([], [str], name="api_get_files_list") + request_file_metadata = QtCore.pyqtSignal(str, name="api_get_gcode_metadata") + request_scan_metadata = QtCore.pyqtSignal(str, name="api_scan_gcode_metadata") + + # Constants + GCODE_EXTENSION = ".gcode" + USB_PREFIX = "USB-" + ITEM_HEIGHT = 80 + LEFT_FONT_SIZE = 17 + RIGHT_FONT_SIZE = 12 + + # Icon paths + ICON_PATHS = { + "back_folder": ":/ui/media/btn_icons/back_folder.svg", + "folder": ":/ui/media/btn_icons/folderIcon.svg", + "right_arrow": ":/arrow_icons/media/btn_icons/right_arrow.svg", + "usb": ":/ui/media/btn_icons/usb_icon.svg", + "back": ":/ui/media/btn_icons/back.svg", + "refresh": ":/ui/media/btn_icons/refresh.svg", + } + + def __init__(self, parent: typing.Optional[QtWidgets.QWidget] = None) -> None: + super().__init__(parent) + + self._file_list: list[dict] = [] + self._files_data: dict[str, dict] = {} # filename -> metadata dict + self._directories: list[dict] = [] + self._curr_dir: str = "" + self._pending_action: bool = False + self._pending_metadata_requests: set[str] = set() # Track pending requests + self._metadata_retry_count: dict[ + str, int + ] = {} # Track retry count per file (max 3) + self._icons: dict[str, QtGui.QPixmap] = {} + + self._model = EntryListModel() + self._entry_delegate = EntryDelegate() + + self._model.rowsInserted.connect(self._delayed_scrollbar_update) + self._model.rowsRemoved.connect(self._delayed_scrollbar_update) + self._model.modelReset.connect(self._delayed_scrollbar_update) + + self._setup_ui() + self._load_icons() + self._connect_signals() + self.setMouseTracking(True) self.setAttribute(QtCore.Qt.WidgetAttribute.WA_AcceptTouchEvents, True) - self.curr_dir: str = "" - self.ReloadButton.clicked.connect( - lambda: self.request_dir_info[str].emit(self.curr_dir) - ) - self.listWidget.verticalScrollBar().valueChanged.connect(self._handle_scrollbar) - self.scrollbar.valueChanged.connect(self._handle_scrollbar) - self.scrollbar.valueChanged.connect( - lambda value: self.listWidget.verticalScrollBar().setValue(value) - ) - self.back_btn.clicked.connect(self.reset_dir) - self.entry_delegate.item_selected.connect(self._on_item_selected) - self._refresh_one_and_half_sec_timer = QtCore.QTimer() - self._refresh_one_and_half_sec_timer.timeout.connect( - lambda: self.request_dir_info[str].emit(self.curr_dir) - ) - self._refresh_one_and_half_sec_timer.start(1500) + @property + def current_directory(self) -> str: + """Get current directory path.""" + return self._curr_dir - @QtCore.pyqtSlot(ListItem, name="on-item-selected") - def _on_item_selected(self, item: ListItem) -> None: - """Slot called when a list item is selected in the UI. - This method is connected to the `item_selected` signal of the entry delegate. - It handles the selection of a `ListItem` and process it accoding it with its type + @current_directory.setter + def current_directory(self, value: str) -> None: + """Set current directory path.""" + self._curr_dir = value + + def reload_gcodes_folder(self) -> None: + """Request reload of the gcodes folder from root.""" + logger.debug("Reloading gcodes folder") + self.request_dir_info[str].emit("") + + def clear_files_data(self) -> None: + """Clear all cached file data.""" + self._files_data.clear() + self._pending_metadata_requests.clear() + self._metadata_retry_count.clear() + + def retry_metadata_request(self, file_path: str) -> bool: + """ + Request metadata with a maximum of 3 retries per file. Args: - item : ListItem The item that was selected by the user. + file_path: Path to the file + Returns: + True if request was made, False if max retries reached """ - if not item.left_icon: - filename = self.curr_dir + "/" + item.text + ".gcode" - self._fileItemClicked(filename) - else: - if item.text == "Go Back": - go_back_path = os.path.dirname(self.curr_dir) - if go_back_path == "/": - go_back_path = "" - self._on_goback_dir(go_back_path) - else: - self._dirItemClicked("/" + item.text) + clean_path = file_path.removeprefix("/") - @QtCore.pyqtSlot(name="reset-dir") - def reset_dir(self) -> None: - """Reset current directory""" - self.curr_dir = "" - self.request_dir_info[str].emit(self.curr_dir) + if not clean_path.lower().endswith(self.GCODE_EXTENSION): + return False - def showEvent(self, a0: QtGui.QShowEvent) -> None: - """Re-implemented method, handle widget show event""" - self._build_file_list() - return super().showEvent(a0) + current_count = self._metadata_retry_count.get(clean_path, 0) - @QtCore.pyqtSlot(list, name="on-file-list") + if current_count > 3: + # Maximum 3 force scan per file + logger.debug(f"Metadata retry limit reached for: {clean_path}") + return False + + self._metadata_retry_count[clean_path] = current_count + 1 + + if current_count == 0: + # First attempt: regular metadata request + self.request_file_metadata.emit(clean_path) + logger.debug(f"Metadata request (attempt 1) for: {clean_path}") + else: + # Subsequent attempts: force scan + self.request_scan_metadata.emit(clean_path) + logger.debug( + f"Metadata scan (attempt {current_count + 1}) for: {clean_path}" + ) + + return True + + @QtCore.pyqtSlot(list, name="on_file_list") def on_file_list(self, file_list: list) -> None: - """Handle receiving files list from websocket""" - self.files_data.clear() - self.file_list = file_list + """Handle receiving full files list.""" + self._file_list = file_list.copy() if file_list else [] + logger.debug(f"Received file list with {len(self._file_list)} files") - @QtCore.pyqtSlot(list, name="on-dirs") + @QtCore.pyqtSlot(list, name="on_dirs") def on_directories(self, directories_data: list) -> None: - """Handle receiving available directories from websocket""" - self.directories = directories_data + """Handle receiving full directories list.""" + self._directories = directories_data.copy() if directories_data else [] + logger.debug(f"Received {len(self._directories)} directories") + if self.isVisible(): self._build_file_list() - @QtCore.pyqtSlot(dict, name="on-fileinfo") + @QtCore.pyqtSlot(dict, name="on_fileinfo") def on_fileinfo(self, filedata: dict) -> None: - """Method called per file to contruct file entry to the list""" + """ + Handle receiving file metadata. + + Updates existing file entry in the list with better info (time, filament). + """ if not filedata or not self.isVisible(): return + filename = filedata.get("filename", "") + if not filename or not filename.lower().endswith(self.GCODE_EXTENSION): + return + + # Cache the file data + self._files_data[filename] = filedata + + # Remove from pending requests and reset retry count (success) + self._pending_metadata_requests.discard(filename) + self._metadata_retry_count.pop(filename, None) + + # Check if this file should be displayed in current view + file_dir = self._get_parent_directory(filename) + current = self._curr_dir.removeprefix("/") + + # Both empty = root directory, otherwise must match exactly + if file_dir != current: + return + + display_name = self._get_display_name(filename) + item = self._create_file_list_item(filedata) + if not item: + return + + self._model.remove_item_by_text(display_name) + + # Find correct position (sorted by modification time, newest first) + insert_position = self._find_file_insert_position(filedata.get("modified", 0)) + self._model.insert_item(insert_position, item) + + logger.debug(f"Updated file in list: {display_name}") + + @QtCore.pyqtSlot(str, name="on_metadata_error") + def on_metadata_error(self, filename: str) -> None: + """ + Handle metadata request failure. + + Triggers retry with scan_gcode_metadata if under retry limit. + Called when metadata request fails. + """ if not filename: return - self.files_data.update({f"{filename}": filedata}) - estimated_time = filedata.get("estimated_time", 0) - seconds = int(estimated_time) if isinstance(estimated_time, (int, float)) else 0 - filament_type = ( - filedata.get("filament_type", "Unknown filament") - if filedata.get("filament_type", "Unknown filament") != -1.0 - else "Unknown filament" - ) - time_str = "" - days, hours, minutes, _ = helper_methods.estimate_print_time(seconds) - if seconds <= 0: - time_str = "??" - elif seconds < 60: - time_str = "less than 1 minute" - else: - if days > 0: - time_str = f"{days}d {hours}h {minutes}m" - elif hours > 0: - time_str = f"{hours}h {minutes}m" - else: - time_str = f"{minutes}m" - name = helper_methods.get_file_name(filename) + clean_filename = filename.removeprefix("/") + + if clean_filename not in self._pending_metadata_requests: + return + + # Try again (will use force scan to the max of 3 times) + if not self.retry_metadata_request(clean_filename): + # Max retries reached, remove from pending + self._pending_metadata_requests.discard(clean_filename) + logger.debug(f"Giving up on metadata for: {clean_filename}") + + @QtCore.pyqtSlot(str, list, name="on_usb_files_loaded") + def on_usb_files_loaded(self, usb_path: str, files: list) -> None: + """ + Handle preloaded USB files. + + Called when USB files are preloaded in background. + If we're currently viewing this USB, update the display. + + Args: + usb_path: The USB mount path + files: List of file dicts from the USB + """ + current = self._curr_dir.removeprefix("/") + + # If we're currently in this USB folder, update the file list + if current == usb_path: + self._file_list = files.copy() + if self.isVisible(): + self._build_file_list() + logger.debug(f"Updated view with preloaded USB files: {usb_path}") + + def _find_file_insert_position(self, modified_time: float) -> int: + """ + Find the correct position to insert a new file. + + Files should be: + 1. After all directories + 2. Sorted by modification time (newest first) + + Returns: + The index at which to insert the new file. + """ + insert_pos = 0 + + for i in range(self._model.rowCount()): + index = self._model.index(i) + item = self._model.data(index, QtCore.Qt.ItemDataRole.UserRole) + + if not item: + continue + + # Skip directories (items with left_icon) + if item.left_icon: + insert_pos = i + 1 + continue + + # This is a file - check its modification time + file_key = self._find_file_key_by_display_name(item.text) + if file_key: + file_data = self._files_data.get(file_key, {}) + file_modified = file_data.get("modified", 0) + + # Files are sorted newest first, so insert before older files + if modified_time > file_modified: + return i + + insert_pos = i + 1 + + return insert_pos + + def _find_file_key_by_display_name(self, display_name: str) -> typing.Optional[str]: + """Find the file key in _files_data by its display name.""" + for key in self._files_data: + if self._get_display_name(key) == display_name: + return key + return None + + @QtCore.pyqtSlot(dict, name="on_file_added") + def on_file_added(self, file_data: dict) -> None: + """ + Handle a single file being added. + + Called when Moonraker sends notify_filelist_changed with create_file action. + Adds file to list immediately, metadata updates later. + """ + path = file_data.get("path", file_data.get("filename", "")) + if not path: + return + + # Normalize paths + path = path.removeprefix("/") + file_dir = self._get_parent_directory(path) + current = self._curr_dir.removeprefix("/") + + # Check if file belongs to current directory + if file_dir != current: + logger.debug( + f"File '{path}' (dir: '{file_dir}') not in current directory ('{current}'), skipping" + ) + return + + # Only update UI if visible + if not self.isVisible(): + logger.debug("Widget not visible, will refresh on show") + return + + display_name = self._get_display_name(path) + + if not self._model_contains_item(display_name): + # Create basic item with unknown info + modified = file_data.get("modified", 0) + + item = ListItem( + text=display_name, + right_text="Unknown Filament - Unknown time", + right_icon=self._icons.get("right_arrow"), + left_icon=None, + callback=None, + selected=False, + allow_check=False, + _lfontsize=self.LEFT_FONT_SIZE, + _rfontsize=self.RIGHT_FONT_SIZE, + height=self.ITEM_HEIGHT, + notificate=False, + ) + + # Find correct position + insert_position = self._find_file_insert_position(modified) + self._model.insert_item(insert_position, item) + self._hide_placeholder() + logger.debug(f"Added new file to list: {display_name}") + + # Request metadata for gcode files using retry mechanism + if path.lower().endswith(self.GCODE_EXTENSION): + if path not in self._pending_metadata_requests: + self._pending_metadata_requests.add(path) + self.retry_metadata_request(path) + logger.debug(f"Requested metadata for new file: {path}") + + @QtCore.pyqtSlot(str, name="on_file_removed") + def on_file_removed(self, filepath: str) -> None: + """ + Handle a file being removed. + + Called when Moonraker sends notify_filelist_changed with delete_file action. + """ + filepath = filepath.removeprefix("/") + file_dir = self._get_parent_directory(filepath) + current = self._curr_dir.removeprefix("/") + + # Always clean up cache + self._files_data.pop(filepath, None) + self._pending_metadata_requests.discard(filepath) + self._metadata_retry_count.pop(filepath, None) + + # Only update UI if visible and in current directory + if not self.isVisible(): + return + + if file_dir != current: + logger.debug( + f"Deleted file '{filepath}' not in current directory ('{current}'), skipping UI update" + ) + return + + filename = self._get_basename(filepath) + display_name = self._get_display_name(filename) + + # Remove from model + removed = self._model.remove_item_by_text(display_name) + + if removed: + self._check_empty_state() + logger.debug(f"File removed from view: {filepath}") + + @QtCore.pyqtSlot(dict, name="on_file_modified") + def on_file_modified(self, file_data: dict) -> None: + """ + Handle a file being modified. + + Called when Moonraker sends notify_filelist_changed with modify_file action. + """ + path = file_data.get("path", file_data.get("filename", "")) + if path: + # Remove old entry and request fresh metadata + self.on_file_removed(path) + self.on_file_added(file_data) + + @QtCore.pyqtSlot(dict, name="on_dir_added") + def on_dir_added(self, dir_data: dict) -> None: + """ + Handle a directory being added. + + Called when Moonraker sends notify_filelist_changed with create_dir action. + Inserts the directory in the correct sorted position (alphabetically, after Go Back). + """ + # Extract dirname from path or dirname field + path = dir_data.get("path", "") + dirname = dir_data.get("dirname", "") + + if not dirname and path: + dirname = self._get_basename(path) + + if not dirname or dirname.startswith("."): + return + + path = path.removeprefix("/") + parent_dir = self._get_parent_directory(path) if path else "" + current = self._curr_dir.removeprefix("/") + + if parent_dir != current: + logger.debug( + f"Directory '{dirname}' (parent: '{parent_dir}') not in current directory ('{current}'), skipping" + ) + return + + if not self.isVisible(): + logger.debug( + f"Widget not visible, skipping UI update for added dir: {dirname}" + ) + return + + # Check if already exists + if self._model_contains_item(dirname): + return + + # Ensure dirname is in dir_data + dir_data["dirname"] = dirname + + # Find the correct sorted position for this directory + insert_position = self._find_directory_insert_position(dirname) + + # Create the list item + icon = self._icons.get("folder") + if self._is_usb_directory(self._curr_dir, dirname): + icon = self._icons.get("usb") + item = ListItem( - text=name[:-6], - right_text=f"{filament_type} - {time_str}", - right_icon=self.path.get("right_arrow"), - left_icon=None, - callback=None, + text=str(dirname), + left_icon=icon, + right_text="", + right_icon=None, selected=False, + callback=None, allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=80, - notificate=False, + _lfontsize=self.LEFT_FONT_SIZE, + _rfontsize=self.RIGHT_FONT_SIZE, + height=self.ITEM_HEIGHT, ) - self.model.add_item(item) + # Insert at the correct position + self._model.insert_item(insert_position, item) - @QtCore.pyqtSlot(str, name="file-item-clicked") - def _fileItemClicked(self, filename: str) -> None: - """Slot for List Item clicked + self._hide_placeholder() + logger.debug( + f"Directory added to view at position {insert_position}: {dirname}" + ) - Args: - filename (str): Clicked item path + def _find_directory_insert_position(self, new_dirname: str) -> int: """ - self.file_selected.emit( - str(filename.removeprefix("/")), - self.files_data.get(filename.removeprefix("/")), + Find the correct position to insert a new directory. + + Directories should be: + 1. After "Go Back" (if present) + 2. Before all files + 3. Sorted alphabetically among other directories + + Returns: + The index at which to insert the new directory. + """ + new_dirname_lower = new_dirname.lower() + insert_pos = 0 + + for i in range(self._model.rowCount()): + index = self._model.index(i) + item = self._model.data(index, QtCore.Qt.ItemDataRole.UserRole) + + if not item: + continue + + # Skip "Go Back" - always stays at top + if item.text == "Go Back": + insert_pos = i + 1 + continue + + # If this item has a left_icon, it's a directory + if item.left_icon: + # Compare alphabetically + if item.text.lower() > new_dirname_lower: + # Found a directory that should come after the new one + return i + else: + # This directory comes before, keep looking + insert_pos = i + 1 + else: + # Hit a file - insert before it (directories come before files) + return i + + # Insert at the end of directories (or end of list if no files) + return insert_pos + + @QtCore.pyqtSlot(str, name="on_dir_removed") + def on_dir_removed(self, dirname_or_path: str) -> None: + """ + Handle a directory being removed. + + Called when Moonraker sends notify_filelist_changed with delete_dir action. + Also handles USB mount removal (which Moonraker reports as delete_file). + """ + dirname_or_path = dirname_or_path.removeprefix("/") + dirname = ( + self._get_basename(dirname_or_path) + if "/" in dirname_or_path + else dirname_or_path ) - def _dirItemClicked(self, directory: str) -> None: - """Method that changes the current view in the list""" - self.curr_dir = self.curr_dir + directory - self.request_dir_info[str].emit(self.curr_dir) + if not dirname: + return + + # Check if user is currently inside the removed directory (e.g., USB removed) + current = self._curr_dir.removeprefix("/") + if current == dirname or current.startswith(dirname + "/"): + logger.warning( + f"Current directory '{current}' was removed, returning to root" + ) + self.on_directory_error() + return + + # Skip UI update if not visible + if not self.isVisible(): + return + + removed = self._model.remove_item_by_text(dirname) + + if removed: + self._check_empty_state() + logger.debug(f"Directory removed from view: {dirname}") + + @QtCore.pyqtSlot(name="on_full_refresh_needed") + def on_full_refresh_needed(self) -> None: + """ + Handle full refresh request. + + Called when Moonraker sends root_update or when major changes occur. + """ + logger.info("Full refresh requested") + self._curr_dir = "" + self.request_dir_info[str].emit(self._curr_dir) + + @QtCore.pyqtSlot(name="on_directory_error") + def on_directory_error(self) -> None: + """ + Handle Directory Error. + + Immediately navigates back to root gcodes folder. + Call this from MainWindow when detecting USB removal or directory errors. + """ + logger.info("Directory Error - returning to root directory") + + # Reset to root directory + self._curr_dir = "" + + # Clear any pending actions + self._pending_action = False + self._pending_metadata_requests.clear() + + # Request fresh data for root directory + self.request_dir_info[str].emit("") + + @QtCore.pyqtSlot(ListItem, name="on_item_selected") + def _on_item_selected(self, item: ListItem) -> None: + """Handle list item selection.""" + if not item.left_icon: + # File selected (files don't have left icon) + filename = self._build_filepath(item.text + self.GCODE_EXTENSION) + self._on_file_item_clicked(filename) + elif item.text == "Go Back": + # Go back selected + go_back_path = self._get_parent_directory(self._curr_dir) + if go_back_path == "/": + go_back_path = "" + self._on_go_back_dir(go_back_path) + else: + # Directory selected + self._on_dir_item_clicked("/" + item.text) + + @QtCore.pyqtSlot(name="reset_dir") + def reset_dir(self) -> None: + """Reset to root directory.""" + self._curr_dir = "" + self.request_dir_info[str].emit(self._curr_dir) + + def showEvent(self, event: QtGui.QShowEvent) -> None: + """Handle widget becoming visible.""" + # Request fresh data when becoming visible + self.request_dir_info[str].emit(self._curr_dir) + super().showEvent(event) + + def hideEvent(self, event: QtGui.QHideEvent) -> None: + """Handle widget being hidden.""" + # Clear pending requests when hidden + self._pending_metadata_requests.clear() + super().hideEvent(event) def _build_file_list(self) -> None: - """Inserts the currently available gcode files on the QListWidget""" - self.listWidget.blockSignals(True) - self.model.clear() - self.entry_delegate.clear() - if ( - not self.file_list - and not self.directories - and os.path.islink(self.curr_dir) - ): - self._add_placeholder() + """Build the complete file list display.""" + self._list_widget.blockSignals(True) + self._model.clear() + self._entry_delegate.clear() + self._pending_action = False + self._pending_metadata_requests.clear() + self._metadata_retry_count.clear() + + # Determine if we're in root directory + is_root = not self._curr_dir or self._curr_dir == "/" + + # Check for empty state in root directory + if not self._file_list and not self._directories and is_root: + self._show_placeholder() + self._list_widget.blockSignals(False) return - if self.directories or self.curr_dir != "": - if self.curr_dir != "" and self.curr_dir != "/": - self._add_back_folder_entry() - for dir_data in self.directories: - if dir_data.get("dirname").startswith("."): - continue + # We have content (or we're in a subdirectory), hide placeholder + self._hide_placeholder() + + # Add back button if not in root + if not is_root: + self._add_back_folder_entry() + + # Add directories (sorted alphabetically) + sorted_dirs = sorted( + self._directories, key=lambda x: x.get("dirname", "").lower() + ) + for dir_data in sorted_dirs: + dirname = dir_data.get("dirname", "") + if dirname and not dirname.startswith("."): self._add_directory_list_item(dir_data) - sorted_list = sorted(self.file_list, key=lambda x: x["modified"], reverse=True) - for item in sorted_list: - self._add_file_list_item(item) - self._setup_scrollbar() - self.listWidget.blockSignals(False) - self.listWidget.update() + # Add files immediately (sorted by modification time, newest first) + sorted_files = sorted( + self._file_list, key=lambda x: x.get("modified", 0), reverse=True + ) + for file_item in sorted_files: + filename = file_item.get("filename", file_item.get("path", "")) + if not filename: + continue + + # Add file to list immediately with basic info + self._add_file_to_list(file_item) + + # Request metadata for gcode files (will update display later) + if filename.lower().endswith(self.GCODE_EXTENSION): + self._request_file_info(file_item) + + self._list_widget.blockSignals(False) + self._list_widget.update() + + def _delayed_scrollbar_update(self) -> None: + """Update scrollbar after model changes.""" + QtCore.QTimer.singleShot(10, self._setup_scrollbar) + + def _add_file_to_list(self, file_item: dict) -> None: + """Add a file entry to the list with basic info.""" + filename = file_item.get("filename", file_item.get("path", "")) + if not filename or not filename.lower().endswith(self.GCODE_EXTENSION): + return + + # Get display name + display_name = self._get_display_name(filename) + + # Check if already in list + if self._model_contains_item(display_name): + return + + # Use cached metadata if available, otherwise show unknown + full_path = self._build_filepath(filename) + cached = self._files_data.get(full_path) + + if cached: + item = self._create_file_list_item(cached) + else: + item = ListItem( + text=display_name, + right_text="Unknown Filament - Unknown time", + right_icon=self._icons.get("right_arrow"), + left_icon=None, + callback=None, + selected=False, + allow_check=False, + _lfontsize=self.LEFT_FONT_SIZE, + _rfontsize=self.RIGHT_FONT_SIZE, + height=self.ITEM_HEIGHT, + notificate=False, + ) + + if item: + self._model.add_item(item) + + def _create_file_list_item(self, filedata: dict) -> typing.Optional[ListItem]: + """Create a ListItem from file metadata.""" + filename = filedata.get("filename", "") + if not filename: + return None + + # Format estimated time + estimated_time = filedata.get("estimated_time", 0) + seconds = int(estimated_time) if isinstance(estimated_time, (int, float)) else 0 + time_str = self._format_print_time(seconds) + + # Get filament type + filament_type = filedata.get("filament_type") + if isinstance(filament_type, str): + text = filament_type.strip() + if text.startswith("[") and text.endswith("]"): + try: + types = json.loads(text) + except json.JSONDecodeError: + types = [text] + else: + types = [text] + else: + types = filament_type or [] + + if not isinstance(types, list): + types = [types] + + filament_type = ",".join(dict.fromkeys(types)) + + if not filament_type or filament_type == -1.0 or filament_type == "Unknown": + filament_type = "Unknown Filament" + + display_name = self._get_display_name(filename) + + return ListItem( + text=display_name, + right_text=f"{filament_type} - {time_str}", + right_icon=self._icons.get("right_arrow"), + left_icon=None, # Files have no left icon + callback=None, + selected=False, + allow_check=False, + _lfontsize=self.LEFT_FONT_SIZE, + _rfontsize=self.RIGHT_FONT_SIZE, + height=self.ITEM_HEIGHT, + notificate=False, + ) def _add_directory_list_item(self, dir_data: dict) -> None: - """Method that adds directories to the list""" + """Add a directory entry to the list.""" dir_name = dir_data.get("dirname", "") if not dir_name: return + + # Choose appropriate icon + icon = self._icons.get("folder") + if self._is_usb_directory(self._curr_dir, dir_name): + icon = self._icons.get("usb") + item = ListItem( text=str(dir_name), - left_icon=self.path.get("folderIcon"), + left_icon=icon, right_text="", + right_icon=None, selected=False, callback=None, allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=80, + _lfontsize=self.LEFT_FONT_SIZE, + _rfontsize=self.RIGHT_FONT_SIZE, + height=self.ITEM_HEIGHT, ) - self.model.add_item(item) + self._model.add_item(item) def _add_back_folder_entry(self) -> None: - """Method to insert in the list the "Go back" item""" - go_back_path = os.path.dirname(self.curr_dir) - if go_back_path == "/": - go_back_path = "" - + """Add the 'Go Back' navigation entry.""" item = ListItem( text="Go Back", right_text="", right_icon=None, - left_icon=self.path.get("back_folder"), + left_icon=self._icons.get("back_folder"), callback=None, selected=False, allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=80, + _lfontsize=self.LEFT_FONT_SIZE, + _rfontsize=self.RIGHT_FONT_SIZE, + height=self.ITEM_HEIGHT, notificate=False, ) - self.model.add_item(item) + self._model.add_item(item) - @QtCore.pyqtSlot(str, str, name="on-goback-dir") - def _on_goback_dir(self, directory) -> None: - """Go back behaviour""" - self.request_dir_info[str].emit(directory) - self.curr_dir = directory - - def _add_file_list_item(self, file_data_item) -> None: - """Request file information and metadata to create filelist""" + def _request_file_info(self, file_data_item: dict) -> None: + """Request metadata for a file item using retry mechanism.""" if not file_data_item: return - name = ( - file_data_item["path"] - if "path" in file_data_item.keys() - else file_data_item["filename"] - ) - if not name.endswith(".gcode"): + name = file_data_item.get("path", file_data_item.get("filename", "")) + if not name or not name.lower().endswith(self.GCODE_EXTENSION): + return + + # Build full path + file_path = self._build_filepath(name) + + # Track pending request + self._pending_metadata_requests.add(file_path) + + # Use retry mechanism (first attempt uses get_gcode_metadata) + self.retry_metadata_request(file_path) + + def _on_file_item_clicked(self, filename: str) -> None: + """Handle file item click.""" + clean_filename = filename.removeprefix("/") + file_data = self._files_data.get(clean_filename, {}) + self.file_selected.emit(clean_filename, file_data) + + def _on_dir_item_clicked(self, directory: str) -> None: + """Handle directory item click.""" + if self._pending_action: return - file_path = ( - name if not self.curr_dir else str(self.curr_dir + "/" + name) - ).removeprefix("/") - self.request_file_metadata.emit(file_path.removeprefix("/")) - self.request_file_info.emit(file_path.removeprefix("/")) + self._curr_dir = self._curr_dir + directory + self.request_dir_info[str].emit(self._curr_dir) + self._pending_action = True + + def _on_go_back_dir(self, directory: str) -> None: + """Handle go back navigation.""" + self.request_dir_info[str].emit(directory) + self._curr_dir = directory + + def _show_placeholder(self) -> None: + """Show the 'No Files found' placeholder.""" + self._scrollbar.hide() + self._list_widget.hide() + self._label.show() + + def _hide_placeholder(self) -> None: + """Hide the placeholder and show the list.""" + self._label.hide() + self._list_widget.show() + + def _check_empty_state(self) -> None: + """Check if list is empty and show placeholder if needed.""" + is_root = not self._curr_dir or self._curr_dir == "/" - def _add_placeholder(self) -> None: - """Shows placeholder when no items exist""" - self.scrollbar.hide() - self.listWidget.hide() - self.label.show() + if self._model.rowCount() == 0 and is_root: + self._show_placeholder() + elif self._model.rowCount() == 0 and not is_root: + # In subdirectory with no files - just show "Go Back" + self._add_back_folder_entry() - def _handle_scrollbar(self, value): - """Updates scrollbar value""" - self.scrollbar.blockSignals(True) - self.scrollbar.setValue(value) - self.scrollbar.blockSignals(False) + def _model_contains_item(self, text: str) -> bool: + """Check if model contains an item with the given text.""" + for i in range(self._model.rowCount()): + index = self._model.index(i) + item = self._model.data(index, QtCore.Qt.ItemDataRole.UserRole) + if item and item.text == text: + return True + return False + + def _handle_scrollbar_value_changed(self, value: int) -> None: + """Sync scrollbar with list widget.""" + self._scrollbar.blockSignals(True) + self._scrollbar.setValue(value) + self._scrollbar.blockSignals(False) def _setup_scrollbar(self) -> None: - """Syncs the scrollbar with the list size""" - self.scrollbar.setMinimum(self.listWidget.verticalScrollBar().minimum()) - self.scrollbar.setMaximum(self.listWidget.verticalScrollBar().maximum()) - self.scrollbar.setPageStep(self.listWidget.verticalScrollBar().pageStep()) - self.scrollbar.show() - - def _setupUI(self): - sizePolicy = QtWidgets.QSizePolicy( + """Configure scrollbar to match list size.""" + list_scrollbar = self._list_widget.verticalScrollBar() + self._scrollbar.setMinimum(list_scrollbar.minimum()) + self._scrollbar.setMaximum(list_scrollbar.maximum()) + self._scrollbar.setPageStep(list_scrollbar.pageStep()) + + if list_scrollbar.maximum() > 0: + self._scrollbar.show() + else: + self._scrollbar.hide() + + @staticmethod + def _get_basename(path: str) -> str: + """ + Get the basename of a path without using os.path. + Works with paths from Moonraker (forward slashes only). + """ + if not path: + return "" + # Remove trailing slashes and get last component + path = path.rstrip("/") + if "/" in path: + return path.rsplit("/", 1)[-1] + return path + + @staticmethod + def _get_parent_directory(path: str) -> str: + """ + Get the parent directory of a path without using os.path. + Works with paths from Moonraker (forward slashes only). + """ + if not path: + return "" + path = path.removeprefix("/").rstrip("/") + if "/" in path: + return path.rsplit("/", 1)[0] + return "" + + def _build_filepath(self, filename: str) -> str: + """Build full file path from current directory and filename.""" + filename = filename.removeprefix("/") + if self._curr_dir: + curr = self._curr_dir.removeprefix("/") + return f"{curr}/{filename}" + return filename + + @staticmethod + def _is_usb_directory(parent_dir: str, directory_name: str) -> bool: + """Check if directory is a USB mount in the root.""" + return parent_dir == "" and directory_name.startswith("USB-") + + @staticmethod + def _format_print_time(seconds: int) -> str: + """Format print time in human-readable form.""" + if seconds <= 0: + return "Unknown time" + if seconds < 60: + return f"{seconds}s" + + days, hours, minutes, _ = helper_methods.estimate_print_time(seconds) + + if days > 0: + return f"{days}d {hours}h {minutes}m" + elif hours > 0: + return f"{hours}h {minutes}m" + else: + return f"{minutes}m" + + def _get_display_name(self, filename: str) -> str: + """Get display name from filename (without path and extension).""" + basename = self._get_basename(filename) + name = helper_methods.get_file_name(basename) + + # Remove .gcode extension + if name.lower().endswith(self.GCODE_EXTENSION): + name = name[:-6] + + return name + + def _load_icons(self) -> None: + """Load all icons into cache.""" + self._icons = { + "back_folder": QtGui.QPixmap(self.ICON_PATHS["back_folder"]), + "folder": QtGui.QPixmap(self.ICON_PATHS["folder"]), + "right_arrow": QtGui.QPixmap(self.ICON_PATHS["right_arrow"]), + "usb": QtGui.QPixmap(self.ICON_PATHS["usb"]), + } + + def _connect_signals(self) -> None: + """Connect internal signals.""" + # Button connections + self._reload_button.clicked.connect( + lambda: self.request_dir_info[str].emit(self._curr_dir) + ) + self.back_btn.clicked.connect(self.reset_dir) + + # List widget connections + self._list_widget.verticalScrollBar().valueChanged.connect( + self._handle_scrollbar_value_changed + ) + self._scrollbar.valueChanged.connect(self._handle_scrollbar_value_changed) + self._scrollbar.valueChanged.connect( + lambda value: self._list_widget.verticalScrollBar().setValue(value) + ) + + # Delegate connections + self._entry_delegate.item_selected.connect(self._on_item_selected) + + def _setup_ui(self) -> None: + """Set up the widget UI.""" + # Size policy + size_policy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, ) - sizePolicy.setHorizontalStretch(1) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) - self.setSizePolicy(sizePolicy) + size_policy.setHorizontalStretch(1) + size_policy.setVerticalStretch(1) + self.setSizePolicy(size_policy) self.setMinimumSize(QtCore.QSize(710, 400)) + + # Font font = QtGui.QFont() font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferAntialias) self.setFont(font) + + # Layout direction and style self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) self.setAutoFillBackground(False) - self.setStyleSheet("#file_page{\n background-color: transparent;\n}") - self.verticalLayout_5 = QtWidgets.QVBoxLayout(self) - self.verticalLayout_5.setObjectName("verticalLayout_5") - self.fp_header_layout = QtWidgets.QHBoxLayout() - self.fp_header_layout.setObjectName("fp_header_layout") + self.setStyleSheet("#file_page { background-color: transparent; }") + + # Main layout + main_layout = QtWidgets.QVBoxLayout(self) + main_layout.setObjectName("main_layout") + + # Header layout + header_layout = self._create_header_layout() + main_layout.addLayout(header_layout) + + # Separator line + line = QtWidgets.QFrame(parent=self) + line.setFrameShape(QtWidgets.QFrame.Shape.HLine) + line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + main_layout.addWidget(line) + + # Content layout + content_layout = self._create_content_layout() + main_layout.addLayout(content_layout) + + def _create_header_layout(self) -> QtWidgets.QHBoxLayout: + """Create the header with back and reload buttons.""" + layout = QtWidgets.QHBoxLayout() + layout.setObjectName("header_layout") + + # Back button self.back_btn = IconButton(parent=self) self.back_btn.setMinimumSize(QtCore.QSize(60, 60)) self.back_btn.setMaximumSize(QtCore.QSize(60, 60)) self.back_btn.setFlat(True) - self.back_btn.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") - ) + self.back_btn.setProperty("icon_pixmap", QtGui.QPixmap(self.ICON_PATHS["back"])) self.back_btn.setObjectName("back_btn") - self.fp_header_layout.addWidget( - self.back_btn, 0, QtCore.Qt.AlignmentFlag.AlignLeft - ) - self.ReloadButton = IconButton(parent=self) - self.ReloadButton.setMinimumSize(QtCore.QSize(60, 60)) - self.ReloadButton.setMaximumSize(QtCore.QSize(60, 60)) - self.ReloadButton.setFlat(True) - self.ReloadButton.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/refresh.svg") + layout.addWidget(self.back_btn, 0, QtCore.Qt.AlignmentFlag.AlignLeft) + + # Reload button + self._reload_button = IconButton(parent=self) + self._reload_button.setMinimumSize(QtCore.QSize(60, 60)) + self._reload_button.setMaximumSize(QtCore.QSize(60, 60)) + self._reload_button.setFlat(True) + self._reload_button.setProperty( + "icon_pixmap", QtGui.QPixmap(self.ICON_PATHS["refresh"]) ) - self.ReloadButton.setObjectName("ReloadButton") - self.fp_header_layout.addWidget( - self.ReloadButton, 0, QtCore.Qt.AlignmentFlag.AlignRight + self._reload_button.setObjectName("reload_button") + layout.addWidget(self._reload_button, 0, QtCore.Qt.AlignmentFlag.AlignRight) + + return layout + + def _create_content_layout(self) -> QtWidgets.QHBoxLayout: + """Create the content area with list and scrollbar.""" + layout = QtWidgets.QHBoxLayout() + layout.setContentsMargins(0, 0, 0, 0) + layout.setObjectName("content_layout") + + # Placeholder label + font = QtGui.QFont() + font.setPointSize(25) + self._label = QtWidgets.QLabel("No Files found") + self._label.setFont(font) + self._label.setStyleSheet("color: gray;") + self._label.hide() + + # List widget + self._list_widget = self._create_list_widget() + + # Scrollbar + self._scrollbar = CustomScrollBar() + self._scrollbar.show() + + # Add widgets to layout + layout.addWidget( + self._label, + alignment=( + QtCore.Qt.AlignmentFlag.AlignHCenter + | QtCore.Qt.AlignmentFlag.AlignVCenter + ), ) - self.verticalLayout_5.addLayout(self.fp_header_layout) - self.line = QtWidgets.QFrame(parent=self) - self.line.setMinimumSize(QtCore.QSize(0, 0)) - self.line.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line.setObjectName("line") - self.verticalLayout_5.addWidget(self.line) - self.fp_content_layout = QtWidgets.QHBoxLayout() - self.fp_content_layout.setContentsMargins(0, 0, 0, 0) - self.fp_content_layout.setObjectName("fp_content_layout") - self.listWidget = QtWidgets.QListView(parent=self) - self.listWidget.setModel(self.model) - self.listWidget.setItemDelegate(self.entry_delegate) - self.listWidget.setSpacing(5) - self.listWidget.setProperty("showDropIndicator", False) - self.listWidget.setProperty("selectionMode", "NoSelection") - self.listWidget.setStyleSheet("background: transparent;") - self.listWidget.setDefaultDropAction(QtCore.Qt.DropAction.IgnoreAction) - self.listWidget.setUniformItemSizes(True) - self.listWidget.setObjectName("listWidget") - self.listWidget.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) - self.listWidget.setSelectionBehavior( + layout.addWidget(self._list_widget) + layout.addWidget(self._scrollbar) + + return layout + + def _create_list_widget(self) -> QtWidgets.QListView: + """Create and configure the list view widget.""" + list_widget = QtWidgets.QListView(parent=self) + list_widget.setModel(self._model) + list_widget.setItemDelegate(self._entry_delegate) + list_widget.setSpacing(5) + list_widget.setProperty("showDropIndicator", False) + list_widget.setProperty("selectionMode", "NoSelection") + list_widget.setStyleSheet("background: transparent;") + list_widget.setDefaultDropAction(QtCore.Qt.DropAction.IgnoreAction) + list_widget.setUniformItemSizes(True) + list_widget.setObjectName("list_widget") + list_widget.setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + list_widget.setSelectionBehavior( QtWidgets.QAbstractItemView.SelectionBehavior.SelectItems ) - self.listWidget.setHorizontalScrollBarPolicy( + list_widget.setHorizontalScrollBarPolicy( QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff ) - self.listWidget.setVerticalScrollMode( + list_widget.setVerticalScrollMode( QtWidgets.QAbstractItemView.ScrollMode.ScrollPerPixel ) - self.listWidget.setVerticalScrollBarPolicy( + list_widget.setVerticalScrollBarPolicy( QtCore.Qt.ScrollBarPolicy.ScrollBarAlwaysOff ) + list_widget.setEditTriggers( + QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers + ) + + # Enable touch gestures QtWidgets.QScroller.grabGesture( - self.listWidget, + list_widget, QtWidgets.QScroller.ScrollerGestureType.TouchGesture, ) QtWidgets.QScroller.grabGesture( - self.listWidget, + list_widget, QtWidgets.QScroller.ScrollerGestureType.LeftMouseButtonGesture, ) - self.listWidget.setEditTriggers( - QtWidgets.QAbstractItemView.EditTrigger.NoEditTriggers - ) - scroller_instance = QtWidgets.QScroller.scroller(self.listWidget) - scroller_props = scroller_instance.scrollerProperties() - scroller_props.setScrollMetric( + # Configure scroller properties + scroller = QtWidgets.QScroller.scroller(list_widget) + props = scroller.scrollerProperties() + props.setScrollMetric( QtWidgets.QScrollerProperties.ScrollMetric.DragVelocitySmoothingFactor, 0.05, ) - scroller_props.setScrollMetric( + props.setScrollMetric( QtWidgets.QScrollerProperties.ScrollMetric.DecelerationFactor, 0.4, ) - QtWidgets.QScroller.scroller(self.listWidget).setScrollerProperties( - scroller_props - ) - - font = QtGui.QFont() - font.setPointSize(25) - self.label = QtWidgets.QLabel("No Files found") - self.label.setFont(font) - self.label.setStyleSheet("color: gray;") - self.label.setMinimumSize( - QtCore.QSize(self.listWidget.width(), self.listWidget.height()) - ) + scroller.setScrollerProperties(props) - self.scrollbar = CustomScrollBar() - - self.fp_content_layout.addWidget( - self.label, - alignment=QtCore.Qt.AlignmentFlag.AlignHCenter - | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - self.fp_content_layout.addWidget(self.listWidget) - self.fp_content_layout.addWidget(self.scrollbar) - self.verticalLayout_5.addLayout(self.fp_content_layout) - self.scrollbar.show() - self.label.hide() - - self.path = { - "back_folder": QtGui.QPixmap(":/ui/media/btn_icons/back_folder.svg"), - "folderIcon": QtGui.QPixmap(":/ui/media/btn_icons/folderIcon.svg"), - "right_arrow": QtGui.QPixmap( - ":/arrow_icons/media/btn_icons/right_arrow.svg" - ), - } + return list_widget diff --git a/BlocksScreen/lib/ui/resources/icon_resources.qrc b/BlocksScreen/lib/ui/resources/icon_resources.qrc index 5239a1fd..f9d1f0a9 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources.qrc +++ b/BlocksScreen/lib/ui/resources/icon_resources.qrc @@ -91,6 +91,7 @@ media/btn_icons/unload_filament.svg
+ media/btn_icons/usb_icon.svg media/btn_icons/garbage-icon.svg media/btn_icons/back.svg media/btn_icons/refresh.svg diff --git a/BlocksScreen/lib/ui/resources/icon_resources_rc.py b/BlocksScreen/lib/ui/resources/icon_resources_rc.py index a7aca514..3bbc3133 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/icon_resources_rc.py @@ -2,7 +2,7 @@ # Resource object code # -# Created by: The Resource Compiler for PyQt5 (Qt v5.15.14) +# Created by: The Resource Compiler for PyQt5 (Qt v5.15.15) # # WARNING! All changes made in this file will be lost! @@ -24751,6 +24751,101 @@ \x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x33\x36\x39\x2e\ \x33\x36\x20\x37\x30\x35\x2e\x33\x38\x29\x20\x72\x6f\x74\x61\x74\ \x65\x28\x2d\x39\x30\x29\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x00\x00\x05\xc4\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\ +\x61\x79\x65\x72\x5f\x31\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\ +\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\ +\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\ +\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x76\ +\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x36\x30\x30\x20\ +\x36\x30\x30\x22\x3e\x0a\x20\x20\x3c\x64\x65\x66\x73\x3e\x0a\x20\ +\x20\x20\x20\x3c\x73\x74\x79\x6c\x65\x3e\x0a\x20\x20\x20\x20\x20\ +\x20\x2e\x63\x6c\x73\x2d\x31\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x66\x69\x6c\x6c\x3a\x20\x23\x65\x30\x65\x30\x64\x66\x3b\ +\x0a\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x73\ +\x74\x79\x6c\x65\x3e\x0a\x20\x20\x3c\x2f\x64\x65\x66\x73\x3e\x0a\ +\x20\x20\x3c\x72\x65\x63\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ +\x6c\x73\x2d\x31\x22\x20\x78\x3d\x22\x34\x35\x36\x2e\x37\x22\x20\ +\x79\x3d\x22\x32\x35\x35\x2e\x39\x31\x22\x20\x77\x69\x64\x74\x68\ +\x3d\x22\x32\x31\x2e\x30\x31\x22\x20\x68\x65\x69\x67\x68\x74\x3d\ +\x22\x33\x30\x2e\x37\x35\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\ +\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x2d\x2e\x38\ +\x20\x31\x2e\x33\x39\x29\x20\x72\x6f\x74\x61\x74\x65\x28\x2d\x2e\ +\x31\x37\x29\x22\x2f\x3e\x0a\x20\x20\x3c\x72\x65\x63\x74\x20\x63\ +\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x78\x3d\x22\ +\x34\x35\x36\x2e\x37\x34\x22\x20\x79\x3d\x22\x33\x31\x32\x2e\x39\ +\x39\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x32\x31\x2e\x30\x36\x22\ +\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x33\x30\x2e\x37\x33\x22\x20\ +\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ +\x6c\x61\x74\x65\x28\x2d\x2e\x36\x39\x20\x2e\x39\x38\x29\x20\x72\ +\x6f\x74\x61\x74\x65\x28\x2d\x2e\x31\x32\x29\x22\x2f\x3e\x0a\x20\ +\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ +\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x35\x34\x38\x2e\x31\x32\x2c\ +\x33\x38\x35\x2e\x38\x37\x6c\x2d\x2e\x31\x39\x2d\x31\x37\x32\x2e\ +\x33\x2d\x31\x31\x34\x2e\x38\x31\x2e\x31\x63\x2e\x35\x36\x2d\x32\ +\x33\x2e\x39\x31\x2d\x31\x36\x2e\x33\x2d\x34\x32\x2e\x30\x33\x2d\ +\x34\x30\x2e\x30\x34\x2d\x34\x31\x2e\x38\x34\x6c\x2d\x33\x30\x34\ +\x2e\x34\x31\x2e\x34\x37\x63\x2d\x32\x33\x2e\x30\x31\x2e\x30\x33\ +\x2d\x33\x36\x2e\x38\x35\x2c\x32\x30\x2e\x34\x32\x2d\x33\x36\x2e\ +\x37\x39\x2c\x34\x31\x2e\x32\x32\x6c\x2e\x34\x38\x2c\x31\x37\x37\ +\x2e\x39\x37\x68\x30\x63\x2e\x30\x36\x2c\x32\x31\x2e\x38\x35\x2c\ +\x31\x39\x2e\x30\x33\x2c\x33\x36\x2e\x37\x31\x2c\x33\x39\x2e\x31\ +\x39\x2c\x33\x36\x2e\x36\x39\x6c\x33\x30\x33\x2e\x31\x36\x2d\x2e\ +\x33\x31\x63\x32\x33\x2e\x31\x2d\x2e\x35\x33\x2c\x33\x39\x2e\x32\ +\x32\x2d\x31\x38\x2e\x35\x37\x2c\x33\x38\x2e\x35\x38\x2d\x34\x31\ +\x2e\x38\x36\x6c\x31\x31\x34\x2e\x38\x34\x2d\x2e\x31\x34\x5a\x4d\ +\x31\x30\x39\x2e\x30\x38\x2c\x33\x34\x31\x2e\x33\x31\x63\x2e\x30\ +\x31\x2c\x38\x2e\x34\x35\x2d\x34\x2e\x39\x37\x2c\x31\x34\x2e\x31\ +\x38\x2d\x31\x32\x2e\x30\x38\x2c\x31\x34\x2e\x36\x31\x2d\x36\x2e\ +\x39\x35\x2e\x34\x32\x2d\x31\x33\x2e\x38\x32\x2d\x34\x2e\x38\x39\ +\x2d\x31\x33\x2e\x38\x34\x2d\x31\x32\x2e\x36\x32\x6c\x2d\x2e\x31\ +\x34\x2d\x38\x35\x2e\x30\x31\x63\x2d\x2e\x30\x31\x2d\x37\x2e\x37\ +\x38\x2c\x35\x2e\x31\x37\x2d\x31\x33\x2e\x35\x36\x2c\x31\x32\x2e\ +\x35\x35\x2d\x31\x33\x2e\x39\x33\x2c\x37\x2e\x35\x31\x2d\x2e\x33\ +\x38\x2c\x31\x33\x2e\x34\x33\x2c\x35\x2e\x36\x39\x2c\x31\x33\x2e\ +\x34\x33\x2c\x31\x33\x2e\x38\x33\x6c\x2e\x30\x38\x2c\x38\x33\x2e\ +\x31\x31\x68\x2d\x2e\x30\x31\x5a\x4d\x33\x36\x31\x2e\x38\x34\x2c\ +\x33\x31\x38\x2e\x34\x34\x6c\x2d\x2e\x33\x34\x2d\x31\x33\x2e\x32\ +\x2d\x31\x31\x32\x2e\x34\x36\x2e\x35\x2c\x32\x34\x2e\x36\x36\x2c\ +\x32\x35\x2e\x35\x33\x2c\x34\x32\x2e\x32\x2d\x2e\x31\x39\x2e\x33\ +\x33\x2d\x31\x31\x2e\x35\x35\x2c\x33\x32\x2e\x39\x2d\x2e\x32\x2e\ +\x32\x32\x2c\x33\x33\x2e\x30\x36\x2d\x33\x32\x2e\x39\x33\x2e\x32\ +\x38\x2d\x2e\x33\x38\x2d\x31\x31\x2e\x35\x33\x2d\x34\x33\x2e\x30\ +\x34\x2e\x32\x32\x63\x2d\x32\x2e\x30\x31\x2e\x30\x31\x2d\x35\x2e\ +\x34\x35\x2d\x31\x2e\x38\x37\x2d\x36\x2e\x39\x37\x2d\x33\x2e\x34\ +\x32\x6c\x2d\x33\x31\x2e\x33\x39\x2d\x33\x32\x2e\x31\x33\x2d\x34\ +\x34\x2e\x35\x39\x2e\x31\x37\x63\x2d\x33\x2e\x30\x39\x2c\x31\x33\ +\x2e\x31\x2d\x31\x34\x2e\x36\x31\x2c\x32\x30\x2e\x38\x34\x2d\x32\ +\x36\x2e\x38\x33\x2c\x31\x39\x2e\x35\x37\x2d\x31\x32\x2e\x35\x32\ +\x2d\x31\x2e\x33\x2d\x32\x31\x2e\x38\x2d\x31\x31\x2e\x38\x33\x2d\ +\x32\x32\x2e\x30\x36\x2d\x32\x33\x2e\x39\x31\x2d\x2e\x32\x37\x2d\ +\x31\x32\x2e\x37\x38\x2c\x39\x2e\x31\x35\x2d\x32\x33\x2e\x34\x33\ +\x2c\x32\x31\x2e\x33\x2d\x32\x34\x2e\x39\x39\x2c\x31\x32\x2e\x36\ +\x36\x2d\x31\x2e\x36\x32\x2c\x32\x34\x2e\x31\x34\x2c\x36\x2c\x32\ +\x37\x2e\x35\x32\x2c\x31\x39\x2e\x31\x38\x6c\x32\x31\x2e\x31\x34\ +\x2e\x30\x38\x2c\x33\x31\x2e\x37\x36\x2d\x33\x33\x2e\x38\x37\x63\ +\x31\x2e\x35\x35\x2d\x31\x2e\x36\x35\x2c\x35\x2e\x30\x32\x2d\x32\ +\x2e\x38\x32\x2c\x37\x2e\x31\x39\x2d\x32\x2e\x38\x32\x6c\x32\x39\ +\x2e\x31\x32\x2d\x2e\x30\x35\x63\x33\x2e\x32\x39\x2d\x37\x2e\x38\ +\x39\x2c\x31\x30\x2d\x31\x32\x2e\x37\x2c\x31\x38\x2e\x33\x2d\x31\ +\x31\x2e\x37\x2c\x37\x2e\x36\x35\x2e\x39\x32\x2c\x31\x34\x2e\x30\ +\x39\x2c\x37\x2e\x32\x36\x2c\x31\x34\x2e\x36\x33\x2c\x31\x35\x2e\ +\x34\x2e\x35\x33\x2c\x37\x2e\x39\x31\x2d\x34\x2e\x36\x31\x2c\x31\ +\x35\x2e\x34\x35\x2d\x31\x32\x2e\x32\x2c\x31\x37\x2e\x33\x34\x2d\ +\x38\x2e\x30\x36\x2c\x32\x2e\x30\x31\x2d\x31\x36\x2e\x36\x38\x2d\ +\x2e\x38\x33\x2d\x32\x30\x2e\x32\x2d\x31\x30\x2e\x38\x35\x6c\x2d\ +\x32\x39\x2e\x38\x35\x2d\x2e\x30\x39\x2d\x32\x34\x2e\x31\x32\x2c\ +\x32\x36\x2e\x34\x39\x2c\x31\x33\x35\x2e\x32\x36\x2d\x2e\x34\x39\ +\x2e\x37\x38\x2d\x31\x33\x2e\x31\x33\x2c\x33\x31\x2e\x34\x33\x2c\ +\x31\x38\x2e\x30\x32\x2d\x33\x31\x2e\x34\x31\x2c\x31\x38\x2e\x33\ +\x31\x5a\x4d\x34\x33\x32\x2e\x38\x33\x2c\x32\x33\x32\x2e\x34\x37\ +\x6c\x39\x36\x2e\x33\x38\x2d\x2e\x31\x36\x2e\x32\x32\x2c\x31\x33\ +\x34\x2e\x37\x36\x2d\x39\x36\x2e\x33\x38\x2e\x31\x36\x2d\x2e\x32\ +\x32\x2d\x31\x33\x34\x2e\x37\x36\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\ +\x76\x67\x3e\ \x00\x00\x03\x4a\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -27117,6 +27212,10 @@ \x09\xcb\x31\x47\ \x00\x4c\ \x00\x43\x00\x44\x00\x5f\x00\x73\x00\x65\x00\x74\x00\x74\x00\x69\x00\x6e\x00\x67\x00\x73\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0c\ +\x09\xf2\x51\x27\ +\x00\x75\ +\x00\x73\x00\x62\x00\x5f\x00\x69\x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x12\ \x0a\x4f\x00\xc7\ \x00\x73\ @@ -27376,7 +27475,7 @@ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ \x00\x00\x13\x14\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xfb\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\xa3\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x29\x00\x00\x00\xa3\ \x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x94\xc1\ \x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x75\ \x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\x9a\ @@ -27396,27 +27495,28 @@ \x00\x00\x15\x04\x00\x00\x00\x00\x00\x01\x00\x05\xea\x6a\ \x00\x00\x15\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xee\x2d\ \x00\x00\x15\x42\x00\x00\x00\x00\x00\x01\x00\x05\xf7\xe1\ -\x00\x00\x15\x6c\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x2f\ -\x00\x00\x15\x8e\x00\x00\x00\x00\x00\x01\x00\x05\xfe\xbd\ -\x00\x00\x15\xb4\x00\x00\x00\x00\x00\x01\x00\x06\x03\x5e\ -\x00\x00\x15\xc8\x00\x00\x00\x00\x00\x01\x00\x06\x0d\x30\ -\x00\x00\x15\xf4\x00\x00\x00\x00\x00\x01\x00\x06\x12\x7a\ -\x00\x00\x16\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x18\x81\ -\x00\x00\x16\x32\x00\x00\x00\x00\x00\x01\x00\x06\x19\x65\ -\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x1b\xae\ -\x00\x00\x16\x74\x00\x00\x00\x00\x00\x01\x00\x06\x22\x5e\ -\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x25\xa2\ -\x00\x00\x16\xa8\x00\x00\x00\x00\x00\x01\x00\x06\x26\xce\ -\x00\x00\x16\xc2\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x8f\ -\x00\x00\x16\xe4\x00\x00\x00\x00\x00\x01\x00\x06\x2d\xb1\ -\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\xa4\ -\x00\x00\x17\x22\x00\x00\x00\x00\x00\x01\x00\x06\x36\xa8\ -\x00\x00\x17\x44\x00\x00\x00\x00\x00\x01\x00\x06\x37\xc9\ -\x00\x00\x17\x64\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x9d\ -\x00\x00\x17\x92\x00\x00\x00\x00\x00\x01\x00\x06\x43\x09\ -\x00\x00\x17\xb6\x00\x00\x00\x00\x00\x01\x00\x06\x4a\xc9\ -\x00\x00\x17\xda\x00\x00\x00\x00\x00\x01\x00\x06\x4f\xe4\ -\x00\x00\x18\x02\x00\x00\x00\x00\x00\x01\x00\x06\x51\x37\ +\x00\x00\x15\x60\x00\x00\x00\x00\x00\x01\x00\x05\xfd\xa9\ +\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x06\x00\xf7\ +\x00\x00\x15\xac\x00\x00\x00\x00\x00\x01\x00\x06\x04\x85\ +\x00\x00\x15\xd2\x00\x00\x00\x00\x00\x01\x00\x06\x09\x26\ +\x00\x00\x15\xe6\x00\x00\x00\x00\x00\x01\x00\x06\x12\xf8\ +\x00\x00\x16\x12\x00\x00\x00\x00\x00\x01\x00\x06\x18\x42\ +\x00\x00\x16\x3a\x00\x00\x00\x00\x00\x01\x00\x06\x1e\x49\ +\x00\x00\x16\x50\x00\x00\x00\x00\x00\x01\x00\x06\x1f\x2d\ +\x00\x00\x16\x7c\x00\x00\x00\x00\x00\x01\x00\x06\x21\x76\ +\x00\x00\x16\x92\x00\x00\x00\x00\x00\x01\x00\x06\x28\x26\ +\x00\x00\x16\xae\x00\x00\x00\x00\x00\x01\x00\x06\x2b\x6a\ +\x00\x00\x16\xc6\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x96\ +\x00\x00\x16\xe0\x00\x00\x00\x00\x00\x01\x00\x06\x32\x57\ +\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\x79\ +\x00\x00\x17\x20\x00\x00\x00\x00\x00\x01\x00\x06\x39\x6c\ +\x00\x00\x17\x40\x00\x00\x00\x00\x00\x01\x00\x06\x3c\x70\ +\x00\x00\x17\x62\x00\x00\x00\x00\x00\x01\x00\x06\x3d\x91\ +\x00\x00\x17\x82\x00\x00\x00\x00\x00\x01\x00\x06\x40\x65\ +\x00\x00\x17\xb0\x00\x00\x00\x00\x00\x01\x00\x06\x48\xd1\ +\x00\x00\x17\xd4\x00\x00\x00\x00\x00\x01\x00\x06\x50\x91\ +\x00\x00\x17\xf8\x00\x00\x00\x00\x00\x01\x00\x06\x55\xac\ +\x00\x00\x18\x20\x00\x00\x00\x00\x00\x01\x00\x06\x56\xff\ " qt_resource_struct_v2 = b"\ @@ -27533,9 +27633,9 @@ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x38\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x05\xfc\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ -\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ +\x00\x00\x01\x9c\x48\x63\x91\xdd\ \x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x41\x71\ -\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ +\x00\x00\x01\x9c\x48\x63\x91\xdd\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3b\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x44\ @@ -27681,9 +27781,9 @@ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x10\x00\x00\x00\x82\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0f\x50\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xfb\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ \x00\x00\x0f\x70\x00\x00\x00\x00\x00\x01\x00\x04\xb0\x94\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ \x00\x00\x0f\x90\x00\x01\x00\x00\x00\x01\x00\x04\xb6\xbb\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ \x00\x00\x0f\xb4\x00\x00\x00\x00\x00\x01\x00\x04\xc2\x0c\ @@ -27693,25 +27793,25 @@ \x00\x00\x0f\xf2\x00\x00\x00\x00\x00\x01\x00\x04\xda\xb1\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ \x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x32\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ \x00\x00\x10\x36\x00\x00\x00\x00\x00\x01\x00\x04\xe9\xb6\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ \x00\x00\x10\x56\x00\x00\x00\x00\x00\x01\x00\x04\xef\x4f\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ \x00\x00\x10\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xf9\x18\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ \x00\x00\x10\x9e\x00\x00\x00\x00\x00\x01\x00\x04\xfe\xb1\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ \x00\x00\x10\xd2\x00\x00\x00\x00\x00\x01\x00\x05\x09\x25\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x10\xee\x00\x00\x00\x00\x00\x01\x00\x05\x0f\x60\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ \x00\x00\x11\x22\x00\x00\x00\x00\x00\x01\x00\x05\x19\xda\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ \x00\x00\x11\x56\x00\x00\x00\x00\x00\x01\x00\x05\x24\x39\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ \x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x84\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ +\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x94\ @@ -27744,7 +27844,7 @@ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\xa3\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x29\x00\x00\x00\xa3\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x94\xc1\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ @@ -27759,7 +27859,7 @@ \x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xac\x9c\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xad\x8c\ -\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ +\x00\x00\x01\x9c\x48\x63\x91\xdd\ \x00\x00\x14\x02\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xb5\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ \x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb7\xec\ @@ -27773,7 +27873,7 @@ \x00\x00\x14\x90\x00\x00\x00\x00\x00\x01\x00\x05\xda\xf2\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x14\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xde\x41\ -\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ +\x00\x00\x01\x9c\x48\x63\x91\xdd\ \x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x62\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ \x00\x00\x14\xf0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x59\ @@ -27783,48 +27883,50 @@ \x00\x00\x15\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xee\x2d\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ \x00\x00\x15\x42\x00\x00\x00\x00\x00\x01\x00\x05\xf7\xe1\ +\x00\x00\x01\x9c\xb3\x48\x8b\xb6\ +\x00\x00\x15\x60\x00\x00\x00\x00\x00\x01\x00\x05\xfd\xa9\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x6c\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x2f\ +\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x06\x00\xf7\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x8e\x00\x00\x00\x00\x00\x01\x00\x05\xfe\xbd\ +\x00\x00\x15\xac\x00\x00\x00\x00\x00\x01\x00\x06\x04\x85\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x15\xb4\x00\x00\x00\x00\x00\x01\x00\x06\x03\x5e\ +\x00\x00\x15\xd2\x00\x00\x00\x00\x00\x01\x00\x06\x09\x26\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\xc8\x00\x00\x00\x00\x00\x01\x00\x06\x0d\x30\ +\x00\x00\x15\xe6\x00\x00\x00\x00\x00\x01\x00\x06\x12\xf8\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x15\xf4\x00\x00\x00\x00\x00\x01\x00\x06\x12\x7a\ +\x00\x00\x16\x12\x00\x00\x00\x00\x00\x01\x00\x06\x18\x42\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x18\x81\ +\x00\x00\x16\x3a\x00\x00\x00\x00\x00\x01\x00\x06\x1e\x49\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x32\x00\x00\x00\x00\x00\x01\x00\x06\x19\x65\ +\x00\x00\x16\x50\x00\x00\x00\x00\x00\x01\x00\x06\x1f\x2d\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x1b\xae\ +\x00\x00\x16\x7c\x00\x00\x00\x00\x00\x01\x00\x06\x21\x76\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x16\x74\x00\x00\x00\x00\x00\x01\x00\x06\x22\x5e\ +\x00\x00\x16\x92\x00\x00\x00\x00\x00\x01\x00\x06\x28\x26\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x25\xa2\ +\x00\x00\x16\xae\x00\x00\x00\x00\x00\x01\x00\x06\x2b\x6a\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\xa8\x00\x00\x00\x00\x00\x01\x00\x06\x26\xce\ +\x00\x00\x16\xc6\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x96\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\xc2\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x8f\ +\x00\x00\x16\xe0\x00\x00\x00\x00\x00\x01\x00\x06\x32\x57\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\xe4\x00\x00\x00\x00\x00\x01\x00\x06\x2d\xb1\ +\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\x79\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\xa4\ +\x00\x00\x17\x20\x00\x00\x00\x00\x00\x01\x00\x06\x39\x6c\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\x22\x00\x00\x00\x00\x00\x01\x00\x06\x36\xa8\ +\x00\x00\x17\x40\x00\x00\x00\x00\x00\x01\x00\x06\x3c\x70\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x44\x00\x00\x00\x00\x00\x01\x00\x06\x37\xc9\ +\x00\x00\x17\x62\x00\x00\x00\x00\x00\x01\x00\x06\x3d\x91\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x64\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x9d\ +\x00\x00\x17\x82\x00\x00\x00\x00\x00\x01\x00\x06\x40\x65\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\x92\x00\x00\x00\x00\x00\x01\x00\x06\x43\x09\ +\x00\x00\x17\xb0\x00\x00\x00\x00\x00\x01\x00\x06\x48\xd1\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x17\xb6\x00\x00\x00\x00\x00\x01\x00\x06\x4a\xc9\ +\x00\x00\x17\xd4\x00\x00\x00\x00\x00\x01\x00\x06\x50\x91\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\xda\x00\x00\x00\x00\x00\x01\x00\x06\x4f\xe4\ +\x00\x00\x17\xf8\x00\x00\x00\x00\x00\x01\x00\x06\x55\xac\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x18\x02\x00\x00\x00\x00\x00\x01\x00\x06\x51\x37\ +\x00\x00\x18\x20\x00\x00\x00\x00\x00\x01\x00\x06\x56\xff\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ " diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/usb_icon.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/usb_icon.svg new file mode 100644 index 00000000..3ea01599 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/usb_icon.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/utils/list_model.py b/BlocksScreen/lib/utils/list_model.py index a622f094..d29128f8 100644 --- a/BlocksScreen/lib/utils/list_model.py +++ b/BlocksScreen/lib/utils/list_model.py @@ -56,6 +56,34 @@ def add_item(self, item: ListItem) -> None: self.entries.append(item) self.endInsertRows() + def remove_item_by_text(self, text: str) -> bool: + """Remove item from model by its text value. + + Args: + text: The text value of the item to remove. + + Returns: + True if item was found and removed, False otherwise. + """ + for i, item in enumerate(self.entries): + if item.text == text: + self.beginRemoveRows(QtCore.QModelIndex(), i, i) + self.entries.pop(i) + self.endRemoveRows() + return True + return False + + def insert_item(self, position: int, item: ListItem) -> None: + """Insert item at a specific position in the model.""" + if position < 0: + position = 0 + if position > len(self.entries): + position = len(self.entries) + + self.beginInsertRows(QtCore.QModelIndex(), position, position) + self.entries.insert(position, item) + self.endInsertRows() + def flags(self, index) -> QtCore.Qt.ItemFlag: """Models item flags, re-implemented method""" item = self.entries[index.row()] From 1216fb8f7dcb945c93caae422abc0fe22cdef876 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Tue, 3 Mar 2026 13:49:31 +0000 Subject: [PATCH 53/70] Bugfix/filament loadscreen (#179) * ADD: reset variable on show * Bugfix: fixed loadpage showing --------- Co-authored-by: Roberto Co-authored-by: Hugo Costa --- BlocksScreen/lib/panels/filamentTab.py | 14 +++++++------- BlocksScreen/lib/panels/widgets/numpadPage.py | 4 ++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/BlocksScreen/lib/panels/filamentTab.py b/BlocksScreen/lib/panels/filamentTab.py index 4b368bd8..04fc5ef4 100644 --- a/BlocksScreen/lib/panels/filamentTab.py +++ b/BlocksScreen/lib/panels/filamentTab.py @@ -147,14 +147,14 @@ def on_extruder_update( @QtCore.pyqtSlot(bool, name="on_load_filament") def on_load_filament(self, status: bool): """Handle load filament object updated""" - if self.loadignore: - self.loadignore = False - return if not self.isVisible: return + if self.loadignore: + return if status: self.call_load_panel.emit(True, "Loading Filament") else: + self.loadignore = True self.target_temp = 0 self.call_load_panel.emit(False, "") self._filament_state = self.FilamentStates.LOADED @@ -163,14 +163,14 @@ def on_load_filament(self, status: bool): @QtCore.pyqtSlot(bool, name="on_unload_filament") def on_unload_filament(self, status: bool): """Handle unload filament object updated""" - if self.unloadignore: - self.unloadignore = False - return if not self.isVisible: return + if self.unloadignore: + return if status: self.call_load_panel.emit(True, "Unloading Filament") else: + self.unloadignore = True self.call_load_panel.emit(False, "") self.target_temp = 0 self._filament_state = self.FilamentStates.UNLOADED @@ -218,7 +218,7 @@ def unload_filament(self, toolhead: int = 0, temp: int = 220) -> None: return self.find_routine_objects() - self.unload_filament = False + self.unloadignore = False self.call_load_panel.emit(True, "Unloading Filament") self.run_gcode.emit(f"UNLOAD_FILAMENT TEMPERATURE={temp}") diff --git a/BlocksScreen/lib/panels/widgets/numpadPage.py b/BlocksScreen/lib/panels/widgets/numpadPage.py index 9674084d..b904645c 100644 --- a/BlocksScreen/lib/panels/widgets/numpadPage.py +++ b/BlocksScreen/lib/panels/widgets/numpadPage.py @@ -39,6 +39,10 @@ def __init__( self.numpad_back_btn.clicked.connect(self.back_button) self.start_glow_animation.connect(self.inserted_value.start_glow_animation) + def showEvent(self, a0: QtGui.QShowEvent | None) -> None: + self.firsttime = True + return super().showEvent(a0) + def value_inserted(self, value: str) -> None: """Handle number insertion on the numpad From b73445b2da636c31d644efe9f76c492bc64e41fb Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Tue, 3 Mar 2026 14:23:40 +0000 Subject: [PATCH 54/70] feat(network): NetworkManager refactor and NetworkControlWindow rewrite (#182) * feat(network): network window refactor and manager architecture * fixed icons generated file --- .github/workflows/dev-ci.yml | 4 + BlocksScreen/configfile.py | 67 +- BlocksScreen/lib/network.py | 1509 ----- BlocksScreen/lib/network/__init__.py | 60 + BlocksScreen/lib/network/manager.py | 368 ++ BlocksScreen/lib/network/models.py | 328 + BlocksScreen/lib/network/worker.py | 2755 +++++++++ BlocksScreen/lib/panels/mainWindow.py | 54 +- BlocksScreen/lib/panels/networkWindow.py | 5442 +++++++++-------- BlocksScreen/lib/qrcode_gen.py | 27 +- .../lib/ui/resources/icon_resources.qrc | 25 +- .../lib/ui/resources/icon_resources_rc.py | 1270 ++-- .../media/btn_icons/network/0bar_wifi.svg | 1 + .../btn_icons/network/0bar_wifi_protected.svg | 1 + .../btn_icons/{ => network}/1bar_wifi.svg | 0 .../btn_icons/network/1bar_wifi_protected.svg | 1 + .../btn_icons/{ => network}/2bar_wifi.svg | 0 .../btn_icons/network/2bar_wifi_protected.svg | 1 + .../btn_icons/{ => network}/3bar_wifi.svg | 0 .../btn_icons/network/3bar_wifi_protected.svg | 1 + .../media/btn_icons/network/4bar_wifi.svg | 1 + .../btn_icons/network/4bar_wifi_protected.svg | 1 + .../btn_icons/network/ethernet_connected.svg | 12 + .../media/btn_icons/network/static_ip.svg | 12 + .../resources/media/topbar/internet_cable.svg | 1 - .../lib/ui/resources/top_bar_resources.qrc | 6 - .../lib/ui/resources/top_bar_resources_rc.py | 4870 +++++++-------- BlocksScreen/lib/utils/blocks_label.py | 11 +- BlocksScreen/lib/utils/list_model.py | 195 +- pytest.ini | 32 + scripts/requirements-dev.txt | 4 + scripts/requirements.txt | 9 +- tests/conftest.py | 5 + tests/network/conftest.py | 730 +++ tests/network/test_manager_unit.py | 621 ++ tests/network/test_models_unit.py | 707 +++ tests/network/test_network_ui.py | 1963 ++++++ tests/network/test_sdbus_integration.py | 421 ++ tests/network/test_worker_unit.py | 3091 ++++++++++ tests/util/test_list_model_unit.py | 77 + tests/util/test_qrcode_gen_unit.py | 66 + 41 files changed, 17330 insertions(+), 7419 deletions(-) delete mode 100644 BlocksScreen/lib/network.py create mode 100644 BlocksScreen/lib/network/__init__.py create mode 100644 BlocksScreen/lib/network/manager.py create mode 100644 BlocksScreen/lib/network/models.py create mode 100644 BlocksScreen/lib/network/worker.py create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/network/0bar_wifi.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/network/0bar_wifi_protected.svg rename BlocksScreen/lib/ui/resources/media/btn_icons/{ => network}/1bar_wifi.svg (100%) create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/network/1bar_wifi_protected.svg rename BlocksScreen/lib/ui/resources/media/btn_icons/{ => network}/2bar_wifi.svg (100%) create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/network/2bar_wifi_protected.svg rename BlocksScreen/lib/ui/resources/media/btn_icons/{ => network}/3bar_wifi.svg (100%) create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/network/3bar_wifi_protected.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/network/4bar_wifi.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/network/4bar_wifi_protected.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/network/ethernet_connected.svg create mode 100644 BlocksScreen/lib/ui/resources/media/btn_icons/network/static_ip.svg delete mode 100644 BlocksScreen/lib/ui/resources/media/topbar/internet_cable.svg create mode 100644 pytest.ini create mode 100644 tests/conftest.py create mode 100644 tests/network/conftest.py create mode 100644 tests/network/test_manager_unit.py create mode 100644 tests/network/test_models_unit.py create mode 100644 tests/network/test_network_ui.py create mode 100644 tests/network/test_sdbus_integration.py create mode 100644 tests/network/test_worker_unit.py create mode 100644 tests/util/test_list_model_unit.py create mode 100644 tests/util/test_qrcode_gen_unit.py diff --git a/.github/workflows/dev-ci.yml b/.github/workflows/dev-ci.yml index 556119c2..28f36966 100644 --- a/.github/workflows/dev-ci.yml +++ b/.github/workflows/dev-ci.yml @@ -28,6 +28,10 @@ jobs: cache: pip cache-dependency-path: scripts/requirements-dev.txt + - name: Install system dependencies + if: matrix.test-type == 'pytest' + run: sudo apt-get install -y libgl1 libglib2.0-0 libegl1 + - name: Install dependencies run: | echo "Installing dependencies" diff --git a/BlocksScreen/configfile.py b/BlocksScreen/configfile.py index 981ac4b2..9536fffd 100644 --- a/BlocksScreen/configfile.py +++ b/BlocksScreen/configfile.py @@ -56,11 +56,19 @@ class ConfigError(Exception): """Exception raised when Configfile errors exist""" def __init__(self, msg) -> None: + """Store the error message on both the exception and the ``msg`` attribute.""" super().__init__(msg) self.msg = msg class BlocksScreenConfig: + """Thread-safe wrapper around :class:`configparser.ConfigParser` with raw-text tracking. + + Maintains a ``raw_config`` list that mirrors the on-disk file so that + ``add_section``, ``add_option``, and ``update_option`` can write back + changes without losing comments or formatting. + """ + config = configparser.ConfigParser( allow_no_value=True, ) @@ -70,6 +78,7 @@ class BlocksScreenConfig: def __init__( self, configfile: typing.Union[str, pathlib.Path], section: str ) -> None: + """Initialise with the path to the config file and the default section name.""" self.configfile = pathlib.Path(configfile) self.section = section self.raw_config: typing.List[str] = [] @@ -77,9 +86,11 @@ def __init__( self.file_lock = threading.Lock() # Thread safety for future work def __getitem__(self, key: str) -> BlocksScreenConfig: + """Return a :class:`BlocksScreenConfig` for *key* section (same as ``get_section``).""" return self.get_section(key) def __contains__(self, key): + """Return True if *key* is a section in the underlying ConfigParser.""" return key in self.config def sections(self) -> typing.List[str]: @@ -193,12 +204,14 @@ def getboolean( ) def _find_section_index(self, section: str) -> int: + """Return the index of the ``[section]`` header line in ``raw_config``.""" try: return self.raw_config.index("[" + section + "]") except ValueError as e: raise configparser.Error(f'Section "{section}" does not exist: {e}') def _find_section_limits(self, section: str) -> typing.Tuple: + """Return ``(start_index, end_index)`` of *section* in ``raw_config``.""" try: section_start = self._find_section_index(section) buffer = self.raw_config[section_start:] @@ -212,6 +225,7 @@ def _find_section_limits(self, section: str) -> typing.Tuple: def _find_option_index( self, section: str, option: str ) -> typing.Union[Sentinel, int, None]: + """Return the index of the *option* line within *section* in ``raw_config``.""" try: start, end = self._find_section_limits(section) section_buffer = self.raw_config[start:][:end] @@ -289,6 +303,40 @@ def add_option( f'Unable to add "{option}" option to section "{section}": {e} ' ) + def update_option( + self, + section: str, + option: str, + value: typing.Any, + ) -> None: + """Update an existing option's value in both raw tracking and configparser.""" + try: + with self.file_lock: + if not self.config.has_section(section): + self.add_section(section) + + if not self.config.has_option(section, option): + self.add_option(section, option, str(value)) + return + + line_idx = self._find_option_line_index(section, option) + self.raw_config[line_idx] = f"{option}: {value}" + self.config.set(section, option, str(value)) + self.update_pending = True + except Exception as e: + logging.error( + f'Unable to update option "{option}" in section "{section}": {e}' + ) + + def _find_option_line_index(self, section: str, option: str) -> int: + """Find the index of an option line within a specific section.""" + start, end = self._find_section_limits(section) + opt_regex = re.compile(rf"^\s*{re.escape(option)}\s*[:=]") + for i in range(start + 1, end): + if opt_regex.match(self.raw_config[i]): + return i + raise configparser.Error(f'Option "{option}" not found in section "{section}"') + def save_configuration(self) -> None: """Save teh configuration to file""" try: @@ -319,6 +367,14 @@ def load_config(self): raise configparser.Error(f"Error loading configuration file: {e}") def _parse_file(self) -> typing.Tuple[typing.List[str], typing.Dict]: + """Read and normalise the config file into a raw line list and a nested dict. + + Strips comments, normalises ``=`` to ``:`` separators, deduplicates + sections/options, and ensures the buffer ends with an empty line. + + Returns: + A tuple of (raw_lines, dict_representation). + """ buffer = [] dict_buff: typing.Dict = {} curr_sec: typing.Union[Sentinel, str] = Sentinel.MISSING @@ -336,7 +392,7 @@ def _parse_file(self) -> typing.Tuple[typing.List[str], typing.Dict]: if not line: continue # remove leading and trailing white spaces - line = re.sub(r"\s*([:=])\s*", r"\1", line) + line = re.sub(r"\s*([:=])\s*", r"\1 ", line) line = re.sub(r"=", r":", line) # find the beginning of sections section_match = re.compile(r"[^\s]*\[([^]]+)\]") @@ -344,9 +400,10 @@ def _parse_file(self) -> typing.Tuple[typing.List[str], typing.Dict]: if match_sec: sec_name = re.sub(r"[\[*\]]", r"", line) if sec_name not in dict_buff.keys(): - buffer.extend( - [""] - ) # REFACTOR: Just add some line separation between sections + if buffer: + buffer.extend( + [""] + ) # REFACTOR: Just add some line separation between sections dict_buff.update({sec_name: {}}) curr_sec = sec_name else: @@ -388,4 +445,4 @@ def get_configparser() -> BlocksScreenConfig: if not config_object.has_section("server"): logging.error("Error loading configuration file for the application.") raise ConfigError("Section [server] is missing from configuration") - return BlocksScreenConfig(configfile=configfile, section="server") + return config_object diff --git a/BlocksScreen/lib/network.py b/BlocksScreen/lib/network.py deleted file mode 100644 index 61ea4078..00000000 --- a/BlocksScreen/lib/network.py +++ /dev/null @@ -1,1509 +0,0 @@ -import asyncio -import enum -import logging -import threading -import typing -from uuid import uuid4 - -import sdbus -from PyQt6 import QtCore -from sdbus_async import networkmanager as dbusNm - -logger = logging.getLogger("logs/BlocksScreen.log") - - -class NetworkManagerRescanError(Exception): - """Exception raised when rescanning the network fails.""" - - def __init__(self, error): - super(NetworkManagerRescanError, self).__init__() - self.error = error - - -class SdbusNetworkManagerAsync(QtCore.QObject): - class ConnectionPriority(enum.Enum): - """Connection priorities""" - - HIGH = 90 - MEDIUM = 50 - LOW = 20 - - nm_state_change: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - str, name="nm-state-changed" - ) - nm_properties_change: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( - tuple, name="nm-properties-changed" - ) - - def __init__(self) -> None: - super().__init__() - self._listeners_running: bool = False - self.listener_thread: threading.Thread = threading.Thread( - name="NMonitor.run_forever", - target=self._listener_run_loop, - daemon=False, - ) - self.listener_task_queue: list = [] - self.loop = asyncio.new_event_loop() - self.stop_listener_event = asyncio.Event() - self.stop_listener_event.clear() - self.system_dbus = sdbus.sd_bus_open_system() - if not self.system_dbus: - logger.error("No dbus found, async network monitor exiting") - self.close() - return - sdbus.set_default_bus(self.system_dbus) - self.nm = dbusNm.NetworkManager() - self.listener_thread.start() - if self.listener_thread.is_alive(): - logger.info( - f"Sdbus NetworkManager Monitor Thread {self.listener_thread.name} Running" - ) - self.hotspot_ssid: str = "PrinterHotspot" - self.hotspot_password: str = "123456789" - self.check_connectivity() - self.available_wired_interfaces = self.get_wired_interfaces() - self.available_wireless_interfaces = self.get_wireless_interfaces() - self.old_ssid: str = "" - wireless_interfaces: typing.List[dbusNm.NetworkDeviceWireless] = ( - self.get_wireless_interfaces() - ) - self.primary_wifi_interface: typing.Optional[dbusNm.NetworkDeviceWireless] = ( - wireless_interfaces[0] if wireless_interfaces else None - ) - wired_interfaces: typing.List[dbusNm.NetworkDeviceWired] = ( - self.get_wired_interfaces() - ) - self.primary_wired_interface: typing.Optional[dbusNm.NetworkDeviceWired] = ( - wired_interfaces[0] if wired_interfaces else None - ) - - self.create_hotspot(self.hotspot_ssid, self.hotspot_password) - if self.primary_wifi_interface: - self.rescan_networks() - - def _listener_run_loop(self) -> None: - try: - asyncio.set_event_loop(self.loop) - self.loop.run_until_complete(asyncio.gather(self.listener_monitor())) - except Exception as e: - logging.error(f"Exception on loop coroutine: {e}") - - async def _end_tasks(self) -> None: - for task in self.listener_task_queue: - task.cancel() - results = await asyncio.gather( - *self.listener_task_queue, return_exceptions=True - ) - for result in results: - if isinstance(result, Exception): - logger.error(f"Caught Exception while ending asyncio tasks: {result}") - return - - def close(self) -> None: - future = asyncio.run_coroutine_threadsafe(self._end_tasks(), self.loop) - try: - future.result(timeout=5) - except Exception as e: - logging.info(f"Exception while ending loop tasks: {e}") - self.stop_listener_event.set() - self.loop.call_soon_threadsafe(self.loop.stop) - self.listener_thread.join() - self.loop.close() - - async def listener_monitor(self) -> None: - """Monitor for NetworkManager properties""" - try: - self._listeners_running = True - - self.listener_task_queue.append( - self.loop.create_task(self._nm_state_listener()) - ) - self.listener_task_queue.append( - self.loop.create_task(self._nm_properties_listener()) - ) - results = asyncio.gather(*self.listener_task_queue, return_exceptions=True) - for result in results: - if isinstance(result, Exception): - logger.error( - f"Caught Exception on network manager asyncio loop: {result}" - ) - raise Exception(result) - await self.stop_listener_event.wait() - - except Exception as e: - logging.error(f"Exception on listener monitor produced coroutine: {e}") - - async def _nm_state_listener(self) -> None: - while self._listeners_running: - try: - async for state in self.nm.state_changed: - enum_state = dbusNm.NetworkManagerState(state) - self.nm_state_change.emit(enum_state.name) - except Exception as e: - logging.error(f"Exception on Network Manager state listener: {e}") - - async def _nm_properties_listener(self) -> None: - while self._listeners_running: - try: - logging.debug("Listening for Network Manager state change") - async for properties in self.nm.properties_changed: - self.nm_properties_change.emit(properties) - - except Exception as e: - logging.error(f"Exception on Network Manager state listener: {e}") - - def check_nm_state(self) -> typing.Union[str, None]: - """Check NetworkManager state""" - if not self.nm: - return - future = asyncio.run_coroutine_threadsafe(self.nm.state.get_async(), self.loop) - try: - state_value = future.result(timeout=2) - return str(dbusNm.NetworkManagerState(state_value).name) - except Exception as e: - logging.error(f"Exception while fetching Network Monitor State: {e}") - return None - - def check_connectivity(self) -> str: - """Checks Network Manager Connectivity state - - UNKNOWN = 0 - Network connectivity is unknown, connectivity checks are disabled. - - NONE = 1 - Host is not connected to any network. - - PORTAL = 2 - Internet connection is hijacked by a captive portal gateway. - - LIMITED = 3 - The host is connected to a network, does not appear to be able to reach full internet. - - FULL = 4 - The host is connected to a network, appears to be able to reach fill internet. - - - Returns: - _type_: _description_ - """ - if not self.nm: - return "" - future = asyncio.run_coroutine_threadsafe( - self.nm.check_connectivity(), self.loop - ) - try: - connectivity = future.result(timeout=2) - return dbusNm.NetworkManagerConnectivityState(connectivity).name - except Exception as e: - logging.error( - f"Exception while fetching Network Monitor Connectivity State: {e}" - ) - return "" - - def check_wifi_interface(self) -> bool: - """Check if wifi interface is set - - Returns: - bool: true if it is. False otherwise - """ - return bool(self.primary_wifi_interface) - - def get_available_interfaces(self) -> typing.Union[typing.List[str], None]: - """Gets the names of all available interfaces - - Returns: - typing.List[str]: List of strings with the available names of all interfaces - """ - try: - future = asyncio.run_coroutine_threadsafe(self.nm.get_devices(), self.loop) - devices = future.result(timeout=2) - interfaces = [] - for device in devices: - interface_future = asyncio.run_coroutine_threadsafe( - dbusNm.NetworkDeviceGeneric( - bus=self.system_dbus, device_path=device - ).interface.get_async(), - self.loop, - ) - interface_name = interface_future.result(timeout=2) - interfaces.append(interface_name) - return interfaces - except Exception as e: - logging.error(f"Exception on fetching available interfaces: {e}") - - def wifi_enabled(self) -> bool: - """Returns a boolean if wireless is enabled on the device. - - Returns: - bool: True if device is enabled | False if not - """ - future = asyncio.run_coroutine_threadsafe( - self.nm.wireless_enabled.get_async(), self.loop - ) - return future.result(timeout=2) - - def toggle_wifi(self, toggle: bool): - """toggle_wifi Enable/Disable wifi - - Args: - toggle (bool): - - - True -> Enable wireless - - - False -> Disable wireless - - Raises: - ValueError: Raised when the argument is not of type boolean. - - """ - if not isinstance(toggle, bool): - raise TypeError("Toggle wifi expected boolean") - if self.wifi_enabled() == toggle: - return - asyncio.run_coroutine_threadsafe( - self.nm.wireless_enabled.set_async(toggle), self.loop - ) - - async def _toggle_networking(self, value: bool = True) -> None: - if not self.primary_wifi_interface: - return - if self.primary_wifi_interface == "/": - return - results = asyncio.gather( - self.loop.create_task(self.nm.enable(value)), - return_exceptions=True, - ) - for result in results: - if isinstance(result, Exception): - logger.error(f"Exception Caught when toggling network : {result}") - - def disable_networking(self) -> None: - """Disable networking""" - if not (self.primary_wifi_interface and self.primary_wired_interface): - return - if self.primary_wifi_interface == "/" and self.primary_wired_interface == "/": - return - asyncio.run_coroutine_threadsafe(self._toggle_networking(False), self.loop) - - def activate_networking(self) -> None: - """Activate networking""" - if not (self.primary_wifi_interface and self.primary_wired_interface): - return - if self.primary_wifi_interface == "/" and self.primary_wired_interface == "/": - return - asyncio.run_coroutine_threadsafe(self._toggle_networking(True), self.loop) - - def toggle_hotspot(self, toggle: bool) -> None: - """Activate/Deactivate device hotspot - - Args: - toggle (bool): toggle option, True to activate Hotspot, False otherwise - - Raises: - ValueError: If the toggle argument is not a Boolean. - """ - if not isinstance(toggle, bool): - raise TypeError("Correct type should be a boolean.") - - if not self.nm: - return - try: - old_ssid: typing.Union[str, None] = self.get_current_ssid() - if old_ssid: - self.old_ssid = old_ssid - if toggle: - self.disconnect_network() - self.connect_network(self.hotspot_ssid) - results = asyncio.gather( - self.nm.reload(0x0), return_exceptions=True - ).result() - for result in results: - if isinstance(result, Exception): - raise Exception(result) - - if self.nm.check_connectivity() == ( - dbusNm.NetworkManagerConnectivityState.FULL - | dbusNm.NetworkManagerConnectivityState.LIMITED - ): - logging.debug(f"Hotspot AP {self.hotspot_ssid} up!") - - return - else: - if self.old_ssid: - self.connect_network(self.old_ssid) - return - except Exception as e: - logging.error(f"Caught Exception while toggling hotspot to {toggle}: {e}") - - def hotspot_enabled(self) -> typing.Optional["bool"]: - """Returns a boolean indicating whether the device hotspot is on or not . - - Returns: - bool: True if Hotspot is activated, False otherwise. - """ - return bool(self.hotspot_ssid == self.get_current_ssid()) - - def get_wired_interfaces(self) -> typing.List[dbusNm.NetworkDeviceWired]: - """get_wired_interfaces Get only the names for the available wired (Ethernet) interfaces. - - Returns: - typing.List[str]: List containing the names of all wired(Ethernet) interfaces. - """ - devs_future = asyncio.run_coroutine_threadsafe(self.nm.get_devices(), self.loop) - devices = devs_future.result(timeout=2) - - return list( - map( - lambda path: dbusNm.NetworkDeviceWired(path), - filter( - lambda path: path, - filter( - lambda device: ( - asyncio.run_coroutine_threadsafe( - dbusNm.NetworkDeviceGeneric( - bus=self.system_dbus, device_path=device - ).device_type.get_async(), - self.loop, - ).result(timeout=2) - == dbusNm.enums.DeviceType.ETHERNET - ), - devices, - ), - ), - ) - ) - - def get_wireless_interfaces( - self, - ) -> typing.List[dbusNm.NetworkDeviceWireless]: - """get_wireless_interfaces Get only the names of wireless interfaces. - - Returns: - typing.List[str]: A list containing the names of wireless interfaces. - """ - # Each interface type has a device flag that is exposed in enums.DeviceType. - devs_future = asyncio.run_coroutine_threadsafe(self.nm.get_devices(), self.loop) - devices = devs_future.result(timeout=2) - return list( - map( - lambda path: dbusNm.NetworkDeviceWireless( - bus=self.system_dbus, device_path=path - ), - filter( - lambda path: path, - filter( - lambda device: ( - asyncio.run_coroutine_threadsafe( - dbusNm.NetworkDeviceGeneric( - bus=self.system_dbus, device_path=device - ).device_type.get_async(), - self.loop, - ).result(timeout=3) - == dbusNm.enums.DeviceType.WIFI - ), - devices, - ), - ), - ) - ) - - async def _gather_ssid(self) -> str: - try: - if not self.nm: - return "" - primary_con = await self.nm.primary_connection.get_async() - if primary_con == "/": - logger.debug("No primary connection") - return "" - active_connection = dbusNm.ActiveConnection( - bus=self.system_dbus, connection_path=primary_con - ) - if not active_connection: - logger.debug("Active connection is none my man") - return "" - con = await active_connection.connection.get_async() - con_settings = dbusNm.NetworkConnectionSettings( - bus=self.system_dbus, settings_path=con - ) - settings = await con_settings.get_settings() - return str(settings["802-11-wireless"]["ssid"][1].decode()) - except Exception as e: - logger.error("Caught exception while gathering ssid %s", e) - return "" - - def get_current_ssid(self) -> str: - """Get current ssid - - Returns: - str: ssid address - """ - try: - future = asyncio.run_coroutine_threadsafe(self._gather_ssid(), self.loop) - return future.result(timeout=5) - except Exception as e: - logging.info(f"Unexpected error occurred: {e}") - return "" - - def get_current_ip_addr(self) -> str: - """Get the current connection ip address. - Returns: - str: A string containing the current ip address - """ - try: - primary_con_fut = asyncio.run_coroutine_threadsafe( - self.nm.primary_connection.get_async(), self.loop - ) - primary_con = primary_con_fut.result(timeout=2) - if primary_con == "/": - logging.info("There is no NetworkManager active connection.") - return "" - - _device_ip4_conf_path = dbusNm.ActiveConnection( - bus=self.system_dbus, connection_path=primary_con - ) - ip4_conf_future = asyncio.run_coroutine_threadsafe( - _device_ip4_conf_path.ip4_config.get_async(), self.loop - ) - - if _device_ip4_conf_path == "/": - logging.info( - "NetworkManager reports no IP configuration for the interface" - ) - return "" - ip4_conf = dbusNm.IPv4Config( - bus=self.system_dbus, ip4_path=ip4_conf_future.result(timeout=2) - ) - addr_data_fut = asyncio.run_coroutine_threadsafe( - ip4_conf.address_data.get_async(), self.loop - ) - addr_data = addr_data_fut.result(timeout=2) - return [address_data["address"][1] for address_data in addr_data][0] - except IndexError as e: - logger.error("List out of index %s", e) - except Exception as e: - logger.error("Error getting current IP address: %s", e) - return "" - - def get_device_ip_by_interface(self, interface_name: str = "wlan0") -> str: - """Get IPv4 address for a specific interface via NetworkManager D-Bus. - - This method retrieves the IP address directly from a specific network - interface, useful for getting hotspot IP when it's the active connection - on that interface. - - Args: - interface_name: The network interface name (e.g., "wlan0", "eth0") - - Returns: - str: The IPv4 address or empty string if not found - """ - if not self.nm: - return "" - - try: - devices_future = asyncio.run_coroutine_threadsafe( - self.nm.get_devices(), self.loop - ) - devices = devices_future.result(timeout=2) - - for device_path in devices: - device = dbusNm.NetworkDeviceGeneric( - bus=self.system_dbus, device_path=device_path - ) - - # Check if this is the interface we want - iface_future = asyncio.run_coroutine_threadsafe( - device.interface.get_async(), self.loop - ) - iface = iface_future.result(timeout=2) - - if iface != interface_name: - continue - - # Get IP4Config path - ip4_path_future = asyncio.run_coroutine_threadsafe( - device.ip4_config.get_async(), self.loop - ) - ip4_path = ip4_path_future.result(timeout=2) - - if not ip4_path or ip4_path == "/": - return "" - - # Get address data - ip4_config = dbusNm.IPv4Config(bus=self.system_dbus, ip4_path=ip4_path) - addr_data_future = asyncio.run_coroutine_threadsafe( - ip4_config.address_data.get_async(), self.loop - ) - addr_data = addr_data_future.result(timeout=2) - - if addr_data and len(addr_data) > 0: - return addr_data[0]["address"][1] - - except Exception as e: - logger.error("Failed to get IP for interface %s: %s", interface_name, e) - - return "" - - async def _gather_primary_interface( - self, - ) -> typing.Union[ - dbusNm.NetworkDeviceWired, - dbusNm.NetworkDeviceWireless, - typing.Tuple, - str, - ]: - if not self.nm: - return "" - - primary_connection = await self.nm.primary_connection.get_async() - if not primary_connection: - return "" - if primary_connection == "/": - if self.primary_wifi_interface and self.primary_wifi_interface != "/": - return self.primary_wifi_interface - elif self.primary_wired_interface and self.primary_wired_interface != "/": - return self.primary_wired_interface - else: - "/" - - primary_conn_type = await self.nm.primary_connection_type.get_async() - active_connection = dbusNm.ActiveConnection( - bus=self.system_dbus, connection_path=primary_connection - ) - gateway = await active_connection.devices.get_async() - device_interface = await dbusNm.NetworkDeviceGeneric( - bus=self.system_dbus, device_path=gateway[0] - ).interface.get_async() - return (device_interface, primary_connection, primary_conn_type) - - def get_primary_interface( - self, - ) -> typing.Union[ - dbusNm.NetworkDeviceWired, - dbusNm.NetworkDeviceWireless, - typing.Tuple, - str, - ]: - """Get the primary interface, - If a there is a connection, returns the interface that is being currently used. - - If there is no connection and wifi is available return de wireless interface. - - If there is no wireless interface and no active connection return the first wired interface that is not (lo). - - - Returns: - typing.List: - """ - future = asyncio.run_coroutine_threadsafe( - self._gather_primary_interface(), self.loop - ) - return future.result(timeout=2) - - async def _rescan(self) -> None: - if not self.primary_wifi_interface: - return - if self.primary_wifi_interface == "/": - return - try: - task = self.loop.create_task(self.primary_wifi_interface.request_scan({})) - results = await asyncio.gather(task, return_exceptions=True) - for result in results: - if isinstance(result, Exception): - raise NetworkManagerRescanError(f"Rescan error: {result}") - return - except Exception as e: - logger.error(f"Caught Exception: {e.__class__.__name__}: {e}") - return - - def rescan_networks(self) -> None: - """Scan for available networks.""" - try: - future = asyncio.run_coroutine_threadsafe(self._rescan(), self.loop) - result = future.result(timeout=2) - return result - - except Exception as e: - logger.error(f"Caught Exception while rescanning networks: {e}") - - async def _get_network_info(self, ap: dbusNm.AccessPoint) -> typing.Tuple: - ssid = await ap.ssid.get_async() - sec = await self._get_security_type(ap) - freq = await ap.frequency.get_async() - channel = await ap.frequency.get_async() - signal = await ap.strength.get_async() - mbit = await ap.max_bitrate.get_async() - bssid = await ap.hw_address.get_async() - return ( - ssid.decode(), - { - "security": sec, - "frequency": freq, - "channel": channel, - "signal_level": signal, - "max_bitrate": mbit, - "bssid": bssid, - }, - ) - - async def _gather_networks( - self, aps: typing.List[dbusNm.AccessPoint] - ) -> typing.Union[typing.List[typing.Tuple], None]: - try: - results = await asyncio.gather( - *(self.loop.create_task(self._get_network_info(ap)) for ap in aps), - return_exceptions=False, - ) - return results - except Exception as e: - logger.error( - f"Caught Exception while asynchronously gathering AP information: {e}" - ) - - async def _get_available_networks(self) -> typing.Union[typing.Dict, None]: - if not self.primary_wifi_interface: - return - if self.primary_wifi_interface == "/": - return - await self._rescan() - try: - last_scan = await self.primary_wifi_interface.last_scan.get_async() - if last_scan != -1: - primary_wifi_dev_type = ( - await self.primary_wifi_interface.device_type.get_async() - ) - if primary_wifi_dev_type == dbusNm.enums.DeviceType.WIFI: - aps = await self.primary_wifi_interface.get_all_access_points() - _aps: typing.List[dbusNm.AccessPoint] = list( - map( - lambda ap_path: dbusNm.AccessPoint( - bus=self.system_dbus, point_path=ap_path - ), - aps, - ) - ) - task = self.loop.create_task(self._gather_networks(_aps)) - result = await asyncio.gather(task, return_exceptions=False) - return dict(*result) if result else None # type:ignore - except Exception as e: - logger.error(f"Caught Exception while gathering access points: {e}") - return {} - - def get_available_networks(self) -> typing.Union[typing.Dict, None]: - """Get available networks""" - future = asyncio.run_coroutine_threadsafe( - self._get_available_networks(), self.loop - ) - return future.result(timeout=20) - - async def _get_security_type(self, ap: dbusNm.AccessPoint) -> typing.Tuple: - """Get the security type from a network AccessPoint - - Args: - ap (AccessPoint): The AccessPoint of the network. - - Returns: - typing.Tuple: A Tuple containing all the flags about the WpaSecurityFlags ans AccessPointCapabilities - - `(flags, wpa_flags, rsn_flags)` - - - - Check: For more information about the flags - :py:class:`WpaSecurityFlags` and `AccessPointCapabilities` from :py:module:`python-sdbus-networkmanager.enums` - """ - if not ap: - return - - _rsn_flag_task = self.loop.create_task(ap.rsn_flags.get_async()) - _wpa_flag_task = self.loop.create_task(ap.wpa_flags.get_async()) - _sec_flags_task = self.loop.create_task(ap.flags.get_async()) - - results = await asyncio.gather( - _rsn_flag_task, - _wpa_flag_task, - _sec_flags_task, - return_exceptions=True, - ) - for result in results: - if isinstance(result, Exception): - logger.error(f"Exception caught getting security type: {result}") - return () - _rsn, _wpa, _sec = results - if len(dbusNm.AccessPointCapabilities(_sec)) == 0: - return ("Open", "") - return ( - dbusNm.WpaSecurityFlags(_rsn), - dbusNm.WpaSecurityFlags(_wpa), - dbusNm.AccessPointCapabilities(_sec), - ) - - def get_saved_networks( - self, - ) -> typing.List[typing.Dict] | None: - """get_saved_networks Gets a list with the names and ids of all saved networks on the device. - - Returns: - typing.List[dict] | None: List that contains the names and ids of all saved networks on the device. - - - - I admit that this implementation is way to complicated, I don't even think it's great on memory and time, but i didn't use for loops so mission achieved. - """ - if not self.nm: - return [] - - try: - _connections: typing.List[str] = asyncio.run_coroutine_threadsafe( - dbusNm.NetworkManagerSettings(bus=self.system_dbus).list_connections(), - self.loop, - ).result(timeout=2) - - saved_cons = list( - map( - lambda connection: dbusNm.NetworkConnectionSettings( - bus=self.system_dbus, settings_path=connection - ), - _connections, - ) - ) - - sv_cons_settings_future = asyncio.run_coroutine_threadsafe( - self._get_settings(saved_cons), - self.loop, - ) - settings_list: typing.List[dbusNm.NetworkManagerConnectionProperties] = ( - sv_cons_settings_future.result(timeout=2) - ) - _known_networks_parameters = list( - filter( - lambda network_entry: network_entry is not None, - list( - map( - lambda network_properties: ( - { - "ssid": network_properties["802-11-wireless"][ - "ssid" - ][1].decode(), - "uuid": network_properties["connection"]["uuid"][1], - "signal": 0 - + self.get_connection_signal_by_ssid( - network_properties["802-11-wireless"]["ssid"][ - 1 - ].decode() - ), - "security": network_properties[ - str( - network_properties["802-11-wireless"][ - "security" - ][1] - ) - ]["key-mgmt"][1], - "mode": network_properties["802-11-wireless"][ - "mode" - ], - "priority": network_properties["connection"].get( - "autoconnect-priority", (None, None) - )[1], - } - if network_properties["connection"]["type"][1] - == "802-11-wireless" - else None - ), - settings_list, - ) - ), - ) - ) - return _known_networks_parameters - except Exception as e: - logger.error(f"Caught exception while fetching saved networks: {e}") - return [] - - @staticmethod - async def _get_settings( - saved_connections: typing.List[dbusNm.NetworkConnectionSettings], - ) -> typing.List[dbusNm.NetworkManagerConnectionProperties]: - tasks = [sc.get_settings() for sc in saved_connections] - return await asyncio.gather(*tasks, return_exceptions=False) - - def get_saved_networks_with_for(self) -> typing.List: - """Get a list with the names and ids of all saved networks on the device. - - Returns: - typing.List[dict]: List that contains the names and ids of all saved networks on the device. - - - This implementation is equal to the klipper screen implementation, this one uses for loops and is simpler. - https://github.com/KlipperScreen/KlipperScreen/blob/master/ks_includes/sdbus_nm.py Alfredo Monclues (alfrix) 2024 - """ - if not self.nm: - return [] - try: - saved_networks: list = [] - conn_future = asyncio.run_coroutine_threadsafe( - dbusNm.NetworkManagerSettings(bus=self.system_dbus).list_connections(), - self.loop, - ) - - connections = conn_future.result(timeout=2) - - # logger.debug(f"got connections from request {connections}") - saved_cons = [ - dbusNm.NetworkConnectionSettings(bus=self.system_dbus, settings_path=c) - for c in connections - ] - # logger.error(f"Getting saved networks with for: {conn_future}") - - sv_cons_settings_future = asyncio.run_coroutine_threadsafe( - self._get_settings(saved_cons), - self.loop, - ) - - settings_list = sv_cons_settings_future.result(timeout=2) - - for connection, conn in zip(connections, settings_list): - if conn["connection"]["type"][1] == "802-11-wireless": - saved_networks.append( - { - "ssid": conn["802-11-wireless"]["ssid"][1].decode(), - "uuid": conn["connection"]["uuid"][1], - "security_type": conn[ - str(conn["802-11-wireless"]["security"][1]) - ]["key-mgmt"][1], - "connection_path": connection, - "mode": conn["802-11-wireless"]["mode"], - } - ) - return saved_networks - except Exception as e: - logger.error(f"Caught Exception while fetching saved networks: {e}") - return [] - - def get_saved_ssid_names(self) -> typing.List[str]: - """Get a list with the current saved network ssid names - - Returns: - typing.List[str]: List that contains the names of the saved ssid network names - """ - try: - _saved_networks = self.get_saved_networks_with_for() - if not _saved_networks: - return [] - return list( - map( - lambda saved_network: saved_network.get("ssid", None), - _saved_networks, - ) - ) - except BaseException as e: - logger.error("Caught exception while getting saved SSID names %s", e) - return [] - - def is_known(self, ssid: str) -> bool: - """Whether or not a network is known - - Args: - ssid (str): The networks ssid - - Returns: - bool: True if the network is known otherwise False - """ - # saved_networks = asyncio.new_event_loop().run_until_complete( - # self.get_saved_networks_with_for() - # ) - saved_networks = self.get_saved_networks_with_for() - return any(net.get("ssid", "") == ssid for net in saved_networks) - - async def _add_wifi_network( - self, - ssid: str, - psk: str, - priority: ConnectionPriority = ConnectionPriority.LOW, - ) -> dict: - """Add new wifi connection - - Args: - ssid (str): Network ssid. - psk (str): Network password - priority (ConnectionPriority, optional): Priority of the network connection. Defaults to ConnectionPriority.LOW. - - Raises: - NotImplementedError: Network security type is not implemented - - Returns: - dict: A dictionary containing the result of the operation - """ - if not self.primary_wifi_interface: - logger.debug("[add wifi network] no primary wifi interface ") - return - if self.primary_wifi_interface == "/": - logger.debug("[add wifi network] no primary wifi interface ") - return - try: - _available_networks = await self._get_available_networks() - if not _available_networks: - logger.debug("Networks not available cancelling adding network") - return {"error": "No networks available"} - if self.is_known(ssid): - self.delete_network(ssid) - if ssid in _available_networks.keys(): - target_network = _available_networks.get(ssid, {}) - if not target_network: - return {"error": "Network unavailable"} - target_interface = ( - await self.primary_wifi_interface.interface.get_async() - ) - _properties: dbusNm.NetworkManagerConnectionProperties = { - "connection": { - "id": ("s", str(ssid)), - "uuid": ("s", str(uuid4())), - "type": ("s", "802-11-wireless"), - "interface-name": ( - "s", - target_interface, - ), - "autoconnect": ("b", bool(True)), - "autoconnect-priority": ( - "u", - priority.value, - ), # We need an integer here - }, - "802-11-wireless": { - "mode": ("s", "infrastructure"), - "ssid": ("ay", ssid.encode("utf-8")), - }, - "ipv4": {"method": ("s", "auto")}, - "ipv6": {"method": ("s", "auto")}, - } - if "security" in target_network.keys(): - _security_types = target_network.get("security") - if not _security_types: - return - if not _security_types[0]: - return - if ( - dbusNm.AccessPointCapabilities.NONE != _security_types[-1] - ): # Normally on last index - _properties["802-11-wireless"]["security"] = ( - "s", - "802-11-wireless-security", - ) - if ( - dbusNm.WpaSecurityFlags.P2P_WEP104 - or dbusNm.WpaSecurityFlags.P2P_WEP40 - or dbusNm.WpaSecurityFlags.BROADCAST_WEP104 - or dbusNm.WpaSecurityFlags.BROADCAST_WEP40 - ) in (_security_types[0] or _security_types[1]): - _properties["802-11-wireless-security"] = { - "key-mgmt": ("s", "none"), - "wep-key-type": ("u", 2), - "wep-key0": ("s", psk), - "auth-alg": ("s", "shared"), - } - elif ( - dbusNm.WpaSecurityFlags.P2P_TKIP - or dbusNm.WpaSecurityFlags.BROADCAST_TKIP - ) in (_security_types[0] or _security_types[1]): - raise NotImplementedError( - "Security type P2P_TKIP OR BRADCAST_TKIP not supported" - ) - elif ( - dbusNm.WpaSecurityFlags.P2P_CCMP - or dbusNm.WpaSecurityFlags.BROADCAST_CCMP - ) in (_security_types[0] or _security_types[1]): - # * AES/CCMP WPA2 - _properties["802-11-wireless-security"] = { - "key-mgmt": ("s", "wpa-psk"), - "auth-alg": ("s", "open"), - "psk": ("s", psk), - "pairwise": ("as", ["ccmp"]), - } - elif (dbusNm.WpaSecurityFlags.AUTH_PSK) in ( - _security_types[0] or _security_types[1] - ): - # * AUTH_PSK -> WPA-PSK - _properties["802-11-wireless-security"] = { - "key-mgmt": ("s", "wpa-psk"), - "auth-alg": ("s", "open"), - "psk": ("s", psk), - } - elif dbusNm.WpaSecurityFlags.AUTH_802_1X in ( - _security_types[0] or _security_types[1] - ): - # * 802.1x IEEE standard ieee802.1x - # Notes: - # IEEE 802.1x standard used 8 to 64 passphrase hashed to derive - # the actual key in the form of 64 hexadecimal character. - # - _properties["802-11-wireless-security"] = { - "key-mgmt": ("s", "wpa-eap"), - "wep-key-type": ("u", 2), - "wep-key0": ("s", psk), - "auth-alg": ("s", "shared"), - } - elif (dbusNm.WpaSecurityFlags.AUTH_SAE) in ( - _security_types[0] or _security_types[1] - ): - # * SAE - # Notes: - # The SAE is WPA3 so they use a passphrase of any length for authentication. - # - _properties["802-11-wireless-security"] = { - "key-mgmt": ("s", "sae"), - "auth-alg": ("s", "open"), - "psk": ("s", psk), - } - elif (dbusNm.WpaSecurityFlags.AUTH_OWE) in ( - _security_types[0] or _security_types[1] - ): - # * OWE - _properties["802-11-wireless-security"] = { - "key-mgmt": ("s", "owe"), - "psk": ("s", psk), - } - elif (dbusNm.WpaSecurityFlags.AUTH_OWE_TM) in ( - _security_types[0] or _security_types[1] - ): - # * OWE TM - raise NotImplementedError("AUTH_OWE_TM not supported") - elif (dbusNm.WpaSecurityFlags.AUTH_EAP_SUITE_B) in ( - _security_types[0] or _security_types[1] - ): - # * EAP SUITE B - raise NotImplementedError("EAP SUITE B Auth not supported") - tasks = [ - self.loop.create_task( - dbusNm.NetworkManagerSettings( - bus=self.system_dbus - ).add_connection(_properties) - ), - self.loop.create_task(self.nm.reload(0x0)), - ] - results = await asyncio.gather(*tasks, return_exceptions=True) - for result in results: - if isinstance(result, Exception): - if isinstance( - result, - dbusNm.exceptions.NmConnectionFailedError, - ): - logger.error( - "Exception caught, could not connect to network: %s", - str(result), - ) - return {"error": f"Connection failed to {ssid}"} - if isinstance( - result, - dbusNm.exceptions.NmConnectionPropertyNotFoundError, - ): - logger.error( - "Exception caught, network properties internal error: %s", - str(result), - ) - return {"error": "Network connection properties error"} - if isinstance( - result, - dbusNm.exceptions.NmConnectionInvalidPropertyError, - ): - logger.error( - "Caught exception while adding new wifi connection: Invalid password: %s", - str(result), - ) - return {"error": "Invalid password"} - if isinstance( - result, - dbusNm.exceptions.NmSettingsPermissionDeniedError, - ): - logger.error( - "Caught exception while adding new wifi connection: Permission Denied: %s", - str(result), - ) - return {"error": "Permission Denied"} - return {"state": "success"} - except NotImplementedError: - logger.error("Network security type not implemented") - return {"error": "Network security type not implemented"} - except Exception as e: - logger.error( - "Caught Exception Unable to add network connection : %s", str(e) - ) - return {"error": "Unable to add network"} - - def add_wifi_network( - self, - ssid: str, - psk: str, - priority: ConnectionPriority = ConnectionPriority.MEDIUM, - ) -> dict: - """Add new wifi password `Synchronous` - - Args: - ssid (str): Network ssid - psk (str): Network password - priority (ConnectionPriority, optional): Network priority. Defaults to ConnectionPriority.MEDIUM. - - Returns: - dict: A dictionary containing the result of the operation - """ - future = asyncio.run_coroutine_threadsafe( - self._add_wifi_network(ssid, psk, priority), self.loop - ) - return future.result(timeout=5) - - def disconnect_network(self) -> None: - """Disconnect the active connection""" - if not self.primary_wifi_interface: - return - if self.primary_wifi_interface == "/": - return - asyncio.run_coroutine_threadsafe( - self.primary_wifi_interface.disconnect(), self.loop - ) - - def get_connection_path_by_ssid(self, ssid: str) -> typing.Union[str, None]: - """Given a ssid, get the connection path, if it's saved - - Raises: - ValueError: If the ssid was not of type string. - - Returns: - str: connection path - """ - if not isinstance(ssid, str): - raise ValueError( - f"SSID argument must be a string, inserted type is : {type(ssid)}" - ) - _connection_path = None - _saved_networks = self.get_saved_networks_with_for() - if not _saved_networks: - raise Exception(f"No network with ssid: {ssid}") - if len(_saved_networks) == 0: - raise Exception("There are no saved networks") - for saved_network in _saved_networks: - if saved_network["ssid"].lower() == ssid.lower(): - _connection_path = saved_network["connection_path"] - return _connection_path - - def get_security_type_by_ssid(self, ssid: str) -> typing.Union[str, None]: - """Get the security type for a saved network by its ssid. - - Args: - ssid (str): SSID of a saved network - - Returns: None or str wit the security type - """ - if not self.nm: - return - if not self.is_known(ssid): - return - _security_type: str = "" - _saved_networks = self.get_saved_networks_with_for() - for network in _saved_networks: - if network["ssid"].lower() == ssid.lower(): - _security_type = network["security_type"] - - return _security_type - - def get_connection_signal_by_ssid(self, ssid: str) -> int: - """Get the signal strength for a ssid - - Args: - ssid (str): Ssid we wan't to scan - - Returns: - int: the signal strength for that ssid - """ - if not self.nm: - return 0 - if not self.primary_wifi_interface: - return 0 - if self.primary_wifi_interface == "/": - return 0 - - self.rescan_networks() - - dev_type = asyncio.run_coroutine_threadsafe( - self.primary_wifi_interface.device_type.get_async(), self.loop - ) - - if dev_type.result(timeout=2) == dbusNm.enums.DeviceType.WIFI: - # Get information on scanned networks: - _aps: typing.List[dbusNm.AccessPoint] = list( - map( - lambda ap_path: dbusNm.AccessPoint( - bus=self.system_dbus, point_path=ap_path - ), - asyncio.run_coroutine_threadsafe( - self.primary_wifi_interface.access_points.get_async(), - self.loop, - ).result(timeout=2), - ) - ) - try: - for ap in _aps: - if ( - asyncio.run_coroutine_threadsafe(ap.ssid.get_async(), self.loop) - .result(timeout=2) - .decode("utf-8") - .lower() - == ssid.lower() - ): - return asyncio.run_coroutine_threadsafe( - ap.strength.get_async(), self.loop - ).result(timeout=2) - except Exception: - return 0 - return 0 - - def connect_network(self, ssid: str) -> str: - """Connect to a saved network given an ssid - - Raises: - ValueError: Raised if the ssid argument is not of type string. - Exception: Raised if there was an error while trying to connect. - - Returns: - str: The active connection path, or a Message. - """ - if not isinstance(ssid, str): - raise ValueError( - f"SSID argument must be a string, inserted type is : {type(ssid)}" - ) - _connection_path = self.get_connection_path_by_ssid(ssid) - if not _connection_path: - raise Exception(f"No saved connection path for the SSID: {ssid}") - try: - if self.nm.primary_connection == _connection_path: - raise Exception(f"Network connection already established with {ssid}") - active_path = asyncio.run_coroutine_threadsafe( - self.nm.activate_connection(str(_connection_path)), self.loop - ).result(timeout=2) - return active_path - except Exception as e: - raise Exception( - f"Unknown error while trying to connect to {ssid} network: {e}" - ) - - async def _delete_network(self, settings_path) -> None: - tasks = [] - tasks.append( - self.loop.create_task( - dbusNm.NetworkConnectionSettings( - bus=self.system_dbus, settings_path=str(settings_path) - ).delete() - ) - ) - - tasks.append( - self.loop.create_task( - dbusNm.NetworkManagerSettings(bus=self.system_dbus).reload_connections() - ) - ) - results = await asyncio.gather(*tasks, return_exceptions=True) - for result in results: - if isinstance(result, Exception): - raise Exception(f"Caught Exception while deleting network: {result}") - - def delete_network(self, ssid: str) -> None: - """Deletes a saved network given a ssid - - Args: - ssid (str): The networks ssid to be deleted - - ### `Should be refactored` - Returns: - typing.Dict: Status key with the outcome of the networks deletion. - """ - if not isinstance(ssid, str): - raise TypeError("SSID argument is of type string") - if not self.is_known(ssid): - logging.debug(f"No known network with SSID {ssid}") - return - try: - self.deactivate_connection_by_ssid(ssid) - _path = self.get_connection_path_by_ssid(ssid) - task = self.loop.create_task(self._delete_network(_path)) - future = asyncio.gather(task, return_exceptions=True) - results = future.result() - for result in results: - if isinstance(result, Exception): - raise Exception(result) - except Exception as e: - logging.debug(f"Caught Exception while deleting network {ssid}: {e}") - - def get_hotspot_ssid(self) -> str: - """Get current hotspot ssid""" - return self.hotspot_ssid - - def deactivate_connection(self, connection_path) -> None: - """Deactivate a connection, by connection path""" - if not self.nm: - return - if not self.primary_wifi_interface: - return - if self.primary_wifi_interface == "/": - return - try: - future = asyncio.run_coroutine_threadsafe( - self.nm.active_connections.get_async(), self.loop - ) - active_connections = future.result(timeout=2) - if connection_path in active_connections: - task = self.loop.create_task( - self.nm.deactivate_connection(active_connection=connection_path) - ) - future = asyncio.gather(task) - except Exception as e: - logger.error( - f"Caught exception while deactivating network {connection_path}: {e}" - ) - - def deactivate_connection_by_ssid(self, ssid: str) -> None: - """Deactivate connection by ssid""" - if not self.nm: - return - if not self.primary_wifi_interface: - return - if self.primary_wifi_interface == "/": - return - - try: - _connection_path = self.get_connection_path_by_ssid(ssid) - if not _connection_path: - raise Exception(f"Network saved network with name {ssid}") - self.deactivate_connection(_connection_path) - except Exception as e: - logger.error(f"Exception Caught while deactivating network {ssid}: {e}") - - def create_hotspot( - self, ssid: str = "PrinterHotspot", password: str = "123456789" - ) -> None: - """Create hostpot - - Args: - ssid (str, optional): Hotspot ssid. Defaults to "PrinterHotspot". - password (str, optional): connection password. Defaults to "123456789". - """ - if self.is_known(ssid): - self.delete_network(ssid) - logger.debug("old hotspot deleted") - try: - self.delete_network(ssid) - # psk = hashlib.sha256(password.encode()).hexdigest() - _properties: dbusNm.NetworkManagerConnectionProperties = { - "connection": { - "id": ("s", str(ssid)), - "uuid": ("s", str(uuid4())), - "type": ("s", "802-11-wireless"), # 802-3-ethernet - "interface-name": ("s", "wlan0"), - }, - "802-11-wireless": { - "ssid": ("ay", ssid.encode("utf-8")), - "mode": ("s", "ap"), - "band": ("s", "bg"), - "channel": ("u", 6), - "security": ("s", "802-11-wireless-security"), - }, - "802-11-wireless-security": { - "key-mgmt": ("s", "wpa-psk"), - "psk": ("s", password), - "pmf": ("u", 0), - }, - "ipv4": { - "method": ("s", "shared"), - }, - "ipv6": {"method": ("s", "ignore")}, - } - - tasks = [ - self.loop.create_task( - dbusNm.NetworkManagerSettings(bus=self.system_dbus).add_connection( - _properties - ) - ), - self.loop.create_task(self.nm.reload(0x0)), - ] - - self.loop.run_until_complete( - asyncio.gather(*tasks, return_exceptions=False) - ) - for task in tasks: - self.loop.run_until_complete(task) - - except Exception as e: - logging.error(f"Caught Exception while creating hotspot: {e}") - - def set_network_priority( - self, ssid: str, priority: ConnectionPriority = ConnectionPriority.LOW - ) -> None: - """Set network priority - - Args: - ssid (str): connection ssid - priority (ConnectionPriority, optional): Priority. Defaults to ConnectionPriority.LOW. - """ - if not self.nm: - return - if not self.is_known(ssid): - return - self.update_connection_settings(ssid=ssid, priority=priority.value) - - def update_connection_settings( - self, - ssid: str, - password: typing.Optional["str"] = None, - new_ssid: typing.Optional["str"] = None, - priority: int = 20, - ) -> None: - """Update the settings for a connection with a specified ssid and or a password - - Args: - ssid (str | None): SSID of the network we want to update - password - Returns: - typing.Dict: status dictionary with possible keys "error" and "status" - """ - - if not self.nm: - raise Exception("NetworkManager Missing") - if not self.is_known(str(ssid)): - raise Exception("%s network is not known, cannot update", ssid) - - _connection_path = self.get_connection_path_by_ssid(str(ssid)) - if not _connection_path: - raise Exception("No saved connection with the specified ssid") - try: - con_settings = dbusNm.NetworkConnectionSettings( - bus=self.system_dbus, settings_path=str(_connection_path) - ) - properties = asyncio.run_coroutine_threadsafe( - con_settings.get_settings(), self.loop - ).result(timeout=2) - if new_ssid: - properties["connection"]["id"] = ("s", str(new_ssid)) - properties["802-11-wireless"]["ssid"] = ( - "ay", - new_ssid.encode("utf-8"), - ) - if password: - # pwd = hashlib.sha256(password.encode()).hexdigest() - properties["802-11-wireless-security"]["psk"] = ( - "s", - str(password.encode("utf-8")), - ) - - if priority != 0: - properties["connection"]["autoconnect-priority"] = ( - "u", - priority, - ) - - tasks = [ - self.loop.create_task(con_settings.update(properties)), - self.loop.create_task(self.nm.reload(0x0)), - ] - self.loop.run_until_complete( - asyncio.gather(*tasks, return_exceptions=False) - ) - - if ssid == self.hotspot_ssid and new_ssid: - self.hotspot_ssid = new_ssid - if password != self.hotspot_password and password: - self.hotspot_password = password - except Exception as e: - logger.error("Caught Exception while updating network: %s", e) diff --git a/BlocksScreen/lib/network/__init__.py b/BlocksScreen/lib/network/__init__.py new file mode 100644 index 00000000..9f06e612 --- /dev/null +++ b/BlocksScreen/lib/network/__init__.py @@ -0,0 +1,60 @@ +"""Network Manager Package + +Architecture: + NetworkManager (manager.py) + └── Main thread interface with signals/slots + └── Non-blocking API + └── Caches state for quick access + + NetworkManagerWorker (worker.py) + └── Runs in dedicated Thread + └── Owns asyncio event loop + └── Handles all D-Bus async operations + + Models (models.py) + └── Data classes for type safety + └── Enums for states and types +""" + +from .manager import NetworkManager +from .models import ( + UNSUPPORTED_SECURITY_TYPES, + ConnectionPriority, + ConnectionResult, + ConnectivityState, + HotspotConfig, + HotspotSecurity, + NetworkInfo, + NetworkState, + NetworkStatus, + PendingOperation, + SavedNetwork, + SecurityType, + VlanInfo, + WifiIconKey, + is_connectable_security, + is_hidden_ssid, + signal_to_bars, +) + +__all__ = [ + "NetworkManager", + "ConnectionPriority", + "ConnectionResult", + "ConnectivityState", + "HotspotConfig", + "HotspotSecurity", + "NetworkInfo", + "NetworkState", + "NetworkStatus", + "PendingOperation", + "SavedNetwork", + "SecurityType", + "UNSUPPORTED_SECURITY_TYPES", + "VlanInfo", + "WifiIconKey", + # Utilities + "is_connectable_security", + "is_hidden_ssid", + "signal_to_bars", +] diff --git a/BlocksScreen/lib/network/manager.py b/BlocksScreen/lib/network/manager.py new file mode 100644 index 00000000..1ef6cd82 --- /dev/null +++ b/BlocksScreen/lib/network/manager.py @@ -0,0 +1,368 @@ +# pylint: disable=protected-access + +import asyncio +import logging + +from PyQt6.QtCore import QObject, QTimer, pyqtSignal, pyqtSlot + +from .models import ( + ConnectionPriority, + ConnectionResult, + ConnectivityState, + NetworkInfo, + NetworkState, + SavedNetwork, +) +from .worker import NetworkManagerWorker + +logger = logging.getLogger(__name__) + +_KEEPALIVE_POLL_MS: int = 300_000 # 5 minutes — safety net for missed signals + + +class NetworkManager(QObject): + """Main-thread manager/interface to the NetworkManager D-Bus worker. + + The UI layer should only interact with this class. Internally it owns + a ``NetworkManagerWorker`` that runs all D-Bus coroutines on its + dedicated asyncio thread. + + Coroutines are submitted to ``worker._asyncio_loop`` — the same loop + on which the D-Bus file-descriptor was registered — so signal delivery + and async I/O always occur on the correct selector. + + """ + + state_changed = pyqtSignal(NetworkState) + networks_scanned = pyqtSignal(list) + saved_networks_loaded = pyqtSignal(list) + connection_result = pyqtSignal(ConnectionResult) + connectivity_changed = pyqtSignal(ConnectivityState) + error_occurred = pyqtSignal(str, str) + reconnect_complete = pyqtSignal() + hotspot_config_updated = pyqtSignal(str, str, str) + + def __init__(self, parent: QObject | None = None) -> None: + """Create the worker, wire all signals""" + super().__init__(parent) + + self._cached_state: NetworkState = NetworkState() + self._cached_networks: list[NetworkInfo] = [] + self._cached_saved: list[SavedNetwork] = [] + self._network_info_map: dict[str, NetworkInfo] = {} + self._saved_network_map: dict[str, SavedNetwork] = {} + + self._shutting_down: bool = False + self._worker_ready: bool = False + + self._pending_futures: set["asyncio.Future"] = set() + + self._worker = NetworkManagerWorker() + + self._cached_hotspot_ssid: str = self._worker._hotspot_config.ssid + self._cached_hotspot_password: str = self._worker._hotspot_config.password + self._cached_hotspot_security: str = self._worker._hotspot_config.security + self._worker.state_changed.connect(self._on_state_changed) + self._worker.networks_scanned.connect(self._on_networks_scanned) + self._worker.saved_networks_loaded.connect(self._on_saved_networks_loaded) + self._worker.connection_result.connect(self.connection_result) + self._worker.connectivity_changed.connect(self.connectivity_changed) + self._worker.error_occurred.connect(self.error_occurred) + self._worker.hotspot_info_ready.connect(self._on_hotspot_info_ready) + self._worker.reconnect_complete.connect(self.reconnect_complete) + self._worker.initialized.connect(self._on_worker_initialized) + + # Keepalive timer — safety net for any missed D-Bus signals. + self._keepalive_timer = QTimer(self) + self._keepalive_timer.setInterval(_KEEPALIVE_POLL_MS) + self._keepalive_timer.timeout.connect(self._on_keepalive_tick) + + logger.info("NetworkManager manager created (waiting for worker init)") + + def _schedule(self, coro: "asyncio.Coroutine") -> None: + """Submit *coro* to the worker's asyncio loop from the main thread. + + Stores a strong reference to the returned + Future to prevent Python's GC from destroying the underlying + asyncio.Task while it is still running. + """ + if self._shutting_down: + coro.close() + return + loop = self._worker._asyncio_loop + if loop.is_running(): + future = asyncio.run_coroutine_threadsafe(coro, loop) + self._pending_futures.add(future) + future.add_done_callback(self._pending_futures.discard) + else: + logger.debug( + "Dropping early coroutine — loop not yet running: %s", + coro.__qualname__, + ) + coro.close() + + @pyqtSlot() + def _on_worker_initialized(self) -> None: + """Called once when the worker finishes + D-Bus init and interface detection. + + Starts the keepalive timer *after* _primary_wifi_path and + _primary_wired_path are populated, eliminating the old 2-second + guess-timer that raced with init on slow boots. + """ + if self._shutting_down: + return + self._worker_ready = True + logger.info( + "Worker initialised — starting keepalive (every %d ms)", + _KEEPALIVE_POLL_MS, + ) + self._keepalive_timer.start() + self._schedule(self._worker._async_get_current_state()) + self._schedule(self._worker._async_scan_networks()) + self._schedule(self._worker._async_load_saved_networks()) + + def shutdown(self) -> None: + """Gracefully stop the worker, asyncio loop, and background thread.""" + self._shutting_down = True + self._keepalive_timer.stop() + + loop = self._worker._asyncio_loop + if loop.is_running(): + future = asyncio.run_coroutine_threadsafe( + self._worker._async_shutdown(), loop + ) + try: + future.result(timeout=5.0) + except Exception as exc: + logger.warning("Worker shutdown coroutine raised: %s", exc) + + self._worker._asyncio_thread.join(timeout=3.0) + if self._worker._asyncio_thread.is_alive(): + logger.warning("Asyncio thread did not exit within 3 s") + + self._pending_futures.clear() + + logger.info("NetworkManager manager shutdown complete") + + def close(self) -> None: + """Alias for ``shutdown``""" + self.shutdown() + + @pyqtSlot(NetworkState) + def _on_state_changed(self, state: NetworkState) -> None: + """Cache the new state and re-emit to UI consumers.""" + if self._shutting_down: + return + self._cached_state = state + self.state_changed.emit(state) + + @pyqtSlot(list) + def _on_networks_scanned(self, networks: list) -> None: + """Cache scan results, rebuild SSID lookup map, and re-emit.""" + if self._shutting_down: + return + self._cached_networks = networks + self._network_info_map = {n.ssid: n for n in networks} + self.networks_scanned.emit(networks) + + @pyqtSlot(list) + def _on_saved_networks_loaded(self, networks: list) -> None: + """Cache saved profiles, rebuild lowercase lookup map, and re-emit.""" + if self._shutting_down: + return + self._cached_saved = networks + self._saved_network_map = {n.ssid.lower(): n for n in networks} + self.saved_networks_loaded.emit(networks) + + @pyqtSlot(str, str, str) + def _on_hotspot_info_ready(self, ssid: str, password: str, security: str) -> None: + """Update the main-thread hotspot cache and notify UI via ``hotspot_config_updated``.""" + self._cached_hotspot_ssid = ssid + self._cached_hotspot_password = password + self._cached_hotspot_security = security + self.hotspot_config_updated.emit(ssid, password, security) + + @pyqtSlot() + def _on_keepalive_tick(self) -> None: + """Safety-net refresh — runs every 5 min to catch any missed signals.""" + if self._shutting_down: + return + self._schedule(self._worker._async_get_current_state()) + self._schedule(self._worker._async_check_connectivity()) + self._schedule(self._worker._async_load_saved_networks()) + + def request_state_soon(self, delay_ms: int = 500) -> None: + """Request a state refresh after a short delay.""" + QTimer.singleShot( + delay_ms, + lambda: self._schedule(self._worker._async_get_current_state()), + ) + + def get_current_state(self) -> None: + """Request an immediate state refresh from the worker.""" + self._schedule(self._worker._async_get_current_state()) + + def refresh_state(self) -> None: + """Request a state refresh and a saved-network reload from the worker.""" + self._schedule(self._worker._async_get_current_state()) + self._schedule(self._worker._async_load_saved_networks()) + + def scan_networks(self) -> None: + """Request an immediate Wi-Fi scan from the worker.""" + self._schedule(self._worker._async_scan_networks()) + + def load_saved_networks(self) -> None: + """Request a reload of saved connection profiles from the worker.""" + self._schedule(self._worker._async_load_saved_networks()) + + def check_connectivity(self) -> None: + """Request an NM connectivity check from the worker.""" + self._schedule(self._worker._async_check_connectivity()) + + def add_network( + self, + ssid: str, + password: str = "", # nosec B107 + priority: int = ConnectionPriority.MEDIUM.value, + ) -> None: + """Add a new Wi-Fi profile (and connect immediately) with optional priority.""" + self._schedule(self._worker._async_add_network(ssid, password, priority)) + + def connect_network(self, ssid: str) -> None: + """Connect to an already-saved network by *ssid*.""" + self._schedule(self._worker._async_connect_network(ssid)) + + def disconnect(self) -> None: + """Disconnect the currently active Wi-Fi connection.""" + self._schedule(self._worker._async_disconnect()) + + def delete_network(self, ssid: str) -> None: + """Delete the saved profile for *ssid*.""" + self._schedule(self._worker._async_delete_network(ssid)) + + def update_network( # nosec B107 + self, ssid: str, password: str = "", priority: int = 0 + ) -> None: + """Update the password and/or autoconnect priority for a saved profile.""" + self._schedule(self._worker._async_update_network(ssid, password, priority)) + + def set_wifi_enabled(self, enabled: bool) -> None: + """Enable or disable the Wi-Fi radio.""" + self._schedule(self._worker._async_set_wifi_enabled(enabled)) + + def create_hotspot( + self, + ssid: str = "", + password: str = "", + security: str = "wpa-psk", # nosec B107 + ) -> None: + """Create and immediately activate a hotspot with the given credentials.""" + self._schedule( + self._worker._async_create_and_activate_hotspot(ssid, password, security) + ) + + def toggle_hotspot(self, enable: bool) -> None: + """Deactivate the hotspot (enable=False) or create+activate (enable=True).""" + self._schedule(self._worker._async_toggle_hotspot(enable)) + + def update_hotspot_config( + self, + old_ssid: str, + new_ssid: str, + new_password: str, + security: str = "wpa-psk", + ) -> None: + """Change hotspot name/password/security — cleans up old profiles.""" + self._schedule( + self._worker._async_update_hotspot_config( + old_ssid, new_ssid, new_password, security + ) + ) + + def disconnect_ethernet(self) -> None: + """Deactivate the primary wired interface.""" + self._schedule(self._worker._async_disconnect_ethernet()) + + def connect_ethernet(self) -> None: + """Activate the primary wired interface.""" + self._schedule(self._worker._async_connect_ethernet()) + + def create_vlan_connection( + self, + vlan_id: int, + ip_address: str, + subnet_mask: str, + gateway: str, + dns1: str = "", + dns2: str = "", + ) -> None: + """Create and activate a VLAN connection with + given static IP settings""" + self._schedule( + self._worker._async_create_vlan( + vlan_id, ip_address, subnet_mask, gateway, dns1, dns2 + ) + ) + + def delete_vlan_connection(self, vlan_id: int) -> None: + """Delete all NM profiles for *vlan_id*.""" + self._schedule(self._worker._async_delete_vlan(vlan_id)) + + def update_wifi_static_ip( + self, + ssid: str, + ip_address: str, + subnet_mask: str, + gateway: str, + dns1: str = "", + dns2: str = "", + ) -> None: + """Apply a static IP configuration to a saved Wi-Fi profile.""" + self._schedule( + self._worker._async_update_wifi_static_ip( + ssid, ip_address, subnet_mask, gateway, dns1, dns2 + ) + ) + + def reset_wifi_to_dhcp(self, ssid: str) -> None: + """Reset a saved Wi-Fi profile back to DHCP.""" + self._schedule(self._worker._async_reset_wifi_to_dhcp(ssid)) + + @property + def current_state(self) -> NetworkState: + """Most recently cached ``NetworkState`` snapshot.""" + return self._cached_state + + @property + def current_ssid(self) -> str | None: + """SSID of the currently active Wi-Fi connection, or ``None``.""" + return self._cached_state.current_ssid + + @property + def saved_networks(self) -> list[SavedNetwork]: + """Most recently cached list of saved ``SavedNetwork`` profiles.""" + return self._cached_saved + + @property + def hotspot_ssid(self) -> str: + """Hotspot SSID — read from main-thread cache (thread-safe).""" + return self._cached_hotspot_ssid + + @property + def hotspot_password(self) -> str: + """Hotspot password — read from main-thread cache (thread-safe).""" + return self._cached_hotspot_password + + @property + def hotspot_security(self) -> str: + """Hotspot security type — always 'wpa-psk' (WPA2-PSK, thread-safe).""" + return self._cached_hotspot_security + + def get_network_info(self, ssid: str) -> NetworkInfo | None: + """Return the scanned ``NetworkInfo`` for *ssid*, or ``None``.""" + return self._network_info_map.get(ssid) + + def get_saved_network(self, ssid: str) -> SavedNetwork | None: + """Return the saved ``SavedNetwork`` for *ssid* (case-insensitive).""" + return self._saved_network_map.get(ssid.lower()) diff --git a/BlocksScreen/lib/network/models.py b/BlocksScreen/lib/network/models.py new file mode 100644 index 00000000..b743d875 --- /dev/null +++ b/BlocksScreen/lib/network/models.py @@ -0,0 +1,328 @@ +"""Data models for the NetworkManager subsystem.""" + +import sys +from dataclasses import dataclass +from enum import Enum, IntEnum + + +class SecurityType(str, Enum): + """Wi-Fi security types.""" + + OPEN = "open" + WEP = "wep" + WPA_PSK = "wpa-psk" + WPA2_PSK = "wpa2-psk" + WPA3_SAE = "sae" + WPA_EAP = "wpa-eap" + OWE = "owe" + UNKNOWN = "unknown" + + +# Security types this device cannot connect to. +UNSUPPORTED_SECURITY_TYPES: frozenset[str] = frozenset( + { + SecurityType.WEP.value, + SecurityType.WPA_EAP.value, + SecurityType.OWE.value, + SecurityType.OPEN.value, + } +) + + +def is_connectable_security(security: "SecurityType | str") -> bool: + """Return True if this device can connect to *security* type.""" + return security not in UNSUPPORTED_SECURITY_TYPES + + +class ConnectivityState(IntEnum): + """NetworkManager connectivity states.""" + + UNKNOWN = 0 + NONE = 1 + PORTAL = 2 + LIMITED = 3 + FULL = 4 + + +class ConnectionPriority(IntEnum): + """Autoconnect priority levels for saved connections (higher = \ + preferred).""" + + LOW = 20 + MEDIUM = 50 + HIGH = 90 + HIGHEST = 100 + + +class PendingOperation(IntEnum): + """Identifies which network transition is currently in-flight.""" + + NONE = 0 + WIFI_ON = 1 + WIFI_OFF = 2 + HOTSPOT_ON = 3 + HOTSPOT_OFF = 4 + CONNECT = 5 + ETHERNET_ON = 6 + ETHERNET_OFF = 7 + WIFI_STATIC_IP = 8 # static IP or resetting to DHCP on a Wi-Fi profile + VLAN_DHCP = 9 # VLAN with DHCP (long-running, up to 45 s) + + +class NetworkStatus(IntEnum): + """State of a Wi-Fi network from the device's perspective. + + Values are ordered so that higher values indicate a "more connected" + state. This lets callers use comparison operators for grouping:: + + is_saved <-> network.network_status >= NetworkStatus.SAVED + is_active <-> network.network_status == NetworkStatus.ACTIVE + + ``is_open`` is **not** encoded here because it is a property of the + network's *security type*, not its connection state. Use + ``NetworkInfo.is_open`` (derived from ``security_type``) instead. + """ + + DISCOVERED = 0 # Seen in scan, not saved — protected security + OPEN = 1 # Seen in scan, not saved — open (no passphrase) + SAVED = 2 # Profile saved on this device + ACTIVE = 3 # Currently connected + HIDDEN = 4 # Hidden-network placeholder + + @property + def label(self) -> str: + """Human-readable status label for UI display.""" + return _STATUS_LABELS[self] + + @staticmethod + def update_status_label(status: "NetworkStatus", label: str) -> None: + """Update the human-readable label for a given network status.""" + _STATUS_LABELS[status] = sys.intern(label) + + +_STATUS_LABELS: dict[NetworkStatus, str] = { + NetworkStatus.DISCOVERED: sys.intern("Protected"), + NetworkStatus.OPEN: sys.intern("Open"), + NetworkStatus.SAVED: sys.intern("Saved"), + NetworkStatus.ACTIVE: sys.intern("Active"), + NetworkStatus.HIDDEN: sys.intern("Hidden"), +} + + +SIGNAL_EXCELLENT_THRESHOLD = 75 +SIGNAL_GOOD_THRESHOLD = 50 +SIGNAL_FAIR_THRESHOLD = 25 +SIGNAL_MINIMUM_THRESHOLD = 5 + + +def signal_to_bars(signal: int) -> int: + """Convert signal strength percentage (0-100) to bar count (0-4).""" + if signal < SIGNAL_MINIMUM_THRESHOLD: + return 0 + if signal >= SIGNAL_EXCELLENT_THRESHOLD: + return 4 + if signal >= SIGNAL_GOOD_THRESHOLD: + return 3 + if signal > SIGNAL_FAIR_THRESHOLD: + return 2 + return 1 + + +class WifiIconKey(IntEnum): + """Lightweight icon key for the header Wi-Fi status icon. + + Encodes signal bars (0-4), protection status, and special states + into a single integer for cheap cross-thread signalling via + pyqtSignal(int). + + Encoding: ethernet = -1, hotspot = 10, wifi = bars * 2 + is_protected + Range: -1, 0..10 + """ + + ETHERNET = -1 + + WIFI_0_OPEN = 0 + WIFI_0_PROTECTED = 1 + WIFI_1_OPEN = 2 + WIFI_1_PROTECTED = 3 + WIFI_2_OPEN = 4 + WIFI_2_PROTECTED = 5 + WIFI_3_OPEN = 6 + WIFI_3_PROTECTED = 7 + WIFI_4_OPEN = 8 + WIFI_4_PROTECTED = 9 + + HOTSPOT = 10 + + @classmethod + def from_bars(cls, bars: int, is_protected: bool) -> "WifiIconKey": + """Encode bar count (0-4) + protection flag into a WifiIconKey.""" + if not 0 <= bars <= 4: + raise ValueError(f"Bars must be 0-4 (got {bars})") + return cls(bars * 2 + int(is_protected)) + + @classmethod + def from_signal(cls, signal_strength: int, is_protected: bool) -> "WifiIconKey": + """Convert raw signal strength + protection to a WifiIconKey.""" + return cls.from_bars(signal_to_bars(signal_strength), is_protected) + + @property + def bars(self) -> int: + """Signal bars (0-4). Raises ValueError for ETHERNET/HOTSPOT.""" + if self is WifiIconKey.ETHERNET or self is WifiIconKey.HOTSPOT: + raise ValueError(f"{self.name} has no bar count") + return self.value // 2 + + @property + def is_protected(self) -> bool: + """Whether the network is protected. + Raises ValueError for ETHERNET/HOTSPOT.""" + if self is WifiIconKey.ETHERNET or self is WifiIconKey.HOTSPOT: + raise ValueError(f"{self.name} has no protection status") + return bool(self.value % 2) + + +@dataclass(frozen=True, slots=True) +class NetworkInfo: + """Represents a single Wi-Fi access point discovered during a scan. + + Connection state is encoded in *network_status* (a single ``int`` + the same width as the four booleans it replaced). Security openness + is derived from *security_type* via the ``is_open`` property. + """ + + ssid: str = "" + signal_strength: int = 0 + network_status: NetworkStatus = NetworkStatus.DISCOVERED + bssid: str = "" + frequency: int = 0 + max_bitrate: int = 0 + security_type: SecurityType | str = SecurityType.UNKNOWN + + @property + def is_open(self) -> bool: + """True when the AP broadcasts no security flags.""" + return self.security_type == SecurityType.OPEN + + @property + def is_saved(self) -> bool: + """True when a profile for this network exists on the device.""" + return self.network_status >= NetworkStatus.SAVED + + @property + def is_active(self) -> bool: + """True when the device is currently connected to this AP.""" + return self.network_status == NetworkStatus.ACTIVE + + @property + def is_hidden(self) -> bool: + """True for hidden-network placeholders.""" + return self.network_status == NetworkStatus.HIDDEN + + @property + def status(self) -> str: + """Human-readable status label (Active > Saved > Open > Protected).""" + return self.network_status.label + + +@dataclass(frozen=True, slots=True) +class SavedNetwork: + """Represents a saved (known) Wi-Fi connection profile.""" + + ssid: str = "" + uuid: str = "" + connection_path: str = "" + security_type: str = "" + mode: str = "infrastructure" + priority: int = ConnectionPriority.MEDIUM.value + signal_strength: int = 0 + timestamp: int = 0 # Unix time of last successful activation + is_dhcp: bool = True # True = auto (DHCP), False = manual (static IP) + + +@dataclass(frozen=True, slots=True) +class ConnectionResult: + """Outcome of a connection/network operation.""" + + success: bool = False + message: str = "" + error_code: str = "" + data: dict[str, object] | None = None + + +@dataclass(frozen=True, slots=True) +class VlanInfo: + """Snapshot of an active VLAN connection.""" + + vlan_id: int = 0 + ip_address: str = "" + interface: str = "" + gateway: str = "" + dns_servers: tuple[str, ...] = () + is_dhcp: bool = False + + +@dataclass(frozen=True, slots=True) +class NetworkState: + """Snapshot of the current network state.""" + + connectivity: ConnectivityState = ConnectivityState.UNKNOWN + current_ssid: str | None = None + current_ip: str = "" + wifi_enabled: bool = False + hotspot_enabled: bool = False + primary_interface: str = "" + signal_strength: int = 0 + security_type: str = "" + ethernet_connected: bool = False + ethernet_carrier: bool = False + active_vlans: tuple[VlanInfo, ...] = () + + +class HotspotSecurity(str, Enum): + """Supported hotspot security protocols. + + The *value* is the internal key passed through manager -> worker; + the NM ``key-mgmt`` and cipher settings are resolved at profile + creation time in ``create_and_activate_hotspot``. + """ + + WPA1 = "wpa1" + WPA2_PSK = "wpa-psk" # WPA2-PSK (CCMP) — default + + @classmethod + def is_valid(cls, value: str) -> bool: + """Return True if *value* matches a known security key.""" + return value in cls._value2member_map_ + + +@dataclass(slots=True) +class HotspotConfig: + """Mutable configuration for the access-point / hotspot.""" + + ssid: str = "PrinterHotspot" + password: str = "123456789" + band: str = "bg" + channel: int = 6 + security: str = HotspotSecurity.WPA2_PSK.value + + +# Patterns that indicate a hidden or invalid SSID +_HIDDEN_INDICATORS = frozenset({"unknown", "hidden", ""}) + + +def is_hidden_ssid(ssid: str | None) -> bool: + """Return True if *ssid* is blank, whitespace, null-bytes, or a + well-known hidden-network placeholder. + + Handles: None, "", " ", "\\x00\\x00", "unknown", "UNKNOWN", + "hidden", "", "". + """ + if not ssid: + return True + stripped = ssid.strip() + if not stripped: + return True + if stripped[0] == "\x00" and all(c == "\x00" for c in stripped): + return True + return stripped.lower() in _HIDDEN_INDICATORS diff --git a/BlocksScreen/lib/network/worker.py b/BlocksScreen/lib/network/worker.py new file mode 100644 index 00000000..227b7e9b --- /dev/null +++ b/BlocksScreen/lib/network/worker.py @@ -0,0 +1,2755 @@ +import asyncio +import fcntl +import ipaddress +import logging +import os +import socket as _socket +import struct +import threading +from uuid import uuid4 + +import sdbus +from configfile import get_configparser +from PyQt6.QtCore import QObject, pyqtSignal +from sdbus_async import networkmanager as dbus_nm + +from .models import ( + ConnectionPriority, + ConnectionResult, + ConnectivityState, + HotspotConfig, + HotspotSecurity, + NetworkInfo, + NetworkState, + NetworkStatus, + SavedNetwork, + SecurityType, + VlanInfo, + is_connectable_security, + is_hidden_ssid, +) + +logger = logging.getLogger(__name__) + +_CAN_RELOAD_CONNECTIONS: bool = os.getuid() == 0 + +# Debounce window for coalescing rapid D-Bus signal bursts (seconds). +_DEBOUNCE_DELAY: float = 0.8 +# Delay before restarting a failed signal listener (seconds). +_LISTENER_RESTART_DELAY: float = 3.0 +# Timeout for _wait_for_connection: must cover 802.11 handshake + DHCP. +_WIFI_CONNECT_TIMEOUT: float = 20.0 + + +class NetworkManagerWorker(QObject): + """Async NetworkManager worker (signal-reactive). + + Owns an asyncio event loop running on a dedicated daemon thread. + All D-Bus operations execute as coroutines on that loop. + + Primary state updates are driven by D-Bus signals, not polling. + """ + + state_changed = pyqtSignal(NetworkState, name="stateChanged") + networks_scanned = pyqtSignal(list, name="networksScanned") + saved_networks_loaded = pyqtSignal(list, name="savedNetworksLoaded") + connection_result = pyqtSignal(ConnectionResult, name="connectionResult") + connectivity_changed = pyqtSignal(ConnectivityState, name="connectivityChanged") + error_occurred = pyqtSignal(str, str, name="errorOccurred") + hotspot_info_ready = pyqtSignal(str, str, str, name="hotspotInfoReady") + reconnect_complete = pyqtSignal(name="reconnectComplete") + + _MAX_DBUS_ERRORS_BEFORE_RECONNECT: int = 3 + + initialized = pyqtSignal(name="workerInitialized") + + def __init__(self) -> None: + """Initialise the worker, creating the asyncio loop and daemon thread. + + Sets up all instance state (interface paths, hotspot config, signal + proxies, debounce handles) and immediately starts the asyncio daemon + thread that opens the system D-Bus and drives all NetworkManager + coroutines. + """ + super().__init__() + self._running: bool = False + self._system_bus: sdbus.SdBus | None = None + + # Path strings only — read-proxies are always created fresh. + self._primary_wifi_path: str = "" + self._primary_wifi_iface: str = "" + self._primary_wired_path: str = "" + self._primary_wired_iface: str = "" + + self._iface_to_device_path: dict[str, str] = {} + + self._hotspot_config = HotspotConfig() + self._load_hotspot_config() + self._saved_cache: list[SavedNetwork] = [] + self._saved_cache_dirty: bool = True + self._is_hotspot_active: bool = False + self._consecutive_dbus_errors: int = 0 + + self._background_tasks: set[asyncio.Task] = set() + self._deleted_vlan_ids: set[int] = set() + + self._signal_nm: dbus_nm.NetworkManager | None = None + self._signal_wifi: dbus_nm.NetworkDeviceWireless | None = None + self._signal_wired: dbus_nm.NetworkDeviceGeneric | None = None + self._signal_settings: dbus_nm.NetworkManagerSettings | None = None + + self._state_debounce_handle: asyncio.TimerHandle | None = None + self._scan_debounce_handle: asyncio.TimerHandle | None = None + + # Tracked for cancellation during shutdown. + self._listener_tasks: list[asyncio.Task] = [] + + # Asyncio loop — created here, driven on the daemon thread. + self.stop_event = asyncio.Event() + self.stop_event.clear() + self._asyncio_loop: asyncio.AbstractEventLoop = asyncio.new_event_loop() + self._asyncio_thread = threading.Thread( + target=self._run_asyncio_loop, + daemon=True, + name="NetworkManagerAsyncLoop", + ) + self._asyncio_thread.start() + + def _run_asyncio_loop(self) -> None: + """Open the system D-Bus and run the asyncio event loop on this thread.""" + asyncio.set_event_loop(self._asyncio_loop) + try: + self._system_bus = sdbus.sd_bus_open_system() + sdbus.set_default_bus(self._system_bus) + self._track_task( + self._asyncio_loop.create_task(self._async_initialize(), name="nm_init") + ) + logger.debug( + "D-Bus opened on asyncio thread '%s'", + threading.current_thread().name, + ) + except Exception as exc: + logger.error("Failed to open system D-Bus: %s", exc) + self._asyncio_loop.run_forever() + + def _track_task(self, task: asyncio.Task) -> None: + """Register a background task so it is cancelled on shutdown.""" + self._background_tasks.add(task) + task.add_done_callback(self._background_tasks.discard) + + async def _async_shutdown(self) -> None: + """Tear down all async state and stop the event loop.""" + self._running = False + + for task in self._listener_tasks: + if not task.done(): + task.cancel() + self._listener_tasks.clear() + + if self._state_debounce_handle: + self._state_debounce_handle.cancel() + self._state_debounce_handle = None + if self._scan_debounce_handle: + self._scan_debounce_handle.cancel() + self._scan_debounce_handle = None + + self._signal_nm = None + self._signal_wifi = None + self._signal_wired = None + self._signal_settings = None + + self._primary_wifi_path = "" + self._primary_wifi_iface = "" + self._primary_wired_path = "" + self._primary_wired_iface = "" + self._iface_to_device_path.clear() + self._saved_cache.clear() + self._deleted_vlan_ids.clear() + + for task in list(self._background_tasks): + if not task.done(): + task.cancel() + self._background_tasks.clear() + self._system_bus = None + logger.info("NetworkManagerWorker async shutdown complete") + self._asyncio_loop.call_soon_threadsafe(self._asyncio_loop.stop) + + def _nm(self) -> dbus_nm.NetworkManager: + """Return a fresh NetworkManager root D-Bus proxy.""" + return dbus_nm.NetworkManager(bus=self._system_bus) + + def _generic(self, path: str) -> dbus_nm.NetworkDeviceGeneric: + """Return a fresh generic network device D-Bus proxy for the given path.""" + return dbus_nm.NetworkDeviceGeneric(bus=self._system_bus, device_path=path) + + def _wifi(self, path: str | None = None) -> dbus_nm.NetworkDeviceWireless: + """Return a fresh wireless device D-Bus proxy (defaults to primary Wi-Fi path).""" + return dbus_nm.NetworkDeviceWireless( + bus=self._system_bus, + device_path=path or self._primary_wifi_path, + ) + + def _wired(self, path: str | None = None) -> dbus_nm.NetworkDeviceWired: + """Return a fresh wired device D-Bus proxy (defaults to primary wired path).""" + return dbus_nm.NetworkDeviceWired( + bus=self._system_bus, + device_path=path or self._primary_wired_path, + ) + + def _get_wifi_iface_name(self) -> str: + """Return the detected Wi-Fi interface name. + + ``_primary_wifi_iface`` is set atomically with ``_primary_wifi_path`` + in ``_detect_interfaces()``. The dict-lookup and ``"wlan0"`` branches + are defensive fallbacks in case the two somehow diverge. + """ + if self._primary_wifi_iface: + return self._primary_wifi_iface + for iface, path in self._iface_to_device_path.items(): + if path == self._primary_wifi_path: + return iface + return "wlan0" # safe fallback + + def _active_conn(self, path: str) -> dbus_nm.ActiveConnection: + """Return a fresh ActiveConnection D-Bus proxy for the given path.""" + return dbus_nm.ActiveConnection(bus=self._system_bus, connection_path=path) + + def _conn_settings(self, path: str) -> dbus_nm.NetworkConnectionSettings: + """Return a fresh NetworkConnectionSettings D-Bus proxy for the given path.""" + return dbus_nm.NetworkConnectionSettings( + bus=self._system_bus, settings_path=path + ) + + def _nm_settings(self) -> dbus_nm.NetworkManagerSettings: + """Return a fresh NetworkManagerSettings D-Bus proxy.""" + return dbus_nm.NetworkManagerSettings(bus=self._system_bus) + + def _ap(self, path: str) -> dbus_nm.AccessPoint: + """Return a fresh AccessPoint D-Bus proxy for the given path.""" + return dbus_nm.AccessPoint(bus=self._system_bus, point_path=path) + + def _ipv4(self, path: str) -> dbus_nm.IPv4Config: + """Return a fresh IPv4Config D-Bus proxy for the given path.""" + return dbus_nm.IPv4Config(bus=self._system_bus, ip4_path=path) + + def _ensure_signal_proxies(self) -> None: + """Create or recreate persistent proxies for D-Bus signal listening. + + These proxies are NOT used for property reads (to avoid the + sdbus_async caching bug). They exist solely so the ``async for`` + signal iterators stay alive. Must be called on the asyncio thread. + """ + if self._signal_nm is None: + self._signal_nm = dbus_nm.NetworkManager(bus=self._system_bus) + + if self._signal_wifi is None and self._primary_wifi_path: + self._signal_wifi = dbus_nm.NetworkDeviceWireless( + bus=self._system_bus, + device_path=self._primary_wifi_path, + ) + + if self._signal_wired is None and self._primary_wired_path: + self._signal_wired = dbus_nm.NetworkDeviceGeneric( + bus=self._system_bus, + device_path=self._primary_wired_path, + ) + + if self._signal_settings is None: + self._signal_settings = dbus_nm.NetworkManagerSettings( + bus=self._system_bus, + ) + + def get_ip_by_interface(self, interface: str = "wlan0") -> str: + """Return the current IPv4 address for *interface*, blocking up to 5 s.""" + future = asyncio.run_coroutine_threadsafe( + self._get_ip_by_interface(interface), self._asyncio_loop + ) + try: + return future.result(timeout=5.0) + except Exception: + # Timeout or cancellation from the async loop; caller treats "" as unknown. + return "" + + @property + def hotspot_ssid(self) -> str: + """The SSID configured for the hotspot.""" + return self._hotspot_config.ssid + + @property + def hotspot_password(self) -> str: + """The password configured for the hotspot.""" + return self._hotspot_config.password + + async def _async_initialize(self) -> None: + """Bootstrap the worker on the asyncio thread. + + Detects network interfaces, enforces the boot-time ethernet/Wi-Fi + mutual exclusion, activates any saved VLANs if ethernet is present, + triggers an initial Wi-Fi scan, and starts all D-Bus signal listeners. + Emits ``initialized`` when done (even on failure, so the manager can + unblock its caller). + """ + try: + if not self._system_bus: + self.error_occurred.emit("initialize", "No D-Bus connection") + return + + self._running = True + await self._detect_interfaces() + await self._enforce_boot_mutual_exclusion() + + if await self._is_ethernet_connected(): + await self._activate_saved_vlans() + + self.hotspot_info_ready.emit( + self._hotspot_config.ssid, + self._hotspot_config.password, + self._hotspot_config.security, + ) + + if self._primary_wifi_path: + try: + await self._wifi().request_scan({}) + except Exception as exc: + logger.debug("Initial Wi-Fi scan request ignored: %s", exc) + + await self._start_signal_listeners() + + logger.info( + "NetworkManagerWorker initialised on thread '%s' " + "(sdbus_async, signal-reactive)", + threading.current_thread().name, + ) + self.initialized.emit() + except Exception as exc: + logger.exception("Failed to initialise NetworkManagerWorker") + self.error_occurred.emit("initialize", str(exc)) + self.initialized.emit() + + async def _detect_interfaces(self) -> None: + """Enumerate NM devices and record the primary Wi-Fi and Ethernet paths. + + Iterates all NetworkManager devices, maps interface names to D-Bus + object paths, and stores the first WIFI and ETHERNET device found as + the primary interfaces used for all subsequent operations. Emits + ``error_occurred`` if no interfaces at all are found. + """ + try: + devices = await self._nm().get_devices() + for device_path in devices: + device = self._generic(device_path) + device_type = await device.device_type + iface_name = await self._generic(device_path).interface + if iface_name: + self._iface_to_device_path[iface_name] = device_path + + if ( + device_type == dbus_nm.enums.DeviceType.WIFI + and not self._primary_wifi_path + ): + self._primary_wifi_path = device_path + self._primary_wifi_iface = iface_name + elif ( + device_type == dbus_nm.enums.DeviceType.ETHERNET + and not self._primary_wired_path + ): + self._primary_wired_path = device_path + self._primary_wired_iface = iface_name + except Exception as exc: + logger.error("Failed to detect interfaces: %s", exc) + + if not self._primary_wifi_path and not self._primary_wired_path: + # Both absent — likely D-Bus not ready yet or no hardware present. + logger.warning("No network interfaces detected after scan") + self.error_occurred.emit("wifi_unavailable", "No network device found") + elif not self._primary_wifi_path: + # Ethernet-only or Wi-Fi driver still loading — log but don't alarm. + logger.warning("No Wi-Fi interface detected; ethernet-only mode") + + async def _enforce_boot_mutual_exclusion(self) -> None: + """Disable Wi-Fi at boot if ethernet is already connected. + + Prevents the device from simultaneously using both interfaces at + startup. If ethernet is active and the Wi-Fi radio is on, the Wi-Fi + device is disconnected and the radio is disabled, then we wait up to + 8 s for the radio to confirm it is off. Failures are logged but not + propagated — a non-fatal best-effort action at boot. + """ + try: + if not await self._is_ethernet_connected(): + return + if not await self._nm().wireless_enabled: + return + logger.info("Boot: ethernet active + Wi-Fi enabled — disabling Wi-Fi") + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Pre-radio-disable disconnect ignored: %s", exc) + await self._nm().wireless_enabled.set_async(False) + await self._wait_for_wifi_radio(False, timeout=8.0) + self._is_hotspot_active = False + except Exception as exc: + logger.warning("Boot mutual exclusion failed (non-fatal): %s", exc) + + async def _start_signal_listeners(self) -> None: + """Create persistent proxies and spawn all D-Bus signal listeners. + + Each listener runs in its own Task and automatically restarts + after transient errors (with a back-off delay). + """ + self._ensure_signal_proxies() + + listeners = [ + ("nm_state", self._listen_nm_state_changed), + ("wifi_ap_added", self._listen_ap_added), + ("wifi_ap_removed", self._listen_ap_removed), + ("wired_state", self._listen_wired_state_changed), + ("wifi_state", self._listen_wifi_state_changed), + ("settings_conn_added", self._listen_settings_new_connection), + ("settings_conn_removed", self._listen_settings_connection_removed), + ] + + for name, coro_fn in listeners: + task = self._asyncio_loop.create_task( + self._resilient_listener(name, coro_fn), + name=f"listener_{name}", + ) + self._listener_tasks.append(task) + self._track_task(task) + + logger.info("Started %d D-Bus signal listeners", len(self._listener_tasks)) + + async def _resilient_listener( + self, name: str, listener_fn: "asyncio.coroutines" + ) -> None: + """Wrapper that restarts *listener_fn* on failure with back-off.""" + while self._running: + try: + await listener_fn() + except asyncio.CancelledError: + logger.debug("Listener '%s' cancelled", name) + return + except Exception as exc: + if not self._running: + return + logger.warning( + "Listener '%s' failed: %s — restarting in %.1f s", + name, + exc, + _LISTENER_RESTART_DELAY, + ) + # Rebuild signal proxies in case the bus was reset + self._signal_nm = None + self._signal_wifi = None + self._signal_wired = None + self._signal_settings = None + await asyncio.sleep(_LISTENER_RESTART_DELAY) + if self._running: + self._ensure_signal_proxies() + + async def _listen_nm_state_changed(self) -> None: + """React to NetworkManager global state transitions.""" + if not self._signal_nm: + return + logger.debug("NM StateChanged listener started") + async for state_value in self._signal_nm.state_changed: + if not self._running: + return + try: + nm_state = dbus_nm.NetworkManagerState(state_value) + logger.debug( + "NM StateChanged: %s (%d)", + nm_state.name, + state_value, + ) + except ValueError: + logger.debug("NM StateChanged: unknown (%d)", state_value) + + self._schedule_debounced_state_rebuild() + self._schedule_debounced_scan() + + async def _listen_ap_added(self) -> None: + """React to new access points appearing in scan results. + + Triggers a debounced scan rebuild (not a full rescan — NM has + already updated its internal AP list). + """ + if not self._signal_wifi: + return + logger.debug("AP Added listener started on %s", self._primary_wifi_path) + async for ap_path in self._signal_wifi.access_point_added: + if not self._running: + return + logger.debug("AP added: %s", ap_path) + self._schedule_debounced_scan() + + async def _listen_ap_removed(self) -> None: + """React to access points disappearing from scan results.""" + if not self._signal_wifi: + return + logger.debug("AP Removed listener started on %s", self._primary_wifi_path) + async for ap_path in self._signal_wifi.access_point_removed: + if not self._running: + return + logger.debug("AP removed: %s", ap_path) + self._schedule_debounced_scan() + + async def _listen_wired_state_changed(self) -> None: + """React to wired device state transitions (cable plug/unplug). + + The ``state_changed`` signal on the Device interface emits + ``(new_state, old_state, reason)`` with signature ``'uuu'``. + """ + if not self._signal_wired: + return + logger.debug("Wired state listener started on %s", self._primary_wired_path) + async for new_state, old_state, reason in self._signal_wired.state_changed: + if not self._running: + return + logger.debug( + "Wired state: %d -> %d (reason %d)", + old_state, + new_state, + reason, + ) + self._schedule_debounced_state_rebuild() + + async def _listen_wifi_state_changed(self) -> None: + """React to Wi-Fi device state transitions. + + Detects enabled/disabled, connecting, disconnected transitions + instantly — complements the NM global ``state_changed`` signal + which may not fire for all device-level transitions. + """ + if not self._signal_wifi: + return + logger.debug("Wi-Fi state listener started on %s", self._primary_wifi_path) + async for new_state, old_state, reason in self._signal_wifi.state_changed: + if not self._running: + return + logger.debug( + "Wi-Fi state: %d -> %d (reason %d)", + old_state, + new_state, + reason, + ) + self._schedule_debounced_state_rebuild() + + async def _listen_settings_new_connection(self) -> None: + """React to new saved connection profiles being added.""" + if not self._signal_settings: + return + logger.debug("Settings NewConnection listener started") + async for conn_path in self._signal_settings.new_connection: + if not self._running: + return + logger.debug("Settings: new connection %s", conn_path) + self._saved_cache_dirty = True + self._track_task( + self._asyncio_loop.create_task( + self._async_load_saved_networks(), + name="saved_on_new_connection", + ) + ) + + async def _listen_settings_connection_removed(self) -> None: + """React to saved connection profiles being deleted.""" + if not self._signal_settings: + return + logger.debug("Settings ConnectionRemoved listener started") + async for conn_path in self._signal_settings.connection_removed: + if not self._running: + return + logger.debug("Settings: connection removed %s", conn_path) + self._saved_cache_dirty = True + self._track_task( + self._asyncio_loop.create_task( + self._async_load_saved_networks(), + name="saved_on_connection_removed", + ) + ) + + def _schedule_debounced_state_rebuild(self) -> None: + """Schedule a state rebuild after a short debounce window. + + Multiple rapid D-Bus signals (e.g. during a roam or reconnect) + coalesce into a single ``_build_current_state`` call, saving + ~12-15 D-Bus round-trips per coalesced burst. + """ + if self._state_debounce_handle: + self._state_debounce_handle.cancel() + self._state_debounce_handle = self._asyncio_loop.call_later( + _DEBOUNCE_DELAY, self._fire_state_rebuild + ) + + def _fire_state_rebuild(self) -> None: + """Debounce callback — spawns the actual async state rebuild.""" + self._state_debounce_handle = None + if self._running: + self._track_task( + self._asyncio_loop.create_task( + self._async_get_current_state(), + name="debounced_state_rebuild", + ) + ) + + def _schedule_debounced_scan(self) -> None: + """Schedule a scan-results rebuild after a debounce window. + + AP Added/Removed signals can fire in rapid bursts when + entering/leaving a dense area. Coalescing prevents NxN AP + property reads. + """ + if self._scan_debounce_handle: + self._scan_debounce_handle.cancel() + self._scan_debounce_handle = self._asyncio_loop.call_later( + _DEBOUNCE_DELAY, self._fire_scan_rebuild + ) + + def _fire_scan_rebuild(self) -> None: + """Debounce callback — spawns the async scan rebuild.""" + self._scan_debounce_handle = None + if self._running: + self._track_task( + self._asyncio_loop.create_task( + self._async_scan_networks(), + name="debounced_scan_rebuild", + ) + ) + + async def _async_fallback_poll(self) -> None: + """Lightweight fallback for missed signals. + + Called at a long interval (default 60 s) by the manager. + Rebuilds state, connectivity, and saved networks. + """ + if not self._running: + return + await self._async_get_current_state() + await self._async_check_connectivity() + await self._async_load_saved_networks() + + async def _ensure_dbus_connection(self) -> bool: + """Verify the D-Bus connection is healthy, reconnecting if needed. + + Performs a lightweight ``version`` property read as a health check. + Consecutive failures increment ``_consecutive_dbus_errors``; once the + threshold is reached, opens a new system bus, re-detects interfaces, + rebuilds signal proxies, and restarts all listener tasks. Returns + ``True`` if the bus is usable (either always-healthy or successfully + reconnected), ``False`` otherwise. + """ + if not self._running: + return False + try: + _ = await self._nm().version + self._consecutive_dbus_errors = 0 + return True + except Exception as exc: + self._consecutive_dbus_errors += 1 + logger.warning( + "D-Bus health check failed (%d/%d): %s", + self._consecutive_dbus_errors, + self._MAX_DBUS_ERRORS_BEFORE_RECONNECT, + exc, + ) + if self._consecutive_dbus_errors < self._MAX_DBUS_ERRORS_BEFORE_RECONNECT: + return False + logger.warning("Attempting D-Bus reconnection...") + try: + self._system_bus = sdbus.sd_bus_open_system() + sdbus.set_default_bus(self._system_bus) + self._primary_wifi_path = "" + self._primary_wifi_iface = "" + self._primary_wired_path = "" + self._primary_wired_iface = "" + self._iface_to_device_path.clear() + await self._detect_interfaces() + # Rebuild signal proxies on new bus + self._signal_nm = None + self._signal_wifi = None + self._signal_wired = None + self._signal_settings = None + self._ensure_signal_proxies() + # Cancel stale listener tasks bound to old proxies + # and restart them on the new bus connection. + for task in self._listener_tasks: + if not task.done(): + task.cancel() + self._listener_tasks.clear() + await self._start_signal_listeners() + self._consecutive_dbus_errors = 0 + logger.info("D-Bus reconnection succeeded") + if self._primary_wifi_path or self._primary_wired_path: + self.error_occurred.emit( + "device_reconnected", "Network device reconnected" + ) + return True + except Exception as re_err: + logger.error("D-Bus reconnection failed: %s", re_err) + return False + + async def _is_ethernet_connected(self) -> bool: + """Return True if the primary wired device is fully activated (state 100).""" + if not self._primary_wired_path: + return False + try: + return await self._generic(self._primary_wired_path).state == 100 + except Exception as exc: + logger.debug("Error checking ethernet state: %s", exc) + return False + + async def _has_ethernet_carrier(self) -> bool: + """Return True if the primary wired device has a physical link (state >= 30). + + State 30 is DISCONNECTED in NM's device state enum, which still implies + a cable is present. This is a weaker check than ``_is_ethernet_connected`` + and is used to populate ``NetworkState.ethernet_carrier`` for UI feedback. + """ + if not self._primary_wired_path: + return False + try: + return await self._generic(self._primary_wired_path).state >= 30 + except Exception: + # D-Bus read failed; carrier state unknown — treat as no carrier. + return False + + async def _wait_for_wifi_radio(self, desired: bool, timeout: float = 3.0) -> bool: + """Poll NM wireless_enabled until it matches *desired* or *timeout* expires.""" + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout + _logged = False + while loop.time() < deadline: + try: + if await self._nm().wireless_enabled == desired: + return True + except Exception as exc: + if not _logged: + logger.debug("Polling wireless_enabled failed: %s", exc) + _logged = True + await asyncio.sleep(0.25) + return False + + async def _wait_for_wifi_device_ready(self, timeout: float = 8.0) -> bool: + """Poll wlan0 device state until it reaches DISCONNECTED (30) or above.""" + if not self._primary_wifi_path: + return False + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout + _logged = False + while loop.time() < deadline: + try: + if await self._generic(self._primary_wifi_path).state >= 30: + return True + except Exception as exc: + if not _logged: + logger.debug("Polling Wi-Fi device state failed: %s", exc) + _logged = True + await asyncio.sleep(0.25) + return False + + async def _async_get_current_state(self) -> None: + """Rebuild and emit the full NetworkState, enforcing runtime mutual exclusion.""" + try: + if not await self._ensure_dbus_connection(): + self.state_changed.emit(NetworkState()) + return + state = await self._build_current_state() + if ( + state.ethernet_connected + and state.wifi_enabled + and not state.hotspot_enabled + and not self._is_hotspot_active + ): + logger.info( + "Runtime mutual exclusion: ethernet active + " + "Wi-Fi — disabling Wi-Fi" + ) + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Disconnect before Wi-Fi disable ignored: %s", exc) + await self._nm().wireless_enabled.set_async(False) + await asyncio.sleep(0.5) + state = await self._build_current_state() + self.state_changed.emit(state) + except Exception as exc: + logger.error("Failed to get current state: %s", exc) + self.error_occurred.emit("get_current_state", str(exc)) + + @staticmethod + def _get_ip_os_fallback(iface: str) -> str: + """Return the IPv4 address for *iface* via a raw ioctl SIOCGIFADDR call. + + Used as a fallback when the NM D-Bus IPv4Config path returns nothing — + common immediately after DHCP on slower hardware. + """ + if not iface: + return "" + _SIOCGIFADDR = 0x8915 + try: + with _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM) as sock: + ifreq = struct.pack("256s", iface[:15].encode()) + result = fcntl.ioctl(sock.fileno(), _SIOCGIFADDR, ifreq) + return _socket.inet_ntoa(result[20:24]) + except Exception: + # ioctl fails when the interface has no address; caller treats "" as unknown. + return "" + + async def _build_current_state(self) -> NetworkState: + """Read all relevant NM properties and assemble a NetworkState snapshot.""" + if not self._system_bus: + return NetworkState() + try: + connectivity_value = await self._nm().check_connectivity() + connectivity = self._map_connectivity(connectivity_value) + wifi_enabled = bool(await self._nm().wireless_enabled) + current_ssid = await self._get_current_ssid() + + eth_connected = await self._is_ethernet_connected() + if eth_connected: + current_ip = await self._get_ip_by_interface( + self._primary_wired_iface or "eth0" + ) + if not current_ip: + current_ip = self._get_ip_os_fallback( + self._primary_wired_iface or "eth0" + ) + current_ssid = "" + elif current_ssid: + current_ip = await self._get_ip_by_interface("wlan0") + if not current_ip: + current_ip = await self._get_current_ip() + else: + current_ip = "" + + if not current_ip and connectivity in ( + ConnectivityState.FULL, + ConnectivityState.LIMITED, + ): + for _iface in ( + self._primary_wired_iface or "eth0", + "wlan0", + ): + _fallback = self._get_ip_os_fallback(_iface) + if _fallback: + current_ip = _fallback + if _iface != "wlan0": + eth_connected = True + logger.debug("OS fallback IP for '%s': %s", _iface, _fallback) + break + + signal = 0 + sec_type = "" + if current_ssid: + signal_map = await self._build_signal_map() + signal = signal_map.get(current_ssid.lower(), 0) + saved = await self._get_saved_network_cached(current_ssid) + sec_type = saved.security_type if saved else "" + + hotspot_enabled = current_ssid == self._hotspot_config.ssid + + if not hotspot_enabled and self._is_hotspot_active and not current_ssid: + hotspot_enabled = True + current_ssid = self._hotspot_config.ssid + logger.debug( + "Hotspot SSID not found via D-Bus, using config: '%s'", + current_ssid, + ) + + if hotspot_enabled: + sec_type = self._hotspot_config.security + if not current_ip: + current_ip = await self._get_ip_by_interface("wlan0") + + return NetworkState( + connectivity=connectivity, + current_ssid=current_ssid, + current_ip=current_ip, + wifi_enabled=wifi_enabled, + hotspot_enabled=hotspot_enabled, + signal_strength=signal, + security_type=sec_type, + ethernet_connected=eth_connected, + ethernet_carrier=await self._has_ethernet_carrier(), + active_vlans=await self._get_active_vlans(), + ) + except Exception as exc: + logger.error("Error building current state: %s", exc) + return NetworkState() + + @staticmethod + def _map_connectivity(value: int) -> ConnectivityState: + """Map a raw NM connectivity integer to a ConnectivityState enum member.""" + try: + return ConnectivityState(value) + except ValueError: + return ConnectivityState.UNKNOWN + + async def _async_check_connectivity(self) -> None: + """Query NM connectivity and emit connectivity_changed.""" + try: + if not self._system_bus: + self.connectivity_changed.emit(ConnectivityState.UNKNOWN) + return + self.connectivity_changed.emit( + self._map_connectivity(await self._nm().check_connectivity()) + ) + except Exception as exc: + logger.error("Failed to check connectivity: %s", exc) + self.connectivity_changed.emit(ConnectivityState.UNKNOWN) + + async def _get_current_ssid(self) -> str: + """Return the SSID of the currently active Wi-Fi connection, or empty string.""" + try: + primary_con = await self._nm().primary_connection + if primary_con and primary_con != "/": + ssid = await self._ssid_from_active_connection(primary_con) + if ssid: + return ssid + return await self._get_ssid_from_any_active() + except Exception as exc: + logger.debug("Error getting current SSID: %s", exc) + return "" + + async def _ssid_from_active_connection(self, active_path: str) -> str: + """Extract the Wi-Fi SSID from an active connection object path, or return ''.""" + try: + conn_path = await self._active_conn(active_path).connection + if not conn_path or conn_path == "/": + return "" + settings = await self._conn_settings(conn_path).get_settings() + if "802-11-wireless" in settings: + ssid = settings["802-11-wireless"]["ssid"][1].decode() + return ssid + except Exception as exc: + logger.debug( + "Error reading active connection %s: %s", + active_path, + exc, + ) + return "" + + async def _get_ssid_from_any_active(self) -> str: + """Scan all active NM connections and return the first Wi-Fi SSID found.""" + try: + active_paths = await self._nm().active_connections + for active_path in active_paths: + ssid = await self._ssid_from_active_connection(active_path) + if ssid: + return ssid + except Exception as exc: + logger.debug("Error scanning active connections: %s", exc) + return "" + + async def _get_current_ip(self) -> str: + """Return the IPv4 address from the primary NM connection's IP4Config.""" + try: + primary_con = await self._nm().primary_connection + if primary_con == "/": + return "" + ip4_path = await self._active_conn(primary_con).ip4_config + if ip4_path == "/": + return "" + addr_data = await self._ipv4(ip4_path).address_data + if addr_data: + return addr_data[0]["address"][1] + return "" + except Exception as exc: + logger.debug("Error getting current IP: %s", exc) + return "" + + async def _get_ip_by_interface(self, interface: str = "wlan0") -> str: + """Return the IPv4 address assigned to *interface* via NM's IP4Config D-Bus object.""" + try: + device_path = self._iface_to_device_path.get(interface) + if not device_path: + devices = await self._nm().get_devices() + for dp in devices: + if await self._generic(dp).interface == interface: + device_path = dp + self._iface_to_device_path[interface] = dp + break + if not device_path: + return "" + ip4_path = await self._generic(device_path).ip4_config + if not ip4_path or ip4_path == "/": + return "" + addr_data = await self._ipv4(ip4_path).address_data + if addr_data: + return addr_data[0]["address"][1] + return "" + except Exception as exc: + logger.error("Failed to get IP for %s: %s", interface, exc) + return "" + + async def _async_scan_networks(self) -> None: + """Request an NM rescan, parse visible APs, and emit networks_scanned.""" + try: + if not self._primary_wifi_path: + self.networks_scanned.emit([]) + return + if not await self._ensure_dbus_connection(): + self.networks_scanned.emit([]) + return + + if not await self._nm().wireless_enabled: + self.networks_scanned.emit([]) + return + + try: + await self._wifi().request_scan({}) + except Exception as exc: + logger.debug( + "Scan request ignored (already scanning or radio off): %s", exc + ) + + if await self._wifi().last_scan == -1: + self.networks_scanned.emit([]) + return + + ap_paths = await self._wifi().get_all_access_points() + current_ssid = await self._get_current_ssid() + saved_ssids = set(await self._get_saved_ssid_names_cached()) + + networks: list[NetworkInfo] = [] + seen_ssids: set[str] = set() + + for ap_path in ap_paths: + try: + info = await self._parse_ap(ap_path, current_ssid, saved_ssids) + if ( + info + and info.ssid not in seen_ssids + and not is_hidden_ssid(info.ssid) + and (info.signal_strength > 0 or info.is_active) + ): + networks.append(info) + seen_ssids.add(info.ssid) + except Exception as exc: + logger.debug("Failed to parse AP %s: %s", ap_path, exc) + + networks.sort(key=lambda n: (-n.network_status, -n.signal_strength)) + self.networks_scanned.emit(networks) + + except Exception as exc: + logger.error("Failed to scan networks: %s", exc) + self.error_occurred.emit("scan_networks", str(exc)) + self.networks_scanned.emit([]) + + async def _get_all_ap_properties(self, ap_path: str) -> dict[str, object]: + """Fetch all D-Bus properties for an AccessPoint in one round-trip.""" + try: + return await self._ap(ap_path).properties_get_all_dict( + on_unknown_member="ignore" + ) + except Exception as exc: + logger.debug("GetAll failed for AP %s: %s", ap_path, exc) + return {} + + async def _build_signal_map(self) -> dict[str, int]: + """Return a mapping of lowercase SSID to best-seen signal strength (0-100).""" + signal_map: dict[str, int] = {} + if not self._primary_wifi_path: + return signal_map + try: + ap_paths = await self._wifi().access_points + for ap_path in ap_paths: + try: + props = await self._get_all_ap_properties(ap_path) + ssid = self._decode_ssid(props.get("ssid", b"")) + if ssid: + strength = int(props.get("strength", 0)) + key = ssid.lower() + if strength > signal_map.get(key, 0): + signal_map[key] = strength + except Exception as exc: + logger.debug("Skipping AP in signal map: %s", exc) + continue + except Exception as exc: + logger.debug("Error building signal map: %s", exc) + return signal_map + + async def _parse_ap( + self, ap_path: str, current_ssid: str, saved_ssids: set + ) -> NetworkInfo | None: + """Parse an AccessPoint D-Bus object into a NetworkInfo, or None if unusable.""" + props = await self._get_all_ap_properties(ap_path) + if not props: + return None + + ssid = self._decode_ssid(props.get("ssid", b"")) + if not ssid or is_hidden_ssid(ssid): + return None + + flags = int(props.get("flags", 0)) + wpa_flags = int(props.get("wpa_flags", 0)) + rsn_flags = int(props.get("rsn_flags", 0)) + is_open = (flags & 1) == 0 + + security = self._determine_security_type(flags, wpa_flags, rsn_flags) + if not is_connectable_security(security): + return None + + is_active = ssid == current_ssid + is_saved = ssid in saved_ssids + if is_active: + net_status = NetworkStatus.ACTIVE + elif is_saved: + net_status = NetworkStatus.SAVED + elif is_open: + net_status = NetworkStatus.OPEN + else: + net_status = NetworkStatus.DISCOVERED + + return NetworkInfo( + ssid=ssid, + signal_strength=int(props.get("strength", 0)), + network_status=net_status, + bssid=str(props.get("hw_address", "")), + frequency=int(props.get("frequency", 0)), + max_bitrate=int(props.get("max_bitrate", 0)), + security_type=security, + ) + + @staticmethod + def _decode_ssid(raw: object) -> str: + """Decode a raw SSID byte string to a UTF-8 str, replacing invalid bytes.""" + if isinstance(raw, bytes): + return raw.decode("utf-8", errors="replace") + return str(raw) if raw else "" + + @staticmethod + def _determine_security_type( + flags: int, wpa_flags: int, rsn_flags: int + ) -> SecurityType: + """Determine the Wi-Fi SecurityType from AP capability flags.""" + if (flags & 1) == 0: + return SecurityType.OPEN + if rsn_flags: + if rsn_flags & 0x400: + return SecurityType.WPA3_SAE + if rsn_flags & 0x200: + return SecurityType.WPA_EAP + return SecurityType.WPA2_PSK + if wpa_flags: + if wpa_flags & 0x200: + return SecurityType.WPA_EAP + return SecurityType.WPA_PSK + return SecurityType.WEP + + def _invalidate_saved_cache(self) -> None: + """Mark the saved-networks cache as dirty so it is rebuilt on next access.""" + self._saved_cache_dirty = True + + async def _get_saved_ssid_names_cached(self) -> list[str]: + """Return SSID names for all saved Wi-Fi profiles, refreshing cache if dirty.""" + if self._saved_cache_dirty: + self._saved_cache = await self._get_saved_networks_impl() + self._saved_cache_dirty = False + return [n.ssid for n in self._saved_cache] + + async def _get_saved_network_cached(self, ssid: str) -> SavedNetwork | None: + """Return the SavedNetwork for *ssid* from cache (case-insensitive), or None.""" + if self._saved_cache_dirty: + self._saved_cache = await self._get_saved_networks_impl() + self._saved_cache_dirty = False + ssid_lower = ssid.lower() + for n in self._saved_cache: + if n.ssid.lower() == ssid_lower: + return n + return None + + async def _async_load_saved_networks(self) -> None: + """Reload all saved Wi-Fi profiles and emit saved_networks_loaded.""" + try: + networks = await self._get_saved_networks_impl() + self._saved_cache = networks + self._saved_cache_dirty = False + self.saved_networks_loaded.emit(networks) + except Exception as exc: + logger.error("Failed to load saved networks: %s", exc) + self.error_occurred.emit("load_saved_networks", str(exc)) + self.saved_networks_loaded.emit([]) + + async def _get_saved_networks_impl(self) -> list[SavedNetwork]: + """Enumerate NM connection profiles and return infrastructure Wi-Fi ones.""" + if not self._system_bus: + return [] + try: + connections = await self._nm_settings().list_connections() + signal_map = await self._build_signal_map() + saved: list[SavedNetwork] = [] + + for conn_path in connections: + try: + settings = await self._conn_settings(conn_path).get_settings() + if settings["connection"]["type"][1] != "802-11-wireless": + continue + + wireless = settings["802-11-wireless"] + ssid = wireless["ssid"][1].decode() + uuid = settings["connection"]["uuid"][1] + mode = str(wireless.get("mode", (None, "infrastructure"))[1]) + + security_key = str(wireless.get("security", (None, ""))[1]) + sec_type = "" + if security_key and security_key in settings: + sec_type = settings[security_key].get("key-mgmt", (None, ""))[1] + + priority = settings["connection"].get( + "autoconnect-priority", + (None, ConnectionPriority.MEDIUM.value), + )[1] + timestamp = settings["connection"].get("timestamp", (None, 0))[1] + signal = signal_map.get(ssid.lower(), 0) + ipv4_method = settings.get("ipv4", {}).get( + "method", (None, "auto") + )[1] + is_dhcp = ipv4_method != "manual" + + saved.append( + SavedNetwork( + ssid=ssid, + uuid=uuid, + connection_path=conn_path, + security_type=sec_type, + mode=mode, + priority=priority or ConnectionPriority.MEDIUM.value, + signal_strength=signal, + timestamp=int(timestamp or 0), + is_dhcp=is_dhcp, + ) + ) + except Exception as exc: + logger.debug("Failed to parse connection: %s", exc) + + return saved + except Exception as exc: + logger.error("Error getting saved networks: %s", exc) + return [] + + async def _is_known(self, ssid: str) -> bool: + """Return True if a saved profile for *ssid* exists in the cache.""" + return await self._get_saved_network_cached(ssid) is not None + + async def _get_connection_path(self, ssid: str) -> str | None: + """Return the D-Bus connection path for a saved *ssid* profile, or None.""" + saved = await self._get_saved_network_cached(ssid) + return saved.connection_path if saved else None + + async def _async_add_network(self, ssid: str, password: str, priority: int) -> None: + """Add and activate a new Wi-Fi profile, emitting connection_result when done.""" + try: + result = await self._add_network_impl(ssid, password, priority) + self._invalidate_saved_cache() + self.connection_result.emit(result) + except Exception as exc: + logger.error("Failed to add network: %s", exc) + self.connection_result.emit( + ConnectionResult( + success=False, + message=str(exc), + error_code="add_failed", + ) + ) + + async def _add_network_impl( + self, ssid: str, password: str, priority: int + ) -> ConnectionResult: + """Scan for the SSID, build a connection profile, add it to NM, and activate it. + + Deletes any pre-existing profile for the same SSID before adding. + Returns a failed ConnectionResult if the SSID is not visible, the + security type is unsupported, or the 20-second activation wait times out. + """ + if not self._primary_wifi_path or not self._system_bus: + return ConnectionResult(False, "No Wi-Fi interface", "no_interface") + + if await self._is_known(ssid): + await self._delete_network_impl(ssid) + self._invalidate_saved_cache() + + try: + await self._wifi().request_scan({}) + except Exception as exc: + logger.debug("Pre-connect scan request ignored: %s", exc) + + ap_paths = await self._wifi().get_all_access_points() + target_ap_path: str | None = None + target_ap_props: dict[str, object] = {} + for ap_path in ap_paths: + props = await self._get_all_ap_properties(ap_path) + if self._decode_ssid(props.get("ssid", b"")) == ssid: + target_ap_path = ap_path + target_ap_props = props + break + + if not target_ap_path: + return ConnectionResult(False, f"Network '{ssid}' not found", "not_found") + + interface = await self._wifi().interface + conn_props = self._build_connection_properties( + ssid, password, interface, priority, target_ap_props + ) + if not conn_props: + return ConnectionResult( + False, + "Unsupported security type", + "unsupported_security", + ) + + try: + nm_settings = self._nm_settings() + conn_path = await nm_settings.add_connection(conn_props) + except Exception as exc: + err_str = str(exc).lower() + if "psk" in err_str and ("invalid" in err_str or "property" in err_str): + return ConnectionResult( + False, + "Wrong password, try again.", + "invalid_password", + ) + return ConnectionResult(False, str(exc), "add_failed") + + if _CAN_RELOAD_CONNECTIONS: + try: + await self._nm_settings().reload_connections() + except Exception as reload_err: + logger.debug("reload_connections non-fatal: %s", reload_err) + + try: + await self._nm().activate_connection(conn_path) + if not await self._wait_for_connection(ssid, timeout=_WIFI_CONNECT_TIMEOUT): + await self._delete_network_impl(ssid) + self._invalidate_saved_cache() + return ConnectionResult( + False, + f"Authentication failed for '{ssid}'.\n" + "The saved profile has been removed.\n" + "Please check the password and try again.", + "auth_failed", + ) + return ConnectionResult(True, f"Network '{ssid}' added and connecting") + except Exception as act_err: + logger.warning("Activate after add failed: %s", act_err) + return ConnectionResult(True, f"Network '{ssid}' added (activate manually)") + + def _build_connection_properties( + self, + ssid: str, + password: str, + interface: str, + priority: int, + ap_props: dict[str, object], + ) -> dict[str, object] | None: + """Build NM connection property dict for *ssid* from its AP capability flags. + + Returns None if the security type is unsupported (e.g. WPA-EAP). + Handles OPEN, WPA-PSK, WPA2-PSK, and WPA3-SAE (including SAE-transition). + """ + flags = int(ap_props.get("flags", 0)) + wpa_flags = int(ap_props.get("wpa_flags", 0)) + rsn_flags = int(ap_props.get("rsn_flags", 0)) + + props: dict[str, object] = { + "connection": { + "id": ("s", ssid), + "uuid": ("s", str(uuid4())), + "type": ("s", "802-11-wireless"), + "interface-name": ("s", interface), + "autoconnect": ("b", True), + "autoconnect-priority": ("i", priority), + }, + "802-11-wireless": { + "mode": ("s", "infrastructure"), + "ssid": ("ay", ssid.encode("utf-8")), + }, + "ipv4": { + "method": ("s", "auto"), + "route-metric": ("i", 200), + }, + "ipv6": {"method": ("s", "auto")}, + } + + if (flags & 1) == 0: + return props + + props["802-11-wireless"]["security"] = ( + "s", + "802-11-wireless-security", + ) + security = self._determine_security_type(flags, wpa_flags, rsn_flags) + + if not is_connectable_security(security): + logger.warning( + "Rejecting connection to '%s': unsupported security %s", + ssid, + security.value, + ) + return None + + if security == SecurityType.WPA3_SAE: + has_psk = bool((rsn_flags & 0x100) or wpa_flags) + if has_psk: + logger.debug( + "SAE transition for '%s' — using wpa-psk + PMF optional", + ssid, + ) + props["802-11-wireless-security"] = { + "key-mgmt": ("s", "wpa-psk"), + "auth-alg": ("s", "open"), + "psk": ("s", password), + "pmf": ("u", 2), # OPTIONAL — required for SAE-transition APs + } + else: + logger.debug("Pure SAE detected for '%s'", ssid) + props["802-11-wireless-security"] = { + "key-mgmt": ("s", "sae"), + "auth-alg": ("s", "open"), + "psk": ("s", password), + "pmf": ("u", 3), # REQUIRED — mandatory for pure WPA3-SAE + } + elif security in ( + SecurityType.WPA2_PSK, + SecurityType.WPA_PSK, + ): + props["802-11-wireless-security"] = { + "key-mgmt": ("s", "wpa-psk"), + "auth-alg": ("s", "open"), + "psk": ("s", password), + } + else: + logger.warning( + "Unsupported security type '%s' for '%s'", + security.value, + ssid, + ) + return None + + return props + + async def _async_connect_network(self, ssid: str) -> None: + """Activate an existing saved Wi-Fi profile and emit connection_result.""" + try: + self._is_hotspot_active = False + result = await self._connect_network_impl(ssid) + self.connection_result.emit(result) + self.state_changed.emit(await self._build_current_state()) + except Exception as exc: + logger.error("Failed to connect: %s", exc) + self.connection_result.emit( + ConnectionResult( + success=False, + message=str(exc), + error_code="connect_failed", + ) + ) + + async def _wait_for_connection( + self, ssid: str, timeout: float = _WIFI_CONNECT_TIMEOUT + ) -> bool: + """Poll until *ssid* is active and has an IP, or until *timeout* expires. + + Starts with a 1.5 s initial delay to let NM begin the association. + Returns False early if the SSID disappears for 3 consecutive polls. + """ + loop = asyncio.get_running_loop() + deadline = loop.time() + timeout + await asyncio.sleep(1.5) + consecutive_empty = 0 + while loop.time() < deadline: + try: + current = await self._get_current_ssid() + if current and current.lower() == ssid.lower(): + ip = await self._get_current_ip() + if ip: + return True + consecutive_empty = 0 + else: + consecutive_empty += 1 + if consecutive_empty >= 3: + return False + except Exception as exc: + logger.debug("Connection wait poll failed: %s", exc) + await asyncio.sleep(0.5) + return False + + async def _connect_network_impl(self, ssid: str) -> ConnectionResult: + """Enable Wi-Fi if needed, locate the saved profile, and activate it.""" + if not self._system_bus: + return ConnectionResult(False, "NetworkManager unavailable", "no_nm") + + if not await self._nm().wireless_enabled: + await self._nm().wireless_enabled.set_async(True) + if not await self._wait_for_wifi_radio(True, timeout=8.0): + return ConnectionResult( + False, + "Wi-Fi radio failed to turn on.\nPlease try again.", + "radio_failed", + ) + await self._wait_for_wifi_device_ready(timeout=8.0) + + conn_path = await self._get_connection_path(ssid) + if not conn_path: + conn_path = await self._find_connection_path_direct(ssid) + if not conn_path: + return ConnectionResult(False, f"Network '{ssid}' not saved", "not_found") + + try: + await self._nm().activate_connection(conn_path) + if not await self._wait_for_connection(ssid, timeout=_WIFI_CONNECT_TIMEOUT): + return ConnectionResult( + False, + f"Could not connect to '{ssid}'.\n" + "Please check signal strength and try again.", + "connect_timeout", + ) + return ConnectionResult(True, f"Connected to '{ssid}'") + except Exception as exc: + return ConnectionResult(False, str(exc), "connect_failed") + + async def _find_connection_path_direct(self, ssid: str) -> str | None: + """Search NM settings for an infrastructure profile matching *ssid* directly.""" + try: + connections = await self._nm_settings().list_connections() + for conn_path in connections: + try: + settings = await self._conn_settings(conn_path).get_settings() + if settings["connection"]["type"][1] != "802-11-wireless": + continue + conn_ssid = settings["802-11-wireless"]["ssid"][1].decode() + if conn_ssid.lower() == ssid.lower(): + self._invalidate_saved_cache() + return conn_path + except Exception as exc: + logger.debug("Skipping connection in path lookup: %s", exc) + continue + except Exception as exc: + logger.debug("Direct connection path lookup failed: %s", exc) + return None + + async def _async_disconnect(self) -> None: + """Disconnect the primary Wi-Fi device and emit connection_result.""" + try: + if self._primary_wifi_path: + await self._wifi().disconnect() + self.connection_result.emit(ConnectionResult(True, "Disconnected")) + except Exception as exc: + logger.error("Disconnect failed: %s", exc) + self.connection_result.emit( + ConnectionResult( + success=False, + message=str(exc), + error_code="disconnect_failed", + ) + ) + + async def _async_delete_network(self, ssid: str) -> None: + """Delete the saved profile for *ssid* and emit connection_result.""" + try: + result = await self._delete_network_impl(ssid) + self._invalidate_saved_cache() + self.connection_result.emit(result) + if result.success: + self.state_changed.emit(await self._build_current_state()) + except Exception as exc: + logger.error("Delete failed: %s", exc) + self.connection_result.emit( + ConnectionResult( + success=False, + message=str(exc), + error_code="delete_failed", + ) + ) + + async def _delete_network_impl(self, ssid: str) -> ConnectionResult: + """Delete the NM connection profile for *ssid* and disconnect if it is active.""" + conn_path = await self._get_connection_path(ssid) + if not conn_path: + return ConnectionResult(False, f"Network '{ssid}' not found", "not_found") + try: + await self._conn_settings(conn_path).delete() + + if _CAN_RELOAD_CONNECTIONS: + try: + await self._nm_settings().reload_connections() + except Exception as reload_err: + logger.debug("reload_connections non-fatal: %s", reload_err) + + current_ssid = await self._get_current_ssid() + if current_ssid and current_ssid.lower() == ssid.lower(): + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Disconnect after network delete ignored: %s", exc) + + return ConnectionResult(True, f"Network '{ssid}' deleted") + except Exception as exc: + return ConnectionResult(False, str(exc), "delete_failed") + + async def _async_update_network( + self, + ssid: str, + password: str = "", + priority: int = 0, + ) -> None: + """Update password and/or priority for a saved profile and emit connection_result.""" + try: + result = await self._update_network_impl( + ssid, + password or None, + priority if priority != 0 else None, + ) + self._invalidate_saved_cache() + self.connection_result.emit(result) + except Exception as exc: + logger.error("Update failed: %s", exc) + self.connection_result.emit( + ConnectionResult( + success=False, + message=str(exc), + error_code="update_failed", + ) + ) + + async def _update_network_impl( + self, + ssid: str, + password: str | None, + priority: int | None, + ) -> ConnectionResult: + """Merge updated password/priority into the existing NM connection settings.""" + conn_path = await self._get_connection_path(ssid) + if not conn_path: + return ConnectionResult(False, f"Network '{ssid}' not found", "not_found") + try: + cs = self._conn_settings(conn_path) + props = await cs.get_settings() + await self._merge_wifi_secrets(cs, props) + + if password and "802-11-wireless-security" in props: + props["802-11-wireless-security"]["psk"] = ( + "s", + password, + ) + + if priority is not None: + props["connection"]["autoconnect-priority"] = ( + "i", + priority, + ) + logger.debug("Setting priority for '%s' to %d", ssid, priority) + + await cs.update(props) + logger.debug("Network '%s' update() succeeded", ssid) + return ConnectionResult(True, f"Network '{ssid}' updated") + except Exception as exc: + logger.error("Update failed for '%s': %s", ssid, exc) + err_str = str(exc).lower() + if "psk" in err_str and ("invalid" in err_str or "property" in err_str): + return ConnectionResult( + False, + "Wrong password, try again.", + "invalid_password", + ) + return ConnectionResult(False, str(exc), "update_failed") + + async def _async_set_wifi_enabled(self, enabled: bool) -> None: + """Enable or disable the Wi-Fi radio, handling ethernet mutual exclusion.""" + try: + if not self._system_bus: + return + if not enabled: + self._is_hotspot_active = False + + if enabled and await self._is_ethernet_connected(): + await self._async_disconnect_ethernet() + + current = await self._nm().wireless_enabled + if current != enabled: + if not enabled: + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug( + "Disconnect before Wi-Fi toggle ignored: %s", exc + ) + await asyncio.sleep(0.5) + + await self._nm().wireless_enabled.set_async(enabled) + + if not await self._wait_for_wifi_radio(enabled, timeout=8.0): + logger.warning( + "Wi-Fi radio did not reach %s within 8 s", + "enabled" if enabled else "disabled", + ) + + self.connection_result.emit( + ConnectionResult( + True, + f"Wi-Fi {'enabled' if enabled else 'disabled'}", + ) + ) + self.state_changed.emit(await self._build_current_state()) + except Exception as exc: + logger.error("Failed to toggle Wi-Fi: %s", exc) + self.error_occurred.emit("set_wifi_enabled", str(exc)) + + async def _async_disconnect_ethernet(self) -> None: + """Deactivate all VLANs, disconnect ethernet, and wait up to 4 s for teardown.""" + if not self._primary_wired_path: + return + try: + await self._deactivate_all_vlans() + await self._wired().disconnect() + loop = asyncio.get_running_loop() + deadline = loop.time() + 4.0 + while loop.time() < deadline: + await asyncio.sleep(0.5) + if not await self._is_ethernet_connected(): + break + logger.info("Ethernet disconnected") + except Exception as exc: + logger.error("Failed to disconnect ethernet: %s", exc) + + async def _async_connect_ethernet(self) -> None: + """Disable Wi-Fi/hotspot, activate the wired device, and restore saved VLANs.""" + if not self._primary_wired_path: + self.error_occurred.emit("connect_ethernet", "No wired device found") + return + try: + if self._is_hotspot_active: + await self._async_toggle_hotspot(False) + + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Pre-VLAN disconnect ignored: %s", exc) + await asyncio.sleep(0.5) + + if await self._nm().wireless_enabled: + await self._nm().wireless_enabled.set_async(False) + await self._wait_for_wifi_radio(False, timeout=8.0) + + await self._nm().activate_connection("/", self._primary_wired_path, "/") + await asyncio.sleep(1.5) + + await self._activate_saved_vlans() + logger.info("Ethernet connection activated") + self.connection_result.emit(ConnectionResult(True, "Ethernet connected")) + self.state_changed.emit(await self._build_current_state()) + except Exception as exc: + logger.error("Failed to connect ethernet: %s", exc) + self.error_occurred.emit("connect_ethernet", str(exc)) + self.state_changed.emit(await self._build_current_state()) + + async def _async_create_vlan( + self, + vlan_id: int, + ip_address: str, + subnet_mask: str, + gateway: str, + dns1: str, + dns2: str, + ) -> None: + """Create and activate a VLAN connection on the primary wired interface. + + If *ip_address* is empty the VLAN uses DHCP and waits up to 45 s for a + lease; otherwise a static configuration is applied. Emits + connection_result and state_changed when done. + """ + if not self._primary_wired_path: + self.error_occurred.emit("create_vlan", "No wired device") + return + try: + if self._is_hotspot_active: + await self._async_toggle_hotspot(False) + + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Pre-VLAN disconnect ignored: %s", exc) + await asyncio.sleep(0.5) + + if await self._nm().wireless_enabled: + await self._nm().wireless_enabled.set_async(False) + await self._wait_for_wifi_radio(False, timeout=8.0) + + if not await self._is_ethernet_connected(): + await self._nm().activate_connection("/", self._primary_wired_path, "/") + await asyncio.sleep(1.5) + + iface = self._primary_wired_iface or "eth0" + + try: + existing_conns = await self._nm_settings().list_connections() + for existing_path in existing_conns: + try: + s = await self._conn_settings(existing_path).get_settings() + if ( + s.get("connection", {}).get("type", (None, ""))[1] == "vlan" + and s.get("vlan", {}).get("id", (None, -1))[1] == vlan_id + and s.get("vlan", {}).get("parent", (None, ""))[1] == iface + ): + self.connection_result.emit( + ConnectionResult( + False, + f"VLAN {vlan_id} already exists on " + f"{iface}.\nRemove it first before " + "creating a new one.", + "duplicate_vlan", + ) + ) + return + except Exception as exc: + logger.debug( + "Skipping connection in duplicate VLAN check: %s", exc + ) + continue + except Exception as dup_err: + logger.debug( + "Duplicate VLAN check failed (non-fatal): %s", + dup_err, + ) + + vlan_conn_id = f"VLAN {vlan_id}" + + if await self._deactivate_connection_by_id(vlan_conn_id): + await asyncio.sleep(1.0) + + await self._delete_all_connections_by_id(vlan_conn_id) + await asyncio.sleep(0.5) + + use_dhcp = not ip_address + + conn_props: dict[str, object] = { + "connection": { + "id": ("s", vlan_conn_id), + "uuid": ("s", str(uuid4())), + "type": ("s", "vlan"), + "autoconnect": ("b", False), + }, + "vlan": { + "id": ("u", vlan_id), + "parent": ("s", iface), + }, + "ipv6": {"method": ("s", "ignore")}, + } + + if use_dhcp: + conn_props["ipv4"] = { + "method": ("s", "auto"), + "route-metric": ("i", 500), + } + else: + prefix = self._mask_to_prefix(subnet_mask) + ip_uint = self._ip_to_nm_uint32(ip_address) + gw_uint = self._ip_to_nm_uint32(gateway) if gateway else 0 + dns_list: list[int] = [] + if dns1: + dns_list.append(self._ip_to_nm_uint32(dns1)) + if dns2: + dns_list.append(self._ip_to_nm_uint32(dns2)) + conn_props["ipv4"] = { + "method": ("s", "manual"), + "addresses": ( + "aau", + [[ip_uint, prefix, gw_uint]], + ), + "gateway": ("s", gateway or ""), + "dns": ("au", dns_list), + "route-metric": ("i", 500), + } + + conn_path = await self._nm_settings().add_connection(conn_props) + + if use_dhcp: + vlan_iface = f"{iface}.{vlan_id}" + ok, _msg = await self._async_activate_vlan_with_timeout( + conn_path, vlan_id, vlan_iface, timeout=45.0 + ) + if not ok: + if vlan_id in self._deleted_vlan_ids: + self._deleted_vlan_ids.discard(vlan_id) + logger.info( + "VLAN %d was manually deleted during DHCP " + "activation — skipping cleanup", + vlan_id, + ) + self.connection_result.emit( + ConnectionResult( + False, + f"VLAN {vlan_id} was removed during DHCP activation.", + "vlan_dhcp_timeout", + ) + ) + return + await self._delete_all_connections_by_id(vlan_conn_id) + logger.info( + "Deleted VLAN %d profile after DHCP failure", + vlan_id, + ) + self.connection_result.emit( + ConnectionResult( + False, + "There isn't a DHCP VLAN server.\n" + "Use a static IP address for this VLAN.", + "vlan_dhcp_timeout", + ) + ) + self.state_changed.emit(await self._build_current_state()) + return + else: + await self._nm().activate_connection(conn_path, "/", "/") + self.state_changed.emit(await self._build_current_state()) + await asyncio.sleep(1.5) + + self.connection_result.emit( + ConnectionResult(True, f"VLAN {vlan_id} connected") + ) + self.state_changed.emit(await self._build_current_state()) + except Exception as exc: + logger.error("Failed to create VLAN %d: %s", vlan_id, exc) + self.error_occurred.emit("create_vlan", str(exc)) + self.state_changed.emit(await self._build_current_state()) + + async def _async_delete_vlan(self, vlan_id: int) -> None: + """Delete all NM connection profiles for *vlan_id* and emit connection_result.""" + try: + self._deleted_vlan_ids.add(vlan_id) + deleted = await self._delete_all_connections_by_id(f"VLAN {vlan_id}") + logger.info( + "Deleted %d VLAN profile(s) for VLAN %d", + deleted, + vlan_id, + ) + self.connection_result.emit( + ConnectionResult(True, f"VLAN {vlan_id} removed") + ) + self.state_changed.emit(await self._build_current_state()) + except Exception as exc: + logger.error("Failed to delete VLAN %d: %s", vlan_id, exc) + self.error_occurred.emit("delete_vlan", str(exc)) + + async def _async_activate_vlan_with_timeout( + self, + conn_path: str, + vlan_id: int, + iface: str, + timeout: float = 45.0, + ) -> tuple[bool, str]: + """Activate a VLAN and wait for DHCP via D-Bus ``state_changed`` signal. + + Subscribes to ``ActiveConnection.state_changed`` (signature ``'uu'``) + which fires ``(ConnectionState, ConnectionStateReason)`` on every NM + transition. This replaces the old poll-and-sleep loop, cutting + latency from ~2 s per poll to near-instant and eliminating + unnecessary D-Bus round-trips on resource-constrained Pi hardware. + + .. note:: The old code used ``_NM_ACTIVATED = 4`` which was + actually ``DEACTIVATED`` — DHCP success was never detected + via state polling. This version uses the correct enum values. + """ + try: + active_path = await self._nm().activate_connection(conn_path, "/", "/") + except Exception as exc: + return ( + False, + f"VLAN {vlan_id}: activation request failed — {exc}", + ) + + # Fresh proxy for signal subscription lifetime. + ac = dbus_nm.ActiveConnection(bus=self._system_bus, connection_path=active_path) + + try: + async with asyncio.timeout(timeout): + # state_changed signature 'uu' -> (state: int, reason: int) + async for ac_state, ac_reason in ac.state_changed: + if not self._running: + return False, "Worker shutting down" + + try: + state_name = dbus_nm.ConnectionState(ac_state).name + except ValueError: + state_name = str(ac_state) + try: + reason_name = dbus_nm.ConnectionStateReason(ac_reason).name + except ValueError: + reason_name = str(ac_reason) + + logger.debug( + "VLAN %d AC: state=%s reason=%s", + vlan_id, + state_name, + reason_name, + ) + + if ac_state == dbus_nm.ConnectionState.ACTIVATED: + logger.info( + "VLAN %d DHCP activated on %s", + vlan_id, + iface, + ) + return True, "" + + if ac_state in ( + dbus_nm.ConnectionState.DEACTIVATING, + dbus_nm.ConnectionState.DEACTIVATED, + ): + logger.warning( + "VLAN %d DHCP failed: %s/%s on %s", + vlan_id, + state_name, + reason_name, + iface, + ) + return ( + False, + f"VLAN {vlan_id}: no DHCP server " + f"responded on {iface}.\n" + "Use a static IP or connect a " + "DHCP server to this segment.", + ) + + except TimeoutError: + logger.warning( + "VLAN %d DHCP timed out after %.0f s — " + "deactivating to stop NM retry loop", + vlan_id, + timeout, + ) + try: + await self._nm().deactivate_connection(active_path) + except Exception as exc: + logger.debug("VLAN deactivation after DHCP timeout ignored: %s", exc) + return ( + False, + f"VLAN {vlan_id}: DHCP timed out after " + f"{int(timeout)} s.\nNo DHCP server responded " + "on this network segment.\n" + "Use a static IP address instead.", + ) + + except Exception as exc: + err_str = str(exc) + if "does not exist" in err_str or "No such" in err_str: + logger.debug( + "VLAN %d active connection gone (%s) — signal iterator ended", + vlan_id, + err_str, + ) + return ( + False, + f"VLAN {vlan_id}: connection was removed externally.", + ) + raise + + # Signal iterator ended without a terminal state (shouldn't + # happen, but defensive). + return False, f"VLAN {vlan_id}: unexpected end of state stream." + + async def _get_active_vlans(self) -> tuple[VlanInfo, ...]: + """Return a tuple of VlanInfo for all currently active VLAN connections.""" + vlans: list[VlanInfo] = [] + try: + active_paths = await self._nm().active_connections + for active_path in active_paths: + try: + ac = self._active_conn(active_path) + conn_path = await ac.connection + settings = await self._conn_settings(conn_path).get_settings() + conn_type = settings.get("connection", {}).get("type", (None, ""))[ + 1 + ] + if conn_type != "vlan": + continue + + vlan_id = settings.get("vlan", {}).get("id", (None, 0))[1] + iface = settings.get("connection", {}).get( + "interface-name", (None, "") + )[1] + if not iface: + parent = settings.get("vlan", {}).get("parent", (None, "eth0"))[ + 1 + ] + iface = f"{parent}.{vlan_id}" + + ipv4_method = settings.get("ipv4", {}).get( + "method", (None, "auto") + )[1] + is_dhcp = ipv4_method != "manual" + + dns_data = settings.get("ipv4", {}).get("dns-data", (None, []))[1] + dns_servers: tuple[str, ...] = () + if dns_data: + dns_servers = tuple(str(d) for d in dns_data) + else: + dns_raw = settings.get("ipv4", {}).get("dns", (None, []))[1] + if dns_raw: + dns_servers = tuple( + self._nm_uint32_to_ip(d) for d in dns_raw + ) + + ip_addr = "" + gateway = "" + try: + ip4_path = await self._active_conn(active_path).ip4_config + if ip4_path and ip4_path != "/": + ip4_cfg = self._ipv4(ip4_path) + addr_data = await ip4_cfg.address_data + if addr_data: + ip_addr = str(addr_data[0]["address"][1]) + gw = await ip4_cfg.gateway + if gw: + gateway = str(gw) + except Exception as exc: + logger.debug( + "D-Bus IP read for VLAN failed, falling back to OS: %s", exc + ) + if iface: + ip_addr = await self._get_ip_by_interface(iface) + + if not ip_addr and iface: + ip_addr = self._get_ip_os_fallback(iface) + + vlans.append( + VlanInfo( + vlan_id=int(vlan_id), + ip_address=ip_addr, + interface=iface, + gateway=gateway, + dns_servers=dns_servers, + is_dhcp=is_dhcp, + ) + ) + except Exception as exc: + logger.debug("Skipping connection in active VLAN list: %s", exc) + continue + except Exception as exc: + logger.debug("Error getting active VLANs: %s", exc) + return tuple(vlans) + + async def _deactivate_all_vlans(self) -> None: + """Deactivate all active VLAN connections via the NM D-Bus interface.""" + try: + active_paths = list(await self._nm().active_connections) + for active_path in active_paths: + try: + conn_path = await self._active_conn(active_path).connection + settings = await self._conn_settings(conn_path).get_settings() + conn_type = settings.get("connection", {}).get("type", (None, ""))[ + 1 + ] + if conn_type != "vlan": + continue + conn_id = settings.get("connection", {}).get("id", (None, ""))[1] + await self._nm().deactivate_connection(active_path) + logger.debug("Deactivated VLAN '%s'", conn_id) + except Exception as exc: + logger.debug("Skipping VLAN during deactivation: %s", exc) + continue + await asyncio.sleep(0.5) + except Exception as exc: + logger.debug("Error deactivating VLANs: %s", exc) + + async def _activate_saved_vlans(self) -> None: + """Activate all saved VLAN connection profiles found in NM settings.""" + try: + nm_settings = self._nm_settings() + connections = await nm_settings.connections + for conn_path in connections: + try: + settings = await self._conn_settings(conn_path).get_settings() + conn_type = settings.get("connection", {}).get("type", (None, ""))[ + 1 + ] + if conn_type != "vlan": + continue + conn_id = settings.get("connection", {}).get("id", (None, ""))[1] + await self._nm().activate_connection(conn_path, "/", "/") + logger.debug("Activated saved VLAN '%s'", conn_id) + await asyncio.sleep(1.0) + except Exception as exc: + logger.debug("Failed to activate VLAN: %s", exc) + except Exception as exc: + logger.debug("Error activating saved VLANs: %s", exc) + + async def _reconnect_wifi_profile(self, ssid: str) -> None: + """Disconnect, then re-activate the saved Wi-Fi profile for *ssid*. + + Waits up to 10 s for an IP address before returning. Used after + updating a connection's static IP or DHCP settings so the new + configuration is applied immediately. + """ + logger.debug( + "Reconnecting Wi-Fi profile '%s' to apply new settings", + ssid, + ) + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as disc_err: + logger.debug("Disconnect before reconnect: %s", disc_err) + await asyncio.sleep(1.5) + + fresh_path = await self._get_connection_path(ssid) + if not fresh_path: + fresh_path = await self._find_connection_path_direct(ssid) + if not fresh_path: + logger.warning( + "Reconnect skipped: could not find saved profile for '%s'", + ssid, + ) + return + + try: + await self._nm().activate_connection(fresh_path) + except Exception as act_err: + logger.warning( + "Reconnect activate failed for '%s': %s", + ssid, + act_err, + ) + return + + loop = asyncio.get_running_loop() + deadline = loop.time() + 10.0 + while loop.time() < deadline: + await asyncio.sleep(1.0) + found_ip: str = "" + try: + current = await self._get_current_ssid() + if current and current.lower() == ssid.lower(): + found_ip = await self._get_current_ip() or "" + if not found_ip: + found_ip = self._get_ip_os_fallback("wlan0") or "" + except Exception as exc: + logger.debug( + "IP address lookup during connection wait ignored: %s", exc + ) + + if found_ip: + logger.info( + "Reconnect complete for '%s': IP=%s", + ssid, + found_ip, + ) + try: + self._invalidate_saved_cache() + self.saved_networks_loaded.emit( + await self._get_saved_networks_impl() + ) + except Exception as cache_err: + logger.debug( + "Cache refresh after reconnect failed: %s", + cache_err, + ) + return + + logger.warning("Reconnect for '%s': IP not assigned within 10 s", ssid) + + async def _async_update_wifi_static_ip( + self, + ssid: str, + ip_address: str, + subnet_mask: str, + gateway: str, + dns1: str, + dns2: str, + ) -> None: + """Apply a static IPv4 configuration to a saved Wi-Fi profile and reconnect.""" + conn_path = await self._get_connection_path(ssid) + if not conn_path: + self.error_occurred.emit("wifi_static_ip", f"'{ssid}' not found") + return + try: + cs = self._conn_settings(conn_path) + props = await cs.get_settings() + await self._merge_wifi_secrets(cs, props) + + prefix = self._mask_to_prefix(subnet_mask) + ip_uint = self._ip_to_nm_uint32(ip_address) + gw_uint = self._ip_to_nm_uint32(gateway) if gateway else 0 + dns_list: list[int] = [] + if dns1: + dns_list.append(self._ip_to_nm_uint32(dns1)) + if dns2: + dns_list.append(self._ip_to_nm_uint32(dns2)) + + props["ipv4"] = { + "method": ("s", "manual"), + "addresses": ( + "aau", + [[ip_uint, prefix, gw_uint]], + ), + "gateway": ("s", gateway or ""), + "dns": ("au", dns_list), + } + props["ipv6"] = {"method": ("s", "disabled")} + await cs.update(props) + self._invalidate_saved_cache() + logger.info( + "Static IP set for '%s': %s/%d gw %s (IPv6 disabled)", + ssid, + ip_address, + prefix, + gateway, + ) + + await self._reconnect_wifi_profile(ssid) + + self.connection_result.emit( + ConnectionResult(True, f"Static IP set for '{ssid}'") + ) + self.state_changed.emit(await self._build_current_state()) + self.reconnect_complete.emit() + except Exception as exc: + logger.error("Failed to set static IP for '%s': %s", ssid, exc) + self.error_occurred.emit("wifi_static_ip", str(exc)) + + async def _async_reset_wifi_to_dhcp(self, ssid: str) -> None: + """Reset a saved Wi-Fi profile's IPv4 settings to DHCP and reconnect.""" + conn_path = await self._get_connection_path(ssid) + if not conn_path: + self.error_occurred.emit("wifi_dhcp", f"'{ssid}' not found") + return + try: + cs = self._conn_settings(conn_path) + props = await cs.get_settings() + await self._merge_wifi_secrets(cs, props) + props["ipv4"] = {"method": ("s", "auto")} + await cs.update(props) + self._invalidate_saved_cache() + logger.info("Reset '%s' to DHCP", ssid) + + await self._reconnect_wifi_profile(ssid) + + self.connection_result.emit(ConnectionResult(True, f"'{ssid}' set to DHCP")) + self.state_changed.emit(await self._build_current_state()) + self.reconnect_complete.emit() + except Exception as exc: + logger.error("Failed to reset '%s' to DHCP: %s", ssid, exc) + self.error_occurred.emit("wifi_dhcp", str(exc)) + + def _load_hotspot_config(self) -> None: + """Populate _hotspot_config from the config file. + + Writes defaults if missing. + """ + try: + cfg = get_configparser() + if not cfg.has_section("hotspot"): + cfg.add_section("hotspot") + + hotspot = cfg.get_section("hotspot") + + if hotspot.has_option("ssid"): + self._hotspot_config.ssid = hotspot.get("ssid", str, "PrinterHotspot") + else: + cfg.add_option("hotspot", "ssid", "PrinterHotspot") + + if hotspot.has_option("password"): + self._hotspot_config.password = hotspot.get( + "password", str, "123456789" + ) + else: + cfg.add_option("hotspot", "password", "123456789") + + cfg.save_configuration() + except Exception as exc: + logger.warning("Could not load hotspot config, using defaults: %s", exc) + + def _save_hotspot_config(self) -> None: + """Persist current _hotspot_config ssid/password to the config file.""" + try: + cfg = get_configparser() + cfg.update_option("hotspot", "ssid", self._hotspot_config.ssid) + cfg.update_option("hotspot", "password", self._hotspot_config.password) + cfg.save_configuration() + except Exception as exc: + logger.warning("Could not save hotspot config: %s", exc) + + async def _async_create_and_activate_hotspot( + self, + ssid: str, + password: str, + security: str = "wpa-psk", + ) -> None: + """Create a new WPA2-PSK AP-mode profile and activate it as a hotspot. + + Removes all stale AP-mode and same-name profiles before adding the new + one. Disconnects ethernet if active so the Wi-Fi radio is available. + """ + try: + config_ssid = ssid or "PrinterHotspot" + config_pwd = password or "123456789" + config_sec = ( + security + if HotspotSecurity.is_valid(security) + else HotspotSecurity.WPA2_PSK.value + ) + self._hotspot_config.ssid = config_ssid + self._hotspot_config.password = config_pwd + self._hotspot_config.security = config_sec + self._save_hotspot_config() + + if not await self._nm().wireless_enabled: + await self._nm().wireless_enabled.set_async(True) + await self._wait_for_wifi_radio(True, timeout=8.0) + + if not await self._wait_for_wifi_device_ready(timeout=8.0): + logger.warning( + "wlan0 did not reach DISCONNECTED within 8 s; " + "proceeding with hotspot activation anyway" + ) + + ethernet_was_active = await self._is_ethernet_connected() + if ethernet_was_active: + try: + await self._async_disconnect_ethernet() + except Exception as exc: + logger.debug("Pre-hotspot ethernet disconnect ignored: %s", exc) + # Brief pause to let eth0 finish deactivating before NM + # processes the hotspot activation request. + await asyncio.sleep(1.0) + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Pre-hotspot Wi-Fi disconnect ignored: %s", exc) + + await self._delete_all_ap_mode_connections() + # Also delete by connection id in case a non-AP profile shares the + # hotspot name (e.g. a leftover infrastructure profile named the + # same as the SSID). _delete_all_ap_mode_connections already caught + # all AP-mode profiles, so this second list_connections call is a + # narrow safety net. + await self._delete_connections_by_id(config_ssid) + + conn_props: dict[str, object] = { + "connection": { + "id": ("s", config_ssid), + "uuid": ("s", str(uuid4())), + "type": ("s", "802-11-wireless"), + "interface-name": ("s", self._get_wifi_iface_name()), + "autoconnect": ("b", False), + }, + "802-11-wireless": { + "ssid": ("ay", config_ssid.encode("utf-8")), + "mode": ("s", "ap"), + "band": ("s", self._hotspot_config.band), + "channel": ( + "u", + self._hotspot_config.channel, + ), + "security": ( + "s", + "802-11-wireless-security", + ), + }, + "ipv4": {"method": ("s", "shared")}, + "ipv6": {"method": ("s", "ignore")}, + } + + conn_props["802-11-wireless-security"] = { + "key-mgmt": ("s", "wpa-psk"), + "psk": ("s", config_pwd), + "pmf": ("u", 0), + } + # AP mode is always WPA2-PSK; WPA3-SAE in AP mode requires driver + # support not guaranteed on the target hardware. + config_sec = HotspotSecurity.WPA2_PSK.value + self._hotspot_config.security = config_sec + + conn_path = await self._nm_settings().add_connection(conn_props) + logger.debug( + "Hotspot profile created at %s (security=%s)", + conn_path, + config_sec, + ) + + await self._nm().activate_connection( + conn_path, self._primary_wifi_path, "/" + ) + self._is_hotspot_active = True + self._invalidate_saved_cache() + + self.hotspot_info_ready.emit(config_ssid, config_pwd, config_sec) + self.connection_result.emit( + ConnectionResult(True, f"Hotspot '{config_ssid}' activated") + ) + + await asyncio.sleep(1.5) + self.state_changed.emit(await self._build_current_state()) + + except Exception as exc: + logger.error("Hotspot create+activate failed: %s", exc) + self._is_hotspot_active = False + self.connection_result.emit( + ConnectionResult(False, str(exc), "hotspot_failed") + ) + + async def _async_update_hotspot_config( + self, + old_ssid: str, + new_ssid: str, + new_password: str, + security: str = "wpa-psk", + ) -> None: + """Update hotspot SSID/password and re-activate if the hotspot was running.""" + try: + was_active = self._is_hotspot_active + + if was_active and self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Pre-hotspot-update disconnect ignored: %s", exc) + self._is_hotspot_active = False + + await self._delete_all_ap_mode_connections() + deleted_old = await self._delete_connections_by_id(old_ssid) + logger.debug( + "Cleaned up %d old hotspot profiles for '%s'", + deleted_old, + old_ssid, + ) + + if new_ssid.lower() != old_ssid.lower(): + await self._delete_connections_by_id(new_ssid) + + validated_sec = ( + security + if HotspotSecurity.is_valid(security) + else HotspotSecurity.WPA2_PSK.value + ) + self._hotspot_config.ssid = new_ssid + self._hotspot_config.password = new_password + self._hotspot_config.security = validated_sec + self._save_hotspot_config() + + self.hotspot_info_ready.emit( + new_ssid, + new_password, + self._hotspot_config.security, + ) + + if was_active: + await self._async_create_and_activate_hotspot( + new_ssid, + new_password, + self._hotspot_config.security, + ) + else: + self.connection_result.emit( + ConnectionResult( + True, + f"Hotspot config updated to '{new_ssid}'", + ) + ) + except Exception as exc: + logger.error("Hotspot config update failed: %s", exc) + self.connection_result.emit( + ConnectionResult(False, str(exc), "hotspot_config_failed") + ) + + async def _async_toggle_hotspot(self, enable: bool) -> None: + """Enable or disable the hotspot, cleaning up profiles and Wi-Fi radio state.""" + try: + if enable: + await self._async_create_and_activate_hotspot( + self._hotspot_config.ssid, + self._hotspot_config.password, + ) + return + + was_hotspot_active = self._is_hotspot_active + self._is_hotspot_active = False + if self._primary_wifi_path: + try: + await self._wifi().disconnect() + except Exception as exc: + logger.debug("Hotspot-off disconnect ignored: %s", exc) + + deleted = await self._delete_connections_by_id(self._hotspot_config.ssid) + logger.debug("Hotspot OFF: cleaned up %d profile(s)", deleted) + + if was_hotspot_active and await self._nm().wireless_enabled: + await self._nm().wireless_enabled.set_async(False) + + self.connection_result.emit(ConnectionResult(True, "Hotspot disabled")) + self.state_changed.emit(await self._build_current_state()) + except Exception as exc: + logger.error("Failed to toggle hotspot: %s", exc) + self._is_hotspot_active = False + self.connection_result.emit( + ConnectionResult( + success=False, + message=str(exc), + error_code="hotspot_toggle_failed", + ) + ) + + async def _merge_wifi_secrets( + self, + conn_settings: dbus_nm.NetworkConnectionSettings, + props: dict, + ) -> None: + """Fetch Wi-Fi secrets from NM and merge them into *props* in place. + + Required before calling update() so that the PSK is re-included; + NM redacts secrets from get_settings() responses. + """ + try: + secrets = await conn_settings.get_secrets("802-11-wireless-security") + sec_key = "802-11-wireless-security" + if sec_key in secrets: + props.setdefault(sec_key, {}).update(secrets[sec_key]) + except Exception as exc: + logger.debug("Could not fetch Wi-Fi secrets (NM may redact): %s", exc) + + async def _deactivate_connection_by_id(self, conn_id: str) -> bool: + """Deactivate the first active connection whose profile id matches *conn_id*.""" + try: + active_paths = await self._nm().active_connections + for active_path in active_paths: + try: + conn_path = await self._active_conn(active_path).connection + settings = await self._conn_settings(conn_path).get_settings() + cid = settings.get("connection", {}).get("id", (None, ""))[1] + if cid == conn_id: + await self._nm().deactivate_connection(active_path) + logger.debug( + "Deactivated active connection '%s'", + conn_id, + ) + return True + except Exception as exc: + logger.debug( + "Skipping connection during deactivation lookup: %s", exc + ) + except Exception as exc: + logger.debug("Error deactivating '%s': %s", conn_id, exc) + return False + + async def _delete_all_connections_by_id(self, conn_id: str) -> int: + """Delete every NM connection profile whose id exactly matches *conn_id*.""" + deleted = 0 + try: + connections = await self._nm_settings().list_connections() + for conn_path in connections: + try: + cs = self._conn_settings(conn_path) + settings = await cs.get_settings() + cid = settings.get("connection", {}).get("id", (None, ""))[1] + if cid == conn_id: + await cs.delete() + deleted += 1 + except Exception as exc: + logger.debug( + "Skipping connection in cleanup for '%s': %s", conn_id, exc + ) + except Exception as exc: + logger.error("Cleanup for '%s' failed: %s", conn_id, exc) + return deleted + + async def _delete_all_ap_mode_connections(self) -> int: + """Delete all saved Wi-Fi connections in AP mode. + + Called before creating a new hotspot to remove stale profiles from + previous hotspot sessions, regardless of their SSID. Without this, + old AP-mode profiles accumulate in NetworkManager and NM may + auto-activate them on the next boot. + """ + deleted = 0 + try: + connections = await self._nm_settings().list_connections() + for conn_path in connections: + try: + cs = self._conn_settings(conn_path) + settings = await cs.get_settings() + conn_type = settings.get("connection", {}).get("type", (None, ""))[ + 1 + ] + if conn_type != "802-11-wireless": + continue + mode = settings.get("802-11-wireless", {}).get("mode", (None, ""))[ + 1 + ] + if mode == "ap": + conn_id = settings.get("connection", {}).get("id", (None, ""))[ + 1 + ] + await cs.delete() + deleted += 1 + logger.debug( + "Removed stale AP profile '%s' at %s", conn_id, conn_path + ) + except Exception as exc: + logger.debug("Skipping connection in AP profile cleanup: %s", exc) + except Exception as exc: + logger.error("Failed to remove stale AP profiles: %s", exc) + if deleted: + self._invalidate_saved_cache() + return deleted + + async def _delete_connections_by_id(self, ssid: str) -> int: + """Delete every NM connection profile whose id matches *ssid* (case-insensitive).""" + deleted = 0 + try: + connections = await self._nm_settings().list_connections() + for conn_path in connections: + try: + cs = self._conn_settings(conn_path) + settings = await cs.get_settings() + conn_id = settings.get("connection", {}).get("id", (None, ""))[1] + if conn_id.lower() == ssid.lower(): + await cs.delete() + deleted += 1 + logger.debug( + "Deleted stale profile '%s' at %s", + conn_id, + conn_path, + ) + except Exception as exc: + logger.debug( + "Skip connection %s during cleanup: %s", + conn_path, + exc, + ) + except Exception as exc: + logger.error( + "Failed to enumerate connections for cleanup: %s", + exc, + ) + if deleted: + self._invalidate_saved_cache() + return deleted + + @staticmethod + def _ip_to_nm_uint32(ip_str: str) -> int: + """Convert a dotted-decimal IPv4 string to a native-endian uint32 for NM.""" + return struct.unpack("=I", ipaddress.IPv4Address(ip_str).packed)[0] + + @staticmethod + def _nm_uint32_to_ip(uint_ip: int) -> str: + """Convert a native-endian uint32 from NM back to a dotted-decimal IPv4 string.""" + return str(ipaddress.IPv4Address(struct.pack("=I", uint_ip))) + + @staticmethod + def _mask_to_prefix(mask_str: str) -> int: + """Convert a subnet mask or CIDR prefix string to an integer prefix length.""" + stripped = mask_str.strip() + if stripped.isdigit(): + prefix = int(stripped) + if 0 <= prefix <= 32: + return prefix + raise ValueError(f"CIDR prefix out of range: {prefix}") + return bin(int(ipaddress.IPv4Address(stripped))).count("1") diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 7b0578fe..322c483e 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -7,9 +7,10 @@ from lib.files import Files from lib.machine import MachineControl from lib.moonrakerComm import MoonWebSocket +from lib.network import WifiIconKey from lib.panels.controlTab import ControlTab from lib.panels.filamentTab import FilamentTab -from lib.panels.networkWindow import NetworkControlWindow +from lib.panels.networkWindow import NetworkControlWindow, PixmapCache from lib.panels.printTab import PrintTab from lib.panels.utilitiesTab import UtilitiesTab from lib.panels.widgets.basePopup import BasePopup @@ -20,8 +21,6 @@ from lib.panels.widgets.updatePage import UpdatePage from lib.printer import Printer from lib.ui.mainWindow_ui import Ui_MainWindow # With header - -# from lib.ui.mainWindow_v2_ui import Ui_MainWindow # No header from lib.ui.resources.background_resources_rc import * from lib.ui.resources.font_rc import * from lib.ui.resources.graphic_resources_rc import * @@ -50,6 +49,34 @@ def wrapper(*args, **kwargs): return wrapper +class HeaderWifiIconProvider: + """Resolves WifiIconKey integer values to cached QPixmaps for the header bar.""" + + _WIFI_PATHS: dict[tuple[int, bool], str] = { + ( + b, + p, + ): f":/network/media/btn_icons/network/{b}bar_wifi{'_protected' if p else ''}.svg" + for b in range(5) + for p in (False, True) + } + _ETHERNET_PATH = ":/network/media/btn_icons/network/ethernet_connected.svg" + _HOTSPOT_PATH = ":/network/media/btn_icons/hotspot.svg" + + @classmethod + def get_pixmap(cls, icon_key: int) -> QtGui.QPixmap: + """Resolve an icon key to a QPixmap (cached via PixmapCache).""" + key = WifiIconKey(icon_key) + if key is WifiIconKey.ETHERNET: + return PixmapCache.get(cls._ETHERNET_PATH) + if key is WifiIconKey.HOTSPOT: + return PixmapCache.get(cls._HOTSPOT_PATH) + path = cls._WIFI_PATHS.get( + (key.bars, key.is_protected), cls._WIFI_PATHS[(0, False)] + ) + return PixmapCache.get(path) + + class MainWindow(QtWidgets.QMainWindow): """GUI MainWindow, handles most of the app logic""" @@ -72,6 +99,7 @@ class MainWindow(QtWidgets.QMainWindow): call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") def __init__(self): + """Set up UI, instantiate subsystems, and wire all inter-component signals.""" super(MainWindow, self).__init__() self.config: BlocksScreenConfig = get_configparser() self.ui = Ui_MainWindow() @@ -162,6 +190,7 @@ def __init__(self): self.ui.main_content_widget.currentChanged.connect(slot=self.reset_tab_indexes) self.call_network_panel.connect(self.networkPanel.show_network_panel) + self.networkPanel.update_wifi_icon.connect(self.change_wifi_icon) self.conn_window.wifi_button_clicked.connect(self.call_network_panel.emit) self.ui.wifi_button.clicked.connect(self.call_network_panel.emit) self.handle_error_response.connect( @@ -216,7 +245,6 @@ def __init__(self): self.printPanel.call_cancel_panel.connect(self.handle_cancel_print) if self.config.has_section("server"): - # @ Start websocket connection with moonraker self.bo_ws_startup.emit() self.reset_tab_indexes() @@ -235,6 +263,7 @@ def handle_cancel_print(self, show: bool = True): @QtCore.pyqtSlot(bool, str, name="show-load-page") def show_LoadScreen(self, show: bool = True, msg: str = ""): + """Show or hide the loading overlay, guarded by the calling panel's visibility.""" _sender = self.sender() if _sender == self.filamentPanel: @@ -424,6 +453,15 @@ def set_current_panel_index(self, panel_index: int) -> None: case 3: self.utilitiesPanel.setCurrentIndex(panel_index) + @QtCore.pyqtSlot(int) + def change_wifi_icon(self, icon_key: int) -> None: + """Change the icon of the netowrk by a key enum match + + Args: + icon_key (int): WifiIconKey mapping for the current network state + """ + self.ui.wifi_button.setPixmap(HeaderWifiIconProvider.get_pixmap(icon_key)) + @QtCore.pyqtSlot(int, int, name="request-change-page") def global_change_page(self, tab_index: int, panel_index: int) -> None: """Changes panels pages globally @@ -507,6 +545,7 @@ def messageReceivedEvent(self, event: events.WebSocketMessageReceived) -> None: @api_handler def _handle_server_message(self, method, data, metadata) -> None: + """Route file-related WebSocket messages to the Files subsystem.""" if "file" in method: file_data_event = events.ReceivedFileData(data, method, metadata) try: @@ -522,8 +561,8 @@ def _handle_server_message(self, method, data, metadata) -> None: @api_handler def _handle_machine_message(self, method, data, metadata) -> None: + """Route machine-state WebSocket messages to the update signal.""" if "ok" in data: - # Here capture if 'ok' if a request for an update was successful return if "update" in method: if ("status" or "refresh") in method: @@ -734,6 +773,11 @@ def set_header_nozzle_diameter(self, diam: str): def closeEvent(self, a0: typing.Optional[QtGui.QCloseEvent]) -> None: """Handles GUI closing""" + try: + self.networkPanel.close() + except Exception as e: + _logger.warning("Network panel shutdown error: %s", e) + _loggers = [ logging.getLogger(name) for name in logging.root.manager.loggerDict ] # Get available logger handlers diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 19574cf5..0d0a6df5 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -1,21 +1,33 @@ +import fcntl +import ipaddress as _ipaddress import logging -import threading +import socket as _socket +import struct +from dataclasses import replace from functools import partial -from typing import ( - Any, - Callable, - Dict, - List, - NamedTuple, - Optional, -) -from lib.network import SdbusNetworkManagerAsync +from lib.network import ( + ConnectionPriority, + ConnectionResult, + ConnectivityState, + NetworkInfo, + NetworkManager, + NetworkState, + NetworkStatus, + PendingOperation, + SavedNetwork, + WifiIconKey, + is_connectable_security, + is_hidden_ssid, + signal_to_bars, +) from lib.panels.widgets.keyboardPage import CustomQwertyKeyboard from lib.panels.widgets.loadWidget import LoadingOverlayWidget from lib.panels.widgets.popupDialogWidget import Popup +from lib.qrcode_gen import generate_wifi_qrcode from lib.utils.blocks_button import BlocksCustomButton from lib.utils.blocks_frame import BlocksCustomFrame +from lib.utils.blocks_label import BlocksLabel from lib.utils.blocks_linedit import BlocksCustomLinEdit from lib.utils.blocks_Scrollbar import CustomScrollBar from lib.utils.blocks_togglebutton import NetworkWidgetbuttons @@ -23,270 +35,132 @@ from lib.utils.icon_button import IconButton from lib.utils.list_model import EntryDelegate, EntryListModel, ListItem from PyQt6 import QtCore, QtGui, QtWidgets -from PyQt6.QtCore import QObject, QRunnable, QThreadPool, pyqtSignal - -logger = logging.getLogger("logs/BlocksScreen.log") +from PyQt6.QtCore import QTimer, pyqtSlot +logger = logging.getLogger(__name__) LOAD_TIMEOUT_MS = 30_000 -NETWORK_CONNECT_DELAY_MS = 5_000 -NETWORK_LIST_REFRESH_MS = 10_000 +VLAN_DHCP_TIMEOUT_MS = 50_000 # Generous: worker has 45 s, UI needs headroom STATUS_CHECK_INTERVAL_MS = 2_000 -DEFAULT_POLL_INTERVAL_MS = 10_000 - -SIGNAL_EXCELLENT_THRESHOLD = 75 -SIGNAL_GOOD_THRESHOLD = 50 -SIGNAL_FAIR_THRESHOLD = 25 -SIGNAL_MINIMUM_THRESHOLD = 5 - -PRIORITY_HIGH = 90 -PRIORITY_MEDIUM = 50 -PRIORITY_LOW = 20 - -SEPARATOR_SIGNAL_VALUE = -10 -PRIVACY_BIT = 1 - -# SSIDs that indicate hidden networks -HIDDEN_NETWORK_INDICATORS = ("", "UNKNOWN", "", None) - - -class NetworkInfo(NamedTuple): - """Information about a network.""" - - signal: int - status: str - is_open: bool = False - is_saved: bool = False - is_hidden: bool = False # Added flag for hidden networks - - -class NetworkScanResult(NamedTuple): - """Result of a network scan.""" - - ssid: str - signal: int - status: str - is_open: bool = False - - -class NetworkScanRunnable(QRunnable): - """Runnable for scanning networks in background thread.""" - - class Signals(QObject): - """Signals for network scan results.""" - scan_results = pyqtSignal(dict, name="scan-results") - finished_network_list_build = pyqtSignal( - list, name="finished-network-list-build" - ) - error = pyqtSignal(str) - def __init__(self, nm: SdbusNetworkManagerAsync) -> None: - """Initialize the network scan runnable.""" - super().__init__() - self._nm = nm - self.signals = NetworkScanRunnable.Signals() +class PixmapCache: + """Process-wide cache for QPixmaps loaded from Qt resource paths. - def run(self) -> None: - """Execute the network scan.""" - try: - self._nm.rescan_networks() - saved_ssids = self._nm.get_saved_ssid_names() - available = self._get_available_networks() - data_dict = self._build_data_dict(available, saved_ssids) - self.signals.scan_results.emit(data_dict) - items = self._build_network_list(data_dict) - self.signals.finished_network_list_build.emit(items) - except Exception as e: - logger.error("Error scanning networks", exc_info=True) - self.signals.error.emit(str(e)) - - def _get_available_networks(self) -> Dict[str, Dict]: - """Get available networks from NetworkManager.""" - if self._nm.check_wifi_interface(): - return self._nm.get_available_networks() or {} - return {} - - def _build_data_dict( - self, available: Dict[str, Dict], saved_ssids: List[str] - ) -> Dict[str, Dict]: - """Build data dictionary from available networks.""" - data_dict: Dict[str, Dict] = {} - for ssid, props in available.items(): - signal = int(props.get("signal_level", 0)) - sec_tuple = props.get("security", (0, 0, 0)) - caps_value = sec_tuple[2] if len(sec_tuple) > 2 else 0 - is_open = (caps_value & PRIVACY_BIT) == 0 - # Check if this is a hidden network - is_hidden = ssid in HIDDEN_NETWORK_INDICATORS or not ssid.strip() - data_dict[ssid] = { - "signal_level": signal, - "is_saved": ssid in saved_ssids, - "is_open": is_open, - "is_hidden": is_hidden, - } - return data_dict + Every SVG is decoded exactly once. Qt's implicit sharing means the + same QPixmap can be safely referenced by any number of widgets. + Must only be called after QApplication is created. + """ - def _build_network_list(self, data_dict: Dict[str, Dict]) -> List[tuple]: - """Build sorted network list for display.""" - current_ssid = self._nm.get_current_ssid() + _cache: dict[str, QtGui.QPixmap] = {} - saved_nets = [ - (ssid, info["signal_level"], info["is_open"], info.get("is_hidden", False)) - for ssid, info in data_dict.items() - if info["is_saved"] - ] - unsaved_nets = [ - (ssid, info["signal_level"], info["is_open"], info.get("is_hidden", False)) - for ssid, info in data_dict.items() - if not info["is_saved"] - ] + @classmethod + def get(cls, path: str) -> QtGui.QPixmap: + """Return the cached QPixmap for *path*, loading it on first access.""" + if path not in cls._cache: + cls._cache[path] = QtGui.QPixmap(path) + return cls._cache[path] - saved_nets.sort(key=lambda x: -x[1]) - unsaved_nets.sort(key=lambda x: -x[1]) + @classmethod + def preload(cls, paths: list[str]) -> None: + """Batch-load a list of paths (called once during init).""" + for path in paths: + cls.get(path) - items: List[tuple] = [] - for ssid, signal, is_open, is_hidden in saved_nets: - status = "Active" if ssid == current_ssid else "Saved" - items.append((ssid, signal, status, is_open, True, is_hidden)) +class WifiIconProvider: + """Maps (signal_strength, is_protected) -> cached QPixmap via PixmapCache.""" - for ssid, signal, is_open, is_hidden in unsaved_nets: - status = "Open" if is_open else "Protected" - items.append((ssid, signal, status, is_open, False, is_hidden)) + _PATHS: dict[tuple[int, bool], str] = { + ( + b, + p, + ): f":/network/media/btn_icons/network/{b}bar_wifi{'_protected' if p else ''}.svg" + for b in range(5) + for p in (False, True) + } - return items + @classmethod + def get_pixmap(cls, signal: int, is_protected: bool = False) -> QtGui.QPixmap: + """Get pixmap for given signal strength and protection status.""" + bars = signal_to_bars(signal) + path = cls._PATHS.get((bars, is_protected), cls._PATHS[(0, False)]) + return PixmapCache.get(path) -class BuildNetworkList(QtCore.QObject): - """Worker class for building network lists with polling support.""" +class IPAddressLineEdit(BlocksCustomLinEdit): + """Line-edit restricted to valid IPv4 addresses.""" - scan_results = pyqtSignal(dict, name="scan-results") - finished_network_list_build = pyqtSignal(list, name="finished-network-list-build") - error = pyqtSignal(str) + _VALID_STYLE = "" + _INVALID_STYLE = "border: 2px solid red; border-radius: 8px;" def __init__( self, - nm: SdbusNetworkManagerAsync, - poll_interval_ms: int = DEFAULT_POLL_INTERVAL_MS, + parent: QtWidgets.QWidget | None = None, + *, + placeholder: str = "0.0.0.0", # nosec B104 — UI placeholder text, not a socket bind ) -> None: - """Initialize the network list builder.""" - super().__init__() - self._nm = nm - self._threadpool = QThreadPool.globalInstance() - self._poll_interval_ms = poll_interval_ms - self._is_scanning = False - self._scan_lock = threading.Lock() - self._timer = QtCore.QTimer(self) - self._timer.setSingleShot(True) - self._timer.timeout.connect(self._do_scan) - - def start_polling(self) -> None: - """Start periodic network scanning.""" - self._schedule_next_scan() - - def stop_polling(self) -> None: - """Stop periodic network scanning.""" - self._timer.stop() - - def build(self) -> None: - """Trigger immediate network scan.""" - self._do_scan() - - def _schedule_next_scan(self) -> None: - """Schedule the next network scan.""" - self._timer.start(self._poll_interval_ms) - - def _on_task_finished(self, items: List) -> None: - """Handle scan completion.""" - with self._scan_lock: - self._is_scanning = False - self.finished_network_list_build.emit(items) - self._schedule_next_scan() - - def _on_task_scan_results(self, data_dict: Dict) -> None: - """Handle scan results.""" - self.scan_results.emit(data_dict) - - def _on_task_error(self, err: str) -> None: - """Handle scan error.""" - with self._scan_lock: - self._is_scanning = False - self.error.emit(err) - self._schedule_next_scan() - - def _do_scan(self) -> None: - """Execute network scan in background thread.""" - with self._scan_lock: - if self._is_scanning: - return - self._is_scanning = True - - task = NetworkScanRunnable(self._nm) - task.signals.finished_network_list_build.connect(self._on_task_finished) - task.signals.scan_results.connect(self._on_task_scan_results) - task.signals.error.connect(self._on_task_error) - self._threadpool.start(task) - + """Initialise the IP-address input field with regex validation and optional placeholder.""" + super().__init__(parent) + self.setPlaceholderText(placeholder) + ip_re = QtCore.QRegularExpression(r"^[\d.]*$") + self.setValidator(QtGui.QRegularExpressionValidator(ip_re, self)) + self.textChanged.connect(self._on_text_changed) + + def is_valid(self) -> bool: + """Return ``True`` when the current text is a valid dotted-quad IPv4 address.""" + try: + _ipaddress.IPv4Address(self.text().strip()) + return True + except ValueError: + return False + + def is_valid_mask(self) -> bool: + """Return ``True`` when the current text is a valid subnet mask or CIDR prefix.""" + txt = self.text().strip() + if txt.isdigit(): + n = int(txt) + if 0 <= n <= 32: + return True + return False -class WifiIconProvider: - """Provider for Wi-Fi signal strength icons.""" - - def __init__(self) -> None: - """Initialize icon paths.""" - self._paths = { - (0, False): ":/network/media/btn_icons/0bar_wifi.svg", - (1, False): ":/network/media/btn_icons/1bar_wifi.svg", - (2, False): ":/network/media/btn_icons/2bar_wifi.svg", - (3, False): ":/network/media/btn_icons/3bar_wifi.svg", - (4, False): ":/network/media/btn_icons/4bar_wifi.svg", - (0, True): ":/network/media/btn_icons/0bar_wifi_protected.svg", - (1, True): ":/network/media/btn_icons/1bar_wifi_protected.svg", - (2, True): ":/network/media/btn_icons/2bar_wifi_protected.svg", - (3, True): ":/network/media/btn_icons/3bar_wifi_protected.svg", - (4, True): ":/network/media/btn_icons/4bar_wifi_protected.svg", - } - - def get_pixmap(self, signal: int, status: str) -> QtGui.QPixmap: - """Get pixmap for given signal strength and status.""" - bars = self._signal_to_bars(signal) - is_protected = status == "Protected" - key = (bars, is_protected) - path = self._paths.get(key, self._paths[(0, False)]) - return QtGui.QPixmap(path) + try: + _ipaddress.IPv4Network(f"0.0.0.0/{txt}", strict=False) + return True + except ValueError: + return False - @staticmethod - def _signal_to_bars(signal: int) -> int: - """Convert signal strength to bar count.""" - if signal < SIGNAL_MINIMUM_THRESHOLD: - return 0 - elif signal >= SIGNAL_EXCELLENT_THRESHOLD: - return 4 - elif signal >= SIGNAL_GOOD_THRESHOLD: - return 3 - elif signal > SIGNAL_FAIR_THRESHOLD: - return 2 - else: - return 1 + def _on_text_changed(self, text: str) -> None: + """Update the field border colour in real-time as the user types.""" + if not text: + self.setStyleSheet(self._VALID_STYLE) + return + try: + _ipaddress.IPv4Address(text.strip()) + self.setStyleSheet(self._VALID_STYLE) + except ValueError: + self.setStyleSheet(self._INVALID_STYLE) + self.update() class NetworkControlWindow(QtWidgets.QStackedWidget): - """Main network control window widget.""" + """Stacked-widget UI for all network control pages (Wi-Fi, Ethernet, VLAN, Hotspot). + + Owns a :class:`~BlocksScreen.lib.network.facade.NetworkManager` instance and + mediates between the UI pages and the async D-Bus worker. + """ - request_network_scan = pyqtSignal(name="scan-network") - new_ip_signal = pyqtSignal(str, name="ip-address-change") - get_hotspot_ssid = pyqtSignal(str, name="hotspot-ssid-name") - delete_network_signal = pyqtSignal(str, name="delete-network") + update_wifi_icon = QtCore.pyqtSignal(int, name="update-wifi-icon") - def __init__(self, parent: Optional[QtWidgets.QWidget] = None, /) -> None: - """Initialize the network control window.""" + def __init__(self, parent: QtWidgets.QWidget | None = None) -> None: + """Construct the stacked-widget UI, wire all signals/slots, and request initial state.""" super().__init__(parent) if parent else super().__init__() self._init_instance_variables() self._setupUI() self._init_timers() self._init_model_view() - self._init_network_worker() + self._init_network_manager() self._setup_navigation_signals() self._setup_action_signals() self._setup_toggle_signals() @@ -296,272 +170,1887 @@ def __init__(self, parent: Optional[QtWidgets.QWidget] = None, /) -> None: self._setup_keyboard() self._setup_scrollbar_signals() - self._network_list_worker.build() - self.request_network_scan.emit() + self._init_ui_state() self.hide() - # Initialize UI state - self._init_ui_state() + def _init_instance_variables(self) -> None: + """Initialize instance variables.""" + self._is_first_run = True + self._previous_panel: QtWidgets.QWidget | None = None + self._current_field: QtWidgets.QLineEdit | None = None + self._current_network_is_open = False + self._current_network_is_hidden = False + self._is_connecting = False + self._target_ssid: str | None = None + self._was_ethernet_connected: bool = False + self._initial_priority: ConnectionPriority = ConnectionPriority.MEDIUM + self._pending_operation: PendingOperation = PendingOperation.NONE + self._pending_expected_ip: str = ( + "" # IP to wait for before clearing WIFI_STATIC_IP loading + ) + self._cached_scan_networks: list[NetworkInfo] = [] + self._last_active_signal_bars: int = -1 + self._active_signal: int = 0 + # Key = SSID, value = (signal_bars, status_label, ListItem). + self._item_cache: dict[str, tuple[int, str, ListItem]] = {} + # Singleton items reused across reconcile calls (zero allocation). + self._separator_item: ListItem | None = None + self._hidden_network_item: ListItem | None = None def _init_ui_state(self) -> None: - """Initialize UI to a clean disconnected state.""" + """Initialize UI to clean disconnected state.""" self.loadingwidget.setVisible(False) + self._pending_operation = PendingOperation.NONE self._hide_all_info_elements() self._configure_info_box_centered() self.mn_info_box.setVisible(True) self.mn_info_box.setText( - "Network connection required.\n\nConnect to Wi-Fi\nor\nTurn on Hotspot" + "There no active\ninternet connection.\nConnect via Ethernet, Wi-Fi,\nor enable a mobile hotspot\n for online features.\nPrinting functions will\nstill work offline." ) - def _hide_all_info_elements(self) -> None: - """Hide ALL elements in the info panel (details, loading, info box).""" - # Hide network details - self.netlist_ip.setVisible(False) - self.netlist_ssuid.setVisible(False) - self.mn_info_seperator.setVisible(False) - self.line_2.setVisible(False) - self.netlist_strength.setVisible(False) - self.netlist_strength_label.setVisible(False) - self.line_3.setVisible(False) - self.netlist_security.setVisible(False) - self.netlist_security_label.setVisible(False) - # Hide loading - self.loadingwidget.setVisible(False) - # Hide info box - self.mn_info_box.setVisible(False) + def _init_network_manager(self) -> None: + """Initialize network manager and connect signals.""" + self._nm = NetworkManager(self) - def _init_instance_variables(self) -> None: - """Initialize all instance variables.""" - self._icon_provider = WifiIconProvider() - self._ongoing_update = False - self._is_first_run = True - self._networks: Dict[str, NetworkInfo] = {} - self._previous_panel: Optional[QtWidgets.QWidget] = None - self._current_field: Optional[QtWidgets.QLineEdit] = None - self._current_network_is_open = False - self._current_network_is_hidden = False - self._is_connecting = False - self._target_ssid: Optional[str] = None - self._last_displayed_ssid: Optional[str] = None - self._current_network_ssid: Optional[str] = ( - None # Track current network for priority - ) + self._nm.state_changed.connect(self._on_network_state_changed) - def _setupUI(self) -> None: - """Setup all UI elements programmatically.""" - self.setObjectName("wifi_stacked_page") - self.resize(800, 480) + self._nm.saved_networks_loaded.connect(self._on_saved_networks_loaded) - size_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum - ) - size_policy.setHorizontalStretch(0) - size_policy.setVerticalStretch(0) - size_policy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) - self.setSizePolicy(size_policy) - self.setMinimumSize(QtCore.QSize(0, 400)) - self.setMaximumSize(QtCore.QSize(16777215, 575)) - self.setStyleSheet( - "#wifi_stacked_page{\n" - " background-image: url(:/background/media/1st_background.png);\n" - "}\n" - ) + self._nm.connection_result.connect(self._on_operation_complete) - self._sdbus_network = SdbusNetworkManagerAsync() - self._popup = Popup(self) - self._right_arrow_icon = QtGui.QPixmap( - ":/arrow_icons/media/btn_icons/right_arrow.svg" - ) + self._nm.error_occurred.connect(self._on_network_error) - # Create all pages - self._setup_main_network_page() - self._setup_network_list_page() - self._setup_add_network_page() - self._setup_saved_connection_page() - self._setup_saved_details_page() - self._setup_hotspot_page() - self._setup_hidden_network_page() + self.rescan_button.clicked.connect(self._nm.scan_networks) - self.setCurrentIndex(0) + self.hotspot_name_input_field.setText(self._nm.hotspot_ssid) + self.hotspot_password_input_field.setText(self._nm.hotspot_password) - def _create_white_palette(self) -> QtGui.QPalette: - """Create a palette with white text.""" - palette = QtGui.QPalette() - white_brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) - white_brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - grey_brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) - grey_brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + self._nm.networks_scanned.connect(self._on_scan_complete) - for group in [ - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorGroup.Inactive, - ]: - palette.setBrush(group, QtGui.QPalette.ColorRole.WindowText, white_brush) - palette.setBrush(group, QtGui.QPalette.ColorRole.Text, white_brush) + self._nm.reconnect_complete.connect(self._on_reconnect_complete) - palette.setBrush( - QtGui.QPalette.ColorGroup.Disabled, - QtGui.QPalette.ColorRole.WindowText, - grey_brush, - ) - palette.setBrush( - QtGui.QPalette.ColorGroup.Disabled, - QtGui.QPalette.ColorRole.Text, - grey_brush, - ) + self._nm.hotspot_config_updated.connect(self._on_hotspot_config_updated) - return palette + self._prefill_ip_from_os() - def _setup_main_network_page(self) -> None: - """Setup the main network page.""" - self.main_network_page = QtWidgets.QWidget() - size_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.main_network_page.setSizePolicy(size_policy) - self.main_network_page.setObjectName("main_network_page") + def _prefill_ip_from_os(self) -> None: + """Read the current IP via SIOCGIFADDR ioctl and show it immediately. - main_layout = QtWidgets.QVBoxLayout(self.main_network_page) - main_layout.setObjectName("verticalLayout_14") + Bypasses NetworkManager D-Bus entirely — runs on the main thread, + costs a single syscall, and completes in microseconds. Called once + during init so the user never sees "IP: --" if a connection was + already active before the UI launched. + """ + _SIOCGIFADDR = 0x8915 + for iface in ("eth0", "wlan0"): + try: + with _socket.socket(_socket.AF_INET, _socket.SOCK_DGRAM) as sock: + ifreq = struct.pack("256s", iface[:15].encode()) + result = fcntl.ioctl(sock.fileno(), _SIOCGIFADDR, ifreq) + ip = _socket.inet_ntoa(result[20:24]) + if ip and not ip.startswith("0."): + self.netlist_ip.setText(f"IP: {ip}") + self.netlist_ip.setVisible(True) + logger.debug("Startup IP prefill from OS (%s): %s", iface, ip) + return + except OSError: + continue - # Header layout - header_layout = QtWidgets.QHBoxLayout() - header_layout.setObjectName("main_network_header_layout") + @pyqtSlot() + def _on_reconnect_complete(self) -> None: + """Navigate back to the main panel after a static-IP or DHCP-reset operation.""" + logger.debug("reconnect_complete received — navigating to main_network_page") + self.setCurrentIndex(self.indexOf(self.main_network_page)) - header_layout.addItem( - QtWidgets.QSpacerItem( - 60, - 60, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) + def _init_timers(self) -> None: + """Initialize timers.""" + self._load_timer = QTimer(self) + self._load_timer.setSingleShot(True) + self._load_timer.timeout.connect(self._handle_load_timeout) - self.network_main_title = QtWidgets.QLabel(parent=self.main_network_page) - title_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum - ) - self.network_main_title.setSizePolicy(title_policy) - self.network_main_title.setMinimumSize(QtCore.QSize(300, 0)) - self.network_main_title.setMaximumSize(QtCore.QSize(16777215, 60)) - font = QtGui.QFont() - font.setPointSize(20) - self.network_main_title.setFont(font) - self.network_main_title.setStyleSheet("color:white") - self.network_main_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.network_main_title.setText("Networks") - self.network_main_title.setObjectName("network_main_title") - header_layout.addWidget(self.network_main_title) + def _init_model_view(self) -> None: + """Initialize list model and view.""" + self._model = EntryListModel() + self._model.setParent(self.listView) + self._entry_delegate = EntryDelegate() + self.listView.setModel(self._model) + self.listView.setItemDelegate(self._entry_delegate) + self._entry_delegate.item_selected.connect(self._on_ssid_item_clicked) + self._configure_list_view_palette() - self.network_backButton = IconButton(parent=self.main_network_page) - self.network_backButton.setMinimumSize(QtCore.QSize(60, 60)) - self.network_backButton.setMaximumSize(QtCore.QSize(60, 60)) - self.network_backButton.setText("") - self.network_backButton.setFlat(True) - self.network_backButton.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + @pyqtSlot(NetworkState) + def _on_network_state_changed(self, state: NetworkState) -> None: + """React to a NetworkState update: sync toggles, populate header and connection info.""" + logger.debug( + "Network state: %s, SSID: %s, IP: %s, eth: %s", + state.connectivity.name, + state.current_ssid, + state.current_ip, + state.ethernet_connected, ) - self.network_backButton.setObjectName("network_backButton") - header_layout.addWidget(self.network_backButton) - main_layout.addLayout(header_layout) + if ( + state.current_ssid + and state.signal_strength > 0 + and not state.hotspot_enabled + ): + self._active_signal = state.signal_strength + elif not state.current_ssid or state.hotspot_enabled: + self._active_signal = 0 - # Content layout - content_layout = QtWidgets.QHBoxLayout() - content_layout.setObjectName("main_network_content_layout") + if self._is_first_run: + self._handle_first_run(state) + self._emit_status_icon(state) + self._is_first_run = False + self._was_ethernet_connected = state.ethernet_connected + return - # Information frame - self.mn_information_layout = BlocksCustomFrame(parent=self.main_network_page) - info_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.mn_information_layout.setSizePolicy(info_policy) - self.mn_information_layout.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.mn_information_layout.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.mn_information_layout.setObjectName("mn_information_layout") + # Cable just plugged in while Wi-Fi is active -> disable Wi-Fi + if ( + state.ethernet_connected + and not self._was_ethernet_connected + and state.wifi_enabled + and not self._is_connecting + ): + logger.info("Ethernet connected — turning off Wi-Fi") + self._was_ethernet_connected = True + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.OFF + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = hotspot_btn.State.OFF + self._nm.set_wifi_enabled(False) + self._sync_ethernet_panel(state) + self._emit_status_icon(state) + return - info_layout = QtWidgets.QVBoxLayout(self.mn_information_layout) - info_layout.setObjectName("verticalLayout_3") + self._was_ethernet_connected = state.ethernet_connected + + # Ethernet panel visibility is pure hardware state (carrier + + # connection) and must update even while a loading operation is + # in-flight. + self._sync_ethernet_panel(state) + + # Sync toggle states (skipped when _is_connecting) + self._sync_toggle_states(state) + + if self._is_connecting: + # OFF operations: complete when radio off + no connection + if self._pending_operation in ( + PendingOperation.WIFI_OFF, + PendingOperation.HOTSPOT_OFF, + ): + if ( + not state.wifi_enabled + and not state.hotspot_enabled + and not state.current_ssid + ): + self._clear_loading() + self._display_disconnected_state() + self._emit_status_icon(state) + return + # Also catch partial-off (wifi still disabling, no ssid) + if not state.current_ssid and not state.hotspot_enabled: + self._clear_loading() + self._display_disconnected_state() + self._emit_status_icon(state) + return + # Still transitioning — keep loading visible + return - # SSID label - self.netlist_ssuid = QtWidgets.QLabel(parent=self.mn_information_layout) - ssid_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum - ) - self.netlist_ssuid.setSizePolicy(ssid_policy) - font = QtGui.QFont() - font.setPointSize(17) - self.netlist_ssuid.setFont(font) - self.netlist_ssuid.setStyleSheet("color: rgb(255, 255, 255);") - self.netlist_ssuid.setText("") - self.netlist_ssuid.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_ssuid.setObjectName("netlist_ssuid") - info_layout.addWidget(self.netlist_ssuid) + # Hotspot ON: complete when hotspot_enabled + SSID + IP + if self._pending_operation == PendingOperation.HOTSPOT_ON: + if state.hotspot_enabled and state.current_ssid and state.current_ip: + self._clear_loading() + self._display_connected_state(state) + self._emit_status_icon(state) + return + # Still waiting for hotspot to fully come up + return - # Separator - self.mn_info_seperator = QtWidgets.QFrame(parent=self.mn_information_layout) - self.mn_info_seperator.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.mn_info_seperator.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.mn_info_seperator.setObjectName("mn_info_seperator") - info_layout.addWidget(self.mn_info_seperator) + if self._pending_operation in ( + PendingOperation.WIFI_ON, + PendingOperation.CONNECT, + ): + if self._target_ssid and state.current_ssid == self._target_ssid: + if state.current_ip and state.connectivity in ( + ConnectivityState.FULL, + ConnectivityState.LIMITED, + ): + self._clear_loading() + self._display_connected_state(state) + self._emit_status_icon(state) + return + return - # IP label - self.netlist_ip = QtWidgets.QLabel(parent=self.mn_information_layout) - font = QtGui.QFont() - font.setPointSize(15) - self.netlist_ip.setFont(font) - self.netlist_ip.setStyleSheet("color: rgb(255, 255, 255);") - self.netlist_ip.setText("") - self.netlist_ip.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_ip.setObjectName("netlist_ip") - info_layout.addWidget(self.netlist_ip) + if self._pending_operation == PendingOperation.ETHERNET_ON: + if state.ethernet_connected: + self._clear_loading() + self._sync_ethernet_panel(state) + self._display_connected_state(state) + self._emit_status_icon(state) + return + return - # Connection info layout - conn_info_layout = QtWidgets.QHBoxLayout() - conn_info_layout.setObjectName("mn_conn_info") + if self._pending_operation == PendingOperation.ETHERNET_OFF: + if not state.ethernet_connected: + self._clear_loading() + self._sync_ethernet_panel(state) + self._display_disconnected_state() + self._emit_status_icon(state) + return + return - # Signal strength section - sg_info_layout = QtWidgets.QVBoxLayout() - sg_info_layout.setObjectName("mn_sg_info_layout") + # VLAN DHCP: keep loading visible. + if self._pending_operation == PendingOperation.VLAN_DHCP: + # Update display behind the loading overlay so state is + # current when loading is eventually cleared. + self._sync_ethernet_panel(state) + return - self.netlist_strength_label = QtWidgets.QLabel( - parent=self.mn_information_layout - ) - self.netlist_strength_label.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(15) - self.netlist_strength_label.setFont(font) - self.netlist_strength_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_strength_label.setText("Signal\nStrength") - self.netlist_strength_label.setObjectName("netlist_strength_label") - sg_info_layout.addWidget(self.netlist_strength_label) + # Wi-Fi static IP / DHCP reset: complete when we have the right IP. + if self._pending_operation == PendingOperation.WIFI_STATIC_IP: + ip = state.current_ip or "" + expected = self._pending_expected_ip + ip_matches = ip and (not expected or ip == expected) + if ip_matches: + self._pending_expected_ip = "" + self._clear_loading() + self._display_connected_state(state) + self._emit_status_icon(state) + return + # IP not yet correct — keep loading visible + return - self.line_2 = QtWidgets.QFrame(parent=self.mn_information_layout) - self.line_2.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.line_2.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line_2.setObjectName("line_2") - sg_info_layout.addWidget(self.line_2) + return - self.netlist_strength = QtWidgets.QLabel(parent=self.mn_information_layout) - font = QtGui.QFont() - font.setPointSize(11) - self.netlist_strength.setFont(font) - self.netlist_strength.setStyleSheet("color: rgb(255, 255, 255);") - self.netlist_strength.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_strength.setText("") - self.netlist_strength.setObjectName("netlist_strength") - sg_info_layout.addWidget(self.netlist_strength) + # Normal (not connecting) display updates. + if state.ethernet_connected: + self._display_connected_state(state) + elif ( + state.current_ssid + and state.current_ip + and state.connectivity + in ( + ConnectivityState.FULL, + ConnectivityState.LIMITED, + ) + ): + self._display_connected_state(state) + elif state.wifi_enabled or state.hotspot_enabled: + self._display_wifi_on_no_connection() + else: + self._display_disconnected_state() - conn_info_layout.addLayout(sg_info_layout) + self._emit_status_icon(state) + self._sync_active_network_list_icon(state) - # Security section - sec_info_layout = QtWidgets.QVBoxLayout() - sec_info_layout.setObjectName("mn_sec_info_layout") + @pyqtSlot(list) + def _on_scan_complete(self, networks: list[NetworkInfo]) -> None: + """Receive scan results, filter/sort them, and rebuild the SSID list view. - self.netlist_security_label = QtWidgets.QLabel( - parent=self.mn_information_layout + Filters out the own hotspot SSID and networks with unsupported security + types before populating the list view. + """ + hotspot_ssid = self._nm.hotspot_ssid + filtered = [ + n + for n in networks + if n.ssid != hotspot_ssid and is_connectable_security(n.security_type) + ] + + current_ssid = self._nm.current_ssid + if current_ssid: + # Stamp the connected AP as ACTIVE so the list is correct on first + # render even when the scan ran before the connection fully settled. + filtered = [ + replace(net, network_status=NetworkStatus.ACTIVE) + if net.ssid == current_ssid + else net + for net in filtered + ] + active = next((n for n in filtered if n.ssid == current_ssid), None) + if active: + self._active_signal = active.signal_strength + self._last_active_signal_bars = signal_to_bars(self._active_signal) + + # Cache for signal-bar-change rebuilds + self._cached_scan_networks = filtered + + self._build_network_list_from_scan(filtered) + + # Update panel text + header icon (both read _active_signal) + if current_ssid: + self.netlist_strength.setText(f"{self._active_signal}%") + state = self._nm.current_state + self._emit_status_icon(state) + + @pyqtSlot(list) + def _on_saved_networks_loaded(self, networks: list[SavedNetwork]) -> None: + """Receive saved-network data and update the priority spinbox for the active SSID.""" + logger.debug("Loaded %d saved networks", len(networks)) + + @pyqtSlot(ConnectionResult) + def _on_operation_complete(self, result: ConnectionResult) -> None: + """Handle network operation completion.""" + logger.debug("Operation: success=%s, msg=%s", result.success, result.message) + + if result.success: + msg_lower = result.message.lower() + if "deleted" in msg_lower: + ssid_deleted = ( + self._target_ssid + ) # capture before _clear_loading wipes it + self._show_info_popup(result.message) + self._clear_loading() + self._display_wifi_on_no_connection() + self.setCurrentIndex(self.indexOf(self.main_network_page)) + if ssid_deleted: + self._patch_cached_network_status( + ssid_deleted, NetworkStatus.DISCOVERED + ) + elif "hotspot" in msg_lower and "activated" in msg_lower: + self._show_hotspot_qr( + self._nm.hotspot_ssid, + self._nm.hotspot_password, + self._nm.hotspot_security, + ) + elif "hotspot disabled" in msg_lower: + self.qrcode_img.clearPixmap() + self.qrcode_img.setText("Hotspot not active") + elif "wi-fi disabled" in msg_lower: + pass + elif "config updated" in msg_lower: + self._show_info_popup(result.message) + elif any( + skip in msg_lower + for skip in ( + "added", + "connecting", + "disconnected", + "wi-fi enabled", + ) + ): + if ( + ("added" in msg_lower or "connecting" in msg_lower) + and self._target_ssid + and not self._current_network_is_hidden + ): + # Hidden networks are not in the scan cache; the next scan + # will surface them once NM reports them as saved/active. + self._patch_cached_network_status( + self._target_ssid, NetworkStatus.SAVED + ) + elif self._pending_operation == PendingOperation.WIFI_STATIC_IP: + # Loading cleared by state machine (IP appears) or reconnect_complete. + # No popup — the updated IP in the header is the confirmation. + pass + elif self._pending_operation == PendingOperation.VLAN_DHCP: + # Worker confirmed VLAN DHCP success — clear loading and + # refresh the display to show the new VLAN interface. + self._clear_loading() + state = self._nm.current_state + self._display_connected_state(state) + self._emit_status_icon(state) + self._show_info_popup(result.message) + else: + self._show_info_popup(result.message) + else: + msg_lower = result.message.lower() + + # DHCP VLAN / Wi-Fi static-IP errors: clear loading and show the + # reason without the generic error prefix. + if result.error_code in ("vlan_dhcp_timeout", "duplicate_vlan"): + self._clear_loading() + self._show_error_popup(result.message) + return + + # When switching from ethernet to wifi, NM may report a + # device-mismatch error because the wired profile hasn't + # fully deactivated yet. Retry the connection instead of + # showing a confusing popup to the user. + is_transient_mismatch = ( + "not compatible with device" in msg_lower + or "mismatching interface" in msg_lower + or "not available because profile" in msg_lower + ) + if ( + is_transient_mismatch + and self._pending_operation + in (PendingOperation.WIFI_ON, PendingOperation.CONNECT) + and self._target_ssid + ): + logger.debug( + "Transient NM device-mismatch during wifi activation " + "— retrying in 2 s: %s", + result.message, + ) + ssid = self._target_ssid + QTimer.singleShot( + 2000, lambda _ssid=ssid: self._nm.connect_network(_ssid) + ) + return # Keep loading visible; state machine handles completion + + self._clear_loading() + self._show_error_popup(result.message) + + @pyqtSlot(str, str) + def _on_network_error(self, operation: str, message: str) -> None: + """Log network errors and surface critical failures in the info box.""" + logger.error("Network error [%s]: %s", operation, message) + + if operation == "wifi_unavailable": + self.wifi_button.setEnabled(False) + self._show_error_popup( + "Wi-Fi interface unavailable. Please check hardware." + ) + return + + if operation == "device_reconnected": + self.wifi_button.setEnabled(True) + self._nm.refresh_state() + return + + self._clear_loading() + self._show_error_popup(f"Error: {message}") + + def _emit_status_icon(self, state: NetworkState) -> None: + """Emit the correct header icon key based on current state. + + Ethernet -> ETHERNET, Hotspot -> HOTSPOT, + Wi-Fi connected -> signal-strength key, otherwise -> 0-bar. + + Uses self._active_signal (the single source of truth) so the + header icon always matches the list icon and panel percentage. + """ + if state.ethernet_connected: + self.update_wifi_icon.emit(WifiIconKey.ETHERNET) + elif state.hotspot_enabled: + self.update_wifi_icon.emit(WifiIconKey.HOTSPOT) + elif state.current_ssid and state.connectivity in ( + ConnectivityState.FULL, + ConnectivityState.LIMITED, + ): + self.update_wifi_icon.emit( + WifiIconKey.from_signal(self._active_signal, False) + ) + else: + # Disconnected / no connection — 0-bar unprotected + self.update_wifi_icon.emit(WifiIconKey.from_bars(0, False)) + + def _sync_active_network_list_icon(self, state: NetworkState) -> None: + """Rebuild the wifi list when the active network's signal bars or status changes. + + Between scans, state polling may report a different signal strength + for the connected AP. Also corrects the status label from SAVED to + ACTIVE when the connection establishes after the last scan ran. + Invalidates the item cache for that SSID so the next reconcile picks + up the new icon/label, without touching other items. + + Uses self._active_signal as the single source of truth. + """ + if not self._cached_scan_networks or not state.current_ssid: + self._last_active_signal_bars = -1 + return + + new_bars = signal_to_bars(self._active_signal) + + # Also check whether the cached status already reflects ACTIVE. + # If not, we must rebuild even when bars haven't changed (e.g. the + # scan ran before the connection was fully established and marked the + # network SAVED instead of ACTIVE). + cached_active = next( + (n for n in self._cached_scan_networks if n.ssid == state.current_ssid), + None, + ) + status_needs_update = cached_active is not None and not cached_active.is_active + + if new_bars == self._last_active_signal_bars and not status_needs_update: + return # No visual change — skip the rebuild + + # Invalidate cache for the active SSID so _get_or_create_item + # creates a fresh ListItem with the updated signal icon and status. + self._item_cache.pop(state.current_ssid, None) + + # Update the cached entry with the authoritative signal and status + updated = [ + replace( + net, + signal_strength=self._active_signal, + network_status=NetworkStatus.ACTIVE, + ) + if net.ssid == state.current_ssid + else net + for net in self._cached_scan_networks + ] + + self._cached_scan_networks = updated + self._last_active_signal_bars = new_bars + self._build_network_list_from_scan(updated) + + def _handle_first_run(self, state: NetworkState) -> None: + """Run first-time UI setup once an initial state arrives (hide loading screen, etc.).""" + self.loadingwidget.setVisible(False) + self._is_connecting = False + self._pending_operation = PendingOperation.NONE + + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + + wifi_on = False + hotspot_on = False + + if state.ethernet_connected: + if state.wifi_enabled: + self._nm.set_wifi_enabled(False) + self._display_connected_state(state) + elif state.connectivity == ConnectivityState.FULL and state.current_ssid: + wifi_on = True + self._display_connected_state(state) + elif state.connectivity == ConnectivityState.LIMITED: + hotspot_on = True + self._display_connected_state(state) + self._show_hotspot_qr( + self._nm.hotspot_ssid, + self._nm.hotspot_password, + self._nm.hotspot_security, + ) + elif state.wifi_enabled: + wifi_on = True + self._display_wifi_on_no_connection() + else: + self._display_disconnected_state() + + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.ON if wifi_on else wifi_btn.State.OFF + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = ( + hotspot_btn.State.ON if hotspot_on else hotspot_btn.State.OFF + ) + + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + self.ethernet_button.setEnabled(True) + self._sync_ethernet_panel(state) + + def _sync_toggle_states(self, state: NetworkState) -> None: + """Synchronise Wi-Fi and hotspot toggle buttons to the current NetworkState + without loops.""" + if self._is_connecting: + return + + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + + wifi_on = False + hotspot_on = False + + if state.ethernet_connected: + pass + elif state.hotspot_enabled: + hotspot_on = True + elif state.wifi_enabled: + wifi_on = True + + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.ON if wifi_on else wifi_btn.State.OFF + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = ( + hotspot_btn.State.ON if hotspot_on else hotspot_btn.State.OFF + ) + + def _sync_ethernet_panel(self, state: NetworkState) -> None: + """Show/hide the ethernet panel and sync its toggle state. + + Visibility is driven by ``ethernet_carrier`` (cable physically + plugged in), while the toggle position reflects the active + connection state (``ethernet_connected``). + """ + eth_btn = self.ethernet_button.toggle_button + + with QtCore.QSignalBlocker(eth_btn): + eth_btn.state = ( + eth_btn.State.ON if state.ethernet_connected else eth_btn.State.OFF + ) + + # Panel visible as long as the cable is physically present + self.ethernet_button.setVisible(state.ethernet_carrier) + + def _display_connected_state(self, state: NetworkState) -> None: + """Display connected network information. + + Ethernet always takes display priority — if ``ethernet_connected`` + is True we show "Ethernet" even if a Wi-Fi SSID is still lingering + (e.g. during the brief overlap before NM finishes disabling wifi). + """ + self._hide_all_info_elements() + + is_ethernet = state.ethernet_connected + + self.netlist_ssuid.setText( + "Ethernet" if is_ethernet else (state.current_ssid or "") + ) + self.netlist_ssuid.setVisible(True) + + if state.current_ip: + self.netlist_ip.setText(f"IP: {state.current_ip}") + else: + self.netlist_ip.setText("IP: --") + self.netlist_ip.setVisible(True) + + # Show interface combo when ethernet is connected AND VLANs exist + if is_ethernet and state.active_vlans: + self.netlist_vlans_combo.blockSignals(True) + self.netlist_vlans_combo.clear() + self.netlist_vlans_combo.addItem( + f"Ethernet — {state.current_ip or '--'}", + state.current_ip or "", + ) + for v in state.active_vlans: + if v.is_dhcp: + ip_label = v.ip_address or "DHCP" + else: + ip_label = v.ip_address or "--" + self.netlist_vlans_combo.addItem( + f"VLAN {v.vlan_id} — {ip_label}", + v.ip_address or "", + ) + self.netlist_vlans_combo.setCurrentIndex(0) + self.netlist_vlans_combo.blockSignals(False) + self.netlist_vlans_combo.setVisible(True) + else: + self.netlist_vlans_combo.setVisible(False) + + self.mn_info_seperator.setVisible(True) + + if not is_ethernet and not state.hotspot_enabled: + signal_text = f"{self._active_signal}%" if self._active_signal > 0 else "--" + self.netlist_strength.setText(signal_text) + self.netlist_strength.setVisible(True) + self.netlist_strength_label.setVisible(True) + self.line_2.setVisible(True) + + sec_text = state.security_type.upper() if state.security_type else "OPEN" + self.netlist_security.setText(sec_text) + self.netlist_security.setVisible(True) + self.netlist_security_label.setVisible(True) + self.line_3.setVisible(True) + + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + self.ethernet_button.setEnabled(True) + + self.update() + + def _display_disconnected_state(self) -> None: + """Display disconnected state — both toggles OFF.""" + self._hide_all_info_elements() + + self.mn_info_box.setVisible(True) + self.mn_info_box.setText( + "There no active\ninternet connection.\nConnect via Ethernet, Wi-Fi,\nor enable a mobile hotspot\n for online features.\nPrinting functions will\nstill work offline." + ) + + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + self.ethernet_button.setEnabled(True) + + self.update() + + def _display_wifi_on_no_connection(self) -> None: + """Display info panel when Wi-Fi is on but not connected. + + Uses the same layout as the connected state but shows + 'No network connected' and empty fields. + """ + self._hide_all_info_elements() + + self.netlist_ssuid.setText("No network connected") + self.netlist_ssuid.setVisible(True) + + self.netlist_ip.setText("IP: --") + self.netlist_ip.setVisible(True) + + self.mn_info_seperator.setVisible(True) + + self.netlist_strength.setText("--") + self.netlist_strength.setVisible(True) + self.netlist_strength_label.setVisible(True) + self.line_2.setVisible(True) + + self.netlist_security.setText("--") + self.netlist_security.setVisible(True) + self.netlist_security_label.setVisible(True) + self.line_3.setVisible(True) + + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + self.ethernet_button.setEnabled(True) + + self.update() + + def _hide_all_info_elements(self) -> None: + """Hide all info panel elements.""" + self.netlist_ip.setVisible(False) + self.netlist_ssuid.setVisible(False) + self.netlist_vlans_combo.setVisible(False) + self.mn_info_seperator.setVisible(False) + self.line_2.setVisible(False) + self.netlist_strength.setVisible(False) + self.netlist_strength_label.setVisible(False) + self.line_3.setVisible(False) + self.netlist_security.setVisible(False) + self.netlist_security_label.setVisible(False) + self.loadingwidget.setVisible(False) + self.mn_info_box.setVisible(False) + + def _set_loading_state( + self, loading: bool, timeout_ms: int = LOAD_TIMEOUT_MS + ) -> None: + """Set loading state with visible feedback text.""" + self.wifi_button.setEnabled(not loading) + self.hotspot_button.setEnabled(not loading) + self.ethernet_button.setEnabled(not loading) + + if loading: + self._is_connecting = True + self._hide_all_info_elements() + self.loadingwidget.setVisible(True) + + if self._load_timer.isActive(): + self._load_timer.stop() + self._load_timer.start(timeout_ms) + else: + self._is_connecting = False + self._target_ssid = None + self._pending_operation = PendingOperation.NONE + self.loadingwidget.setVisible(False) + + if self._load_timer.isActive(): + self._load_timer.stop() + self.update() + + def _clear_loading(self) -> None: + """Hide the loading widget and re-enable the full UI.""" + self._set_loading_state(False) + + def _handle_load_timeout(self) -> None: + """Hide the loading widget if it is still visible after the timeout fires.""" + if not self.loadingwidget.isVisible(): + return + + state = self._nm.current_state + if ( + self._pending_operation == PendingOperation.HOTSPOT_ON + and state.hotspot_enabled + and state.current_ssid + ): + self._clear_loading() + self._display_connected_state(state) + return + if ( + self._pending_operation + in (PendingOperation.WIFI_ON, PendingOperation.CONNECT) + and self._target_ssid + ): + if state.current_ssid == self._target_ssid and state.current_ip: + self._clear_loading() + self._display_connected_state(state) + return + if ( + self._pending_operation == PendingOperation.ETHERNET_ON + and state.ethernet_connected + ): + self._clear_loading() + self._sync_ethernet_panel(state) + self._display_connected_state(state) + return + + # VLAN DHCP — the 50 s UI timer expired before the worker's 45 s + # D-Bus signal timeout. Clear loading and show a specific message. + if self._pending_operation == PendingOperation.VLAN_DHCP: + self._clear_loading() + self._display_connected_state(state) + self._show_error_popup( + "VLAN DHCP timed out.\n" + "No DHCP server responded.\n" + "Use a static IP for this VLAN." + ) + return + + # Static IP / DHCP reset — if a state with an IP has arrived, accept it. + if self._pending_operation == PendingOperation.WIFI_STATIC_IP: + if state.current_ip: + self._clear_loading() + self._display_connected_state(state) + return + # No IP yet after timeout — clear loading and show whatever state we have. + self._clear_loading() + if state.current_ssid: + self._display_connected_state(state) + else: + self._display_disconnected_state() + return + + self._clear_loading() + self._hide_all_info_elements() + self._configure_info_box_centered() + self.mn_info_box.setVisible(True) + + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + eth_btn = self.ethernet_button.toggle_button + + if self._pending_operation == PendingOperation.ETHERNET_ON: + self.mn_info_box.setText( + "Ethernet Connection Failed.\nCheck that the cable\nis plugged in." + ) + with QtCore.QSignalBlocker(eth_btn): + eth_btn.state = eth_btn.State.OFF + elif wifi_btn.state == wifi_btn.State.ON: + self.mn_info_box.setText( + "Wi-Fi Connection Failed.\nThe connection attempt\ntimed out." + ) + elif hotspot_btn.state == hotspot_btn.State.ON: + self.mn_info_box.setText( + "Hotspot Setup Failed.\nPlease restart the hotspot." + ) + else: + self.mn_info_box.setText( + "Loading timed out.\nPlease check your connection\n and \ntry again." + ) + + self.wifi_button.setEnabled(True) + self.hotspot_button.setEnabled(True) + self.ethernet_button.setEnabled(True) + self._show_error_popup("Connection timed out. Please try again.") + + def _configure_info_box_centered(self) -> None: + """Centre-align the info box text and enable word-wrap.""" + self.mn_info_box.setWordWrap(True) + self.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + @QtCore.pyqtSlot(object, name="stateChange") + def _on_toggle_state(self, new_state) -> None: + """Route a toggle-button state change to the correct handler (Wi-Fi or hotspot).""" + sender_button = self.sender() + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + eth_btn = self.ethernet_button.toggle_button + is_on = new_state == sender_button.State.ON + + if sender_button is wifi_btn: + self._handle_wifi_toggle(is_on) + elif sender_button is hotspot_btn: + self._handle_hotspot_toggle(is_on) + elif sender_button is eth_btn: + self._handle_ethernet_toggle(is_on) + + # Both OFF state is now handled by _on_network_state_changed + # when the worker emits the disconnected state. + + def _handle_wifi_toggle(self, is_on: bool) -> None: + """Enable or disable Wi-Fi, enforcing the ethernet/hotspot mutual-exclusion rule.""" + if not is_on: + self._target_ssid = None + self._pending_operation = PendingOperation.WIFI_OFF + self._set_loading_state(True) + self._nm.set_wifi_enabled(False) + return + + hotspot_btn = self.hotspot_button.toggle_button + eth_btn = self.ethernet_button.toggle_button + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = hotspot_btn.State.OFF + with QtCore.QSignalBlocker(eth_btn): + eth_btn.state = eth_btn.State.OFF + + self._nm.set_wifi_enabled(True) + + # NOTE: set_wifi_enabled is dispatched to the worker — cached state + # is STALE here (may still show ethernet). Always proceed to the + # saved-network connection path. + + saved = self._nm.saved_networks + wifi_networks = [n for n in saved if "ap" not in n.mode] + + if not wifi_networks: + self._show_warning_popup("No saved Wi-Fi networks. Please add one first.") + self._display_wifi_on_no_connection() + return + + # Sort by priority descending (highest priority first), + # then by timestamp as tiebreaker — this gives "reconnect to + # highest-priority saved network" behaviour. + wifi_networks.sort(key=lambda n: (n.priority, n.timestamp), reverse=True) + + self._target_ssid = wifi_networks[0].ssid + self._pending_operation = PendingOperation.WIFI_ON + self._set_loading_state(True) + + # Non-blocking: disable hotspot then connect + self._nm.toggle_hotspot(False) + _ssid_to_connect = self._target_ssid + QTimer.singleShot(500, lambda: self._nm.connect_network(_ssid_to_connect)) + + def _handle_hotspot_toggle(self, is_on: bool) -> None: + """Enable or disable the hotspot, enforcing the ethernet/Wi-Fi mutual-exclusion rule.""" + if not is_on: + self._target_ssid = None + self._pending_operation = PendingOperation.HOTSPOT_OFF + self._set_loading_state(True) + self._nm.toggle_hotspot(False) + return + + wifi_btn = self.wifi_button.toggle_button + eth_btn = self.ethernet_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.OFF + with QtCore.QSignalBlocker(eth_btn): + eth_btn.state = eth_btn.State.OFF + + self._target_ssid = None + self._pending_operation = PendingOperation.HOTSPOT_ON + self._set_loading_state(True) + + hotspot_name = self.hotspot_name_input_field.text() or "" + hotspot_pass = self.hotspot_password_input_field.text() or "" + hotspot_sec = "wpa-psk" + + # Single atomic call: disconnect + delete stale + create + activate + self._nm.create_hotspot(hotspot_name, hotspot_pass, hotspot_sec) + + def _handle_ethernet_toggle(self, is_on: bool) -> None: + """Handle ethernet toggle with mutual exclusion.""" + if is_on: + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.OFF + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = hotspot_btn.State.OFF + + self._target_ssid = None + self._pending_operation = PendingOperation.ETHERNET_ON + self._set_loading_state(True) + self._nm.connect_ethernet() + return + + self._target_ssid = None + self._pending_operation = PendingOperation.ETHERNET_OFF + self._set_loading_state(True) + self._nm.disconnect_ethernet() + + @QtCore.pyqtSlot(str, str, str) + def _on_hotspot_config_updated( + self, + ssid: str, + password: str, + security: str, # pylint: disable=unused-argument + ) -> None: + """Refresh hotspot UI fields when worker reports updated config.""" + self.hotspot_name_input_field.setText(ssid) + self.hotspot_password_input_field.setText(password) + + def _on_hotspot_config_save(self) -> None: + """Save hotspot configuration changes. + + Reads new name/password from the UI fields, asks the worker to + delete old profiles and create a new one. If the hotspot was + active, it will be re-activated with the new config (with a + loading screen shown). + """ + new_name = self.hotspot_name_input_field.text().strip() + new_password = self.hotspot_password_input_field.text().strip() + + if not new_name: + self._show_error_popup("Hotspot name cannot be empty.") + return + + if len(new_password) < 8: + self._show_error_popup("Hotspot password must be at least 8 characters.") + return + + old_ssid = self._nm.hotspot_ssid + + self.setCurrentIndex(self.indexOf(self.main_network_page)) + + # If hotspot is currently active, show loading for the reconnect + hotspot_btn = self.hotspot_button.toggle_button + if hotspot_btn.state == hotspot_btn.State.ON: + self._target_ssid = None + self._pending_operation = PendingOperation.HOTSPOT_ON + self._set_loading_state(True) + + new_security = "wpa-psk" + self._nm.update_hotspot_config(old_ssid, new_name, new_password, new_security) + + @QtCore.pyqtSlot() + def _on_hotspot_activate(self) -> None: + """Validate UI fields and immediately create + activate the hotspot.""" + new_name = self.hotspot_name_input_field.text().strip() + new_password = self.hotspot_password_input_field.text().strip() + + if not new_name: + self._show_error_popup("Hotspot name cannot be empty.") + return + + if len(new_password) < 8: + self._show_error_popup("Hotspot password must be at least 8 characters.") + return + + # Mutual exclusion: turn off Wi-Fi and Ethernet + wifi_btn = self.wifi_button.toggle_button + eth_btn = self.ethernet_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.OFF + with QtCore.QSignalBlocker(eth_btn): + eth_btn.state = eth_btn.State.OFF + + hotspot_btn = self.hotspot_button.toggle_button + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = hotspot_btn.State.ON + + self._target_ssid = None + self._pending_operation = PendingOperation.HOTSPOT_ON + self.setCurrentIndex(self.indexOf(self.main_network_page)) + self._set_loading_state(True) + self._nm.create_hotspot(new_name, new_password, "wpa-psk") + + def _show_hotspot_qr(self, ssid: str, password: str, security: str) -> None: + """Generate and display a WiFi QR code on the hotspot page.""" + try: + img = generate_wifi_qrcode(ssid, password, security) + pixmap = QtGui.QPixmap.fromImage(img) + self.qrcode_img.setText("") + self.qrcode_img.setPixmap(pixmap) + except Exception as exc: # pylint: disable=broad-except + logger.debug("QR code generation failed: %s", exc) + self.qrcode_img.clearPixmap() + self.qrcode_img.setText("QR error") + + def _on_ethernet_button_clicked(self) -> None: + """Navigate to the ethernet/VLAN settings page when the ethernet button is clicked.""" + if ( + self.ethernet_button.toggle_button.state + == self.ethernet_button.toggle_button.State.OFF + ): + self._show_warning_popup("Turn on Ethernet first.") + return + self.setCurrentIndex(self.indexOf(self.vlan_page)) + + def _on_vlan_apply(self) -> None: + """Validate VLAN fields and call ``create_vlan_connection`` on the facade.""" + vlan_id = self.vlan_id_spinbox.value() + ip_addr = self.vlan_ip_field.text().strip() + mask = self.vlan_mask_field.text().strip() + gateway = self.vlan_gateway_field.text().strip() + dns1 = self.vlan_dns1_field.text().strip() + dns2 = self.vlan_dns2_field.text().strip() + + # When IP is empty -> DHCP mode (no validation needed) + use_dhcp = not ip_addr + + if not use_dhcp: + if not self.vlan_ip_field.is_valid(): + self._show_error_popup("Invalid IP address.") + return + if not self.vlan_mask_field.is_valid_mask(): + self._show_error_popup("Invalid subnet mask.") + return + if gateway and not self.vlan_gateway_field.is_valid(): + self._show_error_popup("Invalid gateway address.") + return + if dns1 and not self.vlan_dns1_field.is_valid(): + self._show_error_popup("Invalid primary DNS.") + return + if dns2 and not self.vlan_dns2_field.is_valid(): + self._show_error_popup("Invalid secondary DNS.") + return + + self.setCurrentIndex(self.indexOf(self.main_network_page)) + if use_dhcp: + self._pending_operation = PendingOperation.VLAN_DHCP + self._set_loading_state(True, timeout_ms=VLAN_DHCP_TIMEOUT_MS) + else: + self._pending_operation = PendingOperation.ETHERNET_ON + self._set_loading_state(True) + self._nm.create_vlan_connection( + vlan_id, + ip_addr, # empty -> DHCP + mask if not use_dhcp else "", + gateway if not use_dhcp else "", + dns1 if not use_dhcp else "", + dns2 if not use_dhcp else "", + ) + self._nm.request_state_soon(delay_ms=3000) + + def _on_vlan_delete(self) -> None: + """Read the VLAN ID from the spinbox and request deletion via the facade.""" + vlan_id = self.vlan_id_spinbox.value() + self._nm.delete_vlan_connection(vlan_id) + self._show_warning_popup(f"VLAN {vlan_id} profile removed.") + + def _on_interface_combo_changed(self, index: int) -> None: + """Swap the displayed IP when the user selects a different interface.""" + ip = self.netlist_vlans_combo.itemData(index) + if ip is not None: + self.netlist_ip.setText(f"IP: {ip}" if ip else "IP: --") + + def _on_wifi_static_ip_clicked(self) -> None: + """Navigate from saved details page to WiFi static IP page.""" + ssid = self.snd_name.text() + self.wifi_sip_title.setText(ssid) + self.wifi_sip_ip_field.clear() + self.wifi_sip_mask_field.clear() + self.wifi_sip_gateway_field.clear() + self.wifi_sip_dns1_field.clear() + self.wifi_sip_dns2_field.clear() + + # Enable "Reset to DHCP" only when the profile is currently using a + # static IP — if it is already DHCP there is nothing to reset. + saved = self._nm.get_saved_network(ssid) + is_dhcp = saved.is_dhcp if saved else True + self.wifi_sip_dhcp_button.setEnabled(not is_dhcp) + self.wifi_sip_dhcp_button.setToolTip( + "Already using DHCP" if is_dhcp else "Reset this network to DHCP" + ) + + self.setCurrentIndex(self.indexOf(self.wifi_static_ip_page)) + + def _on_wifi_static_ip_apply(self) -> None: + """Validate static-IP fields and apply them to the current Wi-Fi connection. + + Mirrors the VLAN-creation UX: navigate to the main panel immediately, + show the loading overlay, and clear it silently once ``reconnect_complete`` + fires (no popup — the updated IP appears in the panel header instead). + """ + ssid = self.wifi_sip_title.text() + ip_addr = self.wifi_sip_ip_field.text().strip() + mask = self.wifi_sip_mask_field.text().strip() + gateway = self.wifi_sip_gateway_field.text().strip() + dns1 = self.wifi_sip_dns1_field.text().strip() + dns2 = self.wifi_sip_dns2_field.text().strip() + + if not self.wifi_sip_ip_field.is_valid(): + self._show_error_popup("Invalid IP address.") + return + if not self.wifi_sip_mask_field.is_valid_mask(): + self._show_error_popup("Invalid subnet mask.") + return + if gateway and not self.wifi_sip_gateway_field.is_valid(): + self._show_error_popup("Invalid gateway address.") + return + if dns1 and not self.wifi_sip_dns1_field.is_valid(): + self._show_error_popup("Invalid primary DNS.") + return + if dns2 and not self.wifi_sip_dns2_field.is_valid(): + self._show_error_popup("Invalid secondary DNS.") + return + + self.setCurrentIndex(self.indexOf(self.main_network_page)) + self._pending_operation = PendingOperation.WIFI_STATIC_IP + self._pending_expected_ip: str = ip_addr # hold loading until this IP appears + self._active_signal = 0 # reset so signal shows "--" during reconnect + self._set_loading_state(True) + self._nm.update_wifi_static_ip(ssid, ip_addr, mask, gateway, dns1, dns2) + self._nm.request_state_soon(delay_ms=3000) + + def _on_wifi_reset_dhcp(self) -> None: + """Reset the current Wi-Fi connection back to DHCP via the facade. + + Same loading-screen pattern as static IP — no popup on success. + """ + ssid = self.wifi_sip_title.text() + self.setCurrentIndex(self.indexOf(self.main_network_page)) + self._pending_operation = PendingOperation.WIFI_STATIC_IP + self._pending_expected_ip: str = "" # any IP confirms DHCP success + self._active_signal = 0 # reset so signal shows "--" during reconnect + self._set_loading_state(True) + self._nm.reset_wifi_to_dhcp(ssid) + self._nm.request_state_soon(delay_ms=3000) + + def _build_network_list_from_scan(self, networks: list[NetworkInfo]) -> None: + """Build/update network list from scan results. + + Uses the model's built-in reconcile() with an item cache so that + ListItems are only allocated for networks whose visual state + actually changed (different signal bars or status label). + Unchanged items are reused from the cache — zero allocation. + """ + self.listView.blockSignals(True) + + desired_items: list[ListItem] = [] + + saved = [n for n in networks if n.is_saved] + unsaved = [n for n in networks if not n.is_saved] + + for net in saved: + item = self._get_or_create_item(net) + if item is not None: + desired_items.append(item) + + if saved and unsaved: + desired_items.append(self._get_separator_item()) + + for net in unsaved: + item = self._get_or_create_item(net) + if item is not None: + desired_items.append(item) + + desired_items.append(self._get_hidden_network_item()) + + self._model.reconcile(desired_items, self._item_key) + self._entry_delegate.prev_index = 0 + self._sync_scrollbar() + + # Evict cache entries for SSIDs no longer in scan results + live_ssids = {n.ssid for n in networks} + stale = [k for k in self._item_cache if k not in live_ssids] + for k in stale: + del self._item_cache[k] + + self.listView.blockSignals(False) + self.listView.update() + + def _patch_cached_network_status(self, ssid: str, status: NetworkStatus) -> None: + """Optimistically update one entry in the scan cache and rebuild the list. + + Called immediately after add/delete so the list reflects the change + without waiting for the next scan cycle. + """ + self._cached_scan_networks = [ + replace(n, network_status=status) if n.ssid == ssid else n + for n in self._cached_scan_networks + ] + self._item_cache.pop(ssid, None) + self._build_network_list_from_scan(self._cached_scan_networks) + + def _get_or_create_item(self, network: NetworkInfo) -> ListItem | None: + """Return a cached ListItem if the network's visual state is + unchanged, otherwise create a new one and update the cache. + + Visual state = (signal_bars, status_label). When both match + the cached entry, the existing ListItem is returned as-is — + no QPixmap lookup, no allocation. + """ + if network.is_hidden or is_hidden_ssid(network.ssid): + return None + if not is_connectable_security(network.security_type): + return None + + bars = signal_to_bars(network.signal_strength) + status = network.status + ssid = network.ssid + + cached = self._item_cache.get(ssid) + if cached is not None: + cached_bars, cached_status, cached_item = cached + if cached_bars == bars and cached_status == status: + return cached_item + + item = self._make_network_item(network) + if item is not None: + self._item_cache[ssid] = (bars, status, item) + return item + + def _get_separator_item(self) -> ListItem: + """Return the singleton separator item (created once, reused forever).""" + if self._separator_item is None: + self._separator_item = self._make_separator_item() + return self._separator_item + + def _get_hidden_network_item(self) -> ListItem: + """Return the singleton 'Connect to Hidden Network' item.""" + if self._hidden_network_item is None: + self._hidden_network_item = self._make_hidden_network_item() + return self._hidden_network_item + + @staticmethod + def _item_key(item: ListItem) -> str: + """Unique key for a list item (SSID, or sentinel for special rows).""" + if item.not_clickable and not item.text: + return "__separator__" + return item.text + + def _make_network_item(self, network: NetworkInfo) -> ListItem | None: + """Create a ListItem for a scanned network, or None if hidden/unsupported.""" + if network.is_hidden or is_hidden_ssid(network.ssid): + return None + if not is_connectable_security(network.security_type): + return None + + wifi_pixmap = WifiIconProvider.get_pixmap( + network.signal_strength, not network.is_open + ) + + return ListItem( + text=network.ssid, + left_icon=wifi_pixmap, + right_text=network.status, + right_icon=self._right_arrow_icon, + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=80, + not_clickable=False, + ) + + @staticmethod + def _make_separator_item() -> ListItem: + """Create a non-clickable separator item.""" + return ListItem( + text="", + left_icon=None, + right_text="", + right_icon=None, + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=20, + not_clickable=True, + ) + + def _make_hidden_network_item(self) -> ListItem: + """Create the 'Connect to Hidden Network' entry.""" + return ListItem( + text="Connect to Hidden Network...", + left_icon=self._hiden_network_icon, + right_text="", + right_icon=self._right_arrow_icon, + selected=False, + allow_check=False, + _lfontsize=17, + _rfontsize=12, + height=80, + not_clickable=False, + ) + + @QtCore.pyqtSlot(ListItem, name="ssid-item-clicked") + def _on_ssid_item_clicked(self, item: ListItem) -> None: + """Handle a tap on an SSID list item: show the save or connect page as appropriate.""" + ssid = item.text + + if is_hidden_ssid(ssid) or ssid == "Connect to Hidden Network...": + self.setCurrentIndex(self.indexOf(self.hidden_network_page)) + return + + network = self._nm.get_network_info(ssid) + if not network: + return + + # Reject unsupported security types (defence-in-depth) + if not is_connectable_security(network.security_type): + self._show_error_popup( + f"'{ssid}' uses unsupported security " + f"({network.security_type}).\n" + "Only WPA/WPA2 networks are supported." + ) + return + + if network.is_saved: + self._show_saved_network_page(network) + else: + self._show_add_network_page(network) + + def _show_saved_network_page(self, network: NetworkInfo) -> None: + """Populate and navigate to the saved-network detail page for *network*.""" + ssid = network.ssid + + self.saved_connection_network_name.setText(ssid) + self.snd_name.setText(ssid) + + self.saved_connection_change_password_field.clear() + self.saved_connection_change_password_field.setPlaceholderText( + "Enter new password" + ) + self.saved_connection_change_password_field.setHidden(True) + if self.saved_connection_change_password_view.isChecked(): + self.saved_connection_change_password_view.setChecked(False) + + saved = self._nm.get_saved_network(ssid) + + if saved: + self._set_priority_button(saved.priority) + # Track initial values for change detection + self._initial_priority = self._get_selected_priority() + else: + self._initial_priority = ConnectionPriority.MEDIUM + + # Signal strength — for the active network, use the unified + # _active_signal so the details page matches the main panel + # and header icon exactly. + is_active = ssid == self._nm.current_ssid + if is_active and self._active_signal > 0: + signal_value = self._active_signal + else: + signal_value = network.signal_strength + + signal_text = f"{signal_value}%" if signal_value >= 0 else "--%" + + self.saved_connection_signal_strength_info_frame.setText(signal_text) + + if network.is_open: + self.saved_connection_security_type_info_label.setText("OPEN") + else: + sec_type = saved.security_type if saved else "WPA" + self.saved_connection_security_type_info_label.setText(sec_type.upper()) + + self.network_activate_btn.setDisabled(is_active) + self.sn_info.setText("Active Network" if is_active else "Saved Network") + + self.setCurrentIndex(self.indexOf(self.saved_connection_page)) + self.frame.update() + + def _show_add_network_page(self, network: NetworkInfo) -> None: + """Populate and navigate to the add-network page for *network*.""" + self._current_network_is_open = network.is_open + self._current_network_is_hidden = False + + self.add_network_network_label.setText(network.ssid) + self.add_network_password_field.clear() + + self.frame_2.setVisible(not network.is_open) + self.add_network_validation_button.setText( + "Connect" if network.is_open else "Activate" + ) + + self.setCurrentIndex(self.indexOf(self.add_network_page)) + + def _set_priority_button(self, priority: int | None) -> None: + """Set priority button based on value.""" + if priority is not None and priority >= ConnectionPriority.HIGH.value: + target = self.high_priority_btn + elif priority is not None and priority <= ConnectionPriority.LOW.value: + target = self.low_priority_btn + else: + target = self.med_priority_btn + + logger.debug( + "Setting priority button: priority=%r -> %s", priority, target.text() + ) + + target.setChecked(True) + + self.high_priority_btn.update() + self.med_priority_btn.update() + self.low_priority_btn.update() + + def _get_selected_priority(self) -> ConnectionPriority: + """Return the ``ConnectionPriority`` matching the currently selected radio button.""" + checked = self.priority_btn_group.checkedButton() + logger.debug( + "Priority selection: checked=%s, h=%s m=%s l=%s", + checked.text() if checked else "None", + self.high_priority_btn.isChecked(), + self.med_priority_btn.isChecked(), + self.low_priority_btn.isChecked(), + ) + + if checked is self.high_priority_btn: + return ConnectionPriority.HIGH + elif checked is self.low_priority_btn: + return ConnectionPriority.LOW + + if self.high_priority_btn.isChecked(): + return ConnectionPriority.HIGH + if self.low_priority_btn.isChecked(): + return ConnectionPriority.LOW + return ConnectionPriority.MEDIUM + + @QtCore.pyqtSlot(name="add-network") + def _add_network(self) -> None: + """Add network - non-blocking.""" + self.add_network_validation_button.setEnabled(False) + + ssid = self.add_network_network_label.text() + password = self.add_network_password_field.text() + + if not password and not self._current_network_is_open: + self._show_error_popup("Password field cannot be empty.") + self.add_network_validation_button.setEnabled(True) + return + + self._target_ssid = ssid + self._pending_operation = PendingOperation.CONNECT + self._set_loading_state(True) + + self.add_network_password_field.clear() + self.setCurrentIndex(self.indexOf(self.main_network_page)) + + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.ON + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = hotspot_btn.State.OFF + + self._nm.add_network(ssid, password) + + self.add_network_validation_button.setEnabled(True) + + def _on_activate_network(self) -> None: + """Activate the network shown on the saved-connection page.""" + ssid = self.saved_connection_network_name.text() + + self._target_ssid = ssid + self._pending_operation = PendingOperation.CONNECT + self._set_loading_state(True) + + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.ON + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = hotspot_btn.State.OFF + + self.setCurrentIndex(self.indexOf(self.main_network_page)) + self._nm.connect_network(ssid) + + def _on_delete_network(self) -> None: + """Delete the profile shown on the saved-connection page and navigate back.""" + ssid = self.saved_connection_network_name.text() + self._target_ssid = ssid + self._nm.delete_network(ssid) + + def _on_save_network_details(self) -> None: + """Save network settings changes (password / priority). + + Only performs an update if the user actually changed something. + Shows a confirmation popup on success. + """ + ssid = self.saved_connection_network_name.text() + password = self.saved_connection_change_password_field.text() + priority = self._get_selected_priority() + + password_changed = bool(password) + priority_changed = priority != self._initial_priority + + if not password_changed and not priority_changed: + self._show_info_popup("No changes to save.") + return + + self._nm.update_network( + ssid, + password=password or "", + priority=priority.value, + ) + + self._nm.load_saved_networks() + + # Update tracked baseline so a second press won't re-save + self._initial_priority = priority + + self.saved_connection_change_password_field.clear() + + def _on_hidden_network_connect(self) -> None: + """Connect to hidden network - non-blocking.""" + ssid = self.hidden_network_ssid_field.text().strip() + password = self.hidden_network_password_field.text() + + if not ssid: + self._show_error_popup("Please enter a network name.") + return + + self._current_network_is_hidden = True + self._current_network_is_open = not password + self._target_ssid = ssid + self._pending_operation = PendingOperation.CONNECT + self._set_loading_state(True) + + self.hidden_network_ssid_field.clear() + self.hidden_network_password_field.clear() + + self.setCurrentIndex(self.indexOf(self.main_network_page)) + + wifi_btn = self.wifi_button.toggle_button + hotspot_btn = self.hotspot_button.toggle_button + with QtCore.QSignalBlocker(wifi_btn): + wifi_btn.state = wifi_btn.State.ON + with QtCore.QSignalBlocker(hotspot_btn): + hotspot_btn.state = hotspot_btn.State.OFF + + self._nm.add_network(ssid, password) + + def _show_error_popup(self, message: str, timeout: int = 6000) -> None: + """Display *message* in an error-styled info box with an auto-dismiss *timeout* ms.""" + self._popup.raise_() + self._popup.new_message( + message_type=Popup.MessageType.ERROR, + message=message, + timeout=timeout, + userInput=False, + ) + + def _show_info_popup(self, message: str, timeout: int = 4000) -> None: + """Display *message* in a neutral info box with an auto-dismiss *timeout* ms.""" + self._popup.raise_() + self._popup.new_message( + message_type=Popup.MessageType.INFO, + message=message, + timeout=timeout, + userInput=False, + ) + + def _show_warning_popup(self, message: str, timeout: int = 5000) -> None: + """Display *message* in a warning-styled info box with an auto-dismiss *timeout* ms.""" + self._popup.raise_() + self._popup.new_message( + message_type=Popup.MessageType.WARNING, + message=message, + timeout=timeout, + userInput=False, + ) + + def close(self) -> bool: + """Close and cleanup.""" + self._nm.close() + return super().close() + + def closeEvent(self, event: QtGui.QCloseEvent | None) -> None: + """Handle close event.""" + if self._load_timer.isActive(): + self._load_timer.stop() + super().closeEvent(event) + + def showEvent(self, event: QtGui.QShowEvent | None) -> None: + """Handle show event.""" + self._nm.refresh_state() + super().showEvent(event) + + def _setupUI(self) -> None: + """Build and lay out the entire stacked-widget UI tree.""" + self.setObjectName("wifi_stacked_page") + self.resize(800, 480) + + size_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum + ) + size_policy.setHorizontalStretch(0) + size_policy.setVerticalStretch(0) + size_policy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) + self.setSizePolicy(size_policy) + self.setMinimumSize(QtCore.QSize(0, 400)) + self.setMaximumSize(QtCore.QSize(16777215, 575)) + self.setStyleSheet( + "#wifi_stacked_page{\n" + " background-image: url(:/background/media/1st_background.png);\n" + "}\n" + ) + + self._popup = Popup(self) + self._right_arrow_icon = PixmapCache.get( + ":/arrow_icons/media/btn_icons/right_arrow.svg" + ) + self._hiden_network_icon = PixmapCache.get( + ":/network/media/btn_icons/network/0bar_wifi_protected.svg" + ) + + self._setup_main_network_page() + self._setup_network_list_page() + self._setup_add_network_page() + self._setup_saved_connection_page() + self._setup_saved_details_page() + self._setup_hotspot_page() + self._setup_hidden_network_page() + self._setup_vlan_page() + self._setup_wifi_static_ip_page() + + self.setCurrentIndex(0) + + def _create_white_palette(self) -> QtGui.QPalette: + """Return a QPalette with all roles set to white (flat widget backgrounds).""" + palette = QtGui.QPalette() + white_brush = QtGui.QBrush(QtGui.QColor(255, 255, 255)) + white_brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + grey_brush = QtGui.QBrush(QtGui.QColor(120, 120, 120)) + grey_brush.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + + for group in [ + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorGroup.Inactive, + ]: + palette.setBrush(group, QtGui.QPalette.ColorRole.WindowText, white_brush) + palette.setBrush(group, QtGui.QPalette.ColorRole.Text, white_brush) + + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.WindowText, + grey_brush, + ) + palette.setBrush( + QtGui.QPalette.ColorGroup.Disabled, + QtGui.QPalette.ColorRole.Text, + grey_brush, + ) + + return palette + + def _setup_main_network_page(self) -> None: + """Setup the main network page.""" + self.main_network_page = QtWidgets.QWidget() + size_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.main_network_page.setSizePolicy(size_policy) + + main_layout = QtWidgets.QVBoxLayout(self.main_network_page) + + header_layout = QtWidgets.QHBoxLayout() + + header_layout.addItem( + QtWidgets.QSpacerItem( + 60, + 60, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + + self.network_main_title = QtWidgets.QLabel(parent=self.main_network_page) + title_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum + ) + self.network_main_title.setSizePolicy(title_policy) + self.network_main_title.setMinimumSize(QtCore.QSize(300, 0)) + self.network_main_title.setMaximumSize(QtCore.QSize(16777215, 60)) + font = QtGui.QFont() + font.setPointSize(20) + self.network_main_title.setFont(font) + self.network_main_title.setStyleSheet("color:white") + self.network_main_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.network_main_title.setText("Networks") + + header_layout.addWidget(self.network_main_title) + + self.network_backButton = IconButton(parent=self.main_network_page) + self.network_backButton.setMinimumSize(QtCore.QSize(60, 60)) + self.network_backButton.setMaximumSize(QtCore.QSize(60, 60)) + self.network_backButton.setFlat(True) + self.network_backButton.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") + ) + + header_layout.addWidget(self.network_backButton) + + main_layout.addLayout(header_layout) + + content_layout = QtWidgets.QHBoxLayout() + + self.mn_information_layout = BlocksCustomFrame(parent=self.main_network_page) + info_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.mn_information_layout.setSizePolicy(info_policy) + self.mn_information_layout.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.mn_information_layout.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + + info_layout = QtWidgets.QVBoxLayout(self.mn_information_layout) + + self.netlist_ssuid = QtWidgets.QLabel(parent=self.mn_information_layout) + ssid_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum + ) + self.netlist_ssuid.setSizePolicy(ssid_policy) + font = QtGui.QFont() + font.setPointSize(17) + self.netlist_ssuid.setFont(font) + self.netlist_ssuid.setStyleSheet("color: rgb(255, 255, 255);") + self.netlist_ssuid.setText("") + self.netlist_ssuid.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + info_layout.addWidget(self.netlist_ssuid) + + self.mn_info_seperator = QtWidgets.QFrame(parent=self.mn_information_layout) + self.mn_info_seperator.setFrameShape(QtWidgets.QFrame.Shape.HLine) + self.mn_info_seperator.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + + info_layout.addWidget(self.mn_info_seperator) + + self.netlist_ip = QtWidgets.QLabel(parent=self.mn_information_layout) + font = QtGui.QFont() + font.setPointSize(15) + self.netlist_ip.setFont(font) + self.netlist_ip.setStyleSheet("color: rgb(255, 255, 255);") + self.netlist_ip.setText("") + self.netlist_ip.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + + info_layout.addWidget(self.netlist_ip) + + self.netlist_vlans_combo = QtWidgets.QComboBox( + parent=self.mn_information_layout + ) + font = QtGui.QFont() + font.setPointSize(11) + self.netlist_vlans_combo.setFont(font) + self.netlist_vlans_combo.setMinimumSize(QtCore.QSize(240, 50)) + self.netlist_vlans_combo.setMaximumSize(QtCore.QSize(250, 50)) + self.netlist_vlans_combo.setStyleSheet(""" + QComboBox { + background-color: rgba(26, 143, 191, 0.05); + color: rgba(255, 255, 255, 200); + border: 1px solid rgba(255, 255, 255, 80); + border-radius: 8px; + } + QComboBox QAbstractItemView { + background-color: rgb(40, 40, 40); + color: white; + selection-background-color: rgba(26, 143, 191, 0.6); + } + """) + + self.netlist_vlans_combo.setVisible(False) + self.netlist_vlans_combo.currentIndexChanged.connect( + self._on_interface_combo_changed + ) + + info_layout.addWidget( + self.netlist_vlans_combo, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) + + conn_info_layout = QtWidgets.QHBoxLayout() + + sg_info_layout = QtWidgets.QVBoxLayout() + + self.netlist_strength_label = QtWidgets.QLabel( + parent=self.mn_information_layout + ) + self.netlist_strength_label.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + self.netlist_strength_label.setFont(font) + self.netlist_strength_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_strength_label.setText("Signal\nStrength") + + sg_info_layout.addWidget(self.netlist_strength_label) + + self.line_2 = QtWidgets.QFrame(parent=self.mn_information_layout) + self.line_2.setFrameShape(QtWidgets.QFrame.Shape.HLine) + self.line_2.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + + sg_info_layout.addWidget(self.line_2) + + self.netlist_strength = QtWidgets.QLabel(parent=self.mn_information_layout) + font = QtGui.QFont() + font.setPointSize(11) + self.netlist_strength.setFont(font) + self.netlist_strength.setStyleSheet("color: rgb(255, 255, 255);") + self.netlist_strength.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_strength.setText("") + + sg_info_layout.addWidget(self.netlist_strength) + + conn_info_layout.addLayout(sg_info_layout) + + sec_info_layout = QtWidgets.QVBoxLayout() + + self.netlist_security_label = QtWidgets.QLabel( + parent=self.mn_information_layout ) self.netlist_security_label.setPalette(self._create_white_palette()) font = QtGui.QFont() @@ -569,13 +2058,13 @@ def _setup_main_network_page(self) -> None: self.netlist_security_label.setFont(font) self.netlist_security_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.netlist_security_label.setText("Security\nType") - self.netlist_security_label.setObjectName("netlist_security_label") + sec_info_layout.addWidget(self.netlist_security_label) self.line_3 = QtWidgets.QFrame(parent=self.mn_information_layout) self.line_3.setFrameShape(QtWidgets.QFrame.Shape.HLine) self.line_3.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line_3.setObjectName("line_3") + sec_info_layout.addWidget(self.line_3) self.netlist_security = QtWidgets.QLabel(parent=self.mn_information_layout) @@ -585,13 +2074,12 @@ def _setup_main_network_page(self) -> None: self.netlist_security.setStyleSheet("color: rgb(255, 255, 255);") self.netlist_security.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.netlist_security.setText("") - self.netlist_security.setObjectName("netlist_security") + sec_info_layout.addWidget(self.netlist_security) conn_info_layout.addLayout(sec_info_layout) info_layout.addLayout(conn_info_layout) - # Info box self.mn_info_box = QtWidgets.QLabel(parent=self.mn_information_layout) self.mn_info_box.setEnabled(False) font = QtGui.QFont() @@ -599,17 +2087,17 @@ def _setup_main_network_page(self) -> None: self.mn_info_box.setFont(font) self.mn_info_box.setStyleSheet("color: white") self.mn_info_box.setTextFormat(QtCore.Qt.TextFormat.PlainText) - self.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.mn_info_box.setText( - "No network connection.\n\n" - "Try connecting to Wi-Fi \n" - "or turn on the hotspot\n" - "using the buttons on the side." + "There no active\ninternet connection.\nConnect via Ethernet, Wi-Fi,\nor enable a mobile hotspot\n for online features.\nPrinting functions will\nstill work offline." + ) + + self.mn_info_box.setSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Expanding, ) - self.mn_info_box.setObjectName("mn_info_box") + self.mn_info_box.setWordWrap(True) info_layout.addWidget(self.mn_info_box) - # Loading widget self.loadingwidget = LoadingOverlayWidget(parent=self.mn_information_layout) self.loadingwidget.setEnabled(True) loading_policy = QtWidgets.QSizePolicy( @@ -617,39 +2105,42 @@ def _setup_main_network_page(self) -> None: ) self.loadingwidget.setSizePolicy(loading_policy) self.loadingwidget.setText("") - self.loadingwidget.setObjectName("loadingwidget") + info_layout.addWidget(self.loadingwidget) content_layout.addWidget(self.mn_information_layout) - # Option buttons layout option_layout = QtWidgets.QVBoxLayout() - option_layout.setObjectName("mn_option_button_layout") - self.wifi_button = NetworkWidgetbuttons(parent=self.main_network_page) - wifi_policy = QtWidgets.QSizePolicy( + panel_policy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding, ) - self.wifi_button.setSizePolicy(wifi_policy) - self.wifi_button.setMaximumSize(QtCore.QSize(400, 9999)) font = QtGui.QFont() font.setPointSize(20) + + self.wifi_button = NetworkWidgetbuttons(parent=self.main_network_page) + self.wifi_button.setSizePolicy(panel_policy) + self.wifi_button.setMaximumSize(QtCore.QSize(400, 9999)) self.wifi_button.setFont(font) self.wifi_button.setText("Wi-Fi") - self.wifi_button.setObjectName("wifi_button") option_layout.addWidget(self.wifi_button) self.hotspot_button = NetworkWidgetbuttons(parent=self.main_network_page) - self.hotspot_button.setSizePolicy(wifi_policy) + self.hotspot_button.setSizePolicy(panel_policy) self.hotspot_button.setMaximumSize(QtCore.QSize(400, 9999)) - font = QtGui.QFont() - font.setPointSize(20) self.hotspot_button.setFont(font) self.hotspot_button.setText("Hotspot") - self.hotspot_button.setObjectName("hotspot_button") option_layout.addWidget(self.hotspot_button) + self.ethernet_button = NetworkWidgetbuttons(parent=self.main_network_page) + self.ethernet_button.setSizePolicy(panel_policy) + self.ethernet_button.setMaximumSize(QtCore.QSize(400, 9999)) + self.ethernet_button.setFont(font) + self.ethernet_button.setText("Ethernet") + self.ethernet_button.setVisible(False) + option_layout.addWidget(self.ethernet_button) + content_layout.addLayout(option_layout) main_layout.addLayout(content_layout) @@ -658,14 +2149,10 @@ def _setup_main_network_page(self) -> None: def _setup_network_list_page(self) -> None: """Setup the network list page.""" self.network_list_page = QtWidgets.QWidget() - self.network_list_page.setObjectName("network_list_page") main_layout = QtWidgets.QVBoxLayout(self.network_list_page) - main_layout.setObjectName("verticalLayout_9") - # Header layout header_layout = QtWidgets.QHBoxLayout() - header_layout.setObjectName("nl_header_layout") self.rescan_button = IconButton(parent=self.network_list_page) self.rescan_button.setMinimumSize(QtCore.QSize(60, 60)) @@ -673,21 +2160,21 @@ def _setup_network_list_page(self) -> None: self.rescan_button.setText("Reload") self.rescan_button.setFlat(True) self.rescan_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/refresh.svg") + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/refresh.svg") ) self.rescan_button.setProperty("button_type", "icon") - self.rescan_button.setObjectName("rescan_button") + header_layout.addWidget(self.rescan_button) self.network_list_title = QtWidgets.QLabel(parent=self.network_list_page) self.network_list_title.setMaximumSize(QtCore.QSize(16777215, 60)) self.network_list_title.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(20) - self.network_list_title.setFont(font) + title_font = QtGui.QFont() + title_font.setPointSize(20) + self.network_list_title.setFont(title_font) self.network_list_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.network_list_title.setText("Wi-Fi List") - self.network_list_title.setObjectName("network_list_title") + header_layout.addWidget(self.network_list_title) self.nl_back_button = IconButton(parent=self.network_list_page) @@ -696,18 +2183,16 @@ def _setup_network_list_page(self) -> None: self.nl_back_button.setText("Back") self.nl_back_button.setFlat(True) self.nl_back_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") ) self.nl_back_button.setProperty("class", "back_btn") self.nl_back_button.setProperty("button_type", "icon") - self.nl_back_button.setObjectName("nl_back_button") + header_layout.addWidget(self.nl_back_button) main_layout.addLayout(header_layout) - # List view layout list_layout = QtWidgets.QHBoxLayout() - list_layout.setObjectName("horizontalLayout_2") self.listView = QtWidgets.QListView(self.network_list_page) list_policy = QtWidgets.QSizePolicy( @@ -739,7 +2224,6 @@ def _setup_network_list_page(self) -> None: self.listView.setUniformItemSizes(True) self.listView.setSpacing(5) - # Setup touch scrolling QtWidgets.QScroller.grabGesture( self.listView, QtWidgets.QScroller.ScrollerGestureType.TouchGesture, @@ -769,7 +2253,7 @@ def _setup_network_list_page(self) -> None: ) self.verticalScrollBar.setSizePolicy(scrollbar_policy) self.verticalScrollBar.setOrientation(QtCore.Qt.Orientation.Vertical) - self.verticalScrollBar.setObjectName("verticalScrollBar") + self.verticalScrollBar.setAttribute( QtCore.Qt.WidgetAttribute.WA_TransparentForMouseEvents, True ) @@ -791,14 +2275,10 @@ def _setup_network_list_page(self) -> None: def _setup_add_network_page(self) -> None: """Setup the add network page.""" self.add_network_page = QtWidgets.QWidget() - self.add_network_page.setObjectName("add_network_page") main_layout = QtWidgets.QVBoxLayout(self.add_network_page) - main_layout.setObjectName("verticalLayout_10") - # Header layout header_layout = QtWidgets.QHBoxLayout() - header_layout.setObjectName("add_np_header_layout") header_layout.addItem( QtWidgets.QSpacerItem( @@ -823,7 +2303,7 @@ def _setup_add_network_page(self) -> None: self.add_network_network_label.setStyleSheet("color:white") self.add_network_network_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.add_network_network_label.setText("TextLabel") - self.add_network_network_label.setObjectName("add_network_network_label") + header_layout.addWidget(self.add_network_network_label) self.add_network_page_backButton = IconButton(parent=self.add_network_page) @@ -832,21 +2312,19 @@ def _setup_add_network_page(self) -> None: self.add_network_page_backButton.setText("Back") self.add_network_page_backButton.setFlat(True) self.add_network_page_backButton.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") ) self.add_network_page_backButton.setProperty("class", "back_btn") self.add_network_page_backButton.setProperty("button_type", "icon") - self.add_network_page_backButton.setObjectName("add_network_page_backButton") + header_layout.addWidget(self.add_network_page_backButton) main_layout.addLayout(header_layout) - # Content layout content_layout = QtWidgets.QVBoxLayout() content_layout.setSizeConstraint( QtWidgets.QLayout.SizeConstraint.SetMinimumSize ) - content_layout.setObjectName("add_np_content_layout") content_layout.addItem( QtWidgets.QSpacerItem( @@ -857,7 +2335,6 @@ def _setup_add_network_page(self) -> None: ) ) - # Password frame self.frame_2 = BlocksCustomFrame(parent=self.add_network_page) frame_policy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum @@ -868,18 +2345,15 @@ def _setup_add_network_page(self) -> None: self.frame_2.setMaximumSize(QtCore.QSize(16777215, 90)) self.frame_2.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.frame_2.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_2.setObjectName("frame_2") frame_layout_widget = QtWidgets.QWidget(parent=self.frame_2) frame_layout_widget.setGeometry(QtCore.QRect(10, 10, 761, 82)) - frame_layout_widget.setObjectName("layoutWidget_2") password_layout = QtWidgets.QHBoxLayout(frame_layout_widget) password_layout.setSizeConstraint( QtWidgets.QLayout.SizeConstraint.SetMaximumSize ) password_layout.setContentsMargins(0, 0, 0, 0) - password_layout.setObjectName("horizontalLayout_5") self.add_network_password_label = QtWidgets.QLabel(parent=frame_layout_widget) self.add_network_password_label.setPalette(self._create_white_palette()) @@ -890,7 +2364,7 @@ def _setup_add_network_page(self) -> None: QtCore.Qt.AlignmentFlag.AlignCenter ) self.add_network_password_label.setText("Password") - self.add_network_password_label.setObjectName("add_network_password_label") + password_layout.addWidget(self.add_network_password_label) self.add_network_password_field = BlocksCustomLinEdit( @@ -901,7 +2375,7 @@ def _setup_add_network_page(self) -> None: font = QtGui.QFont() font.setPointSize(12) self.add_network_password_field.setFont(font) - self.add_network_password_field.setObjectName("add_network_password_field") + password_layout.addWidget(self.add_network_password_field) self.add_network_password_view = IconButton(parent=frame_layout_widget) @@ -910,11 +2384,11 @@ def _setup_add_network_page(self) -> None: self.add_network_password_view.setText("View") self.add_network_password_view.setFlat(True) self.add_network_password_view.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/unsee.svg") ) self.add_network_password_view.setProperty("class", "back_btn") self.add_network_password_view.setProperty("button_type", "icon") - self.add_network_password_view.setObjectName("add_network_password_view") + password_layout.addWidget(self.add_network_password_view) content_layout.addWidget(self.frame_2) @@ -928,263 +2402,55 @@ def _setup_add_network_page(self) -> None: ) ) - # Validation button layout button_layout = QtWidgets.QHBoxLayout() button_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize) - button_layout.setObjectName("horizontalLayout_6") self.add_network_validation_button = BlocksCustomButton( parent=self.add_network_page ) - self.add_network_validation_button.setEnabled(True) - btn_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.MinimumExpanding, - QtWidgets.QSizePolicy.Policy.MinimumExpanding, - ) - btn_policy.setHorizontalStretch(1) - btn_policy.setVerticalStretch(1) - self.add_network_validation_button.setSizePolicy(btn_policy) - self.add_network_validation_button.setMinimumSize(QtCore.QSize(250, 80)) - self.add_network_validation_button.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(15) - self.add_network_validation_button.setFont(font) - self.add_network_validation_button.setIconSize(QtCore.QSize(16, 16)) - self.add_network_validation_button.setCheckable(False) - self.add_network_validation_button.setChecked(False) - self.add_network_validation_button.setFlat(True) - self.add_network_validation_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") - ) - self.add_network_validation_button.setText("Activate") - self.add_network_validation_button.setObjectName( - "add_network_validation_button" - ) - button_layout.addWidget( - self.add_network_validation_button, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignTop, - ) - - content_layout.addLayout(button_layout) - main_layout.addLayout(content_layout) - - self.addWidget(self.add_network_page) - - def _setup_hidden_network_page(self) -> None: - """Setup the hidden network page for connecting to networks with hidden SSID.""" - self.hidden_network_page = QtWidgets.QWidget() - self.hidden_network_page.setObjectName("hidden_network_page") - - main_layout = QtWidgets.QVBoxLayout(self.hidden_network_page) - main_layout.setObjectName("hidden_network_layout") - - # Header layout - header_layout = QtWidgets.QHBoxLayout() - header_layout.addItem( - QtWidgets.QSpacerItem( - 40, - 60, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) - - self.hidden_network_title = QtWidgets.QLabel(parent=self.hidden_network_page) - self.hidden_network_title.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(20) - self.hidden_network_title.setFont(font) - self.hidden_network_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.hidden_network_title.setText("Hidden Network") - header_layout.addWidget(self.hidden_network_title) - - self.hidden_network_back_button = IconButton(parent=self.hidden_network_page) - self.hidden_network_back_button.setMinimumSize(QtCore.QSize(60, 60)) - self.hidden_network_back_button.setMaximumSize(QtCore.QSize(60, 60)) - self.hidden_network_back_button.setFlat(True) - self.hidden_network_back_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") - ) - self.hidden_network_back_button.setProperty("button_type", "icon") - header_layout.addWidget(self.hidden_network_back_button) - - main_layout.addLayout(header_layout) - - # Content - content_layout = QtWidgets.QVBoxLayout() - content_layout.addItem( - QtWidgets.QSpacerItem( - 20, - 30, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) - - # SSID Frame - ssid_frame = BlocksCustomFrame(parent=self.hidden_network_page) - ssid_frame.setMinimumSize(QtCore.QSize(0, 80)) - ssid_frame.setMaximumSize(QtCore.QSize(16777215, 90)) - ssid_frame_layout = QtWidgets.QHBoxLayout(ssid_frame) - - ssid_label = QtWidgets.QLabel("Network\nName", parent=ssid_frame) - ssid_label.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(15) - ssid_label.setFont(font) - ssid_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - ssid_frame_layout.addWidget(ssid_label) - - self.hidden_network_ssid_field = BlocksCustomLinEdit(parent=ssid_frame) - self.hidden_network_ssid_field.setMinimumSize(QtCore.QSize(500, 60)) - font = QtGui.QFont() - font.setPointSize(12) - self.hidden_network_ssid_field.setFont(font) - self.hidden_network_ssid_field.setPlaceholderText("Enter network name") - ssid_frame_layout.addWidget(self.hidden_network_ssid_field) - - content_layout.addWidget(ssid_frame) - - content_layout.addItem( - QtWidgets.QSpacerItem( - 20, - 20, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) - - # Password Frame - password_frame = BlocksCustomFrame(parent=self.hidden_network_page) - password_frame.setMinimumSize(QtCore.QSize(0, 80)) - password_frame.setMaximumSize(QtCore.QSize(16777215, 90)) - password_frame_layout = QtWidgets.QHBoxLayout(password_frame) - - password_label = QtWidgets.QLabel("Password", parent=password_frame) - password_label.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(15) - password_label.setFont(font) - password_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - password_frame_layout.addWidget(password_label) - - self.hidden_network_password_field = BlocksCustomLinEdit(parent=password_frame) - self.hidden_network_password_field.setHidden(True) - self.hidden_network_password_field.setMinimumSize(QtCore.QSize(500, 60)) - font = QtGui.QFont() - font.setPointSize(12) - self.hidden_network_password_field.setFont(font) - self.hidden_network_password_field.setPlaceholderText( - "Enter password (leave empty for open networks)" - ) - self.hidden_network_password_field.setEchoMode( - QtWidgets.QLineEdit.EchoMode.Password - ) - password_frame_layout.addWidget(self.hidden_network_password_field) - - self.hidden_network_password_view = IconButton(parent=password_frame) - self.hidden_network_password_view.setMinimumSize(QtCore.QSize(60, 60)) - self.hidden_network_password_view.setMaximumSize(QtCore.QSize(60, 60)) - self.hidden_network_password_view.setFlat(True) - self.hidden_network_password_view.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") - ) - self.hidden_network_password_view.setProperty("button_type", "icon") - password_frame_layout.addWidget(self.hidden_network_password_view) - - content_layout.addWidget(password_frame) - - content_layout.addItem( - QtWidgets.QSpacerItem( - 20, - 50, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) - - # Connect button - self.hidden_network_connect_button = BlocksCustomButton( - parent=self.hidden_network_page - ) - self.hidden_network_connect_button.setMinimumSize(QtCore.QSize(250, 80)) - self.hidden_network_connect_button.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setPointSize(15) - self.hidden_network_connect_button.setFont(font) - self.hidden_network_connect_button.setFlat(True) - self.hidden_network_connect_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") - ) - self.hidden_network_connect_button.setText("Connect") - content_layout.addWidget( - self.hidden_network_connect_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - - main_layout.addLayout(content_layout) - self.addWidget(self.hidden_network_page) - - # Connect signals - self.hidden_network_back_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) - ) - self.hidden_network_connect_button.clicked.connect( - self._on_hidden_network_connect + self.add_network_validation_button.setEnabled(True) + btn_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.MinimumExpanding, + QtWidgets.QSizePolicy.Policy.MinimumExpanding, ) - self.hidden_network_ssid_field.clicked.connect( - lambda: self._on_show_keyboard( - self.hidden_network_page, self.hidden_network_ssid_field - ) + btn_policy.setHorizontalStretch(1) + btn_policy.setVerticalStretch(1) + self.add_network_validation_button.setSizePolicy(btn_policy) + self.add_network_validation_button.setMinimumSize(QtCore.QSize(250, 80)) + self.add_network_validation_button.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setFamily("Momcake") + font.setPointSize(15) + self.add_network_validation_button.setFont(font) + self.add_network_validation_button.setIconSize(QtCore.QSize(16, 16)) + self.add_network_validation_button.setCheckable(False) + self.add_network_validation_button.setChecked(False) + self.add_network_validation_button.setFlat(True) + self.add_network_validation_button.setProperty( + "icon_pixmap", PixmapCache.get(":/dialog/media/btn_icons/yes.svg") ) - self.hidden_network_password_field.clicked.connect( - lambda: self._on_show_keyboard( - self.hidden_network_page, self.hidden_network_password_field - ) + self.add_network_validation_button.setText("Activate") + self.add_network_validation_button.setObjectName( + "add_network_validation_button" ) - self._setup_password_visibility_toggle( - self.hidden_network_password_view, self.hidden_network_password_field + button_layout.addWidget( + self.add_network_validation_button, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignTop, ) - def _on_hidden_network_connect(self) -> None: - """Handle connection to hidden network.""" - ssid = self.hidden_network_ssid_field.text().strip() - password = self.hidden_network_password_field.text() - - if not ssid: - self._show_error_popup("Please enter a network name.") - return - - self._current_network_is_hidden = True - self._current_network_is_open = not password - - result = self._sdbus_network.add_wifi_network(ssid=ssid, psk=password) - - if result is None: - self._handle_failed_network_add("Failed to add network") - return - - error_msg = result.get("error", "") if isinstance(result, dict) else "" + content_layout.addLayout(button_layout) + main_layout.addLayout(content_layout) - if not error_msg: - self.hidden_network_ssid_field.clear() - self.hidden_network_password_field.clear() - self._handle_successful_network_add(ssid) - else: - self._handle_failed_network_add(error_msg) + self.addWidget(self.add_network_page) def _setup_saved_connection_page(self) -> None: """Setup the saved connection page.""" self.saved_connection_page = QtWidgets.QWidget() - self.saved_connection_page.setObjectName("saved_connection_page") main_layout = QtWidgets.QVBoxLayout(self.saved_connection_page) - main_layout.setObjectName("verticalLayout_11") - # Header layout header_layout = QtWidgets.QHBoxLayout() - header_layout.setObjectName("horizontalLayout_7") header_layout.addItem( QtWidgets.QSpacerItem( @@ -1222,23 +2488,20 @@ def _setup_saved_connection_page(self) -> None: ) self.saved_connection_back_button.setMinimumSize(QtCore.QSize(60, 60)) self.saved_connection_back_button.setMaximumSize(QtCore.QSize(60, 60)) - self.saved_connection_back_button.setText("Back") self.saved_connection_back_button.setFlat(True) self.saved_connection_back_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") ) self.saved_connection_back_button.setProperty("class", "back_btn") self.saved_connection_back_button.setProperty("button_type", "icon") - self.saved_connection_back_button.setObjectName("saved_connection_back_button") + header_layout.addWidget( self.saved_connection_back_button, 0, QtCore.Qt.AlignmentFlag.AlignRight ) main_layout.addLayout(header_layout) - # Content layout content_layout = QtWidgets.QVBoxLayout() - content_layout.setObjectName("verticalLayout_5") content_layout.addItem( QtWidgets.QSpacerItem( @@ -1249,13 +2512,9 @@ def _setup_saved_connection_page(self) -> None: ) ) - # Main content horizontal layout main_content_layout = QtWidgets.QHBoxLayout() - main_content_layout.setObjectName("horizontalLayout_9") - # Info frame layout info_layout = QtWidgets.QVBoxLayout() - info_layout.setObjectName("verticalLayout_2") self.frame = BlocksCustomFrame(parent=self.saved_connection_page) frame_policy = QtWidgets.QSizePolicy( @@ -1266,14 +2525,10 @@ def _setup_saved_connection_page(self) -> None: self.frame.setMaximumSize(QtCore.QSize(400, 16777215)) self.frame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) self.frame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame.setObjectName("frame") frame_inner_layout = QtWidgets.QVBoxLayout(self.frame) - frame_inner_layout.setObjectName("verticalLayout_6") - # Signal strength row signal_layout = QtWidgets.QHBoxLayout() - signal_layout.setObjectName("horizontalLayout") self.netlist_strength_label_2 = QtWidgets.QLabel(parent=self.frame) self.netlist_strength_label_2.setPalette(self._create_white_palette()) @@ -1282,7 +2537,7 @@ def _setup_saved_connection_page(self) -> None: self.netlist_strength_label_2.setFont(font) self.netlist_strength_label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.netlist_strength_label_2.setText("Signal\nStrength") - self.netlist_strength_label_2.setObjectName("netlist_strength_label_2") + signal_layout.addWidget(self.netlist_strength_label_2) self.saved_connection_signal_strength_info_frame = QtWidgets.QLabel( @@ -1300,10 +2555,6 @@ def _setup_saved_connection_page(self) -> None: self.saved_connection_signal_strength_info_frame.setAlignment( QtCore.Qt.AlignmentFlag.AlignCenter ) - self.saved_connection_signal_strength_info_frame.setText("TextLabel") - self.saved_connection_signal_strength_info_frame.setObjectName( - "saved_connection_signal_strength_info_frame" - ) signal_layout.addWidget(self.saved_connection_signal_strength_info_frame) frame_inner_layout.addLayout(signal_layout) @@ -1311,12 +2562,10 @@ def _setup_saved_connection_page(self) -> None: self.line_4 = QtWidgets.QFrame(parent=self.frame) self.line_4.setFrameShape(QtWidgets.QFrame.Shape.HLine) self.line_4.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line_4.setObjectName("line_4") + frame_inner_layout.addWidget(self.line_4) - # Security type row security_layout = QtWidgets.QHBoxLayout() - security_layout.setObjectName("horizontalLayout_2") self.netlist_security_label_2 = QtWidgets.QLabel(parent=self.frame) self.netlist_security_label_2.setPalette(self._create_white_palette()) @@ -1325,1862 +2574,1258 @@ def _setup_saved_connection_page(self) -> None: self.netlist_security_label_2.setFont(font) self.netlist_security_label_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) self.netlist_security_label_2.setText("Security\nType") - self.netlist_security_label_2.setObjectName("netlist_security_label_2") - security_layout.addWidget(self.netlist_security_label_2) - - self.saved_connection_security_type_info_label = QtWidgets.QLabel( - parent=self.frame - ) - self.saved_connection_security_type_info_label.setMinimumSize( - QtCore.QSize(250, 0) - ) - font = QtGui.QFont() - font.setPointSize(11) - self.saved_connection_security_type_info_label.setFont(font) - self.saved_connection_security_type_info_label.setStyleSheet( - "color: rgb(255, 255, 255);" - ) - self.saved_connection_security_type_info_label.setAlignment( - QtCore.Qt.AlignmentFlag.AlignCenter - ) - self.saved_connection_security_type_info_label.setText("TextLabel") - self.saved_connection_security_type_info_label.setObjectName( - "saved_connection_security_type_info_label" - ) - security_layout.addWidget(self.saved_connection_security_type_info_label) - - frame_inner_layout.addLayout(security_layout) - - self.line_5 = QtWidgets.QFrame(parent=self.frame) - self.line_5.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.line_5.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line_5.setObjectName("line_5") - frame_inner_layout.addWidget(self.line_5) - - # Status row - status_layout = QtWidgets.QHBoxLayout() - status_layout.setObjectName("horizontalLayout_8") - - self.netlist_security_label_4 = QtWidgets.QLabel(parent=self.frame) - self.netlist_security_label_4.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(15) - self.netlist_security_label_4.setFont(font) - self.netlist_security_label_4.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.netlist_security_label_4.setText("Status") - self.netlist_security_label_4.setObjectName("netlist_security_label_4") - status_layout.addWidget(self.netlist_security_label_4) - - self.sn_info = QtWidgets.QLabel(parent=self.frame) - self.sn_info.setMinimumSize(QtCore.QSize(250, 0)) - font = QtGui.QFont() - font.setPointSize(11) - self.sn_info.setFont(font) - self.sn_info.setStyleSheet("color: rgb(255, 255, 255);") - self.sn_info.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.sn_info.setText("TextLabel") - self.sn_info.setObjectName("sn_info") - status_layout.addWidget(self.sn_info) - - frame_inner_layout.addLayout(status_layout) - info_layout.addWidget(self.frame) - main_content_layout.addLayout(info_layout) - - # Action buttons frame - self.frame_8 = BlocksCustomFrame(parent=self.saved_connection_page) - self.frame_8.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_8.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_8.setObjectName("frame_8") - - buttons_layout = QtWidgets.QVBoxLayout(self.frame_8) - buttons_layout.setObjectName("verticalLayout_4") - - self.network_activate_btn = BlocksCustomButton(parent=self.frame_8) - self.network_activate_btn.setMinimumSize(QtCore.QSize(250, 80)) - self.network_activate_btn.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setPointSize(15) - self.network_activate_btn.setFont(font) - self.network_activate_btn.setFlat(True) - self.network_activate_btn.setText("Connect") - self.network_activate_btn.setObjectName("network_activate_btn") - buttons_layout.addWidget( - self.network_activate_btn, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - self.network_details_btn = BlocksCustomButton(parent=self.frame_8) - self.network_details_btn.setMinimumSize(QtCore.QSize(250, 80)) - self.network_details_btn.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setPointSize(15) - self.network_details_btn.setFont(font) - self.network_details_btn.setFlat(True) - self.network_details_btn.setText("Details") - self.network_details_btn.setObjectName("network_details_btn") - buttons_layout.addWidget( - self.network_details_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - - self.network_delete_btn = BlocksCustomButton(parent=self.frame_8) - self.network_delete_btn.setMinimumSize(QtCore.QSize(250, 80)) - self.network_delete_btn.setMaximumSize(QtCore.QSize(250, 80)) - font = QtGui.QFont() - font.setPointSize(15) - self.network_delete_btn.setFont(font) - self.network_delete_btn.setFlat(True) - self.network_delete_btn.setText("Forget") - self.network_delete_btn.setObjectName("network_delete_btn") - buttons_layout.addWidget( - self.network_delete_btn, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - - main_content_layout.addWidget(self.frame_8) - content_layout.addLayout(main_content_layout) - main_layout.addLayout(content_layout) - - self.addWidget(self.saved_connection_page) - - def _setup_saved_details_page(self) -> None: - """Setup the saved network details page.""" - self.saved_details_page = QtWidgets.QWidget() - self.saved_details_page.setObjectName("saved_details_page") - - main_layout = QtWidgets.QVBoxLayout(self.saved_details_page) - main_layout.setObjectName("verticalLayout_19") - - # Header layout - header_layout = QtWidgets.QHBoxLayout() - header_layout.setObjectName("horizontalLayout_14") - - header_layout.addItem( - QtWidgets.QSpacerItem( - 60, - 60, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) - - self.snd_name = QtWidgets.QLabel(parent=self.saved_details_page) - name_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.snd_name.setSizePolicy(name_policy) - self.snd_name.setMaximumSize(QtCore.QSize(16777215, 60)) - self.snd_name.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(20) - self.snd_name.setFont(font) - self.snd_name.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.snd_name.setText("SSID") - self.snd_name.setObjectName("snd_name") - header_layout.addWidget(self.snd_name) - - self.snd_back = IconButton(parent=self.saved_details_page) - self.snd_back.setMinimumSize(QtCore.QSize(60, 60)) - self.snd_back.setMaximumSize(QtCore.QSize(60, 60)) - self.snd_back.setText("Back") - self.snd_back.setFlat(True) - self.snd_back.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") - ) - self.snd_back.setProperty("class", "back_btn") - self.snd_back.setProperty("button_type", "icon") - self.snd_back.setObjectName("snd_back") - header_layout.addWidget(self.snd_back) - - main_layout.addLayout(header_layout) - - # Content layout - content_layout = QtWidgets.QVBoxLayout() - content_layout.setObjectName("verticalLayout_8") - - content_layout.addItem( - QtWidgets.QSpacerItem( - 20, - 20, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) - - # Password change frame - self.frame_9 = BlocksCustomFrame(parent=self.saved_details_page) - frame_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum - ) - self.frame_9.setSizePolicy(frame_policy) - self.frame_9.setMinimumSize(QtCore.QSize(0, 70)) - self.frame_9.setMaximumSize(QtCore.QSize(16777215, 70)) - self.frame_9.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_9.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_9.setObjectName("frame_9") - - frame_layout_widget = QtWidgets.QWidget(parent=self.frame_9) - frame_layout_widget.setGeometry(QtCore.QRect(0, 0, 776, 62)) - frame_layout_widget.setObjectName("layoutWidget_8") - - password_layout = QtWidgets.QHBoxLayout(frame_layout_widget) - password_layout.setContentsMargins(0, 0, 0, 0) - password_layout.setObjectName("horizontalLayout_10") - - self.saved_connection_change_password_label_3 = QtWidgets.QLabel( - parent=frame_layout_widget - ) - self.saved_connection_change_password_label_3.setPalette( - self._create_white_palette() - ) - font = QtGui.QFont() - font.setPointSize(15) - self.saved_connection_change_password_label_3.setFont(font) - self.saved_connection_change_password_label_3.setAlignment( - QtCore.Qt.AlignmentFlag.AlignCenter - ) - self.saved_connection_change_password_label_3.setText("Change\nPassword") - self.saved_connection_change_password_label_3.setObjectName( - "saved_connection_change_password_label_3" - ) - password_layout.addWidget( - self.saved_connection_change_password_label_3, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - self.saved_connection_change_password_field = BlocksCustomLinEdit( - parent=frame_layout_widget - ) - self.saved_connection_change_password_field.setHidden(True) - self.saved_connection_change_password_field.setMinimumSize( - QtCore.QSize(500, 60) + security_layout.addWidget(self.netlist_security_label_2) + + self.saved_connection_security_type_info_label = QtWidgets.QLabel( + parent=self.frame ) - self.saved_connection_change_password_field.setMaximumSize( - QtCore.QSize(500, 16777215) + self.saved_connection_security_type_info_label.setMinimumSize( + QtCore.QSize(250, 0) ) font = QtGui.QFont() - font.setPointSize(12) - self.saved_connection_change_password_field.setFont(font) - self.saved_connection_change_password_field.setObjectName( - "saved_connection_change_password_field" + font.setPointSize(11) + self.saved_connection_security_type_info_label.setFont(font) + self.saved_connection_security_type_info_label.setStyleSheet( + "color: rgb(255, 255, 255);" ) - password_layout.addWidget( - self.saved_connection_change_password_field, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter, + self.saved_connection_security_type_info_label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter ) + security_layout.addWidget(self.saved_connection_security_type_info_label) - self.saved_connection_change_password_view = IconButton( - parent=frame_layout_widget - ) - self.saved_connection_change_password_view.setMinimumSize(QtCore.QSize(60, 60)) - self.saved_connection_change_password_view.setMaximumSize(QtCore.QSize(60, 60)) - self.saved_connection_change_password_view.setText("View") - self.saved_connection_change_password_view.setFlat(True) - self.saved_connection_change_password_view.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") - ) - self.saved_connection_change_password_view.setProperty("class", "back_btn") - self.saved_connection_change_password_view.setProperty("button_type", "icon") - self.saved_connection_change_password_view.setObjectName( - "saved_connection_change_password_view" - ) - password_layout.addWidget( - self.saved_connection_change_password_view, - 0, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) + frame_inner_layout.addLayout(security_layout) - content_layout.addWidget(self.frame_9) + self.line_5 = QtWidgets.QFrame(parent=self.frame) + self.line_5.setFrameShape(QtWidgets.QFrame.Shape.HLine) + self.line_5.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - # Priority buttons layout - priority_outer_layout = QtWidgets.QHBoxLayout() - priority_outer_layout.setObjectName("horizontalLayout_13") + frame_inner_layout.addWidget(self.line_5) - priority_inner_layout = QtWidgets.QVBoxLayout() - priority_inner_layout.setObjectName("verticalLayout_13") + status_layout = QtWidgets.QHBoxLayout() - self.frame_12 = BlocksCustomFrame(parent=self.saved_details_page) - frame_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - self.frame_12.setSizePolicy(frame_policy) - self.frame_12.setMinimumSize(QtCore.QSize(400, 160)) - self.frame_12.setMaximumSize(QtCore.QSize(400, 99999)) - self.frame_12.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_12.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_12.setProperty("text", "Network priority") - self.frame_12.setObjectName("frame_12") + self.netlist_security_label_4 = QtWidgets.QLabel(parent=self.frame) + self.netlist_security_label_4.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + self.netlist_security_label_4.setFont(font) + self.netlist_security_label_4.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.netlist_security_label_4.setText("Status") - frame_inner_layout = QtWidgets.QVBoxLayout(self.frame_12) - frame_inner_layout.setObjectName("verticalLayout_17") + status_layout.addWidget(self.netlist_security_label_4) - frame_inner_layout.addItem( - QtWidgets.QSpacerItem( - 10, - 10, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) + self.sn_info = QtWidgets.QLabel(parent=self.frame) + self.sn_info.setMinimumSize(QtCore.QSize(250, 0)) + font = QtGui.QFont() + font.setPointSize(11) + self.sn_info.setFont(font) + self.sn_info.setStyleSheet("color: rgb(255, 255, 255);") + self.sn_info.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.sn_info.setText("TextLabel") - # Priority buttons - buttons_layout = QtWidgets.QHBoxLayout() - buttons_layout.setObjectName("horizontalLayout_4") + status_layout.addWidget(self.sn_info) - self.priority_btn_group = QtWidgets.QButtonGroup(self) - self.priority_btn_group.setObjectName("priority_btn_group") + frame_inner_layout.addLayout(status_layout) + info_layout.addWidget(self.frame) + main_content_layout.addLayout(info_layout) - self.low_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) - self.low_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) - self.low_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) - self.low_priority_btn.setCheckable(True) - self.low_priority_btn.setAutoExclusive(True) - self.low_priority_btn.setFlat(True) - self.low_priority_btn.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg") - ) - self.low_priority_btn.setText("Low") - self.low_priority_btn.setProperty("class", "back_btn") - self.low_priority_btn.setProperty("button_type", "icon") - self.low_priority_btn.setObjectName("low_priority_btn") - self.priority_btn_group.addButton(self.low_priority_btn) - buttons_layout.addWidget(self.low_priority_btn) + self.frame_8 = BlocksCustomFrame(parent=self.saved_connection_page) + self.frame_8.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_8.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.med_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) - self.med_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) - self.med_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) - self.med_priority_btn.setCheckable(True) - self.med_priority_btn.setChecked(False) # Don't set default checked - self.med_priority_btn.setAutoExclusive(True) - self.med_priority_btn.setFlat(True) - self.med_priority_btn.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg") + buttons_layout = QtWidgets.QVBoxLayout(self.frame_8) + + self.network_activate_btn = BlocksCustomButton(parent=self.frame_8) + self.network_activate_btn.setMinimumSize(QtCore.QSize(250, 80)) + self.network_activate_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.network_activate_btn.setFont(font) + self.network_activate_btn.setFlat(True) + self.network_activate_btn.setText("Connect") + + buttons_layout.addWidget( + self.network_activate_btn, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) - self.med_priority_btn.setText("Medium") - self.med_priority_btn.setProperty("class", "back_btn") - self.med_priority_btn.setProperty("button_type", "icon") - self.med_priority_btn.setObjectName("med_priority_btn") - self.priority_btn_group.addButton(self.med_priority_btn) - buttons_layout.addWidget(self.med_priority_btn) - self.high_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) - self.high_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) - self.high_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) - self.high_priority_btn.setCheckable(True) - self.high_priority_btn.setChecked(False) - self.high_priority_btn.setAutoExclusive(True) - self.high_priority_btn.setFlat(True) - self.high_priority_btn.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/indf_svg.svg") + self.network_details_btn = BlocksCustomButton(parent=self.frame_8) + self.network_details_btn.setMinimumSize(QtCore.QSize(250, 80)) + self.network_details_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.network_details_btn.setFont(font) + self.network_details_btn.setFlat(True) + self.network_details_btn.setText("Details") + + buttons_layout.addWidget( + self.network_details_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter ) - self.high_priority_btn.setText("High") - self.high_priority_btn.setProperty("class", "back_btn") - self.high_priority_btn.setProperty("button_type", "icon") - self.high_priority_btn.setObjectName("high_priority_btn") - self.priority_btn_group.addButton(self.high_priority_btn) - buttons_layout.addWidget(self.high_priority_btn) - frame_inner_layout.addLayout(buttons_layout) + self.network_delete_btn = BlocksCustomButton(parent=self.frame_8) + self.network_delete_btn.setMinimumSize(QtCore.QSize(250, 80)) + self.network_delete_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.network_delete_btn.setFont(font) + self.network_delete_btn.setFlat(True) + self.network_delete_btn.setText("Forget") - priority_inner_layout.addWidget( - self.frame_12, + buttons_layout.addWidget( + self.network_delete_btn, 0, QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) - priority_outer_layout.addLayout(priority_inner_layout) - content_layout.addLayout(priority_outer_layout) + main_content_layout.addWidget(self.frame_8) + content_layout.addLayout(main_content_layout) main_layout.addLayout(content_layout) - self.addWidget(self.saved_details_page) + self.addWidget(self.saved_connection_page) - def _setup_hotspot_page(self) -> None: - """Setup the hotspot configuration page.""" - self.hotspot_page = QtWidgets.QWidget() - self.hotspot_page.setObjectName("hotspot_page") + def _setup_saved_details_page(self) -> None: + """Setup the saved network details page.""" + self.saved_details_page = QtWidgets.QWidget() - main_layout = QtWidgets.QVBoxLayout(self.hotspot_page) - main_layout.setObjectName("verticalLayout_12") + main_layout = QtWidgets.QVBoxLayout(self.saved_details_page) - # Header layout header_layout = QtWidgets.QHBoxLayout() - header_layout.setObjectName("hospot_page_header_layout") header_layout.addItem( QtWidgets.QSpacerItem( - 40, - 20, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) - - self.hotspot_header_title = QtWidgets.QLabel(parent=self.hotspot_page) - self.hotspot_header_title.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setPointSize(20) - self.hotspot_header_title.setFont(font) - self.hotspot_header_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.hotspot_header_title.setText("Hotspot") - self.hotspot_header_title.setObjectName("hotspot_header_title") - header_layout.addWidget(self.hotspot_header_title) - - self.hotspot_back_button = IconButton(parent=self.hotspot_page) - self.hotspot_back_button.setMinimumSize(QtCore.QSize(60, 60)) - self.hotspot_back_button.setMaximumSize(QtCore.QSize(60, 60)) - self.hotspot_back_button.setText("Back") - self.hotspot_back_button.setFlat(True) - self.hotspot_back_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") - ) - self.hotspot_back_button.setProperty("class", "back_btn") - self.hotspot_back_button.setProperty("button_type", "icon") - self.hotspot_back_button.setObjectName("hotspot_back_button") - header_layout.addWidget(self.hotspot_back_button) - - main_layout.addLayout(header_layout) - - # Content layout - content_layout = QtWidgets.QVBoxLayout() - content_layout.setContentsMargins(-1, 5, -1, 5) - content_layout.setObjectName("hotspot_page_content_layout") - - content_layout.addItem( - QtWidgets.QSpacerItem( - 20, - 50, + 60, + 60, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum, ) ) - # Hotspot name frame - self.frame_6 = BlocksCustomFrame(parent=self.hotspot_page) - frame_policy = QtWidgets.QSizePolicy( + self.snd_name = QtWidgets.QLabel(parent=self.saved_details_page) + name_policy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding, ) - self.frame_6.setSizePolicy(frame_policy) - self.frame_6.setMinimumSize(QtCore.QSize(70, 80)) - self.frame_6.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_6.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_6.setObjectName("frame_6") - - frame_layout_widget = QtWidgets.QWidget(parent=self.frame_6) - frame_layout_widget.setGeometry(QtCore.QRect(0, 10, 776, 61)) - frame_layout_widget.setObjectName("layoutWidget_6") + self.snd_name.setSizePolicy(name_policy) + self.snd_name.setMaximumSize(QtCore.QSize(16777215, 60)) + self.snd_name.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(20) + self.snd_name.setFont(font) + self.snd_name.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.snd_name.setText("SSID") - name_layout = QtWidgets.QHBoxLayout(frame_layout_widget) - name_layout.setContentsMargins(0, 0, 0, 0) - name_layout.setObjectName("horizontalLayout_11") + header_layout.addWidget(self.snd_name) - self.hotspot_info_name_label = QtWidgets.QLabel(parent=frame_layout_widget) - label_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Maximum - ) - self.hotspot_info_name_label.setSizePolicy(label_policy) - self.hotspot_info_name_label.setMaximumSize(QtCore.QSize(150, 16777215)) - self.hotspot_info_name_label.setPalette(self._create_white_palette()) - font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(10) - self.hotspot_info_name_label.setFont(font) - self.hotspot_info_name_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) - self.hotspot_info_name_label.setText("Hotspot Name: ") - self.hotspot_info_name_label.setObjectName("hotspot_info_name_label") - name_layout.addWidget(self.hotspot_info_name_label) - - self.hotspot_name_input_field = BlocksCustomLinEdit(parent=frame_layout_widget) - field_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.MinimumExpanding, - ) - self.hotspot_name_input_field.setSizePolicy(field_policy) - self.hotspot_name_input_field.setMinimumSize(QtCore.QSize(500, 40)) - self.hotspot_name_input_field.setMaximumSize(QtCore.QSize(500, 60)) - font = QtGui.QFont() - font.setPointSize(12) - self.hotspot_name_input_field.setFont(font) - # Name should be visible, not masked - self.hotspot_name_input_field.setEchoMode(QtWidgets.QLineEdit.EchoMode.Normal) - self.hotspot_name_input_field.setObjectName("hotspot_name_input_field") - name_layout.addWidget( - self.hotspot_name_input_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + self.snd_back = IconButton(parent=self.saved_details_page) + self.snd_back.setMinimumSize(QtCore.QSize(60, 60)) + self.snd_back.setMaximumSize(QtCore.QSize(60, 60)) + self.snd_back.setText("Back") + self.snd_back.setFlat(True) + self.snd_back.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") ) + self.snd_back.setProperty("class", "back_btn") + self.snd_back.setProperty("button_type", "icon") - name_layout.addItem( - QtWidgets.QSpacerItem( - 60, - 20, - QtWidgets.QSizePolicy.Policy.Minimum, - QtWidgets.QSizePolicy.Policy.Minimum, - ) - ) + header_layout.addWidget(self.snd_back) - content_layout.addWidget(self.frame_6) + main_layout.addLayout(header_layout) + + content_layout = QtWidgets.QVBoxLayout() content_layout.addItem( QtWidgets.QSpacerItem( - 773, - 128, + 20, + 20, QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum, ) ) - # Hotspot password frame - self.frame_7 = BlocksCustomFrame(parent=self.hotspot_page) + self.frame_9 = BlocksCustomFrame(parent=self.saved_details_page) frame_policy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum + QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum ) - self.frame_7.setSizePolicy(frame_policy) - self.frame_7.setMinimumSize(QtCore.QSize(0, 80)) - self.frame_7.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) - self.frame_7.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - self.frame_7.setObjectName("frame_7") + self.frame_9.setSizePolicy(frame_policy) + self.frame_9.setMinimumSize(QtCore.QSize(0, 70)) + self.frame_9.setMaximumSize(QtCore.QSize(16777215, 70)) + self.frame_9.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_9.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) - password_layout_widget = QtWidgets.QWidget(parent=self.frame_7) - password_layout_widget.setGeometry(QtCore.QRect(0, 10, 776, 62)) - password_layout_widget.setObjectName("layoutWidget_7") + frame_layout_widget = QtWidgets.QWidget(parent=self.frame_9) + frame_layout_widget.setGeometry(QtCore.QRect(0, 0, 776, 62)) - password_layout = QtWidgets.QHBoxLayout(password_layout_widget) + password_layout = QtWidgets.QHBoxLayout(frame_layout_widget) password_layout.setContentsMargins(0, 0, 0, 0) - password_layout.setObjectName("horizontalLayout_12") - self.hotspot_info_password_label = QtWidgets.QLabel( - parent=password_layout_widget + self.saved_connection_change_password_label_3 = QtWidgets.QLabel( + parent=frame_layout_widget + ) + self.saved_connection_change_password_label_3.setPalette( + self._create_white_palette() ) - self.hotspot_info_password_label.setSizePolicy(label_policy) - self.hotspot_info_password_label.setMaximumSize(QtCore.QSize(150, 16777215)) - self.hotspot_info_password_label.setPalette(self._create_white_palette()) font = QtGui.QFont() - font.setFamily("Momcake") - font.setPointSize(10) - self.hotspot_info_password_label.setFont(font) - self.hotspot_info_password_label.setAlignment( + font.setPointSize(15) + self.saved_connection_change_password_label_3.setFont(font) + self.saved_connection_change_password_label_3.setAlignment( QtCore.Qt.AlignmentFlag.AlignCenter ) - self.hotspot_info_password_label.setText("Hotspot Password:") - self.hotspot_info_password_label.setObjectName("hotspot_info_password_label") - password_layout.addWidget(self.hotspot_info_password_label) - - self.hotspot_password_input_field = BlocksCustomLinEdit( - parent=password_layout_widget - ) - self.hotspot_password_input_field.setHidden(True) - self.hotspot_password_input_field.setSizePolicy(field_policy) - self.hotspot_password_input_field.setMinimumSize(QtCore.QSize(500, 40)) - self.hotspot_password_input_field.setMaximumSize(QtCore.QSize(500, 60)) - font = QtGui.QFont() - font.setPointSize(12) - self.hotspot_password_input_field.setFont(font) - self.hotspot_password_input_field.setEchoMode( - QtWidgets.QLineEdit.EchoMode.Password + self.saved_connection_change_password_label_3.setText("Change\nPassword") + self.saved_connection_change_password_label_3.setObjectName( + "saved_connection_change_password_label_3" ) - self.hotspot_password_input_field.setObjectName("hotspot_password_input_field") password_layout.addWidget( - self.hotspot_password_input_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - - self.hotspot_password_view_button = IconButton(parent=password_layout_widget) - self.hotspot_password_view_button.setMinimumSize(QtCore.QSize(60, 60)) - self.hotspot_password_view_button.setMaximumSize(QtCore.QSize(60, 60)) - self.hotspot_password_view_button.setText("View") - self.hotspot_password_view_button.setFlat(True) - self.hotspot_password_view_button.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") - ) - self.hotspot_password_view_button.setProperty("class", "back_btn") - self.hotspot_password_view_button.setProperty("button_type", "icon") - self.hotspot_password_view_button.setObjectName("hotspot_password_view_button") - password_layout.addWidget(self.hotspot_password_view_button) - - content_layout.addWidget(self.frame_7) - - # Save button - self.hotspot_change_confirm = BlocksCustomButton(parent=self.hotspot_page) - self.hotspot_change_confirm.setMinimumSize(QtCore.QSize(200, 80)) - self.hotspot_change_confirm.setMaximumSize(QtCore.QSize(250, 100)) - font = QtGui.QFont() - font.setPointSize(18) - font.setBold(True) - font.setWeight(75) - self.hotspot_change_confirm.setFont(font) - self.hotspot_change_confirm.setProperty( - "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/save.svg") - ) - self.hotspot_change_confirm.setText("Save") - self.hotspot_change_confirm.setObjectName("hotspot_change_confirm") - content_layout.addWidget( - self.hotspot_change_confirm, + self.saved_connection_change_password_label_3, 0, QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) - main_layout.addLayout(content_layout) - - self.addWidget(self.hotspot_page) - - def _init_timers(self) -> None: - """Initialize all timers.""" - self._status_check_timer = QtCore.QTimer(self) - self._status_check_timer.setInterval(STATUS_CHECK_INTERVAL_MS) - - self._delayed_action_timer = QtCore.QTimer(self) - self._delayed_action_timer.setSingleShot(True) - - self._load_timer = QtCore.QTimer(self) - self._load_timer.setSingleShot(True) - self._load_timer.timeout.connect(self._handle_load_timeout) - - def _init_model_view(self) -> None: - """Initialize the model and view for network list.""" - self._model = EntryListModel() - self._model.setParent(self.listView) - self._entry_delegate = EntryDelegate() - self.listView.setModel(self._model) - self.listView.setItemDelegate(self._entry_delegate) - self._entry_delegate.item_selected.connect(self._on_ssid_item_clicked) - self._configure_list_view_palette() - - def _init_network_worker(self) -> None: - """Initialize the network list worker.""" - self._network_list_worker = BuildNetworkList( - nm=self._sdbus_network, poll_interval_ms=DEFAULT_POLL_INTERVAL_MS - ) - self._network_list_worker.finished_network_list_build.connect( - self._handle_network_list - ) - self._network_list_worker.start_polling() - self.rescan_button.clicked.connect(self._network_list_worker.build) - - def _setup_navigation_signals(self) -> None: - """Setup navigation button signals.""" - self.wifi_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) - ) - self.hotspot_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.hotspot_page)) - ) - self.nl_back_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) - ) - self.network_backButton.clicked.connect(self.hide) - - self.add_network_page_backButton.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) - ) - - self.saved_connection_back_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) - ) - self.snd_back.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.saved_connection_page)) - ) - self.network_details_btn.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.saved_details_page)) + self.saved_connection_change_password_field = BlocksCustomLinEdit( + parent=frame_layout_widget ) - - self.hotspot_back_button.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) + self.saved_connection_change_password_field.setHidden(True) + self.saved_connection_change_password_field.setMinimumSize( + QtCore.QSize(500, 60) ) - self.hotspot_change_confirm.clicked.connect( - partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) + self.saved_connection_change_password_field.setMaximumSize( + QtCore.QSize(500, 16777215) ) - - def _setup_action_signals(self) -> None: - """Setup action button signals.""" - self._sdbus_network.nm_state_change.connect(self._evaluate_network_state) - self.request_network_scan.connect(self._rescan_networks) - self.delete_network_signal.connect(self._delete_network) - - self.add_network_validation_button.clicked.connect(self._add_network) - - self.snd_back.clicked.connect(self._on_save_network_settings) - self.network_activate_btn.clicked.connect(self._on_saved_wifi_option_selected) - self.network_delete_btn.clicked.connect(self._on_saved_wifi_option_selected) - - self._status_check_timer.timeout.connect(self._check_connection_status) - - def _setup_toggle_signals(self) -> None: - """Setup toggle button signals.""" - self.wifi_button.toggle_button.stateChange.connect(self._on_toggle_state) - self.hotspot_button.toggle_button.stateChange.connect(self._on_toggle_state) - - def _setup_password_visibility_signals(self) -> None: - """Setup password visibility toggle signals.""" - self._setup_password_visibility_toggle( - self.add_network_password_view, - self.add_network_password_field, + font = QtGui.QFont() + font.setPointSize(12) + self.saved_connection_change_password_field.setFont(font) + self.saved_connection_change_password_field.setObjectName( + "saved_connection_change_password_field" ) - self._setup_password_visibility_toggle( - self.saved_connection_change_password_view, + password_layout.addWidget( self.saved_connection_change_password_field, - ) - self._setup_password_visibility_toggle( - self.hotspot_password_view_button, - self.hotspot_password_input_field, - ) - - def _setup_password_visibility_toggle( - self, view_button: QtWidgets.QWidget, password_field: QtWidgets.QLineEdit - ) -> None: - """Setup password visibility toggle for a button/field pair.""" - view_button.setCheckable(True) - - see_icon = QtGui.QPixmap(":/ui/media/btn_icons/see.svg") - unsee_icon = QtGui.QPixmap(":/ui/media/btn_icons/unsee.svg") - - # Connect toggle signal - view_button.toggled.connect( - lambda checked: password_field.setHidden(not checked) - ) - - # Update icon based on toggle state - view_button.toggled.connect( - lambda checked: view_button.setPixmap( - unsee_icon if not checked else see_icon - ) + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter, ) - def _setup_icons(self) -> None: - """Setup button icons.""" - self.hotspot_button.setPixmap( - QtGui.QPixmap(":/network/media/btn_icons/hotspot.svg") - ) - self.wifi_button.setPixmap( - QtGui.QPixmap(":/network/media/btn_icons/wifi_config.svg") + self.saved_connection_change_password_view = IconButton( + parent=frame_layout_widget ) - self.network_delete_btn.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/garbage-icon.svg") + self.saved_connection_change_password_view.setMinimumSize(QtCore.QSize(60, 60)) + self.saved_connection_change_password_view.setMaximumSize(QtCore.QSize(60, 60)) + self.saved_connection_change_password_view.setText("View") + self.saved_connection_change_password_view.setFlat(True) + self.saved_connection_change_password_view.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/unsee.svg") ) - self.network_activate_btn.setPixmap( - QtGui.QPixmap(":/dialog/media/btn_icons/yes.svg") + self.saved_connection_change_password_view.setProperty("class", "back_btn") + self.saved_connection_change_password_view.setProperty("button_type", "icon") + self.saved_connection_change_password_view.setObjectName( + "saved_connection_change_password_view" ) - self.network_details_btn.setPixmap( - QtGui.QPixmap(":/ui/media/btn_icons/printer_settings.svg") + password_layout.addWidget( + self.saved_connection_change_password_view, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, ) - def _setup_input_fields(self) -> None: - """Setup input field properties.""" - self.add_network_password_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) - self.hotspot_name_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) - self.hotspot_password_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) - - self.hotspot_password_input_field.setPlaceholderText("Defaults to: 123456789") - self.hotspot_name_input_field.setText( - str(self._sdbus_network.get_hotspot_ssid() or "PrinterHotspot") - ) - self.hotspot_password_input_field.setText( - str(self._sdbus_network.hotspot_password or "123456789") - ) + content_layout.addWidget(self.frame_9) - def _setup_keyboard(self) -> None: - """Setup the on-screen keyboard.""" - self._qwerty = CustomQwertyKeyboard(self) - self.addWidget(self._qwerty) - self._qwerty.value_selected.connect(self._on_qwerty_value_selected) - self._qwerty.request_back.connect(self._on_qwerty_go_back) + priority_outer_layout = QtWidgets.QHBoxLayout() - self.add_network_password_field.clicked.connect( - lambda: self._on_show_keyboard( - self.add_network_page, self.add_network_password_field - ) - ) - self.hotspot_password_input_field.clicked.connect( - lambda: self._on_show_keyboard( - self.hotspot_page, self.hotspot_password_input_field - ) - ) - self.hotspot_name_input_field.clicked.connect( - lambda: self._on_show_keyboard( - self.hotspot_page, self.hotspot_name_input_field - ) - ) - self.saved_connection_change_password_field.clicked.connect( - lambda: self._on_show_keyboard( - self.saved_connection_page, - self.saved_connection_change_password_field, - ) - ) + priority_inner_layout = QtWidgets.QVBoxLayout() - def _setup_scrollbar_signals(self) -> None: - """Setup scrollbar synchronization signals.""" - self.listView.verticalScrollBar().valueChanged.connect( - self._handle_scrollbar_change - ) - self.verticalScrollBar.valueChanged.connect(self._handle_scrollbar_change) - self.verticalScrollBar.valueChanged.connect( - lambda value: self.listView.verticalScrollBar().setValue(value) + self.frame_12 = BlocksCustomFrame(parent=self.saved_details_page) + frame_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Expanding, ) - self.verticalScrollBar.show() - - def _configure_list_view_palette(self) -> None: - """Configure the list view palette for transparency.""" - palette = QtGui.QPalette() - - for group in [ - QtGui.QPalette.ColorGroup.Active, - QtGui.QPalette.ColorGroup.Inactive, - QtGui.QPalette.ColorGroup.Disabled, - ]: - transparent = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) - transparent.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(group, QtGui.QPalette.ColorRole.Button, transparent) - palette.setBrush(group, QtGui.QPalette.ColorRole.Window, transparent) + self.frame_12.setSizePolicy(frame_policy) + self.frame_12.setMinimumSize(QtCore.QSize(400, 160)) + self.frame_12.setMaximumSize(QtCore.QSize(400, 99999)) + self.frame_12.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel) + self.frame_12.setFrameShadow(QtWidgets.QFrame.Shadow.Raised) + self.frame_12.setProperty("text", "Network priority") - no_brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) - no_brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) - palette.setBrush(group, QtGui.QPalette.ColorRole.Base, no_brush) + frame_inner_layout = QtWidgets.QVBoxLayout(self.frame_12) - highlight = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) - highlight.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(group, QtGui.QPalette.ColorRole.Highlight, highlight) + frame_inner_layout.addItem( + QtWidgets.QSpacerItem( + 10, + 10, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) - link = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) - link.setStyle(QtCore.Qt.BrushStyle.SolidPattern) - palette.setBrush(group, QtGui.QPalette.ColorRole.Link, link) + buttons_layout = QtWidgets.QHBoxLayout() - self.listView.setPalette(palette) + self.priority_btn_group = QtWidgets.QButtonGroup(self) - def _show_error_popup(self, message: str, timeout: int = 6000) -> None: - """Show an error popup message.""" - self._popup.raise_() - self._popup.new_message( - message_type=Popup.MessageType.ERROR, - message=message, - timeout=timeout, - userInput=False, + self.low_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) + self.low_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) + self.low_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) + self.low_priority_btn.setCheckable(True) + self.low_priority_btn.setFlat(True) + self.low_priority_btn.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/indf_svg.svg") ) + self.low_priority_btn.setText("Low") + self.low_priority_btn.setProperty("class", "back_btn") + self.low_priority_btn.setProperty("button_type", "icon") - def _show_info_popup(self, message: str, timeout: int = 4000) -> None: - """Show an info popup message.""" - self._popup.raise_() - self._popup.new_message( - message_type=Popup.MessageType.INFO, - message=message, - timeout=timeout, - userInput=False, - ) + self.priority_btn_group.addButton(self.low_priority_btn) + buttons_layout.addWidget(self.low_priority_btn) - def _show_warning_popup(self, message: str, timeout: int = 5000) -> None: - """Show a warning popup message.""" - self._popup.raise_() - self._popup.new_message( - message_type=Popup.MessageType.WARNING, - message=message, - timeout=timeout, - userInput=False, + self.med_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) + self.med_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) + self.med_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) + self.med_priority_btn.setCheckable(True) + self.med_priority_btn.setChecked(False) # Don't set default checked + self.med_priority_btn.setFlat(True) + self.med_priority_btn.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/indf_svg.svg") ) + self.med_priority_btn.setText("Medium") + self.med_priority_btn.setProperty("class", "back_btn") + self.med_priority_btn.setProperty("button_type", "icon") - def closeEvent(self, event: Optional[QtGui.QCloseEvent]) -> None: - """Handle close event.""" - self._stop_all_timers() - self._network_list_worker.stop_polling() - super().closeEvent(event) + self.priority_btn_group.addButton(self.med_priority_btn) + buttons_layout.addWidget(self.med_priority_btn) - def showEvent(self, event: Optional[QtGui.QShowEvent]) -> None: - """Handle show event.""" - if self._networks: - self._build_model_list() - self._evaluate_network_state() - super().showEvent(event) + self.high_priority_btn = BlocksCustomCheckButton(parent=self.frame_12) + self.high_priority_btn.setMinimumSize(QtCore.QSize(100, 100)) + self.high_priority_btn.setMaximumSize(QtCore.QSize(100, 100)) + self.high_priority_btn.setCheckable(True) + self.high_priority_btn.setChecked(False) + self.high_priority_btn.setFlat(True) + self.high_priority_btn.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/indf_svg.svg") + ) + self.high_priority_btn.setText("High") + self.high_priority_btn.setProperty("class", "back_btn") + self.high_priority_btn.setProperty("button_type", "icon") - def _stop_all_timers(self) -> None: - """Stop all active timers.""" - timers = [ - self._load_timer, - self._status_check_timer, - self._delayed_action_timer, - ] - for timer in timers: - if timer.isActive(): - timer.stop() + self.priority_btn_group.addButton(self.high_priority_btn) + buttons_layout.addWidget(self.high_priority_btn) - def _on_show_keyboard( - self, panel: QtWidgets.QWidget, field: QtWidgets.QLineEdit - ) -> None: - """Show the on-screen keyboard for a field.""" - self._previous_panel = panel - self._current_field = field - self._qwerty.set_value(field.text()) - self.setCurrentIndex(self.indexOf(self._qwerty)) + frame_inner_layout.addLayout(buttons_layout) - def _on_qwerty_go_back(self) -> None: - """Handle keyboard back button.""" - if self._previous_panel: - self.setCurrentIndex(self.indexOf(self._previous_panel)) + priority_inner_layout.addWidget( + self.frame_12, + 0, + QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) - def _on_qwerty_value_selected(self, value: str) -> None: - """Handle keyboard value selection.""" - if self._previous_panel: - self.setCurrentIndex(self.indexOf(self._previous_panel)) - if self._current_field: - self._current_field.setText(value) + priority_outer_layout.addLayout(priority_inner_layout) + content_layout.addLayout(priority_outer_layout) - def _set_loading_state(self, loading: bool) -> None: - """Set loading state - controls loading widget visibility. + bottom_btn_layout = QtWidgets.QHBoxLayout() + bottom_btn_layout.setSpacing(20) - This method ensures mutual exclusivity between - loading widget, network details, and info box. - """ - self.wifi_button.setEnabled(not loading) - self.hotspot_button.setEnabled(not loading) + self.saved_details_save_btn = BlocksCustomButton(parent=self.saved_details_page) + self.saved_details_save_btn.setMinimumSize(QtCore.QSize(200, 80)) + self.saved_details_save_btn.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(16) + self.saved_details_save_btn.setFont(font) + self.saved_details_save_btn.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/save.svg") + ) + self.saved_details_save_btn.setText("Save") + bottom_btn_layout.addWidget( + self.saved_details_save_btn, + 0, + QtCore.Qt.AlignmentFlag.AlignRight | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) - if loading: - self._is_connecting = True - # - # Hide ALL other elements first before showing loading - # This prevents the dual panel visibility bug - self._hide_all_info_elements() - # Force UI update to ensure elements are hidden - self.repaint() - # Now show loading - self.loadingwidget.setVisible(True) + self.wifi_static_ip_btn = BlocksCustomButton(parent=self.saved_details_page) + self.wifi_static_ip_btn.setMinimumSize(QtCore.QSize(200, 80)) + self.wifi_static_ip_btn.setMaximumSize(QtCore.QSize(250, 80)) + self.wifi_static_ip_btn.setFont(font) + self.wifi_static_ip_btn.setFlat(True) + self.wifi_static_ip_btn.setText("Static\nIP") + self.wifi_static_ip_btn.setProperty( + "icon_pixmap", + PixmapCache.get(":/network/media/btn_icons/network/static_ip.svg"), + ) + bottom_btn_layout.addWidget( + self.wifi_static_ip_btn, + 0, + QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.AlignmentFlag.AlignVCenter, + ) - if self._load_timer.isActive(): - self._load_timer.stop() - self._load_timer.start(LOAD_TIMEOUT_MS) - if not self._status_check_timer.isActive(): - self._status_check_timer.start() - else: - self._is_connecting = False - self._target_ssid = None - # Just hide loading - caller decides what to show next - self.loadingwidget.setVisible(False) + content_layout.addLayout(bottom_btn_layout) - if self._load_timer.isActive(): - self._load_timer.stop() - if self._status_check_timer.isActive(): - self._status_check_timer.stop() + main_layout.addLayout(content_layout) - def _show_network_details(self) -> None: - """Show network details panel - HIDES everything else first.""" - # Hide everything else first to prevent dual panel - self.loadingwidget.setVisible(False) - self.mn_info_box.setVisible(False) - # Force UI update - self.repaint() + self.addWidget(self.saved_details_page) - # Then show only the details - self.netlist_ip.setVisible(True) - self.netlist_ssuid.setVisible(True) - self.mn_info_seperator.setVisible(True) - self.line_2.setVisible(True) - self.netlist_strength.setVisible(True) - self.netlist_strength_label.setVisible(True) - self.line_3.setVisible(True) - self.netlist_security.setVisible(True) - self.netlist_security_label.setVisible(True) + def _setup_hotspot_page(self) -> None: + """Setup the hotspot configuration page.""" + self.hotspot_page = QtWidgets.QWidget() - def _show_disconnected_message(self) -> None: - """Show the disconnected state message - HIDES everything else first.""" - # Hide everything else first to prevent dual panel - self.loadingwidget.setVisible(False) - self._hide_network_detail_labels() - # Force UI update - self.repaint() + main_layout = QtWidgets.QVBoxLayout(self.hotspot_page) - # Then show info box - self._configure_info_box_centered() - self.mn_info_box.setVisible(True) - self.mn_info_box.setText( - "Network connection required.\n\nConnect to Wi-Fi\nor\nTurn on Hotspot" + header_layout = QtWidgets.QHBoxLayout() + + header_layout.addItem( + QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) ) + title_font = QtGui.QFont() + title_font.setPointSize(20) - def _hide_network_detail_labels(self) -> None: - """Hide only the network detail labels (not loading or info box).""" - self.netlist_ip.setVisible(False) - self.netlist_ssuid.setVisible(False) - self.mn_info_seperator.setVisible(False) - self.line_2.setVisible(False) - self.netlist_strength.setVisible(False) - self.netlist_strength_label.setVisible(False) - self.line_3.setVisible(False) - self.netlist_security.setVisible(False) - self.netlist_security_label.setVisible(False) + self.hotspot_header_title = QtWidgets.QLabel(parent=self.hotspot_page) + self.hotspot_header_title.setPalette(self._create_white_palette()) + self.hotspot_header_title.setFont(title_font) + self.hotspot_header_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.hotspot_header_title.setText("Hotspot") - def _check_connection_status(self) -> None: - """Backup periodic check to detect successful connections.""" - if not self.loadingwidget.isVisible(): - if self._status_check_timer.isActive(): - self._status_check_timer.stop() - return + header_layout.addWidget(self.hotspot_header_title) - connectivity = self._sdbus_network.check_connectivity() - is_connected = connectivity in ("FULL", "LIMITED") + self.hotspot_back_button = IconButton(parent=self.hotspot_page) + self.hotspot_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.hotspot_back_button.setMaximumSize(QtCore.QSize(60, 60)) + self.hotspot_back_button.setFlat(True) + self.hotspot_back_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") + ) + self.hotspot_back_button.setProperty("class", "back_btn") + self.hotspot_back_button.setProperty("button_type", "icon") - wifi_btn = self.wifi_button.toggle_button - hotspot_btn = self.hotspot_button.toggle_button + header_layout.addWidget(self.hotspot_back_button) - if hotspot_btn.state == hotspot_btn.State.ON: - hotspot_ip = self._sdbus_network.get_device_ip_by_interface("wlan0") - if hotspot_ip: - logger.debug("Hotspot connection detected via status check") - # Stop loading first, then show details - self._set_loading_state(False) - self._update_hotspot_display() - self._show_network_details() - return + main_layout.addLayout(header_layout) - if wifi_btn.state == wifi_btn.State.ON: - current_ssid = self._sdbus_network.get_current_ssid() + self.hotspot_header_title.setMaximumSize(QtCore.QSize(16777215, 60)) - if self._target_ssid: - if current_ssid == self._target_ssid and is_connected: - logger.debug("Target Wi-Fi connection detected: %s", current_ssid) - # Stop loading first, then show details - self._set_loading_state(False) - self._update_wifi_display() - self._show_network_details() - return - else: - if current_ssid and is_connected: - logger.debug("Wi-Fi connection detected: %s", current_ssid) - # Stop loading first, then show details - self._set_loading_state(False) - self._update_wifi_display() - self._show_network_details() - return + content_layout = QtWidgets.QHBoxLayout() + content_layout.setContentsMargins(-1, 5, -1, 5) - def _handle_load_timeout(self) -> None: - """Handle connection timeout.""" - if not self.loadingwidget.isVisible(): - return + # Left side: QR code frame + self.frame_4 = QtWidgets.QFrame(parent=self.hotspot_page) + frame_4_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Expanding, + ) + self.frame_4.setSizePolicy(frame_4_policy) + qr_frame_font = QtGui.QFont() + qr_frame_font.setPointSize(15) + self.frame_4.setFont(qr_frame_font) + self.frame_4.setStyleSheet("color: white;") - connectivity = self._sdbus_network.check_connectivity() - is_connected = connectivity in ("FULL", "LIMITED") + frame_4_layout = QtWidgets.QHBoxLayout(self.frame_4) - wifi_btn = self.wifi_button - hotspot_btn = self.hotspot_button + self.qrcode_img = BlocksLabel(parent=self.frame_4) + self.qrcode_img.setMinimumSize(QtCore.QSize(325, 325)) + self.qrcode_img.setMaximumSize(QtCore.QSize(325, 325)) + qrcode_font = QtGui.QFont() + qrcode_font.setPointSize(15) + self.qrcode_img.setFont(qrcode_font) + self.qrcode_img.setText("Hotspot not active") - # Final check if connection succeeded - if wifi_btn.toggle_button.state == wifi_btn.toggle_button.State.ON: - current_ssid = self._sdbus_network.get_current_ssid() + frame_4_layout.addWidget(self.qrcode_img) - if self._target_ssid: - if current_ssid == self._target_ssid and is_connected: - logger.debug("Target connection succeeded on timeout check") - self._set_loading_state(False) - self._update_wifi_display() - self._show_network_details() - return - else: - if current_ssid and is_connected: - logger.debug("Connection succeeded on timeout check") - self._set_loading_state(False) - self._update_wifi_display() - self._show_network_details() - return + content_layout.addWidget(self.frame_4) - elif hotspot_btn.toggle_button.state == hotspot_btn.toggle_button.State.ON: - hotspot_ip = self._sdbus_network.get_device_ip_by_interface("wlan0") - if hotspot_ip: - logger.debug("Hotspot succeeded on timeout check") - self._set_loading_state(False) - self._update_hotspot_display() - self._show_network_details() - return + # Right side: form fields frame + self.frame_3 = QtWidgets.QFrame(parent=self.hotspot_page) + self.frame_3.setMaximumWidth(350) - # Connection actually failed - self._is_connecting = False - self._target_ssid = None - self._set_loading_state(False) + frame_3_layout = QtWidgets.QVBoxLayout(self.frame_3) - # Show error message - self._hide_all_info_elements() - self._configure_info_box_centered() - self.mn_info_box.setVisible(True) - self.mn_info_box.setText(self._get_timeout_message(wifi_btn, hotspot_btn)) + label_font = QtGui.QFont() + label_font.setPointSize(15) + label_font.setFamily("Momcake") + field_font = QtGui.QFont() + field_font.setPointSize(12) - hotspot_btn.setEnabled(True) - wifi_btn.setEnabled(True) + self.hotspot_info_name_label = QtWidgets.QLabel(parent=self.frame_3) + name_label_policy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Maximum, + ) + self.hotspot_info_name_label.setSizePolicy(name_label_policy) + self.hotspot_info_name_label.setMinimumSize(QtCore.QSize(173, 0)) + self.hotspot_info_name_label.setPalette(self._create_white_palette()) + self.hotspot_info_name_label.setFont(label_font) + self.hotspot_info_name_label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter | QtCore.Qt.AlignmentFlag.AlignBottom + ) + self.hotspot_info_name_label.setText("Hotspot Name") - self._show_error_popup("Connection timed out. Please try again.") + frame_3_layout.addWidget(self.hotspot_info_name_label) - def _get_timeout_message(self, wifi_btn, hotspot_btn) -> str: - """Get appropriate timeout message based on state.""" - if wifi_btn.toggle_button.state == wifi_btn.toggle_button.State.ON: - return "Wi-Fi Connection Failed.\nThe connection attempt\n timed out." - elif hotspot_btn.toggle_button.state == hotspot_btn.toggle_button.State.ON: - return "Hotspot Setup Failed.\nPlease restart the hotspot." - else: - return "Loading timed out.\nPlease check your connection\n and try again." + self.hotspot_name_input_field = BlocksCustomLinEdit(parent=self.frame_3) + self.hotspot_name_input_field.setMinimumSize(QtCore.QSize(300, 40)) + self.hotspot_name_input_field.setMaximumSize(QtCore.QSize(300, 60)) + self.hotspot_name_input_field.setFont(field_font) + self.hotspot_name_input_field.setEchoMode(QtWidgets.QLineEdit.EchoMode.Normal) - def _configure_info_box_centered(self) -> None: - """Configure info box for centered text.""" - self.mn_info_box.setWordWrap(True) - self.mn_info_box.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + frame_3_layout.addWidget( + self.hotspot_name_input_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) - def _clear_network_display(self) -> None: - """Clear all network display labels.""" - self.netlist_ssuid.setText("") - self.netlist_ip.setText("") - self.netlist_strength.setText("") - self.netlist_security.setText("") - self._last_displayed_ssid = None + self.hotspot_info_password_label = QtWidgets.QLabel(parent=self.frame_3) + self.hotspot_info_password_label.setSizePolicy(name_label_policy) + self.hotspot_info_password_label.setMinimumSize(QtCore.QSize(173, 0)) + self.hotspot_info_password_label.setPalette(self._create_white_palette()) + self.hotspot_info_password_label.setFont(label_font) + self.hotspot_info_password_label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignCenter | QtCore.Qt.AlignmentFlag.AlignBottom + ) + self.hotspot_info_password_label.setText("Hotspot Password") - @QtCore.pyqtSlot(object, name="stateChange") - def _on_toggle_state(self, new_state) -> None: - """Handle toggle button state change.""" - sender_button = self.sender() - wifi_btn = self.wifi_button.toggle_button - hotspot_btn = self.hotspot_button.toggle_button - is_sender_now_on = new_state == sender_button.State.ON + frame_3_layout.addWidget(self.hotspot_info_password_label) - # Show loading IMMEDIATELY when turning something on - if is_sender_now_on: - self._set_loading_state(True) - self.repaint() + self.hotspot_password_input_field = BlocksCustomLinEdit(parent=self.frame_3) + self.hotspot_password_input_field.setMinimumSize(QtCore.QSize(300, 40)) + self.hotspot_password_input_field.setMaximumSize(QtCore.QSize(300, 60)) + self.hotspot_password_input_field.setFont(field_font) + self.hotspot_password_input_field.setEchoMode( + QtWidgets.QLineEdit.EchoMode.Password + ) - saved_networks = self._sdbus_network.get_saved_networks_with_for() + frame_3_layout.addWidget( + self.hotspot_password_input_field, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) - if sender_button is wifi_btn: - self._handle_wifi_toggle(is_sender_now_on, hotspot_btn, saved_networks) - elif sender_button is hotspot_btn: - self._handle_hotspot_toggle(is_sender_now_on, wifi_btn, saved_networks) + frame_3_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 40, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) - # Handle both OFF - if ( - hotspot_btn.state == hotspot_btn.State.OFF - and wifi_btn.state == wifi_btn.State.OFF - ): - self._set_loading_state(False) - self._show_disconnected_message() + self.hotspot_change_confirm = BlocksCustomButton(parent=self.frame_3) + self.hotspot_change_confirm.setMinimumSize(QtCore.QSize(250, 80)) + self.hotspot_change_confirm.setMaximumSize(QtCore.QSize(250, 80)) + confirm_font = QtGui.QFont() + confirm_font.setPointSize(18) + confirm_font.setBold(True) + confirm_font.setWeight(75) + self.hotspot_change_confirm.setFont(confirm_font) + self.hotspot_change_confirm.setProperty( + "icon_pixmap", PixmapCache.get(":/dialog/media/btn_icons/yes.svg") + ) + self.hotspot_change_confirm.setText("Activate") - def _handle_wifi_toggle( - self, is_on: bool, hotspot_btn, saved_networks: List[Dict] - ) -> None: - """Handle Wi-Fi toggle state change.""" - if not is_on: - self._target_ssid = None - return + frame_3_layout.addWidget( + self.hotspot_change_confirm, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) + + content_layout.addWidget(self.frame_3) - hotspot_btn.state = hotspot_btn.State.OFF - self._sdbus_network.toggle_hotspot(False) + main_layout.addLayout(content_layout) - # Check if already connected - current_ssid = self._sdbus_network.get_current_ssid() - connectivity = self._sdbus_network.check_connectivity() + self.addWidget(self.hotspot_page) - if current_ssid and connectivity == "FULL": - # Already connected - show immediately - self._target_ssid = current_ssid - self._set_loading_state(False) - self._update_wifi_display() - self._show_network_details() - return + def _setup_hidden_network_page(self) -> None: + """Setup the hidden network page for connecting to networks with hidden SSID.""" + self.hidden_network_page = QtWidgets.QWidget() - # Filter wifi networks (not hotspots) - wifi_networks = [ - n for n in saved_networks if "ap" not in str(n.get("mode", "")) - ] + main_layout = QtWidgets.QVBoxLayout(self.hidden_network_page) - if not wifi_networks: - self._set_loading_state(False) - self._show_warning_popup( - "No saved Wi-Fi networks. Please add a network first." + header_layout = QtWidgets.QHBoxLayout() + header_layout.addItem( + QtWidgets.QSpacerItem( + 40, + 60, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) - self._show_disconnected_message() - return + ) - try: - ssid = wifi_networks[0]["ssid"] - self._target_ssid = ssid - self._sdbus_network.connect_network(str(ssid)) - except Exception as e: - logger.error("Error when turning ON wifi: %s", e) - self._set_loading_state(False) - self._show_error_popup("Failed to connect to Wi-Fi") - - def _handle_hotspot_toggle( - self, is_on: bool, wifi_btn, saved_networks: List[Dict] - ) -> None: - """Handle hotspot toggle state change.""" - if not is_on: - self._target_ssid = None - return + self.hidden_network_title = QtWidgets.QLabel(parent=self.hidden_network_page) + self.hidden_network_title.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(20) + self.hidden_network_title.setFont(font) + self.hidden_network_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.hidden_network_title.setText("Hidden Network") + header_layout.addWidget(self.hidden_network_title) - wifi_btn.state = wifi_btn.State.OFF - self._target_ssid = None + self.hidden_network_back_button = IconButton(parent=self.hidden_network_page) + self.hidden_network_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.hidden_network_back_button.setMaximumSize(QtCore.QSize(60, 60)) + self.hidden_network_back_button.setFlat(True) + self.hidden_network_back_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") + ) + self.hidden_network_back_button.setProperty("button_type", "icon") + header_layout.addWidget(self.hidden_network_back_button) - new_hotspot_name = self.hotspot_name_input_field.text() or "PrinterHotspot" - new_hotspot_password = self.hotspot_password_input_field.text() or "123456789" + main_layout.addLayout(header_layout) - # Use QTimer to defer async operations - def setup_hotspot(): - try: - self._sdbus_network.create_hotspot( - new_hotspot_name, new_hotspot_password - ) - self._sdbus_network.toggle_hotspot(True) - except Exception as e: - logger.error("Error creating/activating hotspot: %s", e) - self._show_error_popup("Failed to start hotspot") - self._set_loading_state(False) + content_layout = QtWidgets.QVBoxLayout() + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 30, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) - QtCore.QTimer.singleShot(100, setup_hotspot) + ssid_frame = BlocksCustomFrame(parent=self.hidden_network_page) + ssid_frame.setMinimumSize(QtCore.QSize(0, 80)) + ssid_frame.setMaximumSize(QtCore.QSize(16777215, 90)) + ssid_frame_layout = QtWidgets.QHBoxLayout(ssid_frame) - @QtCore.pyqtSlot(str, name="nm-state-changed") - def _evaluate_network_state(self, nm_state: str = "") -> None: - """Evaluate and update network state.""" - wifi_btn = self.wifi_button.toggle_button - hotspot_btn = self.hotspot_button.toggle_button + ssid_label = QtWidgets.QLabel("Network\nName", parent=ssid_frame) + ssid_label.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + ssid_label.setFont(font) + ssid_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + ssid_frame_layout.addWidget(ssid_label) - state = nm_state or self._sdbus_network.check_nm_state() - if not state: - return + self.hidden_network_ssid_field = BlocksCustomLinEdit(parent=ssid_frame) + self.hidden_network_ssid_field.setMinimumSize(QtCore.QSize(500, 60)) + font = QtGui.QFont() + font.setPointSize(12) + self.hidden_network_ssid_field.setFont(font) + self.hidden_network_ssid_field.setPlaceholderText("Enter network name") + ssid_frame_layout.addWidget(self.hidden_network_ssid_field) - if self._is_first_run: - self._handle_first_run_state() - self._is_first_run = False - return + content_layout.addWidget(ssid_frame) - if not self._sdbus_network.check_wifi_interface(): - return + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) - # Handle both OFF first - if ( - wifi_btn.state == wifi_btn.State.OFF - and hotspot_btn.state == hotspot_btn.State.OFF - ): - self._sdbus_network.disconnect_network() - self._clear_network_display() - self._set_loading_state(False) - self._show_disconnected_message() - return + password_frame = BlocksCustomFrame(parent=self.hidden_network_page) + password_frame.setMinimumSize(QtCore.QSize(0, 80)) + password_frame.setMaximumSize(QtCore.QSize(16777215, 90)) + password_frame_layout = QtWidgets.QHBoxLayout(password_frame) - connectivity = self._sdbus_network.check_connectivity() - is_connected = connectivity in ("FULL", "LIMITED") + password_label = QtWidgets.QLabel("Password", parent=password_frame) + password_label.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(15) + password_label.setFont(font) + password_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + password_frame_layout.addWidget(password_label) - # Handle hotspot - if hotspot_btn.state == hotspot_btn.State.ON: - hotspot_ip = self._sdbus_network.get_device_ip_by_interface("wlan0") - if hotspot_ip or is_connected: - # Stop loading first, then update display, then show details - self._set_loading_state(False) - self._update_hotspot_display() - self._show_network_details() - self.wifi_button.setEnabled(True) - self.hotspot_button.setEnabled(True) - return + self.hidden_network_password_field = BlocksCustomLinEdit(parent=password_frame) + self.hidden_network_password_field.setHidden(True) + self.hidden_network_password_field.setMinimumSize(QtCore.QSize(500, 60)) + font = QtGui.QFont() + font.setPointSize(12) + self.hidden_network_password_field.setFont(font) + self.hidden_network_password_field.setPlaceholderText( + "Enter password (leave empty for open networks)" + ) + self.hidden_network_password_field.setEchoMode( + QtWidgets.QLineEdit.EchoMode.Password + ) + password_frame_layout.addWidget(self.hidden_network_password_field) - # Handle wifi - if wifi_btn.state == wifi_btn.State.ON: - current_ssid = self._sdbus_network.get_current_ssid() - - if self._target_ssid: - if current_ssid == self._target_ssid and is_connected: - logger.debug("Connected to target: %s", current_ssid) - # Stop loading first, then update display, then show details - self._set_loading_state(False) - self._update_wifi_display() - self._show_network_details() - self.wifi_button.setEnabled(True) - self.hotspot_button.setEnabled(True) - else: - if current_ssid and is_connected: - # Stop loading first, then update display, then show details - self._set_loading_state(False) - self._update_wifi_display() - self._show_network_details() - self.wifi_button.setEnabled(True) - self.hotspot_button.setEnabled(True) - self.update() + self.hidden_network_password_view = IconButton(parent=password_frame) + self.hidden_network_password_view.setMinimumSize(QtCore.QSize(60, 60)) + self.hidden_network_password_view.setMaximumSize(QtCore.QSize(60, 60)) + self.hidden_network_password_view.setFlat(True) + self.hidden_network_password_view.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/unsee.svg") + ) + self.hidden_network_password_view.setProperty("button_type", "icon") + password_frame_layout.addWidget(self.hidden_network_password_view) - def _handle_first_run_state(self) -> None: - """Handle initial state on first run.""" - saved_networks = self._sdbus_network.get_saved_networks_with_for() + content_layout.addWidget(password_frame) - old_hotspot = next( - (n for n in saved_networks if "ap" in str(n.get("mode", ""))), None + content_layout.addItem( + QtWidgets.QSpacerItem( + 20, + 50, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) ) - if old_hotspot: - self.hotspot_name_input_field.setText(old_hotspot["ssid"]) - connectivity = self._sdbus_network.check_connectivity() - wifi_btn = self.wifi_button.toggle_button - hotspot_btn = self.hotspot_button.toggle_button - current_ssid = self._sdbus_network.get_current_ssid() + self.hidden_network_connect_button = BlocksCustomButton( + parent=self.hidden_network_page + ) + self.hidden_network_connect_button.setMinimumSize(QtCore.QSize(250, 80)) + self.hidden_network_connect_button.setMaximumSize(QtCore.QSize(250, 80)) + font = QtGui.QFont() + font.setPointSize(15) + self.hidden_network_connect_button.setFont(font) + self.hidden_network_connect_button.setFlat(True) + self.hidden_network_connect_button.setProperty( + "icon_pixmap", PixmapCache.get(":/dialog/media/btn_icons/yes.svg") + ) + self.hidden_network_connect_button.setText("Connect") + content_layout.addWidget( + self.hidden_network_connect_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) - self._is_connecting = False - self.loadingwidget.setVisible(False) + main_layout.addLayout(content_layout) + self.addWidget(self.hidden_network_page) - with QtCore.QSignalBlocker(wifi_btn), QtCore.QSignalBlocker(hotspot_btn): - if connectivity == "FULL" and current_ssid: - wifi_btn.state = wifi_btn.State.ON - hotspot_btn.state = hotspot_btn.State.OFF - self._update_wifi_display() - self._show_network_details() - self.wifi_button.setEnabled(True) - self.hotspot_button.setEnabled(True) - elif connectivity == "LIMITED": - wifi_btn.state = wifi_btn.State.OFF - hotspot_btn.state = hotspot_btn.State.ON - self._update_hotspot_display() - self._show_network_details() - self.wifi_button.setEnabled(True) - self.hotspot_button.setEnabled(True) - else: - wifi_btn.state = wifi_btn.State.OFF - hotspot_btn.state = hotspot_btn.State.OFF - self._clear_network_display() - self._show_disconnected_message() - self.wifi_button.setEnabled(True) - self.hotspot_button.setEnabled(True) - - def _update_hotspot_display(self) -> None: - """Update display for hotspot mode.""" - ipv4_addr = self._sdbus_network.get_device_ip_by_interface("wlan0") - if not ipv4_addr: - ipv4_addr = self._sdbus_network.get_current_ip_addr() - - hotspot_name = self.hotspot_name_input_field.text() - if not hotspot_name: - hotspot_name = self._sdbus_network.hotspot_ssid or "Hotspot" - self.hotspot_name_input_field.setText(hotspot_name) - - self.netlist_ssuid.setText(hotspot_name) - # Handle empty IP properly - if ipv4_addr and ipv4_addr.strip(): - self.netlist_ip.setText(f"IP: {ipv4_addr}") - else: - self.netlist_ip.setText("IP: Obtaining...") - self.netlist_strength.setText("--") - self.netlist_security.setText("WPA2") - self._last_displayed_ssid = hotspot_name + self.hidden_network_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) + ) + self.hidden_network_connect_button.clicked.connect( + self._on_hidden_network_connect + ) + self.hidden_network_ssid_field.clicked.connect( + lambda: self._on_show_keyboard( + self.hidden_network_page, self.hidden_network_ssid_field + ) + ) + self.hidden_network_password_field.clicked.connect( + lambda: self._on_show_keyboard( + self.hidden_network_page, self.hidden_network_password_field + ) + ) + self._setup_password_visibility_toggle( + self.hidden_network_password_view, self.hidden_network_password_field + ) - def _update_wifi_display(self) -> None: - """Update display for wifi connection.""" - current_ssid = self._sdbus_network.get_current_ssid() + def _setup_vlan_page(self) -> None: + """Construct the VLAN settings page widgets and add it to the stacked widget.""" + self.vlan_page = QtWidgets.QWidget() + main_layout = QtWidgets.QVBoxLayout(self.vlan_page) - if current_ssid: - ipv4_addr = self._sdbus_network.get_current_ip_addr() - sec_type = self._sdbus_network.get_security_type_by_ssid(current_ssid) - signal_strength = self._sdbus_network.get_connection_signal_by_ssid( - current_ssid + header_layout = QtWidgets.QHBoxLayout() + header_layout.addItem( + QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, ) + ) + vlan_title = QtWidgets.QLabel("VLAN Configuration", parent=self.vlan_page) + vlan_title.setPalette(self._create_white_palette()) + title_font = QtGui.QFont() + title_font.setPointSize(20) + vlan_title.setFont(title_font) + vlan_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + header_layout.addWidget(vlan_title) + + self.vlan_back_button = IconButton(parent=self.vlan_page) + self.vlan_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.vlan_back_button.setMaximumSize(QtCore.QSize(60, 60)) + self.vlan_back_button.setFlat(True) + self.vlan_back_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") + ) + self.vlan_back_button.setProperty("button_type", "icon") + header_layout.addWidget(self.vlan_back_button) + main_layout.addLayout(header_layout) - self.netlist_ssuid.setText(current_ssid) - # Handle empty IP properly - if ipv4_addr and ipv4_addr.strip(): - self.netlist_ip.setText(f"IP: {ipv4_addr}") - else: - self.netlist_ip.setText("IP: Obtaining...") - self.netlist_security.setText(str(sec_type or "OPEN").upper()) - self.netlist_strength.setText( - f"{signal_strength}%" - if signal_strength and signal_strength != -1 - else "--" + content_layout = QtWidgets.QVBoxLayout() + content_layout.setContentsMargins(-1, 5, -1, 5) + + label_font = QtGui.QFont() + label_font.setPointSize(13) + label_font.setBold(True) + field_font = QtGui.QFont() + field_font.setPointSize(12) + field_min = QtCore.QSize(360, 45) + field_max = QtCore.QSize(500, 55) + + def _make_row(label_text, field): + """Build a labelled row widget containing *field* for the VLAN settings form.""" + frame = BlocksCustomFrame(parent=self.vlan_page) + frame.setMinimumSize(QtCore.QSize(0, 50)) + frame.setMaximumSize(QtCore.QSize(16777215, 50)) + row = QtWidgets.QHBoxLayout(frame) + row.setContentsMargins(10, 2, 10, 2) + label = QtWidgets.QLabel(label_text, parent=frame) + label.setPalette(self._create_white_palette()) + label.setFont(label_font) + label.setMinimumWidth(120) + label.setMaximumWidth(160) + label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignVCenter ) - self._last_displayed_ssid = current_ssid - else: - self._clear_network_display() + row.addWidget(label) + field.setFont(field_font) + field.setMinimumSize(field_min) + field.setMaximumSize(field_max) + row.addWidget(field) + return frame + + self.vlan_id_spinbox = QtWidgets.QSpinBox(parent=self.vlan_page) + self.vlan_id_spinbox.setRange(1, 4094) + self.vlan_id_spinbox.setValue(1) + self.vlan_id_spinbox.lineEdit().setFocusPolicy(QtCore.Qt.FocusPolicy.NoFocus) + self.vlan_id_spinbox.lineEdit().setReadOnly(True) + # Prevent text selection when stepping — deselect after each value change + self.vlan_id_spinbox.valueChanged.connect( + lambda: self.vlan_id_spinbox.lineEdit().deselect() + ) + self.vlan_id_spinbox.setStyleSheet(""" + QSpinBox { + color: white; + background: rgba(13,99,128,54); + border: 1px solid rgba(255,255,255,60); + border-radius: 8px; + padding: 4px 8px; + nohighlights; + } + QSpinBox::up-button { + width: 55px; + height: 22px; + } + QSpinBox::down-button { + width: 55px; + height: 22px; + } + """) + content_layout.addWidget(_make_row("VLAN ID", self.vlan_id_spinbox)) - @QtCore.pyqtSlot(str, name="delete-network") - def _delete_network(self, ssid: str) -> None: - """Delete a network.""" - try: - self._sdbus_network.delete_network(ssid=ssid) - except Exception as e: - logger.error("Failed to delete network %s: %s", ssid, e) - self._show_error_popup("Failed to delete network") + self.vlan_ip_field = IPAddressLineEdit( + parent=self.vlan_page, placeholder="192.168.1.100 (empty = DHCP)" + ) + content_layout.addWidget(_make_row("IP Address", self.vlan_ip_field)) - @QtCore.pyqtSlot(name="rescan-networks") - def _rescan_networks(self) -> None: - """Trigger network rescan.""" - self._sdbus_network.rescan_networks() + self.vlan_mask_field = IPAddressLineEdit( + parent=self.vlan_page, placeholder="255.255.255.0 or 24" + ) + content_layout.addWidget(_make_row("Subnet Mask", self.vlan_mask_field)) - @QtCore.pyqtSlot(name="add-network") - def _add_network(self) -> None: - """Add a new network.""" - self.add_network_validation_button.setEnabled(False) - self.add_network_validation_button.update() + self.vlan_gateway_field = IPAddressLineEdit( + parent=self.vlan_page, placeholder="192.168.1.1" + ) + content_layout.addWidget(_make_row("Gateway", self.vlan_gateway_field)) - password = self.add_network_password_field.text() - ssid = self.add_network_network_label.text() + self.vlan_dns1_field = IPAddressLineEdit( + parent=self.vlan_page, placeholder="8.8.8.8" + ) + content_layout.addWidget(_make_row("DNS 1", self.vlan_dns1_field)) - if not password and not self._current_network_is_open: - self._show_error_popup("Password field cannot be empty.") - self.add_network_validation_button.setEnabled(True) - return + self.vlan_dns2_field = IPAddressLineEdit( + parent=self.vlan_page, placeholder="8.8.4.4 (optional)" + ) + content_layout.addWidget(_make_row("DNS 2", self.vlan_dns2_field)) - result = self._sdbus_network.add_wifi_network(ssid=ssid, psk=password) - self.add_network_password_field.clear() + btn_layout = QtWidgets.QHBoxLayout() + btn_layout.addItem( + QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + btn_font = QtGui.QFont() + btn_font.setPointSize(16) + btn_font.setBold(True) - if result is None: - self._handle_failed_network_add("Failed to add network") - return + self.vlan_apply_button = BlocksCustomButton(parent=self.vlan_page) + self.vlan_apply_button.setMinimumSize(QtCore.QSize(180, 60)) + self.vlan_apply_button.setMaximumSize(QtCore.QSize(220, 60)) + self.vlan_apply_button.setFont(btn_font) + self.vlan_apply_button.setText("Apply") + self.vlan_apply_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/save.svg") + ) + btn_layout.addWidget( + self.vlan_apply_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) - error_msg = result.get("error", "") if isinstance(result, dict) else "" + self.vlan_delete_button = BlocksCustomButton(parent=self.vlan_page) + self.vlan_delete_button.setMinimumSize(QtCore.QSize(180, 60)) + self.vlan_delete_button.setMaximumSize(QtCore.QSize(220, 60)) + self.vlan_delete_button.setFont(btn_font) + self.vlan_delete_button.setText("Delete") + self.vlan_delete_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/garbage-icon.svg") + ) + btn_layout.addWidget( + self.vlan_delete_button, 0, QtCore.Qt.AlignmentFlag.AlignHCenter + ) - if not error_msg: - self._handle_successful_network_add(ssid) - else: - self._handle_failed_network_add(error_msg) + content_layout.addLayout(btn_layout) + main_layout.addLayout(content_layout) + self.addWidget(self.vlan_page) - def _handle_successful_network_add(self, ssid: str) -> None: - """Handle successful network addition.""" - self._target_ssid = ssid - self._set_loading_state(True) - self.setCurrentIndex(self.indexOf(self.main_network_page)) + def _setup_wifi_static_ip_page(self) -> None: + """Construct the Wi-Fi static-IP settings page widgets and add it to the stacked widget.""" + self.wifi_static_ip_page = QtWidgets.QWidget() + main_layout = QtWidgets.QVBoxLayout(self.wifi_static_ip_page) - wifi_btn = self.wifi_button.toggle_button - hotspot_btn = self.hotspot_button.toggle_button - with QtCore.QSignalBlocker(wifi_btn), QtCore.QSignalBlocker(hotspot_btn): - wifi_btn.state = wifi_btn.State.ON - hotspot_btn.state = hotspot_btn.State.OFF + header_layout = QtWidgets.QHBoxLayout() + header_layout.addItem( + QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Minimum, + QtWidgets.QSizePolicy.Policy.Minimum, + ) + ) + self.wifi_sip_title = QtWidgets.QLabel( + "Static IP", parent=self.wifi_static_ip_page + ) + self.wifi_sip_title.setPalette(self._create_white_palette()) + font = QtGui.QFont() + font.setPointSize(20) + self.wifi_sip_title.setFont(font) + self.wifi_sip_title.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + header_layout.addWidget(self.wifi_sip_title) + + self.wifi_sip_back_button = IconButton(parent=self.wifi_static_ip_page) + self.wifi_sip_back_button.setMinimumSize(QtCore.QSize(60, 60)) + self.wifi_sip_back_button.setMaximumSize(QtCore.QSize(60, 60)) + self.wifi_sip_back_button.setFlat(True) + self.wifi_sip_back_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/back.svg") + ) + self.wifi_sip_back_button.setProperty("button_type", "icon") + header_layout.addWidget(self.wifi_sip_back_button) + main_layout.addLayout(header_layout) + + content_layout = QtWidgets.QVBoxLayout() + content_layout.setContentsMargins(-1, 5, -1, 5) + + label_font = QtGui.QFont() + label_font.setPointSize(13) + label_font.setBold(True) + field_font = QtGui.QFont() + field_font.setPointSize(12) + field_min = QtCore.QSize(360, 45) + field_max = QtCore.QSize(500, 55) + + def _make_row(label_text, field): + """Build a labelled row widget containing *field* for the static-IP settings form.""" + frame = BlocksCustomFrame(parent=self.wifi_static_ip_page) + frame.setMinimumSize(QtCore.QSize(0, 50)) + frame.setMaximumSize(QtCore.QSize(16777215, 50)) + row = QtWidgets.QHBoxLayout(frame) + row.setContentsMargins(10, 2, 10, 2) + label = QtWidgets.QLabel(label_text, parent=frame) + label.setPalette(self._create_white_palette()) + label.setFont(label_font) + label.setMinimumWidth(120) + label.setMaximumWidth(160) + label.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignVCenter + ) + row.addWidget(label) + field.setFont(field_font) + field.setMinimumSize(field_min) + field.setMaximumSize(field_max) + row.addWidget(field) + return frame + + self.wifi_sip_ip_field = IPAddressLineEdit( + parent=self.wifi_static_ip_page, placeholder="192.168.1.100" + ) + content_layout.addWidget(_make_row("IP Address", self.wifi_sip_ip_field)) + + self.wifi_sip_mask_field = IPAddressLineEdit( + parent=self.wifi_static_ip_page, placeholder="255.255.255.0 or 24" + ) + content_layout.addWidget(_make_row("Subnet Mask", self.wifi_sip_mask_field)) - self._schedule_delayed_action( - self._network_list_worker.build, NETWORK_CONNECT_DELAY_MS + self.wifi_sip_gateway_field = IPAddressLineEdit( + parent=self.wifi_static_ip_page, placeholder="192.168.1.1" ) + content_layout.addWidget(_make_row("Gateway", self.wifi_sip_gateway_field)) - def connect_and_refresh(): - try: - self._sdbus_network.connect_network(ssid) - except Exception as e: - logger.error("Failed to connect to %s: %s", ssid, e) - self._show_error_popup(f"Failed to connect to {ssid}") - self._set_loading_state(False) + self.wifi_sip_dns1_field = IPAddressLineEdit( + parent=self.wifi_static_ip_page, placeholder="8.8.8.8" + ) + content_layout.addWidget(_make_row("DNS 1", self.wifi_sip_dns1_field)) - QtCore.QTimer.singleShot(NETWORK_CONNECT_DELAY_MS, connect_and_refresh) + self.wifi_sip_dns2_field = IPAddressLineEdit( + parent=self.wifi_static_ip_page, placeholder="8.8.4.4 (optional)" + ) + content_layout.addWidget(_make_row("DNS 2", self.wifi_sip_dns2_field)) - self.add_network_validation_button.setEnabled(True) - self.wifi_button.setEnabled(False) - self.hotspot_button.setEnabled(False) - self.add_network_validation_button.update() + btn_layout = QtWidgets.QHBoxLayout() + btn_layout.setSpacing(10) + btn_font = QtGui.QFont() + btn_font.setPointSize(16) + btn_font.setBold(True) - def _handle_failed_network_add(self, error_msg: str) -> None: - """Handle failed network addition.""" - logging.error(error_msg) - error_messages = { - "Invalid password": "Invalid password. Please try again", - "Network connection properties error": ( - "Network connection properties error. Please try again" - ), - "Permission Denied": "Permission Denied. Please try again", - } + self.wifi_sip_apply_button = BlocksCustomButton(parent=self.wifi_static_ip_page) + self.wifi_sip_apply_button.setMinimumSize(QtCore.QSize(180, 80)) + self.wifi_sip_apply_button.setMaximumSize(QtCore.QSize(220, 80)) + self.wifi_sip_apply_button.setFont(btn_font) + self.wifi_sip_apply_button.setText("Apply") + self.wifi_sip_apply_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/save.svg") + ) + btn_layout.addWidget( + self.wifi_sip_apply_button, 0, QtCore.Qt.AlignmentFlag.AlignVCenter + ) - message = error_messages.get( - error_msg, "Error while adding network. Please try again" + self.wifi_sip_dhcp_button = BlocksCustomButton(parent=self.wifi_static_ip_page) + self.wifi_sip_dhcp_button.setMinimumSize(QtCore.QSize(180, 80)) + self.wifi_sip_dhcp_button.setMaximumSize(QtCore.QSize(220, 80)) + self.wifi_sip_dhcp_button.setFont(btn_font) + self.wifi_sip_dhcp_button.setText("Reset\nDHCP") + self.wifi_sip_dhcp_button.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/garbage-icon.svg") + ) + btn_layout.addWidget( + self.wifi_sip_dhcp_button, + 0, + QtCore.Qt.AlignmentFlag.AlignVCenter, ) - self.add_network_validation_button.setEnabled(True) - self.add_network_validation_button.update() - self._show_error_popup(message) + content_layout.addLayout(btn_layout) + main_layout.addLayout(content_layout) + self.addWidget(self.wifi_static_ip_page) - def _on_save_network_settings(self) -> None: - """Save network settings.""" - self._update_network( - ssid=self.saved_connection_network_name.text(), - password=self.saved_connection_change_password_field.text(), - new_ssid=None, + def _setup_navigation_signals(self) -> None: + """Connect all navigation-button clicked signals to their target page indexes.""" + self.wifi_button.clicked.connect(self._on_wifi_button_clicked) + self.hotspot_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.hotspot_page)) + ) + self.ethernet_button.clicked.connect(self._on_ethernet_button_clicked) + self.nl_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) ) + self.network_backButton.clicked.connect(self.hide) - def _update_network( - self, - ssid: str, - password: Optional[str], - new_ssid: Optional[str], - ) -> None: - """Update network settings.""" - if not self._sdbus_network.is_known(ssid): - return + self.add_network_page_backButton.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) + ) + self.saved_connection_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.network_list_page)) + ) + self.network_details_btn.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.saved_details_page)) + ) + self.hotspot_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) + ) + self.hotspot_change_confirm.clicked.connect(self._on_hotspot_activate) - priority = self._get_selected_priority() + self.vlan_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.main_network_page)) + ) + self.vlan_apply_button.clicked.connect(self._on_vlan_apply) + self.vlan_delete_button.clicked.connect(self._on_vlan_delete) - try: - self._sdbus_network.update_connection_settings( - ssid=ssid, password=password, new_ssid=new_ssid, priority=priority - ) - except Exception as e: - logger.error("Failed to update network settings: %s", e) - self._show_error_popup("Failed to update network settings") + self.wifi_static_ip_btn.clicked.connect(self._on_wifi_static_ip_clicked) + self.wifi_sip_back_button.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.saved_details_page)) + ) + self.wifi_sip_apply_button.clicked.connect(self._on_wifi_static_ip_apply) + self.wifi_sip_dhcp_button.clicked.connect(self._on_wifi_reset_dhcp) + def _on_wifi_button_clicked(self) -> None: + """Navigate to the Wi-Fi scan page, starting or stopping scan polling as needed.""" + if ( + self.wifi_button.toggle_button.state + == self.wifi_button.toggle_button.State.OFF + ): + self._show_warning_popup("Turn on Wi-Fi first.") + return self.setCurrentIndex(self.indexOf(self.network_list_page)) - def _get_selected_priority(self) -> int: - """Get selected priority from radio buttons.""" - checked_btn = self.priority_btn_group.checkedButton() - - if checked_btn == self.high_priority_btn: - return PRIORITY_HIGH - elif checked_btn == self.low_priority_btn: - return PRIORITY_LOW - else: - return PRIORITY_MEDIUM + def _setup_action_signals(self) -> None: + """Setup action signals.""" + self.add_network_validation_button.clicked.connect(self._add_network) + self.snd_back.clicked.connect( + partial(self.setCurrentIndex, self.indexOf(self.saved_connection_page)) + ) + self.saved_details_save_btn.clicked.connect(self._on_save_network_details) + self.network_activate_btn.clicked.connect(self._on_activate_network) + self.network_delete_btn.clicked.connect(self._on_delete_network) - def _on_saved_wifi_option_selected(self) -> None: - """Handle saved wifi option selection.""" - sender = self.sender() + def _setup_toggle_signals(self) -> None: + """Setup toggle button signals.""" + self.wifi_button.toggle_button.stateChange.connect(self._on_toggle_state) + self.hotspot_button.toggle_button.stateChange.connect(self._on_toggle_state) + self.ethernet_button.toggle_button.stateChange.connect(self._on_toggle_state) - wifi_toggle = self.wifi_button.toggle_button - hotspot_toggle = self.hotspot_button.toggle_button + def _setup_password_visibility_signals(self) -> None: + """Setup password visibility toggle signals.""" + self._setup_password_visibility_toggle( + self.add_network_password_view, + self.add_network_password_field, + ) + self._setup_password_visibility_toggle( + self.saved_connection_change_password_view, + self.saved_connection_change_password_field, + ) - with QtCore.QSignalBlocker(wifi_toggle), QtCore.QSignalBlocker(hotspot_toggle): - wifi_toggle.state = wifi_toggle.State.ON - hotspot_toggle.state = hotspot_toggle.State.OFF + def _setup_password_visibility_toggle( + self, view_button: QtWidgets.QWidget, password_field: QtWidgets.QLineEdit + ) -> None: + """Setup password visibility toggle for a button/field pair.""" + view_button.setCheckable(True) - ssid = self.saved_connection_network_name.text() + see_icon = PixmapCache.get(":/ui/media/btn_icons/see.svg") + unsee_icon = PixmapCache.get(":/ui/media/btn_icons/unsee.svg") - if sender == self.network_delete_btn: - self._handle_network_delete(ssid) - elif sender == self.network_activate_btn: - self._handle_network_activate(ssid) + view_button.toggled.connect( + lambda checked: password_field.setHidden(not checked) + ) - def _handle_network_delete(self, ssid: str) -> None: - """Handle network deletion.""" - try: - self._sdbus_network.delete_network(ssid) - if ssid in self._networks: - del self._networks[ssid] - self.setCurrentIndex(self.indexOf(self.network_list_page)) - self._build_model_list() - self._network_list_worker.build() - self._show_info_popup(f"Network '{ssid}' deleted") - except Exception as e: - logger.error("Failed to delete network %s: %s", ssid, e) - self._show_error_popup("Failed to delete network") - - def _handle_network_activate(self, ssid: str) -> None: - """Handle network activation.""" - self._target_ssid = ssid - # Show loading IMMEDIATELY - self._set_loading_state(True) - self.repaint() + view_button.toggled.connect( + lambda checked: view_button.setPixmap( + unsee_icon if not checked else see_icon + ) + ) - self.setCurrentIndex(self.indexOf(self.main_network_page)) + def _setup_icons(self) -> None: + """Setup button icons.""" + self.hotspot_button.setPixmap( + PixmapCache.get(":/network/media/btn_icons/hotspot.svg") + ) + self.wifi_button.setPixmap( + PixmapCache.get(":/network/media/btn_icons/wifi_config.svg") + ) + self.ethernet_button.setPixmap( + PixmapCache.get(":/network/media/btn_icons/network/ethernet_connected.svg"), + ) + self.network_delete_btn.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/garbage-icon.svg") + ) + self.network_activate_btn.setProperty( + "icon_pixmap", PixmapCache.get(":/dialog/media/btn_icons/yes.svg") + ) + self.network_details_btn.setProperty( + "icon_pixmap", PixmapCache.get(":/ui/media/btn_icons/printer_settings.svg") + ) - try: - self._sdbus_network.connect_network(ssid) - except Exception as e: - logger.error("Failed to connect to %s: %s", ssid, e) - self._set_loading_state(False) - self._show_disconnected_message() - self._show_error_popup("Failed to connect to network") - - @QtCore.pyqtSlot(list, name="finished-network-list-build") - def _handle_network_list(self, data: List[tuple]) -> None: - """Handle network list build completion.""" - self._networks.clear() - hotspot_ssid = self._sdbus_network.hotspot_ssid - - for entry in data: - # Handle different tuple lengths - if len(entry) >= 6: - ssid, signal, status, is_open, is_saved, is_hidden = entry - elif len(entry) >= 5: - ssid, signal, status, is_open, is_saved = entry - is_hidden = self._is_hidden_ssid(ssid) - elif len(entry) >= 4: - ssid, signal, status, is_open = entry - is_saved = status in ("Active", "Saved") - is_hidden = self._is_hidden_ssid(ssid) - else: - ssid, signal, status = entry[0], entry[1], entry[2] - is_open = status == "Open" - is_saved = status in ("Active", "Saved") - is_hidden = self._is_hidden_ssid(ssid) + def _setup_input_fields(self) -> None: + """Setup input field properties.""" + self.add_network_password_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) + self.hotspot_name_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) + self.hotspot_password_input_field.setCursor(QtCore.Qt.CursorShape.BlankCursor) - if ssid == hotspot_ssid: - continue + self.hotspot_password_input_field.setPlaceholderText("Defaults to: 123456789") + self.hotspot_name_input_field.setText(str(self._nm.hotspot_ssid)) - self._networks[ssid] = NetworkInfo( - signal=signal, - status=status, - is_open=is_open, - is_saved=is_saved, - is_hidden=is_hidden, - ) + self.hotspot_password_input_field.setText(str(self._nm.hotspot_password)) - self._build_model_list() + def _setup_keyboard(self) -> None: + """Setup the on-screen keyboard.""" + self._qwerty = CustomQwertyKeyboard(self) + self.addWidget(self._qwerty) + self._qwerty.value_selected.connect(self._on_qwerty_value_selected) + self._qwerty.request_back.connect(self._on_qwerty_go_back) - # Update main panel if connected - if self._last_displayed_ssid and self._last_displayed_ssid in self._networks: - network_info = self._networks[self._last_displayed_ssid] - self.netlist_strength.setText( - f"{network_info.signal}%" if network_info.signal != -1 else "--" + self.add_network_password_field.clicked.connect( + lambda: self._on_show_keyboard( + self.add_network_page, self.add_network_password_field ) - - def _is_hidden_ssid(self, ssid: str) -> bool: - """Check if an SSID indicates a hidden network.""" - if ssid is None: - return True - ssid_stripped = ssid.strip() - ssid_lower = ssid_stripped.lower() - # Check for empty, unknown, or hidden indicators - return ( - ssid_stripped == "" - or ssid_lower == "unknown" - or ssid_lower == "" - or ssid_lower == "hidden" - or not ssid_stripped - ) - - def _build_model_list(self) -> None: - """Build the network list model.""" - self.listView.blockSignals(True) - self._reset_view_model() - - saved_networks = [] - unsaved_networks = [] - - for ssid, info in self._networks.items(): - if info.is_saved: - saved_networks.append((ssid, info)) - else: - unsaved_networks.append((ssid, info)) - - saved_networks.sort(key=lambda x: -x[1].signal) - unsaved_networks.sort(key=lambda x: -x[1].signal) - - for ssid, info in saved_networks: - self._add_network_entry( - ssid=ssid, - signal=info.signal, - status=info.status, - is_open=info.is_open, - is_hidden=info.is_hidden, + ) + self.hotspot_password_input_field.clicked.connect( + lambda: self._on_show_keyboard( + self.hotspot_page, self.hotspot_password_input_field ) - - if saved_networks and unsaved_networks: - self._add_separator_entry() - - for ssid, info in unsaved_networks: - self._add_network_entry( - ssid=ssid, - signal=info.signal, - status=info.status, - is_open=info.is_open, - is_hidden=info.is_hidden, + ) + self.hotspot_name_input_field.clicked.connect( + lambda: self._on_show_keyboard( + self.hotspot_page, self.hotspot_name_input_field ) - - # Add "Connect to Hidden Network" entry at the end - self._add_hidden_network_entry() - - self._sync_scrollbar() - self.listView.blockSignals(False) - self.listView.update() - - def _reset_view_model(self) -> None: - """Reset the view model.""" - self._model.clear() - self._entry_delegate.clear() - - def _add_separator_entry(self) -> None: - """Add a separator entry to the list.""" - item = ListItem( - text="", - left_icon=None, - right_text="", - right_icon=None, - selected=False, - allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=20, - not_clickable=True, ) - self._model.add_item(item) - - def _add_hidden_network_entry(self) -> None: - """Add a 'Connect to Hidden Network' entry at the end of the list.""" - wifi_pixmap = QtGui.QPixmap(":/network/media/btn_icons/0bar_wifi_protected.svg") - item = ListItem( - text="Connect to Hidden Network...", - left_icon=wifi_pixmap, - right_text="", - right_icon=self._right_arrow_icon, - selected=False, - allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=80, - not_clickable=False, + self.saved_connection_change_password_field.clicked.connect( + lambda: self._on_show_keyboard( + self.saved_details_page, + self.saved_connection_change_password_field, + ) ) - self._model.add_item(item) - - def _add_network_entry( - self, - ssid: str, - signal: int, - status: str, - is_open: bool = False, - is_hidden: bool = False, - ) -> None: - """Add a network entry to the list.""" - wifi_pixmap = self._icon_provider.get_pixmap(signal=signal, status=status) - # Skipping hidden networks - # Check both the is_hidden flag AND the ssid content - if is_hidden or self._is_hidden_ssid(ssid): - return - display_ssid = ssid + for field, page in [ + (self.vlan_ip_field, self.vlan_page), + (self.vlan_mask_field, self.vlan_page), + (self.vlan_gateway_field, self.vlan_page), + (self.vlan_dns1_field, self.vlan_page), + (self.vlan_dns2_field, self.vlan_page), + (self.wifi_sip_ip_field, self.wifi_static_ip_page), + (self.wifi_sip_mask_field, self.wifi_static_ip_page), + (self.wifi_sip_gateway_field, self.wifi_static_ip_page), + (self.wifi_sip_dns1_field, self.wifi_static_ip_page), + (self.wifi_sip_dns2_field, self.wifi_static_ip_page), + ]: + field.clicked.connect( + lambda _=False, f=field, p=page: self._on_show_keyboard(p, f) + ) - item = ListItem( - text=display_ssid, - left_icon=wifi_pixmap, - right_text=status, - right_icon=self._right_arrow_icon, - selected=False, - allow_check=False, - _lfontsize=17, - _rfontsize=12, - height=80, - not_clickable=False, # All entries are clickable + def _setup_scrollbar_signals(self) -> None: + """Setup scrollbar synchronization signals.""" + self.listView.verticalScrollBar().valueChanged.connect( + self._handle_scrollbar_change ) - self._model.add_item(item) - - @QtCore.pyqtSlot(ListItem, name="ssid-item-clicked") - def _on_ssid_item_clicked(self, item: ListItem) -> None: - """Handle network item click.""" - ssid = item.text - - # Handle hidden network entries - check for various hidden indicators - if ( - self._is_hidden_ssid(ssid) - or ssid == "Hidden Network" - or ssid == "Connect to Hidden Network..." - ): - self.setCurrentIndex(self.indexOf(self.hidden_network_page)) - return - - network_info = self._networks.get(ssid) - if network_info is None: - # Also check if it might be a hidden network in the _networks dict - # Hidden networks might have empty or UNKNOWN as key - for key, info in self._networks.items(): - if info.is_hidden: - self.setCurrentIndex(self.indexOf(self.hidden_network_page)) - return - return + self.verticalScrollBar.valueChanged.connect(self._handle_scrollbar_change) + self.verticalScrollBar.valueChanged.connect( + lambda value: self.listView.verticalScrollBar().setValue(value) + ) + self.verticalScrollBar.show() - if network_info.is_saved: - saved_networks = self._sdbus_network.get_saved_networks_with_for() - self._show_saved_network_page(ssid, saved_networks) - else: - self._show_add_network_page(ssid, is_open=network_info.is_open) + def _configure_list_view_palette(self) -> None: + """Configure the list view palette for transparency.""" + palette = QtGui.QPalette() - def _show_saved_network_page(self, ssid: str, saved_networks: List[Dict]) -> None: - """Show the saved network page.""" - self.saved_connection_network_name.setText(str(ssid)) - self.snd_name.setText(str(ssid)) - self._current_network_ssid = ssid # Track for priority lookup + for group in [ + QtGui.QPalette.ColorGroup.Active, + QtGui.QPalette.ColorGroup.Inactive, + QtGui.QPalette.ColorGroup.Disabled, + ]: + transparent = QtGui.QBrush(QtGui.QColor(0, 0, 0, 0)) + transparent.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(group, QtGui.QPalette.ColorRole.Button, transparent) + palette.setBrush(group, QtGui.QPalette.ColorRole.Window, transparent) - # Fetch priority from get_saved_networks() which includes priority - # get_saved_networks_with_for() does NOT include priority field - priority = None - try: - full_saved_networks = self._sdbus_network.get_saved_networks() - if full_saved_networks: - for net in full_saved_networks: - if net.get("ssid") == ssid: - priority = net.get("priority") - logger.debug("Found priority %s for network %s", priority, ssid) - break - except Exception as e: - logger.error("Failed to get priority for %s: %s", ssid, e) - - self._set_priority_button(priority) - - network_info = self._networks.get(ssid) - if network_info: - signal_text = ( - f"{network_info.signal}%" if network_info.signal >= 0 else "--%" - ) - self.saved_connection_signal_strength_info_frame.setText(signal_text) + no_brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) + no_brush.setStyle(QtCore.Qt.BrushStyle.NoBrush) + palette.setBrush(group, QtGui.QPalette.ColorRole.Base, no_brush) - if network_info.is_open: - self.saved_connection_security_type_info_label.setText("OPEN") - else: - sec_type = self._sdbus_network.get_security_type_by_ssid(ssid) - self.saved_connection_security_type_info_label.setText( - str(sec_type or "WPA").upper() - ) - else: - self.saved_connection_signal_strength_info_frame.setText("--%") - self.saved_connection_security_type_info_label.setText("--") + highlight = QtGui.QBrush(QtGui.QColor(0, 120, 215, 0)) + highlight.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(group, QtGui.QPalette.ColorRole.Highlight, highlight) - current_ssid = self._sdbus_network.get_current_ssid() - if current_ssid != ssid: - self.network_activate_btn.setDisabled(False) - self.sn_info.setText("Saved Network") - else: - self.network_activate_btn.setDisabled(True) - self.sn_info.setText("Active Network") + link = QtGui.QBrush(QtGui.QColor(0, 0, 255, 0)) + link.setStyle(QtCore.Qt.BrushStyle.SolidPattern) + palette.setBrush(group, QtGui.QPalette.ColorRole.Link, link) - self.setCurrentIndex(self.indexOf(self.saved_connection_page)) - self.frame.repaint() + self.listView.setPalette(palette) - def _set_priority_button(self, priority: Optional[int]) -> None: - """Set the priority button based on value. + def _on_show_keyboard( + self, panel: QtWidgets.QWidget, field: QtWidgets.QLineEdit + ) -> None: + """Show the QWERTY keyboard panel, saving the originating panel and input field.""" + self._previous_panel = panel + self._current_field = field + self._qwerty.set_value(field.text()) + self.setCurrentIndex(self.indexOf(self._qwerty)) - Block signals while setting to prevent unwanted triggers. - """ - # Block signals to prevent any side effects - with ( - QtCore.QSignalBlocker(self.high_priority_btn), - QtCore.QSignalBlocker(self.med_priority_btn), - QtCore.QSignalBlocker(self.low_priority_btn), - ): - # Uncheck all first - self.high_priority_btn.setChecked(False) - self.med_priority_btn.setChecked(False) - self.low_priority_btn.setChecked(False) - - # Then check the correct one - if priority is not None: - if priority >= PRIORITY_HIGH: - self.high_priority_btn.setChecked(True) - elif priority <= PRIORITY_LOW: - self.low_priority_btn.setChecked(True) - else: - self.med_priority_btn.setChecked(True) - else: - # Default to medium if no priority set - self.med_priority_btn.setChecked(True) + def _on_qwerty_go_back(self) -> None: + """Hide the keyboard and return to the previously active panel.""" + if self._previous_panel: + self.setCurrentIndex(self.indexOf(self._previous_panel)) - def _show_add_network_page(self, ssid: str, is_open: bool = False) -> None: - """Show the add network page.""" - self._current_network_is_open = is_open - self._current_network_is_hidden = False - self.add_network_network_label.setText(str(ssid)) - self.setCurrentIndex(self.indexOf(self.add_network_page)) + def _on_qwerty_value_selected(self, value: str) -> None: + """Apply the keyboard-selected *value* to the previously focused input field.""" + if self._previous_panel: + self.setCurrentIndex(self.indexOf(self._previous_panel)) + if self._current_field: + self._current_field.setText(value) def _handle_scrollbar_change(self, value: int) -> None: - """Handle scrollbar value change.""" + """Synchronise the custom scrollbar thumb to the list-view scroll position.""" self.verticalScrollBar.blockSignals(True) self.verticalScrollBar.setValue(value) self.verticalScrollBar.blockSignals(False) def _sync_scrollbar(self) -> None: - """Synchronize scrollbar with list view.""" + """Push the current list-view scroll position into the custom scrollbar.""" list_scrollbar = self.listView.verticalScrollBar() self.verticalScrollBar.setMinimum(list_scrollbar.minimum()) self.verticalScrollBar.setMaximum(list_scrollbar.maximum()) self.verticalScrollBar.setPageStep(list_scrollbar.pageStep()) - def _schedule_delayed_action(self, callback: Callable, delay_ms: int) -> None: - """Schedule a delayed action.""" - try: - self._delayed_action_timer.timeout.disconnect() - except TypeError: - pass - - self._delayed_action_timer.timeout.connect(callback) - self._delayed_action_timer.start(delay_ms) - - def close(self) -> bool: - """Close the window.""" - self._network_list_worker.stop_polling() - self._sdbus_network.close() - return super().close() - def setCurrentIndex(self, index: int) -> None: """Set the current page index.""" if not self.isVisible(): @@ -3191,7 +3836,7 @@ def setCurrentIndex(self, index: int) -> None: elif index == self.indexOf(self.saved_connection_page): self._setup_saved_connection_page_state() - self.repaint() + self.update() super().setCurrentIndex(index) def _setup_add_network_page_state(self) -> None: @@ -3215,9 +3860,9 @@ def _setup_saved_connection_page_state(self) -> None: "Change network password" ) - def setProperty(self, name: str, value: Any) -> bool: + def setProperty(self, name: str, value: object) -> bool: """Set a property value.""" - if name == "backgroundPixmap": + if name == "wifi_button_pixmap": self._background = value return super().setProperty(name, value) @@ -3233,3 +3878,4 @@ def show_network_panel(self) -> None: self.updateGeometry() self.repaint() self.show() + self._nm.scan_networks() diff --git a/BlocksScreen/lib/qrcode_gen.py b/BlocksScreen/lib/qrcode_gen.py index 1901cef1..160ec6fd 100644 --- a/BlocksScreen/lib/qrcode_gen.py +++ b/BlocksScreen/lib/qrcode_gen.py @@ -5,10 +5,11 @@ RF50_MANUAL_PAGE = "https://blockstec.com/RF50" RF50_PRODUCT_PAGE = "https://blockstec.com/rf-50" RF50_DATASHEET_PAGE = "https://www.blockstec.com/assets/downloads/rf50_datasheet.pdf" -RF50_DATASHEET_PAGE = "https://blockstec.com/assets/files/rf50_user_manual.pdf" +RF50_USER_MANUAL_PAGE = "https://blockstec.com/assets/files/rf50_user_manual.pdf" def make_qrcode(data) -> ImageQt.ImageQt: + """Generate a QR code image from *data* and return it as a Qt-compatible image.""" qr = qrcode.QRCode( version=1, error_correction=qrcode.ERROR_CORRECT_L, @@ -19,14 +20,28 @@ def make_qrcode(data) -> ImageQt.ImageQt: qr.make(fit=True) img = qr.make_image(fill_color="black", back_color="white") pil_image = img.get_image() - pil_image.show() - return pil_image.toqimage() + return ImageQt.toqimage(pil_image) + + +_NM_TO_WIFI_QR_AUTH: dict[str, str] = { + "wpa-psk": "WPA", + "wpa2-psk": "WPA", + "sae": "WPA", + "wep": "WEP", + "open": "nopass", + "nopass": "nopass", + "owe": "nopass", +} def generate_wifi_qrcode( ssid: str, password: str, auth_type: str, hidden: bool = False ) -> ImageQt.ImageQt: - wifi_data = ( - f"WIFI:T:{auth_type};S:{ssid};P:{password};{'H:true;' if hidden else ''};" - ) + """Build a Wi-Fi QR code for the given SSID/password/auth combination. + + *auth_type* is a NetworkManager key-mgmt value (e.g. ``"wpa-psk"``, + ``"sae"``). Unknown values default to WPA. + """ + qr_auth = _NM_TO_WIFI_QR_AUTH.get(auth_type.lower(), "WPA") + wifi_data = f"WIFI:T:{qr_auth};S:{ssid};P:{password};H:{str(hidden).lower()};;" return make_qrcode(wifi_data) diff --git a/BlocksScreen/lib/ui/resources/icon_resources.qrc b/BlocksScreen/lib/ui/resources/icon_resources.qrc index f9d1f0a9..8f66472a 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources.qrc +++ b/BlocksScreen/lib/ui/resources/icon_resources.qrc @@ -1,20 +1,19 @@ - media/btn_icons/0bar_wifi.svg - media/btn_icons/0bar_wifi_protected.svg - media/btn_icons/1bar_wifi.svg - media/btn_icons/1bar_wifi_protected.svg - media/btn_icons/2bar_wifi.svg - media/btn_icons/2bar_wifi_protected.svg - media/btn_icons/3bar_wifi.svg - media/btn_icons/3bar_wifi_protected.svg - media/btn_icons/4bar_wifi.svg - media/btn_icons/4bar_wifi_protected.svg + media/btn_icons/network/static_ip.svg media/btn_icons/wifi_config.svg - media/btn_icons/wifi_locked.svg - media/btn_icons/wifi_unlocked.svg + media/btn_icons/network/0bar_wifi.svg + media/btn_icons/network/0bar_wifi_protected.svg + media/btn_icons/network/1bar_wifi.svg + media/btn_icons/network/1bar_wifi_protected.svg + media/btn_icons/network/2bar_wifi.svg + media/btn_icons/network/2bar_wifi_protected.svg + media/btn_icons/network/3bar_wifi.svg + media/btn_icons/network/3bar_wifi_protected.svg + media/btn_icons/network/4bar_wifi.svg + media/btn_icons/network/4bar_wifi_protected.svg + media/btn_icons/network/ethernet_connected.svg media/btn_icons/hotspot.svg - media/btn_icons/no_wifi.svg media/btn_icons/retry_wifi.svg diff --git a/BlocksScreen/lib/ui/resources/icon_resources_rc.py b/BlocksScreen/lib/ui/resources/icon_resources_rc.py index 3bbc3133..4e2ad197 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/icon_resources_rc.py @@ -2,7 +2,7 @@ # Resource object code # -# Created by: The Resource Compiler for PyQt5 (Qt v5.15.15) +# Created by: The Resource Compiler for PyQt6 (Qt v5.15.15) # # WARNING! All changes made in this file will be lost! @@ -19325,199 +19325,6 @@ \x22\x32\x34\x36\x2e\x32\x38\x22\x20\x77\x69\x64\x74\x68\x3d\x22\ \x35\x32\x35\x2e\x39\x31\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\ \x31\x30\x37\x2e\x34\x35\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x05\x95\ -\x3c\ -\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ -\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ -\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\ -\x35\x34\x30\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ -\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ -\x31\x38\x2c\x37\x37\x43\x34\x30\x32\x2c\x37\x38\x2e\x37\x37\x2c\ -\x34\x39\x30\x2e\x31\x2c\x31\x31\x37\x2e\x30\x37\x2c\x35\x36\x33\ -\x2e\x39\x2c\x31\x39\x33\x2e\x30\x39\x63\x31\x34\x2e\x38\x2c\x31\ -\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\x2e\x39\x33\ -\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\x31\x2e\x31\x32\x2c\x31\ -\x33\x2e\x31\x37\x2d\x32\x38\x2e\x36\x32\x2c\x31\x33\x2d\x34\x31\ -\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\x30\x38\x2d\x31\x34\x2e\ -\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\x2e\x37\x31\x2d\x34\x36\ -\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\ -\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\x37\x35\x2d\x31\x32\x37\ -\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\x35\x34\x2d\x39\x2e\x31\ -\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\x2e\x33\x31\x2d\x31\x35\ -\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x2d\x34\x35\x2c\x31\x37\x2e\ -\x30\x35\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\x33\x2d\x31\ -\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x33\x2d\x38\x2e\x35\x37\ -\x2c\x39\x2e\x31\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\x32\x2e\x33\ -\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\x30\x39\x2d\ -\x37\x2e\x35\x37\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\x30\x39\x2d\ -\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\x33\x38\x30\x2c\x33\ -\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x37\x2e\x36\x32\x2d\x35\ -\x30\x2e\x33\x38\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\x38\x39\ -\x2e\x39\x33\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\x37\x2d\ -\x36\x30\x2e\x34\x36\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x39\x2e\ -\x38\x37\x2c\x32\x37\x37\x2e\x34\x33\x2c\x37\x38\x2e\x34\x38\x2c\ -\x32\x39\x30\x2e\x31\x38\x2c\x37\x37\x5a\x22\x2f\x3e\x3c\x70\x61\ -\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\ -\x20\x64\x3d\x22\x4d\x34\x36\x39\x2e\x38\x37\x2c\x33\x33\x32\x2e\ -\x32\x31\x63\x2d\x31\x31\x2c\x2e\x31\x38\x2d\x31\x37\x2e\x38\x33\ -\x2d\x33\x2e\x30\x37\x2d\x32\x33\x2e\x35\x32\x2d\x39\x2e\x31\x32\ -\x43\x34\x31\x34\x2c\x32\x38\x38\x2e\x36\x35\x2c\x33\x37\x35\x2e\ -\x32\x32\x2c\x32\x36\x37\x2e\x31\x36\x2c\x33\x33\x30\x2e\x31\x2c\ -\x32\x36\x30\x2e\x36\x38\x63\x2d\x36\x37\x2e\x33\x37\x2d\x39\x2e\ -\x36\x37\x2d\x31\x32\x36\x2e\x31\x37\x2c\x31\x30\x2e\x38\x33\x2d\ -\x31\x37\x35\x2e\x33\x39\x2c\x36\x31\x2e\x34\x31\x2d\x31\x36\x2e\ -\x33\x35\x2c\x31\x36\x2e\x38\x2d\x34\x30\x2e\x36\x37\x2c\x31\x32\ -\x2d\x34\x37\x2e\x39\x31\x2d\x31\x30\x2d\x33\x2e\x39\x2d\x31\x31\ -\x2e\x39\x2d\x31\x2e\x33\x38\x2d\x32\x32\x2e\x38\x2c\x36\x2e\x38\ -\x39\x2d\x33\x31\x2e\x35\x32\x2c\x34\x31\x2d\x34\x33\x2e\x32\x34\ -\x2c\x38\x39\x2e\x37\x35\x2d\x37\x30\x2e\x34\x38\x2c\x31\x34\x36\ -\x2e\x38\x34\x2d\x37\x39\x2e\x32\x38\x2c\x37\x35\x2e\x36\x2d\x31\ -\x31\x2e\x36\x36\x2c\x31\x34\x33\x2e\x36\x39\x2c\x38\x2e\x30\x35\ -\x2c\x32\x30\x33\x2e\x39\x31\x2c\x35\x38\x2e\x33\x36\x61\x32\x30\ -\x35\x2e\x37\x34\x2c\x32\x30\x35\x2e\x37\x34\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x32\x33\x2e\x32\x35\x2c\x32\x32\x2e\x36\x39\x63\x38\x2e\ -\x30\x38\x2c\x39\x2e\x33\x2c\x39\x2e\x35\x2c\x32\x30\x2e\x36\x32\ -\x2c\x34\x2e\x35\x39\x2c\x33\x32\x2e\x33\x34\x53\x34\x37\x38\x2e\ -\x36\x32\x2c\x33\x33\x31\x2e\x36\x36\x2c\x34\x36\x39\x2e\x38\x37\ -\x2c\x33\x33\x32\x2e\x32\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\ -\x3d\x22\x4d\x31\x38\x34\x2e\x35\x32\x2c\x33\x38\x37\x2e\x34\x63\ -\x30\x2d\x39\x2e\x32\x32\x2c\x33\x2e\x35\x37\x2d\x31\x36\x2e\x36\ -\x35\x2c\x39\x2e\x36\x31\x2d\x32\x32\x2e\x38\x31\x43\x32\x31\x38\ -\x2c\x33\x34\x30\x2e\x32\x34\x2c\x32\x34\x36\x2c\x33\x32\x34\x2e\ -\x37\x38\x2c\x32\x37\x38\x2e\x37\x37\x2c\x33\x32\x30\x2e\x31\x35\ -\x63\x34\x38\x2e\x36\x36\x2d\x36\x2e\x38\x36\x2c\x39\x30\x2e\x38\ -\x33\x2c\x38\x2e\x32\x35\x2c\x31\x32\x36\x2e\x36\x33\x2c\x34\x34\ -\x2c\x31\x30\x2e\x31\x38\x2c\x31\x30\x2e\x31\x35\x2c\x31\x32\x2e\ -\x38\x31\x2c\x32\x34\x2c\x37\x2e\x34\x35\x2c\x33\x36\x2e\x30\x35\ -\x2d\x38\x2e\x34\x34\x2c\x31\x39\x2d\x33\x31\x2c\x32\x33\x2e\x34\ -\x35\x2d\x34\x35\x2e\x33\x32\x2c\x38\x2e\x36\x36\x2d\x31\x33\x2e\ -\x34\x2d\x31\x33\x2e\x38\x33\x2d\x32\x38\x2e\x38\x2d\x32\x33\x2e\ -\x36\x33\x2d\x34\x37\x2e\x31\x31\x2d\x32\x37\x2e\x35\x34\x2d\x33\ -\x33\x2e\x32\x32\x2d\x37\x2e\x31\x2d\x36\x32\x2e\x33\x36\x2c\x31\ -\x2e\x36\x35\x2d\x38\x37\x2c\x32\x36\x2e\x37\x37\x2d\x31\x36\x2e\ -\x36\x36\x2c\x31\x37\x2d\x34\x32\x2e\x33\x2c\x31\x30\x2d\x34\x37\ -\x2e\x39\x33\x2d\x31\x33\x2e\x32\x37\x41\x36\x39\x2e\x32\x38\x2c\ -\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x38\x34\x2e\ -\x35\x32\x2c\x33\x38\x37\x2e\x34\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ -\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\ -\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x32\x33\x63\x2d\x32\x32\x2c\ -\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x36\x2d\x33\x39\ -\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\x2e\x33\ -\x38\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\ -\x2e\x38\x36\x2d\x34\x31\x2e\x38\x2c\x32\x32\x2e\x32\x35\x2c\x30\ -\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x38\x2c\x33\x39\x2e\ -\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ -\x32\x33\x2c\x33\x30\x30\x2c\x35\x32\x33\x5a\x22\x2f\x3e\x3c\x2f\ -\x73\x76\x67\x3e\ -\x00\x00\x06\x23\ -\x3c\ -\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ -\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ -\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x35\x65\x36\ -\x30\x36\x31\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ -\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ -\x32\x36\x2c\x39\x34\x2e\x37\x63\x31\x31\x30\x2e\x39\x34\x2c\x31\ -\x2e\x37\x37\x2c\x31\x39\x38\x2e\x33\x39\x2c\x33\x39\x2e\x37\x37\ -\x2c\x32\x37\x31\x2e\x36\x32\x2c\x31\x31\x35\x2e\x32\x31\x2c\x31\ -\x34\x2e\x36\x39\x2c\x31\x35\x2e\x31\x33\x2c\x31\x31\x2e\x37\x32\ -\x2c\x33\x33\x2e\x36\x37\x2c\x33\x2e\x32\x34\x2c\x34\x33\x2e\x37\ -\x2d\x31\x31\x2c\x31\x33\x2e\x30\x37\x2d\x32\x38\x2e\x34\x2c\x31\ -\x32\x2e\x38\x39\x2d\x34\x31\x2e\x31\x35\x2e\x38\x33\x2d\x31\x35\ -\x2d\x31\x34\x2e\x31\x36\x2d\x33\x30\x2d\x32\x38\x2e\x35\x2d\x34\ -\x36\x2e\x32\x35\x2d\x34\x30\x2e\x37\x39\x2d\x33\x38\x2e\x31\x31\ -\x2d\x32\x38\x2e\x37\x38\x2d\x38\x30\x2e\x37\x31\x2d\x34\x36\x2e\ -\x33\x39\x2d\x31\x32\x36\x2e\x37\x38\x2d\x35\x34\x2e\x32\x33\x2d\ -\x35\x33\x2e\x36\x2d\x39\x2e\x31\x32\x2d\x31\x30\x36\x2e\x30\x39\ -\x2d\x34\x2e\x32\x38\x2d\x31\x35\x37\x2e\x32\x38\x2c\x31\x35\x2e\ -\x31\x31\x43\x31\x34\x39\x2c\x31\x39\x31\x2e\x34\x35\x2c\x31\x31\ -\x30\x2c\x32\x31\x38\x2e\x31\x32\x2c\x37\x36\x2e\x34\x2c\x32\x35\ -\x33\x2e\x38\x35\x63\x2d\x38\x2e\x35\x2c\x39\x2d\x31\x38\x2e\x35\ -\x2c\x31\x32\x2e\x32\x33\x2d\x33\x30\x2c\x37\x2e\x39\x31\x2d\x31\ -\x39\x2e\x39\x33\x2d\x37\x2e\x35\x2d\x32\x35\x2d\x33\x33\x2e\x38\ -\x32\x2d\x39\x2e\x36\x36\x2d\x35\x30\x2e\x32\x31\x61\x33\x37\x37\ -\x2e\x33\x2c\x33\x37\x37\x2e\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x35\ -\x37\x2e\x31\x38\x2d\x35\x30\x63\x34\x32\x2e\x37\x2d\x33\x30\x2e\ -\x33\x35\x2c\x38\x39\x2e\x32\x34\x2d\x35\x30\x2e\x37\x31\x2c\x31\ -\x33\x39\x2e\x36\x39\x2d\x36\x30\x43\x32\x35\x35\x2e\x34\x31\x2c\ -\x39\x37\x2e\x35\x35\x2c\x32\x37\x37\x2e\x36\x2c\x39\x36\x2e\x31\ -\x38\x2c\x32\x39\x30\x2e\x32\x36\x2c\x39\x34\x2e\x37\x5a\x22\x2f\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x36\x38\x2e\x35\x31\x2c\ -\x33\x34\x38\x63\x2d\x31\x30\x2e\x39\x31\x2e\x31\x38\x2d\x31\x37\ -\x2e\x36\x39\x2d\x33\x2e\x30\x35\x2d\x32\x33\x2e\x33\x34\x2d\x39\ -\x2e\x30\x36\x2d\x33\x32\x2e\x31\x32\x2d\x33\x34\x2e\x31\x38\x2d\ -\x37\x30\x2e\x35\x39\x2d\x35\x35\x2e\x35\x2d\x31\x31\x35\x2e\x33\ -\x36\x2d\x36\x31\x2e\x39\x33\x2d\x36\x36\x2e\x38\x35\x2d\x39\x2e\ -\x35\x39\x2d\x31\x32\x35\x2e\x32\x2c\x31\x30\x2e\x37\x35\x2d\x31\ -\x37\x34\x2c\x36\x30\x2e\x39\x34\x2d\x31\x36\x2e\x32\x32\x2c\x31\ -\x36\x2e\x36\x37\x2d\x34\x30\x2e\x33\x36\x2c\x31\x31\x2e\x39\x31\ -\x2d\x34\x37\x2e\x35\x34\x2d\x31\x30\x2d\x33\x2e\x38\x38\x2d\x31\ -\x31\x2e\x38\x2d\x31\x2e\x33\x37\x2d\x32\x32\x2e\x36\x32\x2c\x36\ -\x2e\x38\x34\x2d\x33\x31\x2e\x32\x37\x2c\x34\x30\x2e\x36\x38\x2d\ -\x34\x32\x2e\x39\x31\x2c\x38\x39\x2e\x30\x36\x2d\x36\x39\x2e\x39\ -\x34\x2c\x31\x34\x35\x2e\x37\x31\x2d\x37\x38\x2e\x36\x38\x2c\x37\ -\x35\x2d\x31\x31\x2e\x35\x37\x2c\x31\x34\x32\x2e\x35\x39\x2c\x38\ -\x2c\x32\x30\x32\x2e\x33\x35\x2c\x35\x37\x2e\x39\x32\x61\x32\x30\ -\x33\x2e\x34\x37\x2c\x32\x30\x33\x2e\x34\x37\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x32\x33\x2e\x30\x37\x2c\x32\x32\x2e\x35\x32\x63\x38\x2c\ -\x39\x2e\x32\x32\x2c\x39\x2e\x34\x33\x2c\x32\x30\x2e\x34\x36\x2c\ -\x34\x2e\x35\x36\x2c\x33\x32\x2e\x30\x38\x53\x34\x37\x37\x2e\x31\ -\x39\x2c\x33\x34\x37\x2e\x34\x31\x2c\x34\x36\x38\x2e\x35\x31\x2c\ -\x33\x34\x38\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x31\ -\x38\x35\x2e\x33\x36\x2c\x34\x30\x32\x2e\x37\x33\x63\x30\x2d\x39\ -\x2e\x31\x35\x2c\x33\x2e\x35\x35\x2d\x31\x36\x2e\x35\x32\x2c\x39\ -\x2e\x35\x34\x2d\x32\x32\x2e\x36\x34\x2c\x32\x33\x2e\x36\x36\x2d\ -\x32\x34\x2e\x31\x35\x2c\x35\x31\x2e\x34\x34\x2d\x33\x39\x2e\x35\ -\x2c\x38\x34\x2d\x34\x34\x2e\x30\x39\x2c\x34\x38\x2e\x32\x38\x2d\ -\x36\x2e\x38\x31\x2c\x39\x30\x2e\x31\x34\x2c\x38\x2e\x31\x39\x2c\ -\x31\x32\x35\x2e\x36\x37\x2c\x34\x33\x2e\x36\x32\x2c\x31\x30\x2e\ -\x30\x39\x2c\x31\x30\x2e\x30\x37\x2c\x31\x32\x2e\x37\x2c\x32\x33\ -\x2e\x38\x32\x2c\x37\x2e\x33\x39\x2c\x33\x35\x2e\x37\x37\x2d\x38\ -\x2e\x33\x39\x2c\x31\x38\x2e\x38\x35\x2d\x33\x30\x2e\x37\x37\x2c\ -\x32\x33\x2e\x32\x37\x2d\x34\x35\x2c\x38\x2e\x36\x2d\x31\x33\x2e\ -\x33\x2d\x31\x33\x2e\x37\x33\x2d\x32\x38\x2e\x35\x38\x2d\x32\x33\ -\x2e\x34\x35\x2d\x34\x36\x2e\x37\x35\x2d\x32\x37\x2e\x33\x33\x2d\ -\x33\x33\x2d\x37\x2e\x30\x35\x2d\x36\x31\x2e\x38\x38\x2c\x31\x2e\ -\x36\x34\x2d\x38\x36\x2e\x33\x2c\x32\x36\x2e\x35\x36\x2d\x31\x36\ -\x2e\x35\x34\x2c\x31\x36\x2e\x38\x38\x2d\x34\x32\x2c\x39\x2e\x39\ -\x33\x2d\x34\x37\x2e\x35\x37\x2d\x31\x33\x2e\x31\x37\x41\x37\x30\ -\x2e\x34\x31\x2c\x37\x30\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x31\x38\x35\x2e\x33\x36\x2c\x34\x30\x32\x2e\x37\x33\x5a\x22\x2f\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x33\x37\ -\x2e\x33\x63\x2d\x32\x31\x2e\x38\x33\x2c\x30\x2d\x33\x38\x2e\x38\ -\x34\x2d\x31\x38\x2e\x33\x31\x2d\x33\x38\x2e\x38\x31\x2d\x34\x31\ -\x2e\x37\x39\x2c\x30\x2d\x32\x33\x2e\x31\x39\x2c\x31\x37\x2e\x30\ -\x36\x2d\x34\x31\x2e\x35\x31\x2c\x33\x38\x2e\x35\x36\x2d\x34\x31\ -\x2e\x34\x37\x2c\x32\x32\x2e\x30\x39\x2c\x30\x2c\x33\x39\x2c\x31\ -\x38\x2c\x33\x39\x2c\x34\x31\x2e\x34\x39\x53\x33\x32\x31\x2e\x39\ -\x31\x2c\x35\x33\x37\x2e\x32\x39\x2c\x33\x30\x30\x2c\x35\x33\x37\ -\x2e\x33\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x31\x35\ -\x36\x2e\x37\x36\x2c\x36\x31\x2c\x33\x30\x30\x2c\x32\x35\x39\x2e\ -\x38\x2c\x34\x34\x33\x2e\x32\x35\x2c\x36\x31\x68\x35\x37\x2e\x39\ -\x33\x4c\x33\x32\x39\x2c\x33\x30\x30\x2c\x35\x30\x31\x2e\x31\x39\ -\x2c\x35\x33\x39\x48\x34\x34\x33\x2e\x32\x35\x4c\x33\x30\x30\x2c\ -\x33\x34\x30\x2e\x31\x39\x2c\x31\x35\x36\x2e\x37\x36\x2c\x35\x33\ -\x39\x48\x39\x38\x2e\x38\x31\x4c\x32\x37\x31\x2c\x33\x30\x30\x2c\ -\x39\x38\x2e\x38\x33\x2c\x36\x31\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\ -\x67\x3e\ \x00\x00\x0b\x4d\ \x00\ \x00\x38\xfa\x78\x9c\xed\x9b\x49\x6f\x1d\xc7\x15\x85\xff\x0a\xc1\ @@ -20100,7 +19907,7 @@ \x32\x36\x39\x2e\x39\x2c\x33\x38\x35\x2e\x34\x37\x2c\x32\x37\x34\ \x2e\x34\x34\x2c\x33\x38\x35\x2e\x37\x34\x2c\x32\x37\x37\x2e\x31\ \x34\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x09\x7d\ +\x00\x00\x05\x95\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ \x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ @@ -20109,151 +19916,262 @@ \x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ \x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ \x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x65\x30\x65\x30\x64\x66\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x65\x65\x32\ -\x66\x32\x36\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ +\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ +\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\ +\x35\x34\x30\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ \x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ \x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ -\x31\x38\x2c\x37\x36\x2e\x35\x33\x43\x34\x30\x32\x2c\x37\x38\x2e\ -\x33\x31\x2c\x34\x39\x30\x2e\x31\x2c\x31\x31\x36\x2e\x36\x31\x2c\ -\x35\x36\x33\x2e\x39\x2c\x31\x39\x32\x2e\x36\x33\x63\x31\x34\x2e\ -\x38\x2c\x31\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\ -\x2e\x39\x33\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\x31\x2e\x31\ -\x32\x2c\x31\x33\x2e\x31\x36\x2d\x32\x38\x2e\x36\x32\x2c\x31\x33\ -\x2d\x34\x31\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\x30\x38\x2d\ -\x31\x34\x2e\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\x2e\x37\x31\ -\x2d\x34\x36\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\x38\x2e\x34\ -\x2d\x32\x39\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\x37\x35\x2d\ -\x31\x32\x37\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\x35\x34\x2d\ -\x39\x2e\x31\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\x2e\x33\x32\ -\x2d\x31\x35\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x2d\x34\x35\x2c\ -\x31\x37\x2e\x30\x35\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\ -\x33\x2d\x31\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x33\x2d\x38\ -\x2e\x35\x37\x2c\x39\x2e\x31\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\ -\x32\x2e\x33\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\ -\x30\x39\x2d\x37\x2e\x35\x36\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\ -\x30\x38\x2d\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x41\x33\x38\x30\ -\x2c\x33\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x39\x32\x2e\x33\x37\ -\x2c\x31\x34\x33\x2e\x39\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\ -\x38\x39\x2e\x39\x33\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\ -\x37\x2d\x36\x30\x2e\x34\x36\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\ -\x39\x2e\x34\x31\x2c\x32\x37\x37\x2e\x34\x33\x2c\x37\x38\x2c\x32\ -\x39\x30\x2e\x31\x38\x2c\x37\x36\x2e\x35\x33\x5a\x22\x2f\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x31\x22\x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x32\x32\x2e\x35\ -\x35\x63\x2d\x32\x32\x2c\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\ -\x2e\x34\x36\x2d\x33\x39\x2e\x31\x31\x2d\x34\x32\x2e\x31\x32\x2c\ -\x30\x2d\x32\x33\x2e\x33\x37\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\ -\x2e\x38\x32\x2c\x33\x38\x2e\x38\x36\x2d\x34\x31\x2e\x37\x39\x2c\ -\x32\x32\x2e\x32\x35\x2c\x30\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\ -\x2e\x31\x38\x2c\x33\x39\x2e\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\ -\x33\x32\x32\x2e\x31\x2c\x35\x32\x32\x2e\x35\x34\x2c\x33\x30\x30\ -\x2c\x35\x32\x32\x2e\x35\x35\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ +\x31\x38\x2c\x37\x37\x43\x34\x30\x32\x2c\x37\x38\x2e\x37\x37\x2c\ +\x34\x39\x30\x2e\x31\x2c\x31\x31\x37\x2e\x30\x37\x2c\x35\x36\x33\ +\x2e\x39\x2c\x31\x39\x33\x2e\x30\x39\x63\x31\x34\x2e\x38\x2c\x31\ +\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\x2e\x39\x33\ +\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\x31\x2e\x31\x32\x2c\x31\ +\x33\x2e\x31\x37\x2d\x32\x38\x2e\x36\x32\x2c\x31\x33\x2d\x34\x31\ +\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\x30\x38\x2d\x31\x34\x2e\ +\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\x2e\x37\x31\x2d\x34\x36\ +\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\ +\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\x37\x35\x2d\x31\x32\x37\ +\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\x35\x34\x2d\x39\x2e\x31\ +\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\x2e\x33\x31\x2d\x31\x35\ +\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x2d\x34\x35\x2c\x31\x37\x2e\ +\x30\x35\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\x33\x2d\x31\ +\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x33\x2d\x38\x2e\x35\x37\ +\x2c\x39\x2e\x31\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\x32\x2e\x33\ +\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\x30\x39\x2d\ +\x37\x2e\x35\x37\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\x30\x39\x2d\ +\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\x33\x38\x30\x2c\x33\ +\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x37\x2e\x36\x32\x2d\x35\ +\x30\x2e\x33\x38\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\x38\x39\ +\x2e\x39\x33\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\x37\x2d\ +\x36\x30\x2e\x34\x36\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x39\x2e\ +\x38\x37\x2c\x32\x37\x37\x2e\x34\x33\x2c\x37\x38\x2e\x34\x38\x2c\ +\x32\x39\x30\x2e\x31\x38\x2c\x37\x37\x5a\x22\x2f\x3e\x3c\x70\x61\ +\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\ +\x20\x64\x3d\x22\x4d\x34\x36\x39\x2e\x38\x37\x2c\x33\x33\x32\x2e\ +\x32\x31\x63\x2d\x31\x31\x2c\x2e\x31\x38\x2d\x31\x37\x2e\x38\x33\ +\x2d\x33\x2e\x30\x37\x2d\x32\x33\x2e\x35\x32\x2d\x39\x2e\x31\x32\ +\x43\x34\x31\x34\x2c\x32\x38\x38\x2e\x36\x35\x2c\x33\x37\x35\x2e\ +\x32\x32\x2c\x32\x36\x37\x2e\x31\x36\x2c\x33\x33\x30\x2e\x31\x2c\ +\x32\x36\x30\x2e\x36\x38\x63\x2d\x36\x37\x2e\x33\x37\x2d\x39\x2e\ +\x36\x37\x2d\x31\x32\x36\x2e\x31\x37\x2c\x31\x30\x2e\x38\x33\x2d\ +\x31\x37\x35\x2e\x33\x39\x2c\x36\x31\x2e\x34\x31\x2d\x31\x36\x2e\ +\x33\x35\x2c\x31\x36\x2e\x38\x2d\x34\x30\x2e\x36\x37\x2c\x31\x32\ +\x2d\x34\x37\x2e\x39\x31\x2d\x31\x30\x2d\x33\x2e\x39\x2d\x31\x31\ +\x2e\x39\x2d\x31\x2e\x33\x38\x2d\x32\x32\x2e\x38\x2c\x36\x2e\x38\ +\x39\x2d\x33\x31\x2e\x35\x32\x2c\x34\x31\x2d\x34\x33\x2e\x32\x34\ +\x2c\x38\x39\x2e\x37\x35\x2d\x37\x30\x2e\x34\x38\x2c\x31\x34\x36\ +\x2e\x38\x34\x2d\x37\x39\x2e\x32\x38\x2c\x37\x35\x2e\x36\x2d\x31\ +\x31\x2e\x36\x36\x2c\x31\x34\x33\x2e\x36\x39\x2c\x38\x2e\x30\x35\ +\x2c\x32\x30\x33\x2e\x39\x31\x2c\x35\x38\x2e\x33\x36\x61\x32\x30\ +\x35\x2e\x37\x34\x2c\x32\x30\x35\x2e\x37\x34\x2c\x30\x2c\x30\x2c\ +\x31\x2c\x32\x33\x2e\x32\x35\x2c\x32\x32\x2e\x36\x39\x63\x38\x2e\ +\x30\x38\x2c\x39\x2e\x33\x2c\x39\x2e\x35\x2c\x32\x30\x2e\x36\x32\ +\x2c\x34\x2e\x35\x39\x2c\x33\x32\x2e\x33\x34\x53\x34\x37\x38\x2e\ +\x36\x32\x2c\x33\x33\x31\x2e\x36\x36\x2c\x34\x36\x39\x2e\x38\x37\ +\x2c\x33\x33\x32\x2e\x32\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ \x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\ -\x3d\x22\x4d\x34\x35\x36\x2e\x37\x32\x2c\x33\x31\x32\x2e\x37\x61\ -\x33\x32\x2e\x35\x33\x2c\x33\x32\x2e\x35\x33\x2c\x30\x2c\x30\x2c\ -\x30\x2d\x31\x36\x2e\x34\x31\x2c\x33\x2e\x37\x32\x71\x33\x2e\x30\ -\x36\x2c\x33\x2c\x36\x2c\x36\x2e\x32\x31\x63\x35\x2e\x36\x39\x2c\ -\x36\x2e\x30\x35\x2c\x31\x32\x2e\x35\x33\x2c\x39\x2e\x33\x2c\x32\ -\x33\x2e\x35\x32\x2c\x39\x2e\x31\x32\x61\x32\x33\x2e\x39\x2c\x32\ -\x33\x2e\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x33\x2e\x35\x2d\x35\ -\x2e\x33\x31\x41\x33\x35\x2e\x35\x33\x2c\x33\x35\x2e\x35\x33\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x34\x35\x36\x2e\x37\x32\x2c\x33\x31\x32\ -\x2e\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x34\ -\x36\x2e\x32\x36\x2c\x32\x36\x36\x2e\x33\x34\x61\x37\x36\x2e\x32\ -\x33\x2c\x37\x36\x2e\x32\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x37\ -\x2e\x39\x31\x2c\x31\x2e\x34\x37\x71\x2d\x34\x2e\x37\x36\x2d\x34\ -\x2e\x35\x2d\x39\x2e\x37\x33\x2d\x38\x2e\x36\x35\x63\x2d\x36\x30\ -\x2e\x32\x32\x2d\x35\x30\x2e\x33\x31\x2d\x31\x32\x38\x2e\x33\x31\ -\x2d\x37\x30\x2d\x32\x30\x33\x2e\x39\x31\x2d\x35\x38\x2e\x33\x37\ -\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\x31\x2d\x31\x30\x35\x2e\ -\x38\x34\x2c\x33\x36\x2e\x30\x35\x2d\x31\x34\x36\x2e\x38\x34\x2c\ -\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\x37\x2c\x38\x2e\x37\x33\x2d\ -\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\x36\x33\x2d\x36\x2e\x38\x39\ -\x2c\x33\x31\x2e\x35\x33\x2c\x37\x2e\x32\x34\x2c\x32\x32\x2c\x33\ -\x31\x2e\x35\x36\x2c\x32\x36\x2e\x38\x33\x2c\x34\x37\x2e\x39\x31\ -\x2c\x31\x30\x2c\x34\x39\x2e\x32\x32\x2d\x35\x30\x2e\x35\x37\x2c\ -\x31\x30\x38\x2d\x37\x31\x2e\x30\x37\x2c\x31\x37\x35\x2e\x33\x39\ -\x2d\x36\x31\x2e\x34\x61\x31\x38\x37\x2c\x31\x38\x37\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x37\x32\x2e\x36\x36\x2c\x32\x36\x2e\x33\x39\x41\ -\x38\x30\x2e\x36\x2c\x38\x30\x2e\x36\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x34\x34\x36\x2e\x32\x36\x2c\x32\x36\x36\x2e\x33\x34\x5a\x22\x2f\ +\x3d\x22\x4d\x31\x38\x34\x2e\x35\x32\x2c\x33\x38\x37\x2e\x34\x63\ +\x30\x2d\x39\x2e\x32\x32\x2c\x33\x2e\x35\x37\x2d\x31\x36\x2e\x36\ +\x35\x2c\x39\x2e\x36\x31\x2d\x32\x32\x2e\x38\x31\x43\x32\x31\x38\ +\x2c\x33\x34\x30\x2e\x32\x34\x2c\x32\x34\x36\x2c\x33\x32\x34\x2e\ +\x37\x38\x2c\x32\x37\x38\x2e\x37\x37\x2c\x33\x32\x30\x2e\x31\x35\ +\x63\x34\x38\x2e\x36\x36\x2d\x36\x2e\x38\x36\x2c\x39\x30\x2e\x38\ +\x33\x2c\x38\x2e\x32\x35\x2c\x31\x32\x36\x2e\x36\x33\x2c\x34\x34\ +\x2c\x31\x30\x2e\x31\x38\x2c\x31\x30\x2e\x31\x35\x2c\x31\x32\x2e\ +\x38\x31\x2c\x32\x34\x2c\x37\x2e\x34\x35\x2c\x33\x36\x2e\x30\x35\ +\x2d\x38\x2e\x34\x34\x2c\x31\x39\x2d\x33\x31\x2c\x32\x33\x2e\x34\ +\x35\x2d\x34\x35\x2e\x33\x32\x2c\x38\x2e\x36\x36\x2d\x31\x33\x2e\ +\x34\x2d\x31\x33\x2e\x38\x33\x2d\x32\x38\x2e\x38\x2d\x32\x33\x2e\ +\x36\x33\x2d\x34\x37\x2e\x31\x31\x2d\x32\x37\x2e\x35\x34\x2d\x33\ +\x33\x2e\x32\x32\x2d\x37\x2e\x31\x2d\x36\x32\x2e\x33\x36\x2c\x31\ +\x2e\x36\x35\x2d\x38\x37\x2c\x32\x36\x2e\x37\x37\x2d\x31\x36\x2e\ +\x36\x36\x2c\x31\x37\x2d\x34\x32\x2e\x33\x2c\x31\x30\x2d\x34\x37\ +\x2e\x39\x33\x2d\x31\x33\x2e\x32\x37\x41\x36\x39\x2e\x32\x38\x2c\ +\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x38\x34\x2e\ +\x35\x32\x2c\x33\x38\x37\x2e\x34\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ +\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\ +\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x32\x33\x63\x2d\x32\x32\x2c\ +\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x36\x2d\x33\x39\ +\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\x2e\x33\ +\x38\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\ +\x2e\x38\x36\x2d\x34\x31\x2e\x38\x2c\x32\x32\x2e\x32\x35\x2c\x30\ +\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x38\x2c\x33\x39\x2e\ +\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ +\x32\x33\x2c\x33\x30\x30\x2c\x35\x32\x33\x5a\x22\x2f\x3e\x3c\x2f\ +\x73\x76\x67\x3e\ +\x00\x00\x06\x23\ +\x3c\ +\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ +\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ +\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ +\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ +\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ +\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ +\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ +\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ +\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x35\x65\x36\ +\x30\x36\x31\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ +\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ +\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ +\x32\x36\x2c\x39\x34\x2e\x37\x63\x31\x31\x30\x2e\x39\x34\x2c\x31\ +\x2e\x37\x37\x2c\x31\x39\x38\x2e\x33\x39\x2c\x33\x39\x2e\x37\x37\ +\x2c\x32\x37\x31\x2e\x36\x32\x2c\x31\x31\x35\x2e\x32\x31\x2c\x31\ +\x34\x2e\x36\x39\x2c\x31\x35\x2e\x31\x33\x2c\x31\x31\x2e\x37\x32\ +\x2c\x33\x33\x2e\x36\x37\x2c\x33\x2e\x32\x34\x2c\x34\x33\x2e\x37\ +\x2d\x31\x31\x2c\x31\x33\x2e\x30\x37\x2d\x32\x38\x2e\x34\x2c\x31\ +\x32\x2e\x38\x39\x2d\x34\x31\x2e\x31\x35\x2e\x38\x33\x2d\x31\x35\ +\x2d\x31\x34\x2e\x31\x36\x2d\x33\x30\x2d\x32\x38\x2e\x35\x2d\x34\ +\x36\x2e\x32\x35\x2d\x34\x30\x2e\x37\x39\x2d\x33\x38\x2e\x31\x31\ +\x2d\x32\x38\x2e\x37\x38\x2d\x38\x30\x2e\x37\x31\x2d\x34\x36\x2e\ +\x33\x39\x2d\x31\x32\x36\x2e\x37\x38\x2d\x35\x34\x2e\x32\x33\x2d\ +\x35\x33\x2e\x36\x2d\x39\x2e\x31\x32\x2d\x31\x30\x36\x2e\x30\x39\ +\x2d\x34\x2e\x32\x38\x2d\x31\x35\x37\x2e\x32\x38\x2c\x31\x35\x2e\ +\x31\x31\x43\x31\x34\x39\x2c\x31\x39\x31\x2e\x34\x35\x2c\x31\x31\ +\x30\x2c\x32\x31\x38\x2e\x31\x32\x2c\x37\x36\x2e\x34\x2c\x32\x35\ +\x33\x2e\x38\x35\x63\x2d\x38\x2e\x35\x2c\x39\x2d\x31\x38\x2e\x35\ +\x2c\x31\x32\x2e\x32\x33\x2d\x33\x30\x2c\x37\x2e\x39\x31\x2d\x31\ +\x39\x2e\x39\x33\x2d\x37\x2e\x35\x2d\x32\x35\x2d\x33\x33\x2e\x38\ +\x32\x2d\x39\x2e\x36\x36\x2d\x35\x30\x2e\x32\x31\x61\x33\x37\x37\ +\x2e\x33\x2c\x33\x37\x37\x2e\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x35\ +\x37\x2e\x31\x38\x2d\x35\x30\x63\x34\x32\x2e\x37\x2d\x33\x30\x2e\ +\x33\x35\x2c\x38\x39\x2e\x32\x34\x2d\x35\x30\x2e\x37\x31\x2c\x31\ +\x33\x39\x2e\x36\x39\x2d\x36\x30\x43\x32\x35\x35\x2e\x34\x31\x2c\ +\x39\x37\x2e\x35\x35\x2c\x32\x37\x37\x2e\x36\x2c\x39\x36\x2e\x31\ +\x38\x2c\x32\x39\x30\x2e\x32\x36\x2c\x39\x34\x2e\x37\x5a\x22\x2f\ \x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x35\x35\x35\x2e\x33\x32\x2c\ -\x33\x39\x39\x2e\x37\x63\x30\x2d\x38\x2e\x37\x32\x2d\x34\x2e\x33\ -\x34\x2d\x31\x33\x2e\x32\x31\x2d\x31\x32\x2e\x38\x31\x2d\x31\x33\ -\x2e\x34\x32\x2d\x34\x2e\x35\x39\x2d\x2e\x31\x31\x2d\x39\x2e\x31\ -\x39\x2c\x30\x2d\x31\x34\x2e\x31\x31\x2c\x30\x2c\x30\x2d\x35\x2e\ -\x31\x35\x2e\x31\x39\x2d\x39\x2e\x33\x39\x2c\x30\x2d\x31\x33\x2e\ -\x36\x31\x2d\x2e\x37\x35\x2d\x31\x34\x2e\x31\x37\x2e\x32\x34\x2d\ -\x32\x38\x2e\x37\x39\x2d\x32\x2e\x38\x33\x2d\x34\x32\x2e\x34\x2d\ -\x38\x2e\x32\x31\x2d\x33\x36\x2e\x32\x39\x2d\x34\x33\x2d\x36\x30\ -\x2e\x31\x39\x2d\x37\x38\x2e\x31\x34\x2d\x35\x35\x2e\x34\x38\x2d\ -\x33\x36\x2e\x37\x32\x2c\x34\x2e\x39\x32\x2d\x36\x33\x2e\x36\x33\ -\x2c\x33\x36\x2e\x37\x34\x2d\x36\x33\x2e\x35\x31\x2c\x37\x35\x2e\ -\x30\x36\x2c\x30\x2c\x31\x31\x2e\x38\x38\x2c\x30\x2c\x32\x33\x2e\ -\x37\x35\x2c\x30\x2c\x33\x36\x2e\x34\x31\x68\x2d\x37\x2e\x34\x33\ -\x63\x2d\x32\x2e\x34\x35\x2c\x30\x2d\x34\x2e\x39\x2d\x2e\x30\x35\ -\x2d\x37\x2e\x33\x34\x2c\x30\x2d\x38\x2e\x35\x33\x2e\x32\x38\x2d\ -\x31\x32\x2e\x31\x32\x2c\x34\x2e\x31\x36\x2d\x31\x32\x2e\x31\x32\ -\x2c\x31\x33\x2e\x31\x31\x71\x30\x2c\x35\x34\x2e\x38\x37\x2c\x30\ -\x2c\x31\x30\x39\x2e\x37\x35\x63\x30\x2c\x31\x30\x2e\x34\x34\x2c\ -\x33\x2e\x35\x39\x2c\x31\x34\x2e\x33\x31\x2c\x31\x33\x2e\x35\x32\ -\x2c\x31\x34\x2e\x33\x32\x71\x38\x35\x2e\x36\x33\x2c\x30\x2c\x31\ -\x37\x31\x2e\x32\x37\x2c\x30\x63\x39\x2e\x32\x34\x2c\x30\x2c\x31\ -\x33\x2e\x35\x33\x2d\x34\x2e\x34\x33\x2c\x31\x33\x2e\x35\x33\x2d\ -\x31\x34\x51\x35\x35\x35\x2e\x34\x2c\x34\x35\x34\x2e\x35\x38\x2c\ -\x35\x35\x35\x2e\x33\x32\x2c\x33\x39\x39\x2e\x37\x5a\x6d\x2d\x35\ -\x35\x2e\x39\x34\x2d\x31\x33\x2e\x38\x31\x48\x34\x31\x33\x2e\x32\ -\x63\x30\x2d\x31\x35\x2d\x31\x2e\x33\x33\x2d\x32\x39\x2e\x37\x39\ -\x2e\x33\x31\x2d\x34\x34\x2e\x31\x39\x2c\x32\x2e\x35\x32\x2d\x32\ -\x32\x2e\x30\x38\x2c\x32\x32\x2e\x36\x31\x2d\x33\x38\x2e\x33\x38\ -\x2c\x34\x33\x2e\x35\x37\x2d\x33\x37\x2e\x34\x39\x61\x34\x33\x2e\ -\x39\x34\x2c\x34\x33\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x34\ -\x32\x2e\x31\x36\x2c\x34\x31\x2e\x35\x33\x43\x35\x30\x30\x2c\x33\ -\x35\x39\x2c\x34\x39\x39\x2e\x33\x38\x2c\x33\x37\x32\x2e\x34\x31\ -\x2c\x34\x39\x39\x2e\x33\x38\x2c\x33\x38\x35\x2e\x38\x39\x5a\x22\ -\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x36\x38\x2e\x38\x34\ -\x2c\x33\x37\x37\x2e\x37\x38\x63\x31\x2e\x38\x38\x2d\x2e\x30\x36\ -\x2c\x33\x2e\x37\x31\x2c\x30\x2c\x35\x2e\x34\x37\x2c\x30\x68\x31\ -\x2e\x30\x39\x76\x2d\x33\x2e\x34\x39\x63\x30\x2d\x38\x2e\x32\x39\ -\x2c\x30\x2d\x31\x36\x2e\x33\x34\x2c\x30\x2d\x32\x34\x2e\x33\x39\ -\x61\x38\x39\x2e\x37\x37\x2c\x38\x39\x2e\x37\x37\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x2e\x35\x35\x2d\x31\x30\x63\x2d\x32\x38\x2e\x38\x39\ -\x2d\x31\x38\x2e\x33\x31\x2d\x36\x31\x2e\x32\x36\x2d\x32\x35\x2e\ -\x32\x33\x2d\x39\x37\x2e\x31\x37\x2d\x32\x30\x2e\x31\x37\x2d\x33\ -\x32\x2e\x38\x2c\x34\x2e\x36\x33\x2d\x36\x30\x2e\x38\x2c\x32\x30\ -\x2e\x30\x39\x2d\x38\x34\x2e\x36\x34\x2c\x34\x34\x2e\x34\x34\x2d\ -\x36\x2c\x36\x2e\x31\x36\x2d\x39\x2e\x36\x33\x2c\x31\x33\x2e\x35\ -\x39\x2d\x39\x2e\x36\x31\x2c\x32\x32\x2e\x38\x31\x61\x36\x39\x2e\ -\x32\x38\x2c\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x37\x2e\x33\x38\x63\x35\x2e\x36\x33\x2c\x32\x33\x2e\x32\x37\ -\x2c\x33\x31\x2e\x32\x37\x2c\x33\x30\x2e\x32\x38\x2c\x34\x37\x2e\ -\x39\x33\x2c\x31\x33\x2e\x32\x37\x2c\x32\x34\x2e\x36\x31\x2d\x32\ -\x35\x2e\x31\x32\x2c\x35\x33\x2e\x37\x35\x2d\x33\x33\x2e\x38\x37\ -\x2c\x38\x37\x2d\x32\x36\x2e\x37\x37\x41\x38\x33\x2e\x37\x35\x2c\ -\x38\x33\x2e\x37\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x34\x39\x2e\ -\x31\x35\x2c\x33\x39\x33\x43\x33\x35\x31\x2e\x31\x37\x2c\x33\x38\ -\x33\x2e\x34\x37\x2c\x33\x35\x38\x2c\x33\x37\x38\x2e\x31\x33\x2c\ -\x33\x36\x38\x2e\x38\x34\x2c\x33\x37\x37\x2e\x37\x38\x5a\x22\x2f\ -\x3e\x3c\x72\x65\x63\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x32\x22\x20\x78\x3d\x22\x34\x34\x37\x2e\x36\x39\x22\x20\ -\x79\x3d\x22\x33\x38\x36\x2e\x31\x33\x22\x20\x77\x69\x64\x74\x68\ -\x3d\x22\x31\x36\x2e\x39\x34\x22\x20\x68\x65\x69\x67\x68\x74\x3d\ -\x22\x31\x32\x37\x2e\x36\x31\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\ -\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x34\x35\ -\x31\x2e\x37\x35\x20\x2d\x31\x39\x30\x2e\x37\x37\x29\x20\x72\x6f\ -\x74\x61\x74\x65\x28\x34\x35\x29\x22\x2f\x3e\x3c\x72\x65\x63\x74\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x78\ -\x3d\x22\x34\x34\x37\x2e\x36\x39\x22\x20\x79\x3d\x22\x33\x38\x36\ -\x2e\x31\x33\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x36\x2e\x39\ -\x34\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x31\x32\x37\x2e\x36\ -\x31\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\ -\x61\x6e\x73\x6c\x61\x74\x65\x28\x31\x30\x39\x36\x2e\x38\x36\x20\ -\x34\x34\x35\x2e\x35\x33\x29\x20\x72\x6f\x74\x61\x74\x65\x28\x31\ -\x33\x35\x29\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ +\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x36\x38\x2e\x35\x31\x2c\ +\x33\x34\x38\x63\x2d\x31\x30\x2e\x39\x31\x2e\x31\x38\x2d\x31\x37\ +\x2e\x36\x39\x2d\x33\x2e\x30\x35\x2d\x32\x33\x2e\x33\x34\x2d\x39\ +\x2e\x30\x36\x2d\x33\x32\x2e\x31\x32\x2d\x33\x34\x2e\x31\x38\x2d\ +\x37\x30\x2e\x35\x39\x2d\x35\x35\x2e\x35\x2d\x31\x31\x35\x2e\x33\ +\x36\x2d\x36\x31\x2e\x39\x33\x2d\x36\x36\x2e\x38\x35\x2d\x39\x2e\ +\x35\x39\x2d\x31\x32\x35\x2e\x32\x2c\x31\x30\x2e\x37\x35\x2d\x31\ +\x37\x34\x2c\x36\x30\x2e\x39\x34\x2d\x31\x36\x2e\x32\x32\x2c\x31\ +\x36\x2e\x36\x37\x2d\x34\x30\x2e\x33\x36\x2c\x31\x31\x2e\x39\x31\ +\x2d\x34\x37\x2e\x35\x34\x2d\x31\x30\x2d\x33\x2e\x38\x38\x2d\x31\ +\x31\x2e\x38\x2d\x31\x2e\x33\x37\x2d\x32\x32\x2e\x36\x32\x2c\x36\ +\x2e\x38\x34\x2d\x33\x31\x2e\x32\x37\x2c\x34\x30\x2e\x36\x38\x2d\ +\x34\x32\x2e\x39\x31\x2c\x38\x39\x2e\x30\x36\x2d\x36\x39\x2e\x39\ +\x34\x2c\x31\x34\x35\x2e\x37\x31\x2d\x37\x38\x2e\x36\x38\x2c\x37\ +\x35\x2d\x31\x31\x2e\x35\x37\x2c\x31\x34\x32\x2e\x35\x39\x2c\x38\ +\x2c\x32\x30\x32\x2e\x33\x35\x2c\x35\x37\x2e\x39\x32\x61\x32\x30\ +\x33\x2e\x34\x37\x2c\x32\x30\x33\x2e\x34\x37\x2c\x30\x2c\x30\x2c\ +\x31\x2c\x32\x33\x2e\x30\x37\x2c\x32\x32\x2e\x35\x32\x63\x38\x2c\ +\x39\x2e\x32\x32\x2c\x39\x2e\x34\x33\x2c\x32\x30\x2e\x34\x36\x2c\ +\x34\x2e\x35\x36\x2c\x33\x32\x2e\x30\x38\x53\x34\x37\x37\x2e\x31\ +\x39\x2c\x33\x34\x37\x2e\x34\x31\x2c\x34\x36\x38\x2e\x35\x31\x2c\ +\x33\x34\x38\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ +\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x31\ +\x38\x35\x2e\x33\x36\x2c\x34\x30\x32\x2e\x37\x33\x63\x30\x2d\x39\ +\x2e\x31\x35\x2c\x33\x2e\x35\x35\x2d\x31\x36\x2e\x35\x32\x2c\x39\ +\x2e\x35\x34\x2d\x32\x32\x2e\x36\x34\x2c\x32\x33\x2e\x36\x36\x2d\ +\x32\x34\x2e\x31\x35\x2c\x35\x31\x2e\x34\x34\x2d\x33\x39\x2e\x35\ +\x2c\x38\x34\x2d\x34\x34\x2e\x30\x39\x2c\x34\x38\x2e\x32\x38\x2d\ +\x36\x2e\x38\x31\x2c\x39\x30\x2e\x31\x34\x2c\x38\x2e\x31\x39\x2c\ +\x31\x32\x35\x2e\x36\x37\x2c\x34\x33\x2e\x36\x32\x2c\x31\x30\x2e\ +\x30\x39\x2c\x31\x30\x2e\x30\x37\x2c\x31\x32\x2e\x37\x2c\x32\x33\ +\x2e\x38\x32\x2c\x37\x2e\x33\x39\x2c\x33\x35\x2e\x37\x37\x2d\x38\ +\x2e\x33\x39\x2c\x31\x38\x2e\x38\x35\x2d\x33\x30\x2e\x37\x37\x2c\ +\x32\x33\x2e\x32\x37\x2d\x34\x35\x2c\x38\x2e\x36\x2d\x31\x33\x2e\ +\x33\x2d\x31\x33\x2e\x37\x33\x2d\x32\x38\x2e\x35\x38\x2d\x32\x33\ +\x2e\x34\x35\x2d\x34\x36\x2e\x37\x35\x2d\x32\x37\x2e\x33\x33\x2d\ +\x33\x33\x2d\x37\x2e\x30\x35\x2d\x36\x31\x2e\x38\x38\x2c\x31\x2e\ +\x36\x34\x2d\x38\x36\x2e\x33\x2c\x32\x36\x2e\x35\x36\x2d\x31\x36\ +\x2e\x35\x34\x2c\x31\x36\x2e\x38\x38\x2d\x34\x32\x2c\x39\x2e\x39\ +\x33\x2d\x34\x37\x2e\x35\x37\x2d\x31\x33\x2e\x31\x37\x41\x37\x30\ +\x2e\x34\x31\x2c\x37\x30\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\ +\x31\x38\x35\x2e\x33\x36\x2c\x34\x30\x32\x2e\x37\x33\x5a\x22\x2f\ +\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ +\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x33\x37\ +\x2e\x33\x63\x2d\x32\x31\x2e\x38\x33\x2c\x30\x2d\x33\x38\x2e\x38\ +\x34\x2d\x31\x38\x2e\x33\x31\x2d\x33\x38\x2e\x38\x31\x2d\x34\x31\ +\x2e\x37\x39\x2c\x30\x2d\x32\x33\x2e\x31\x39\x2c\x31\x37\x2e\x30\ +\x36\x2d\x34\x31\x2e\x35\x31\x2c\x33\x38\x2e\x35\x36\x2d\x34\x31\ +\x2e\x34\x37\x2c\x32\x32\x2e\x30\x39\x2c\x30\x2c\x33\x39\x2c\x31\ +\x38\x2c\x33\x39\x2c\x34\x31\x2e\x34\x39\x53\x33\x32\x31\x2e\x39\ +\x31\x2c\x35\x33\x37\x2e\x32\x39\x2c\x33\x30\x30\x2c\x35\x33\x37\ +\x2e\x33\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ +\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x31\x35\ +\x36\x2e\x37\x36\x2c\x36\x31\x2c\x33\x30\x30\x2c\x32\x35\x39\x2e\ +\x38\x2c\x34\x34\x33\x2e\x32\x35\x2c\x36\x31\x68\x35\x37\x2e\x39\ +\x33\x4c\x33\x32\x39\x2c\x33\x30\x30\x2c\x35\x30\x31\x2e\x31\x39\ +\x2c\x35\x33\x39\x48\x34\x34\x33\x2e\x32\x35\x4c\x33\x30\x30\x2c\ +\x33\x34\x30\x2e\x31\x39\x2c\x31\x35\x36\x2e\x37\x36\x2c\x35\x33\ +\x39\x48\x39\x38\x2e\x38\x31\x4c\x32\x37\x31\x2c\x33\x30\x30\x2c\ +\x39\x38\x2e\x38\x33\x2c\x36\x31\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\ +\x67\x3e\ +\x00\x00\x04\x51\ +\x3c\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\ +\x61\x79\x65\x72\x5f\x31\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\ +\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\ +\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\ +\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x76\ +\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x36\x30\x30\x20\ +\x36\x30\x30\x22\x3e\x0a\x20\x20\x3c\x64\x65\x66\x73\x3e\x0a\x20\ +\x20\x20\x20\x3c\x73\x74\x79\x6c\x65\x3e\x0a\x20\x20\x20\x20\x20\ +\x20\x2e\x63\x6c\x73\x2d\x31\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x66\x69\x6c\x6c\x3a\x20\x23\x65\x30\x65\x30\x64\x66\x3b\ +\x0a\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x73\ +\x74\x79\x6c\x65\x3e\x0a\x20\x20\x3c\x2f\x64\x65\x66\x73\x3e\x0a\ +\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ +\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x37\x37\x2e\x38\x35\ +\x2c\x31\x39\x38\x2e\x36\x37\x63\x30\x2d\x38\x2e\x33\x31\x2d\x32\ +\x2e\x35\x39\x2d\x31\x34\x2e\x37\x37\x2d\x37\x2e\x37\x36\x2d\x31\ +\x39\x2e\x33\x35\x2d\x35\x2e\x31\x38\x2d\x34\x2e\x35\x38\x2d\x31\ +\x33\x2e\x30\x33\x2d\x36\x2e\x38\x37\x2d\x32\x33\x2e\x35\x35\x2d\ +\x36\x2e\x38\x37\x68\x2d\x32\x38\x76\x35\x32\x2e\x31\x39\x68\x32\ +\x38\x63\x31\x30\x2e\x35\x32\x2c\x30\x2c\x31\x38\x2e\x33\x37\x2d\ +\x32\x2e\x32\x39\x2c\x32\x33\x2e\x35\x35\x2d\x36\x2e\x38\x37\x2c\ +\x35\x2e\x31\x37\x2d\x34\x2e\x35\x38\x2c\x37\x2e\x37\x36\x2d\x31\ +\x30\x2e\x39\x34\x2c\x37\x2e\x37\x36\x2d\x31\x39\x2e\x30\x39\x5a\ +\x22\x2f\x3e\x0a\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ +\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x35\ +\x31\x2e\x34\x36\x2c\x34\x38\x31\x2e\x37\x39\x76\x2d\x36\x2e\x37\ +\x37\x63\x30\x2d\x38\x2e\x39\x38\x2d\x37\x2e\x32\x38\x2d\x31\x36\ +\x2e\x32\x35\x2d\x31\x36\x2e\x32\x35\x2d\x31\x36\x2e\x32\x35\x68\ +\x2d\x31\x33\x2e\x35\x34\x76\x2d\x35\x34\x2e\x38\x34\x68\x31\x30\ +\x32\x2e\x39\x31\x63\x33\x32\x2e\x31\x36\x2c\x30\x2c\x35\x38\x2e\ +\x32\x33\x2d\x32\x36\x2e\x30\x37\x2c\x35\x38\x2e\x32\x33\x2d\x35\ +\x38\x2e\x32\x33\x56\x39\x36\x2e\x35\x34\x63\x30\x2d\x33\x32\x2e\ +\x31\x36\x2d\x32\x36\x2e\x30\x37\x2d\x35\x38\x2e\x32\x33\x2d\x35\ +\x38\x2e\x32\x33\x2d\x35\x38\x2e\x32\x33\x68\x2d\x32\x34\x39\x2e\ +\x31\x36\x63\x2d\x33\x32\x2e\x31\x36\x2c\x30\x2d\x35\x38\x2e\x32\ +\x33\x2c\x32\x36\x2e\x30\x37\x2d\x35\x38\x2e\x32\x33\x2c\x35\x38\ +\x2e\x32\x33\x76\x32\x34\x39\x2e\x31\x36\x63\x30\x2c\x33\x32\x2e\ +\x31\x36\x2c\x32\x36\x2e\x30\x37\x2c\x35\x38\x2e\x32\x33\x2c\x35\ +\x38\x2e\x32\x33\x2c\x35\x38\x2e\x32\x33\x68\x31\x30\x32\x2e\x39\ +\x31\x76\x35\x34\x2e\x38\x34\x68\x2d\x31\x33\x2e\x35\x34\x63\x2d\ +\x38\x2e\x39\x37\x2c\x30\x2d\x31\x36\x2e\x32\x35\x2c\x37\x2e\x32\ +\x37\x2d\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\x32\x35\x76\x36\x2e\ +\x37\x37\x48\x38\x38\x2e\x37\x36\x76\x35\x36\x2e\x38\x37\x68\x31\ +\x35\x39\x2e\x37\x39\x76\x36\x2e\x37\x37\x63\x30\x2c\x38\x2e\x39\ +\x38\x2c\x37\x2e\x32\x38\x2c\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\ +\x32\x35\x2c\x31\x36\x2e\x32\x35\x68\x37\x30\x2e\x34\x31\x63\x38\ +\x2e\x39\x37\x2c\x30\x2c\x31\x36\x2e\x32\x35\x2d\x37\x2e\x32\x37\ +\x2c\x31\x36\x2e\x32\x35\x2d\x31\x36\x2e\x32\x35\x76\x2d\x36\x2e\ +\x37\x37\x68\x31\x35\x39\x2e\x37\x39\x76\x2d\x35\x36\x2e\x38\x37\ +\x68\x2d\x31\x35\x39\x2e\x37\x39\x5a\x4d\x32\x33\x32\x2e\x32\x34\ +\x2c\x33\x31\x30\x2e\x39\x33\x68\x2d\x35\x30\x2e\x34\x31\x76\x2d\ +\x31\x37\x38\x2e\x32\x68\x35\x30\x2e\x34\x31\x76\x31\x37\x38\x2e\ +\x32\x5a\x4d\x33\x31\x38\x2e\x35\x34\x2c\x33\x31\x30\x2e\x39\x33\ +\x68\x2d\x35\x30\x2e\x34\x31\x76\x2d\x31\x37\x38\x2e\x32\x68\x38\ +\x31\x2e\x34\x36\x63\x31\x36\x2e\x31\x32\x2c\x30\x2c\x33\x30\x2e\ +\x31\x32\x2c\x32\x2e\x36\x37\x2c\x34\x32\x2c\x38\x2e\x30\x32\x2c\ +\x31\x31\x2e\x38\x38\x2c\x35\x2e\x33\x35\x2c\x32\x31\x2e\x30\x34\ +\x2c\x31\x32\x2e\x39\x34\x2c\x32\x37\x2e\x35\x2c\x32\x32\x2e\x37\ +\x38\x2c\x36\x2e\x34\x35\x2c\x39\x2e\x38\x35\x2c\x39\x2e\x36\x37\ +\x2c\x32\x31\x2e\x35\x36\x2c\x39\x2e\x36\x37\x2c\x33\x35\x2e\x31\ +\x33\x73\x2d\x33\x2e\x32\x33\x2c\x32\x35\x2e\x30\x34\x2d\x39\x2e\ +\x36\x37\x2c\x33\x34\x2e\x38\x38\x63\x2d\x36\x2e\x34\x35\x2c\x39\ +\x2e\x38\x35\x2d\x31\x35\x2e\x36\x32\x2c\x31\x37\x2e\x34\x34\x2d\ +\x32\x37\x2e\x35\x2c\x32\x32\x2e\x37\x38\x2d\x31\x31\x2e\x38\x38\ +\x2c\x35\x2e\x33\x35\x2d\x32\x35\x2e\x38\x38\x2c\x38\x2e\x30\x32\ +\x2d\x34\x32\x2c\x38\x2e\x30\x32\x68\x2d\x33\x31\x2e\x30\x36\x76\ +\x34\x36\x2e\x35\x39\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ +\ \x00\x00\x05\x80\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -20436,165 +20354,88 @@ \x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ \x32\x33\x2c\x33\x30\x30\x2c\x35\x32\x33\x5a\x22\x2f\x3e\x3c\x2f\ \x73\x76\x67\x3e\ -\x00\x00\x09\xc5\ +\x00\x00\x04\xfe\ \x3c\ -\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ -\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ -\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x65\x30\x65\x30\x64\x66\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x35\x66\x62\ -\x62\x34\x36\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ -\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ -\x31\x38\x2c\x37\x36\x2e\x35\x33\x43\x34\x30\x32\x2c\x37\x38\x2e\ -\x33\x31\x2c\x34\x39\x30\x2e\x31\x2c\x31\x31\x36\x2e\x36\x31\x2c\ -\x35\x36\x33\x2e\x39\x2c\x31\x39\x32\x2e\x36\x33\x63\x31\x34\x2e\ -\x38\x2c\x31\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\ -\x2e\x39\x33\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\x31\x2e\x31\ -\x32\x2c\x31\x33\x2e\x31\x36\x2d\x32\x38\x2e\x36\x32\x2c\x31\x33\ -\x2d\x34\x31\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\x30\x38\x2d\ -\x31\x34\x2e\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\x2e\x37\x31\ -\x2d\x34\x36\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\x38\x2e\x34\ -\x2d\x32\x39\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\x37\x35\x2d\ -\x31\x32\x37\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\x35\x34\x2d\ -\x39\x2e\x31\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\x2e\x33\x32\ -\x2d\x31\x35\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x2d\x34\x35\x2c\ -\x31\x37\x2e\x30\x35\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\ -\x33\x2d\x31\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x33\x2d\x38\ -\x2e\x35\x37\x2c\x39\x2e\x31\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\ -\x32\x2e\x33\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\ -\x30\x39\x2d\x37\x2e\x35\x36\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\ -\x30\x38\x2d\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x41\x33\x38\x30\ -\x2c\x33\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x39\x32\x2e\x33\x37\ -\x2c\x31\x34\x33\x2e\x39\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\ -\x38\x39\x2e\x39\x33\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\ -\x37\x2d\x36\x30\x2e\x34\x36\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\ -\x39\x2e\x34\x31\x2c\x32\x37\x37\x2e\x34\x33\x2c\x37\x38\x2c\x32\ -\x39\x30\x2e\x31\x38\x2c\x37\x36\x2e\x35\x33\x5a\x22\x2f\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x31\x22\x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x32\x32\x2e\x35\ -\x35\x63\x2d\x32\x32\x2c\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\ -\x2e\x34\x36\x2d\x33\x39\x2e\x31\x31\x2d\x34\x32\x2e\x31\x32\x2c\ -\x30\x2d\x32\x33\x2e\x33\x37\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\ -\x2e\x38\x32\x2c\x33\x38\x2e\x38\x36\x2d\x34\x31\x2e\x37\x39\x2c\ -\x32\x32\x2e\x32\x35\x2c\x30\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\ -\x2e\x31\x38\x2c\x33\x39\x2e\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\ -\x33\x32\x32\x2e\x31\x2c\x35\x32\x32\x2e\x35\x34\x2c\x33\x30\x30\ -\x2c\x35\x32\x32\x2e\x35\x35\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\ -\x3d\x22\x4d\x34\x35\x36\x2e\x37\x32\x2c\x33\x31\x32\x2e\x37\x61\ -\x33\x32\x2e\x35\x33\x2c\x33\x32\x2e\x35\x33\x2c\x30\x2c\x30\x2c\ -\x30\x2d\x31\x36\x2e\x34\x31\x2c\x33\x2e\x37\x32\x71\x33\x2e\x30\ -\x36\x2c\x33\x2c\x36\x2c\x36\x2e\x32\x31\x63\x35\x2e\x36\x39\x2c\ -\x36\x2e\x30\x35\x2c\x31\x32\x2e\x35\x33\x2c\x39\x2e\x33\x2c\x32\ -\x33\x2e\x35\x32\x2c\x39\x2e\x31\x32\x61\x32\x33\x2e\x39\x2c\x32\ -\x33\x2e\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x33\x2e\x35\x2d\x35\ -\x2e\x33\x31\x41\x33\x35\x2e\x35\x33\x2c\x33\x35\x2e\x35\x33\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x34\x35\x36\x2e\x37\x32\x2c\x33\x31\x32\ -\x2e\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x34\ -\x36\x2e\x32\x36\x2c\x32\x36\x36\x2e\x33\x34\x61\x37\x36\x2e\x32\ -\x33\x2c\x37\x36\x2e\x32\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x37\ -\x2e\x39\x31\x2c\x31\x2e\x34\x37\x71\x2d\x34\x2e\x37\x36\x2d\x34\ -\x2e\x35\x2d\x39\x2e\x37\x33\x2d\x38\x2e\x36\x35\x63\x2d\x36\x30\ -\x2e\x32\x32\x2d\x35\x30\x2e\x33\x31\x2d\x31\x32\x38\x2e\x33\x31\ -\x2d\x37\x30\x2d\x32\x30\x33\x2e\x39\x31\x2d\x35\x38\x2e\x33\x37\ -\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\x31\x2d\x31\x30\x35\x2e\ -\x38\x34\x2c\x33\x36\x2e\x30\x35\x2d\x31\x34\x36\x2e\x38\x34\x2c\ -\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\x37\x2c\x38\x2e\x37\x33\x2d\ -\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\x36\x33\x2d\x36\x2e\x38\x39\ -\x2c\x33\x31\x2e\x35\x33\x2c\x37\x2e\x32\x34\x2c\x32\x32\x2c\x33\ -\x31\x2e\x35\x36\x2c\x32\x36\x2e\x38\x33\x2c\x34\x37\x2e\x39\x31\ -\x2c\x31\x30\x2c\x34\x39\x2e\x32\x32\x2d\x35\x30\x2e\x35\x37\x2c\ -\x31\x30\x38\x2d\x37\x31\x2e\x30\x37\x2c\x31\x37\x35\x2e\x33\x39\ -\x2d\x36\x31\x2e\x34\x61\x31\x38\x37\x2c\x31\x38\x37\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x37\x32\x2e\x36\x36\x2c\x32\x36\x2e\x33\x39\x41\ -\x38\x30\x2e\x36\x2c\x38\x30\x2e\x36\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x34\x34\x36\x2e\x32\x36\x2c\x32\x36\x36\x2e\x33\x34\x5a\x22\x2f\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x36\x38\x2e\x38\x34\x2c\ -\x33\x37\x37\x2e\x37\x38\x63\x31\x2e\x38\x38\x2d\x2e\x30\x36\x2c\ -\x33\x2e\x37\x31\x2c\x30\x2c\x35\x2e\x34\x37\x2c\x30\x68\x31\x2e\ -\x30\x39\x76\x2d\x33\x2e\x34\x39\x63\x30\x2d\x38\x2e\x32\x39\x2c\ -\x30\x2d\x31\x36\x2e\x33\x34\x2c\x30\x2d\x32\x34\x2e\x33\x39\x61\ -\x38\x39\x2e\x37\x37\x2c\x38\x39\x2e\x37\x37\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x2e\x35\x35\x2d\x31\x30\x63\x2d\x32\x38\x2e\x38\x39\x2d\ -\x31\x38\x2e\x33\x31\x2d\x36\x31\x2e\x32\x36\x2d\x32\x35\x2e\x32\ -\x33\x2d\x39\x37\x2e\x31\x37\x2d\x32\x30\x2e\x31\x37\x2d\x33\x32\ -\x2e\x38\x2c\x34\x2e\x36\x33\x2d\x36\x30\x2e\x38\x2c\x32\x30\x2e\ -\x30\x39\x2d\x38\x34\x2e\x36\x34\x2c\x34\x34\x2e\x34\x34\x2d\x36\ -\x2c\x36\x2e\x31\x36\x2d\x39\x2e\x36\x33\x2c\x31\x33\x2e\x35\x39\ -\x2d\x39\x2e\x36\x31\x2c\x32\x32\x2e\x38\x31\x61\x36\x39\x2e\x32\ -\x38\x2c\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x37\x2e\x33\x38\x63\x35\x2e\x36\x33\x2c\x32\x33\x2e\x32\x37\x2c\ -\x33\x31\x2e\x32\x37\x2c\x33\x30\x2e\x32\x38\x2c\x34\x37\x2e\x39\ -\x33\x2c\x31\x33\x2e\x32\x37\x2c\x32\x34\x2e\x36\x31\x2d\x32\x35\ -\x2e\x31\x32\x2c\x35\x33\x2e\x37\x35\x2d\x33\x33\x2e\x38\x37\x2c\ -\x38\x37\x2d\x32\x36\x2e\x37\x37\x41\x38\x33\x2e\x37\x35\x2c\x38\ -\x33\x2e\x37\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x34\x39\x2e\x31\ -\x35\x2c\x33\x39\x33\x43\x33\x35\x31\x2e\x31\x37\x2c\x33\x38\x33\ -\x2e\x34\x37\x2c\x33\x35\x38\x2c\x33\x37\x38\x2e\x31\x33\x2c\x33\ -\x36\x38\x2e\x38\x34\x2c\x33\x37\x37\x2e\x37\x38\x5a\x22\x2f\x3e\ -\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\ -\x2d\x31\x22\x20\x64\x3d\x22\x4d\x35\x34\x32\x2e\x35\x31\x2c\x33\ -\x38\x36\x2e\x32\x38\x63\x2d\x34\x2e\x35\x39\x2d\x2e\x31\x31\x2d\ -\x39\x2e\x31\x39\x2c\x30\x2d\x31\x34\x2e\x31\x31\x2c\x30\x76\x2d\ -\x2e\x33\x37\x48\x34\x31\x33\x2e\x32\x63\x30\x2d\x31\x35\x2d\x31\ -\x2e\x33\x33\x2d\x32\x39\x2e\x37\x39\x2e\x33\x31\x2d\x34\x34\x2e\ -\x31\x39\x2c\x32\x2e\x35\x32\x2d\x32\x32\x2e\x30\x38\x2c\x32\x32\ -\x2e\x36\x31\x2d\x33\x38\x2e\x33\x38\x2c\x34\x33\x2e\x35\x37\x2d\ -\x33\x37\x2e\x34\x39\x61\x34\x34\x2c\x34\x34\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x33\x34\x2e\x37\x35\x2c\x31\x39\x2e\x34\x31\x6c\x32\x31\ -\x2e\x32\x34\x2d\x32\x30\x2e\x34\x34\x63\x2d\x31\x35\x2e\x31\x34\ -\x2d\x32\x30\x2e\x33\x2d\x34\x30\x2e\x33\x33\x2d\x33\x31\x2e\x38\ -\x31\x2d\x36\x35\x2e\x36\x37\x2d\x32\x38\x2e\x34\x31\x2d\x33\x36\ -\x2e\x37\x32\x2c\x34\x2e\x39\x32\x2d\x36\x33\x2e\x36\x33\x2c\x33\ -\x36\x2e\x37\x34\x2d\x36\x33\x2e\x35\x31\x2c\x37\x35\x2e\x30\x36\ -\x2c\x30\x2c\x31\x31\x2e\x38\x38\x2c\x30\x2c\x32\x33\x2e\x37\x35\ -\x2c\x30\x2c\x33\x36\x2e\x34\x31\x68\x2d\x37\x2e\x34\x33\x63\x2d\ -\x32\x2e\x34\x35\x2c\x30\x2d\x34\x2e\x39\x2d\x2e\x30\x35\x2d\x37\ -\x2e\x33\x34\x2c\x30\x2d\x38\x2e\x35\x33\x2e\x32\x38\x2d\x31\x32\ -\x2e\x31\x32\x2c\x34\x2e\x31\x36\x2d\x31\x32\x2e\x31\x32\x2c\x31\ -\x33\x2e\x31\x31\x71\x30\x2c\x35\x34\x2e\x38\x37\x2c\x30\x2c\x31\ -\x30\x39\x2e\x37\x35\x63\x30\x2c\x31\x30\x2e\x34\x34\x2c\x33\x2e\ -\x35\x39\x2c\x31\x34\x2e\x33\x31\x2c\x31\x33\x2e\x35\x32\x2c\x31\ -\x34\x2e\x33\x32\x71\x38\x35\x2e\x36\x33\x2c\x30\x2c\x31\x37\x31\ -\x2e\x32\x37\x2c\x30\x63\x39\x2e\x32\x34\x2c\x30\x2c\x31\x33\x2e\ -\x35\x33\x2d\x34\x2e\x34\x33\x2c\x31\x33\x2e\x35\x33\x2d\x31\x34\ -\x71\x2e\x30\x36\x2d\x35\x34\x2e\x38\x37\x2c\x30\x2d\x31\x30\x39\ -\x2e\x37\x35\x43\x35\x35\x35\x2e\x33\x31\x2c\x33\x39\x31\x2c\x35\ -\x35\x31\x2c\x33\x38\x36\x2e\x34\x39\x2c\x35\x34\x32\x2e\x35\x31\ -\x2c\x33\x38\x36\x2e\x32\x38\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\ -\x3d\x22\x4d\x35\x32\x38\x2e\x32\x39\x2c\x34\x31\x36\x2e\x36\x38\ -\x63\x2d\x2e\x31\x37\x2e\x32\x31\x2d\x2e\x32\x39\x2e\x34\x2d\x2e\ -\x34\x34\x2e\x35\x36\x6c\x2d\x2e\x37\x36\x2e\x37\x37\x71\x2d\x34\ -\x34\x2e\x36\x39\x2c\x34\x34\x2e\x36\x37\x2d\x38\x39\x2e\x33\x34\ -\x2c\x38\x39\x2e\x33\x36\x63\x2d\x31\x2c\x31\x2d\x31\x2e\x35\x34\ -\x2c\x31\x2e\x32\x37\x2d\x32\x2e\x37\x32\x2e\x30\x37\x71\x2d\x31\ -\x39\x2e\x33\x36\x2d\x31\x39\x2e\x35\x33\x2d\x33\x38\x2e\x38\x39\ -\x2d\x33\x38\x2e\x39\x31\x63\x2d\x2e\x38\x39\x2d\x2e\x38\x38\x2d\ -\x31\x2e\x30\x38\x2d\x31\x2e\x33\x2d\x2e\x30\x35\x2d\x32\x2e\x32\ -\x39\x2c\x33\x2e\x35\x35\x2d\x33\x2e\x33\x38\x2c\x37\x2d\x36\x2e\ -\x38\x36\x2c\x31\x30\x2e\x34\x2d\x31\x30\x2e\x34\x2c\x31\x2e\x31\ -\x31\x2d\x31\x2e\x31\x36\x2c\x31\x2e\x37\x35\x2d\x31\x2e\x31\x35\ -\x2c\x32\x2e\x38\x37\x2c\x30\x2c\x38\x2e\x35\x38\x2c\x38\x2e\x36\ -\x38\x2c\x31\x37\x2e\x32\x34\x2c\x31\x37\x2e\x32\x38\x2c\x32\x35\ -\x2e\x38\x33\x2c\x32\x35\x2e\x39\x35\x2e\x39\x33\x2e\x39\x34\x2c\ -\x31\x2e\x33\x39\x2c\x31\x2e\x30\x36\x2c\x32\x2e\x34\x32\x2c\x30\ -\x71\x33\x38\x2e\x32\x2d\x33\x38\x2e\x33\x32\x2c\x37\x36\x2e\x34\ -\x38\x2d\x37\x36\x2e\x35\x38\x63\x31\x2e\x31\x33\x2d\x31\x2e\x31\ -\x33\x2c\x31\x2e\x37\x33\x2d\x31\x2e\x32\x35\x2c\x32\x2e\x38\x38\ -\x2c\x30\x2c\x33\x2e\x31\x39\x2c\x33\x2e\x33\x37\x2c\x36\x2e\x35\ -\x33\x2c\x36\x2e\x35\x39\x2c\x39\x2e\x38\x31\x2c\x39\x2e\x38\x37\ -\x43\x35\x32\x37\x2e\x32\x38\x2c\x34\x31\x35\x2e\x35\x38\x2c\x35\ -\x32\x37\x2e\x37\x35\x2c\x34\x31\x36\x2e\x31\x31\x2c\x35\x32\x38\ -\x2e\x32\x39\x2c\x34\x31\x36\x2e\x36\x38\x5a\x22\x2f\x3e\x3c\x2f\ -\x73\x76\x67\x3e\ +\x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ +\x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ +\x2d\x38\x22\x3f\x3e\x0a\x3c\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\ +\x61\x79\x65\x72\x5f\x31\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\ +\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\ +\x73\x3d\x22\x68\x74\x74\x70\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\ +\x2e\x6f\x72\x67\x2f\x32\x30\x30\x30\x2f\x73\x76\x67\x22\x20\x76\ +\x69\x65\x77\x42\x6f\x78\x3d\x22\x30\x20\x30\x20\x36\x30\x30\x20\ +\x36\x30\x30\x22\x3e\x0a\x20\x20\x3c\x64\x65\x66\x73\x3e\x0a\x20\ +\x20\x20\x20\x3c\x73\x74\x79\x6c\x65\x3e\x0a\x20\x20\x20\x20\x20\ +\x20\x2e\x63\x6c\x73\x2d\x31\x20\x7b\x0a\x20\x20\x20\x20\x20\x20\ +\x20\x20\x66\x69\x6c\x6c\x3a\x20\x23\x65\x30\x65\x30\x64\x66\x3b\ +\x0a\x20\x20\x20\x20\x20\x20\x7d\x0a\x20\x20\x20\x20\x3c\x2f\x73\ +\x74\x79\x6c\x65\x3e\x0a\x20\x20\x3c\x2f\x64\x65\x66\x73\x3e\x0a\ +\x20\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ +\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x39\x38\x2e\x38\x33\ +\x2c\x32\x31\x36\x2e\x38\x31\x68\x2d\x31\x39\x37\x2e\x36\x36\x63\ +\x2d\x31\x36\x2e\x36\x39\x2c\x30\x2d\x32\x30\x2e\x35\x35\x2c\x33\ +\x35\x2e\x36\x35\x2d\x31\x38\x2e\x30\x37\x2c\x35\x32\x2e\x31\x36\ +\x6c\x32\x31\x2e\x35\x31\x2c\x31\x32\x32\x2e\x32\x35\x63\x32\x2e\ +\x30\x31\x2c\x31\x33\x2e\x33\x35\x2c\x31\x33\x2e\x34\x38\x2c\x32\ +\x33\x2e\x32\x33\x2c\x32\x36\x2e\x39\x39\x2c\x32\x33\x2e\x32\x33\ +\x68\x31\x2e\x36\x32\x63\x2d\x35\x2e\x38\x37\x2c\x30\x2d\x31\x30\ +\x2e\x36\x32\x2c\x34\x2e\x37\x36\x2d\x31\x30\x2e\x36\x32\x2c\x31\ +\x30\x2e\x36\x32\x73\x34\x2e\x37\x36\x2c\x31\x30\x2e\x36\x32\x2c\ +\x31\x30\x2e\x36\x32\x2c\x31\x30\x2e\x36\x32\x63\x2d\x35\x2e\x38\ +\x37\x2c\x30\x2d\x31\x30\x2e\x36\x32\x2c\x34\x2e\x37\x36\x2d\x31\ +\x30\x2e\x36\x32\x2c\x31\x30\x2e\x36\x32\x73\x34\x2e\x37\x36\x2c\ +\x31\x30\x2e\x36\x32\x2c\x31\x30\x2e\x36\x32\x2c\x31\x30\x2e\x36\ +\x32\x63\x2d\x35\x2e\x38\x37\x2c\x30\x2d\x31\x30\x2e\x36\x32\x2c\ +\x34\x2e\x37\x36\x2d\x31\x30\x2e\x36\x32\x2c\x31\x30\x2e\x36\x32\ +\x73\x34\x2e\x37\x36\x2c\x31\x30\x2e\x36\x32\x2c\x31\x30\x2e\x36\ +\x32\x2c\x31\x30\x2e\x36\x32\x68\x33\x34\x2e\x39\x31\x76\x38\x33\ +\x2e\x34\x39\x68\x36\x33\x2e\x37\x35\x76\x2d\x38\x33\x2e\x34\x39\ +\x68\x33\x34\x2e\x39\x31\x63\x35\x2e\x38\x37\x2c\x30\x2c\x31\x30\ +\x2e\x36\x32\x2d\x34\x2e\x37\x36\x2c\x31\x30\x2e\x36\x32\x2d\x31\ +\x30\x2e\x36\x32\x73\x2d\x34\x2e\x37\x36\x2d\x31\x30\x2e\x36\x32\ +\x2d\x31\x30\x2e\x36\x32\x2d\x31\x30\x2e\x36\x32\x63\x35\x2e\x38\ +\x37\x2c\x30\x2c\x31\x30\x2e\x36\x32\x2d\x34\x2e\x37\x36\x2c\x31\ +\x30\x2e\x36\x32\x2d\x31\x30\x2e\x36\x32\x73\x2d\x34\x2e\x37\x36\ +\x2d\x31\x30\x2e\x36\x32\x2d\x31\x30\x2e\x36\x32\x2d\x31\x30\x2e\ +\x36\x32\x63\x35\x2e\x38\x37\x2c\x30\x2c\x31\x30\x2e\x36\x32\x2d\ +\x34\x2e\x37\x36\x2c\x31\x30\x2e\x36\x32\x2d\x31\x30\x2e\x36\x32\ +\x73\x2d\x34\x2e\x37\x36\x2d\x31\x30\x2e\x36\x32\x2d\x31\x30\x2e\ +\x36\x32\x2d\x31\x30\x2e\x36\x32\x68\x31\x2e\x36\x32\x63\x31\x33\ +\x2e\x35\x31\x2c\x30\x2c\x32\x34\x2e\x39\x38\x2d\x39\x2e\x38\x38\ +\x2c\x32\x36\x2e\x39\x39\x2d\x32\x33\x2e\x32\x33\x6c\x32\x31\x2e\ +\x35\x31\x2d\x31\x32\x32\x2e\x32\x35\x63\x32\x2e\x34\x38\x2d\x31\ +\x36\x2e\x35\x2d\x31\x2e\x33\x38\x2d\x35\x32\x2e\x31\x36\x2d\x31\ +\x38\x2e\x30\x37\x2d\x35\x32\x2e\x31\x36\x5a\x22\x2f\x3e\x0a\x20\ +\x20\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ +\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x35\x37\x2e\x32\x2c\x35\ +\x38\x2e\x30\x35\x68\x2d\x34\x2e\x38\x31\x6c\x2d\x31\x2e\x31\x33\ +\x2d\x33\x2e\x38\x63\x30\x2d\x38\x2e\x38\x2d\x37\x2e\x31\x34\x2d\ +\x31\x35\x2e\x39\x34\x2d\x31\x35\x2e\x39\x34\x2d\x31\x35\x2e\x39\ +\x34\x68\x2d\x37\x30\x2e\x36\x34\x63\x2d\x38\x2e\x38\x2c\x30\x2d\ +\x31\x35\x2e\x39\x34\x2c\x37\x2e\x31\x33\x2d\x31\x35\x2e\x39\x34\ +\x2c\x31\x35\x2e\x39\x34\x6c\x2d\x31\x2e\x31\x33\x2c\x33\x2e\x38\ +\x68\x2d\x34\x2e\x38\x31\x63\x2d\x32\x32\x2e\x32\x36\x2c\x30\x2d\ +\x34\x30\x2e\x33\x31\x2c\x31\x38\x2e\x30\x35\x2d\x34\x30\x2e\x33\ +\x31\x2c\x34\x30\x2e\x33\x31\x76\x31\x30\x37\x2e\x34\x34\x68\x31\ +\x39\x35\x2e\x30\x33\x76\x2d\x31\x30\x37\x2e\x34\x34\x63\x30\x2d\ +\x32\x32\x2e\x32\x36\x2d\x31\x38\x2e\x30\x35\x2d\x34\x30\x2e\x33\ +\x31\x2d\x34\x30\x2e\x33\x31\x2d\x34\x30\x2e\x33\x31\x5a\x4d\x32\ +\x34\x32\x2e\x32\x2c\x31\x31\x34\x2e\x37\x31\x68\x2d\x31\x36\x2e\ +\x31\x39\x76\x2d\x34\x30\x2e\x35\x32\x68\x31\x36\x2e\x31\x39\x76\ +\x34\x30\x2e\x35\x32\x5a\x4d\x32\x36\x38\x2e\x35\x36\x2c\x31\x31\ +\x34\x2e\x37\x31\x68\x2d\x31\x36\x2e\x31\x39\x76\x2d\x34\x30\x2e\ +\x35\x32\x68\x31\x36\x2e\x31\x39\x76\x34\x30\x2e\x35\x32\x5a\x4d\ +\x32\x39\x34\x2e\x39\x32\x2c\x31\x31\x34\x2e\x37\x31\x68\x2d\x31\ +\x36\x2e\x31\x39\x76\x2d\x34\x30\x2e\x35\x32\x68\x31\x36\x2e\x31\ +\x39\x76\x34\x30\x2e\x35\x32\x5a\x4d\x33\x32\x31\x2e\x32\x37\x2c\ +\x31\x31\x34\x2e\x37\x31\x68\x2d\x31\x36\x2e\x31\x39\x76\x2d\x34\ +\x30\x2e\x35\x32\x68\x31\x36\x2e\x31\x39\x76\x34\x30\x2e\x35\x32\ +\x5a\x4d\x33\x34\x37\x2e\x36\x33\x2c\x31\x31\x34\x2e\x37\x31\x68\ +\x2d\x31\x36\x2e\x31\x39\x76\x2d\x34\x30\x2e\x35\x32\x68\x31\x36\ +\x2e\x31\x39\x76\x34\x30\x2e\x35\x32\x5a\x4d\x33\x37\x33\x2e\x39\ +\x39\x2c\x31\x31\x34\x2e\x37\x31\x68\x2d\x31\x36\x2e\x31\x39\x76\ +\x2d\x34\x30\x2e\x35\x32\x68\x31\x36\x2e\x31\x39\x76\x34\x30\x2e\ +\x35\x32\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x05\x95\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -20856,108 +20697,6 @@ \x38\x39\x2e\x36\x38\x2c\x36\x37\x2e\x34\x39\x2c\x36\x37\x2e\x34\ \x39\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x33\x36\x2e\x36\x32\x2c\x32\ \x37\x38\x2e\x34\x38\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x06\x37\ -\x3c\ -\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ -\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ -\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x39\x32\x39\ -\x34\x39\x37\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ -\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ -\x32\x36\x2c\x39\x35\x2e\x35\x37\x63\x31\x31\x30\x2e\x39\x34\x2c\ -\x31\x2e\x37\x37\x2c\x31\x39\x38\x2e\x33\x39\x2c\x33\x39\x2e\x37\ -\x38\x2c\x32\x37\x31\x2e\x36\x32\x2c\x31\x31\x35\x2e\x32\x31\x2c\ -\x31\x34\x2e\x36\x39\x2c\x31\x35\x2e\x31\x34\x2c\x31\x31\x2e\x37\ -\x32\x2c\x33\x33\x2e\x36\x37\x2c\x33\x2e\x32\x34\x2c\x34\x33\x2e\ -\x37\x31\x2d\x31\x31\x2c\x31\x33\x2e\x30\x36\x2d\x32\x38\x2e\x34\ -\x2c\x31\x32\x2e\x38\x39\x2d\x34\x31\x2e\x31\x35\x2e\x38\x33\x2d\ -\x31\x35\x2d\x31\x34\x2e\x31\x36\x2d\x33\x30\x2d\x32\x38\x2e\x35\ -\x2d\x34\x36\x2e\x32\x35\x2d\x34\x30\x2e\x37\x39\x43\x34\x33\x39\ -\x2e\x36\x31\x2c\x31\x38\x35\x2e\x37\x34\x2c\x33\x39\x37\x2c\x31\ -\x36\x38\x2e\x31\x34\x2c\x33\x35\x30\x2e\x39\x34\x2c\x31\x36\x30\ -\x2e\x33\x63\x2d\x35\x33\x2e\x36\x2d\x39\x2e\x31\x33\x2d\x31\x30\ -\x36\x2e\x30\x39\x2d\x34\x2e\x32\x39\x2d\x31\x35\x37\x2e\x32\x38\ -\x2c\x31\x35\x2e\x31\x31\x43\x31\x34\x39\x2c\x31\x39\x32\x2e\x33\ -\x33\x2c\x31\x31\x30\x2c\x32\x31\x39\x2c\x37\x36\x2e\x34\x2c\x32\ -\x35\x34\x2e\x37\x33\x63\x2d\x38\x2e\x35\x2c\x39\x2d\x31\x38\x2e\ -\x35\x2c\x31\x32\x2e\x32\x33\x2d\x33\x30\x2c\x37\x2e\x39\x31\x2d\ -\x31\x39\x2e\x39\x33\x2d\x37\x2e\x35\x2d\x32\x35\x2d\x33\x33\x2e\ -\x38\x32\x2d\x39\x2e\x36\x36\x2d\x35\x30\x2e\x32\x32\x61\x33\x37\ -\x37\x2e\x32\x34\x2c\x33\x37\x37\x2e\x32\x34\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x35\x37\x2e\x31\x38\x2d\x35\x30\x63\x34\x32\x2e\x37\x2d\ -\x33\x30\x2e\x33\x35\x2c\x38\x39\x2e\x32\x34\x2d\x35\x30\x2e\x37\ -\x31\x2c\x31\x33\x39\x2e\x36\x39\x2d\x36\x30\x43\x32\x35\x35\x2e\ -\x34\x31\x2c\x39\x38\x2e\x34\x33\x2c\x32\x37\x37\x2e\x36\x2c\x39\ -\x37\x2e\x30\x35\x2c\x32\x39\x30\x2e\x32\x36\x2c\x39\x35\x2e\x35\ -\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x36\x38\ -\x2e\x35\x31\x2c\x33\x34\x38\x2e\x38\x34\x63\x2d\x31\x30\x2e\x39\ -\x31\x2e\x31\x38\x2d\x31\x37\x2e\x36\x39\x2d\x33\x2d\x32\x33\x2e\ -\x33\x34\x2d\x39\x2d\x33\x32\x2e\x31\x32\x2d\x33\x34\x2e\x31\x38\ -\x2d\x37\x30\x2e\x35\x39\x2d\x35\x35\x2e\x35\x2d\x31\x31\x35\x2e\ -\x33\x36\x2d\x36\x31\x2e\x39\x33\x2d\x36\x36\x2e\x38\x35\x2d\x39\ -\x2e\x35\x39\x2d\x31\x32\x35\x2e\x32\x2c\x31\x30\x2e\x37\x35\x2d\ -\x31\x37\x34\x2c\x36\x30\x2e\x39\x33\x2d\x31\x36\x2e\x32\x32\x2c\ -\x31\x36\x2e\x36\x38\x2d\x34\x30\x2e\x33\x36\x2c\x31\x31\x2e\x39\ -\x32\x2d\x34\x37\x2e\x35\x34\x2d\x31\x30\x2d\x33\x2e\x38\x38\x2d\ -\x31\x31\x2e\x38\x2d\x31\x2e\x33\x37\x2d\x32\x32\x2e\x36\x32\x2c\ -\x36\x2e\x38\x34\x2d\x33\x31\x2e\x32\x38\x2c\x34\x30\x2e\x36\x38\ -\x2d\x34\x32\x2e\x39\x2c\x38\x39\x2e\x30\x36\x2d\x36\x39\x2e\x39\ -\x33\x2c\x31\x34\x35\x2e\x37\x31\x2d\x37\x38\x2e\x36\x37\x2c\x37\ -\x35\x2d\x31\x31\x2e\x35\x37\x2c\x31\x34\x32\x2e\x35\x39\x2c\x38\ -\x2c\x32\x30\x32\x2e\x33\x35\x2c\x35\x37\x2e\x39\x31\x61\x32\x30\ -\x32\x2e\x37\x31\x2c\x32\x30\x32\x2e\x37\x31\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x32\x33\x2e\x30\x37\x2c\x32\x32\x2e\x35\x33\x63\x38\x2c\ -\x39\x2e\x32\x32\x2c\x39\x2e\x34\x33\x2c\x32\x30\x2e\x34\x35\x2c\ -\x34\x2e\x35\x36\x2c\x33\x32\x2e\x30\x38\x53\x34\x37\x37\x2e\x31\ -\x39\x2c\x33\x34\x38\x2e\x32\x39\x2c\x34\x36\x38\x2e\x35\x31\x2c\ -\x33\x34\x38\x2e\x38\x34\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\ -\x22\x4d\x31\x38\x35\x2e\x33\x36\x2c\x34\x30\x33\x2e\x36\x31\x63\ -\x30\x2d\x39\x2e\x31\x35\x2c\x33\x2e\x35\x35\x2d\x31\x36\x2e\x35\ -\x32\x2c\x39\x2e\x35\x34\x2d\x32\x32\x2e\x36\x34\x2c\x32\x33\x2e\ -\x36\x36\x2d\x32\x34\x2e\x31\x36\x2c\x35\x31\x2e\x34\x34\x2d\x33\ -\x39\x2e\x35\x2c\x38\x34\x2d\x34\x34\x2e\x30\x39\x2c\x34\x38\x2e\ -\x32\x38\x2d\x36\x2e\x38\x31\x2c\x39\x30\x2e\x31\x34\x2c\x38\x2e\ -\x31\x38\x2c\x31\x32\x35\x2e\x36\x37\x2c\x34\x33\x2e\x36\x32\x2c\ -\x31\x30\x2e\x30\x39\x2c\x31\x30\x2e\x30\x37\x2c\x31\x32\x2e\x37\ -\x2c\x32\x33\x2e\x38\x32\x2c\x37\x2e\x33\x39\x2c\x33\x35\x2e\x37\ -\x37\x2d\x38\x2e\x33\x39\x2c\x31\x38\x2e\x38\x34\x2d\x33\x30\x2e\ -\x37\x37\x2c\x32\x33\x2e\x32\x37\x2d\x34\x35\x2c\x38\x2e\x36\x2d\ -\x31\x33\x2e\x33\x2d\x31\x33\x2e\x37\x33\x2d\x32\x38\x2e\x35\x38\ -\x2d\x32\x33\x2e\x34\x35\x2d\x34\x36\x2e\x37\x35\x2d\x32\x37\x2e\ -\x33\x34\x2d\x33\x33\x2d\x37\x2d\x36\x31\x2e\x38\x38\x2c\x31\x2e\ -\x36\x34\x2d\x38\x36\x2e\x33\x2c\x32\x36\x2e\x35\x37\x2d\x31\x36\ -\x2e\x35\x34\x2c\x31\x36\x2e\x38\x38\x2d\x34\x32\x2c\x39\x2e\x39\ -\x32\x2d\x34\x37\x2e\x35\x37\x2d\x31\x33\x2e\x31\x37\x41\x37\x30\ -\x2e\x37\x38\x2c\x37\x30\x2e\x37\x38\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x31\x38\x35\x2e\x33\x36\x2c\x34\x30\x33\x2e\x36\x31\x5a\x22\x2f\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x33\x38\ -\x2e\x31\x38\x63\x2d\x32\x31\x2e\x38\x33\x2c\x30\x2d\x33\x38\x2e\ -\x38\x34\x2d\x31\x38\x2e\x33\x31\x2d\x33\x38\x2e\x38\x31\x2d\x34\ -\x31\x2e\x37\x39\x2c\x30\x2d\x32\x33\x2e\x32\x2c\x31\x37\x2e\x30\ -\x36\x2d\x34\x31\x2e\x35\x31\x2c\x33\x38\x2e\x35\x36\x2d\x34\x31\ -\x2e\x34\x37\x2c\x32\x32\x2e\x30\x39\x2c\x30\x2c\x33\x39\x2c\x31\ -\x38\x2c\x33\x39\x2c\x34\x31\x2e\x34\x39\x53\x33\x32\x31\x2e\x39\ -\x31\x2c\x35\x33\x38\x2e\x31\x37\x2c\x33\x30\x30\x2c\x35\x33\x38\ -\x2e\x31\x38\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x31\ -\x35\x36\x2e\x37\x36\x2c\x36\x31\x2e\x39\x31\x2c\x33\x30\x30\x2c\ -\x32\x36\x30\x2e\x36\x38\x2c\x34\x34\x33\x2e\x32\x35\x2c\x36\x31\ -\x2e\x39\x31\x68\x35\x37\x2e\x39\x33\x4c\x33\x32\x39\x2c\x33\x30\ -\x30\x2e\x38\x38\x6c\x31\x37\x32\x2e\x32\x32\x2c\x32\x33\x39\x48\ -\x34\x34\x33\x2e\x32\x35\x4c\x33\x30\x30\x2c\x33\x34\x31\x2e\x30\ -\x37\x2c\x31\x35\x36\x2e\x37\x36\x2c\x35\x33\x39\x2e\x38\x34\x48\ -\x39\x38\x2e\x38\x31\x4c\x32\x37\x31\x2c\x33\x30\x30\x2e\x38\x38\ -\x2c\x39\x38\x2e\x38\x33\x2c\x36\x31\x2e\x39\x31\x5a\x22\x2f\x3e\ -\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x0a\x76\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -27026,14 +26765,6 @@ \x0c\xa5\x75\xc7\ \x00\x6c\ \x00\x6f\x00\x67\x00\x6f\x00\x5f\x00\x42\x00\x4c\x00\x4f\x00\x43\x00\x4b\x00\x53\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0d\ -\x00\xdd\x57\xa7\ -\x00\x31\ -\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0d\ -\x02\xdd\x57\xa7\ -\x00\x30\ -\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0f\ \x05\x15\x19\xa7\ \x00\x77\ @@ -27046,10 +26777,18 @@ \x07\xb9\x8d\x87\ \x00\x68\ \x00\x6f\x00\x74\x00\x73\x00\x70\x00\x6f\x00\x74\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0f\ -\x08\x5a\xc7\xe7\ -\x00\x77\ -\x00\x69\x00\x66\x00\x69\x00\x5f\x00\x6c\x00\x6f\x00\x63\x00\x6b\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0d\ +\x00\xdd\x57\xa7\ +\x00\x31\ +\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0d\ +\x02\xdd\x57\xa7\ +\x00\x30\ +\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ +\x00\x0d\ +\x03\x52\x04\x07\ +\x00\x73\ +\x00\x74\x00\x61\x00\x74\x00\x69\x00\x63\x00\x5f\x00\x69\x00\x70\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0d\ \x0a\xdd\x57\x87\ \x00\x34\ @@ -27058,11 +26797,11 @@ \x0c\xdd\x57\x87\ \x00\x33\ \x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x11\ -\x0d\x4a\xc3\xc7\ -\x00\x77\ -\x00\x69\x00\x66\x00\x69\x00\x5f\x00\x75\x00\x6e\x00\x6c\x00\x6f\x00\x63\x00\x6b\x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ -\ +\x00\x16\ +\x0d\x98\xb3\x87\ +\x00\x65\ +\x00\x74\x00\x68\x00\x65\x00\x72\x00\x6e\x00\x65\x00\x74\x00\x5f\x00\x63\x00\x6f\x00\x6e\x00\x6e\x00\x65\x00\x63\x00\x74\x00\x65\ +\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0d\ \x0e\xdd\x57\x87\ \x00\x32\ @@ -27072,10 +26811,6 @@ \x00\x32\ \x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x5f\x00\x70\x00\x72\x00\x6f\x00\x74\x00\x65\x00\x63\x00\x74\ \x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0b\ -\x0f\x22\xf7\x67\ -\x00\x6e\ -\x00\x6f\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x17\ \x0f\x29\x74\x27\ \x00\x33\ @@ -27442,81 +27177,80 @@ \x00\x00\x0f\x00\x00\x00\x00\x00\x00\x01\x00\x04\x9a\xb0\ \x00\x00\x0f\x2c\x00\x00\x00\x00\x00\x01\x00\x04\xa1\x97\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x81\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x10\x00\x00\x00\x82\ -\x00\x00\x0f\x50\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xfb\ -\x00\x00\x0f\x70\x00\x00\x00\x00\x00\x01\x00\x04\xb0\x94\ -\x00\x00\x0f\x90\x00\x01\x00\x00\x00\x01\x00\x04\xb6\xbb\ -\x00\x00\x0f\xb4\x00\x00\x00\x00\x00\x01\x00\x04\xc2\x0c\ -\x00\x00\x0f\xd6\x00\x00\x00\x00\x00\x01\x00\x04\xc9\xb1\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x82\ +\x00\x00\x00\x46\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x86\ +\x00\x00\x0f\x50\x00\x01\x00\x00\x00\x01\x00\x04\xaa\xfb\ +\x00\x00\x0f\x74\x00\x00\x00\x00\x00\x01\x00\x04\xb6\x4c\ +\x00\x00\x0f\x96\x00\x00\x00\x00\x00\x01\x00\x04\xbd\xf1\ +\x00\x00\x0f\xb2\x00\x00\x00\x00\x00\x01\x00\x04\xce\xf1\ +\x00\x00\x0f\xd2\x00\x00\x00\x00\x00\x01\x00\x04\xd4\x8a\ \x00\x00\x0f\xf2\x00\x00\x00\x00\x00\x01\x00\x04\xda\xb1\ -\x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x32\ -\x00\x00\x10\x36\x00\x00\x00\x00\x00\x01\x00\x04\xe9\xb6\ -\x00\x00\x10\x56\x00\x00\x00\x00\x00\x01\x00\x04\xef\x4f\ -\x00\x00\x10\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xf9\x18\ -\x00\x00\x10\x9e\x00\x00\x00\x00\x00\x01\x00\x04\xfe\xb1\ -\x00\x00\x10\xd2\x00\x00\x00\x00\x00\x01\x00\x05\x09\x25\ -\x00\x00\x10\xee\x00\x00\x00\x00\x00\x01\x00\x05\x0f\x60\ -\x00\x00\x11\x22\x00\x00\x00\x00\x00\x01\x00\x05\x19\xda\ -\x00\x00\x11\x56\x00\x00\x00\x00\x00\x01\x00\x05\x24\x39\ -\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x84\ +\x00\x00\x10\x12\x00\x00\x00\x00\x00\x01\x00\x04\xdf\x06\ +\x00\x00\x10\x32\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x8a\ +\x00\x00\x10\x52\x00\x00\x00\x00\x00\x01\x00\x04\xea\x23\ +\x00\x00\x10\x84\x00\x00\x00\x00\x00\x01\x00\x04\xef\x25\ +\x00\x00\x10\xa4\x00\x00\x00\x00\x00\x01\x00\x04\xf4\xbe\ +\x00\x00\x10\xd8\x00\x00\x00\x00\x00\x01\x00\x04\xff\x32\ +\x00\x00\x11\x0c\x00\x00\x00\x00\x00\x01\x00\x05\x09\xac\ +\x00\x00\x11\x40\x00\x00\x00\x00\x00\x01\x00\x05\x14\x0b\ +\x00\x00\x11\x74\x00\x00\x00\x00\x00\x01\x00\x05\x1f\x56\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x94\ -\x00\x00\x11\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x39\xf8\ -\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x43\x8e\ -\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6a\ -\x00\x00\x12\x42\x00\x00\x00\x00\x00\x01\x00\x05\x55\xae\ -\x00\x00\x12\x6c\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x37\ -\x00\x00\x12\x98\x00\x00\x00\x00\x00\x01\x00\x05\x63\x95\ -\x00\x00\x12\xce\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x84\ -\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x79\x27\ -\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x7e\x5c\ -\x00\x00\x12\xec\x00\x00\x00\x00\x00\x01\x00\x05\x88\x31\ +\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x29\xca\ +\x00\x00\x11\xd8\x00\x00\x00\x00\x00\x01\x00\x05\x33\x60\ +\x00\x00\x12\x08\x00\x00\x00\x00\x00\x01\x00\x05\x3f\x3c\ +\x00\x00\x12\x2c\x00\x00\x00\x00\x00\x01\x00\x05\x45\x80\ +\x00\x00\x12\x56\x00\x00\x00\x00\x00\x01\x00\x05\x4d\x09\ +\x00\x00\x12\x82\x00\x00\x00\x00\x00\x01\x00\x05\x53\x67\ +\x00\x00\x12\xb8\x00\x00\x00\x00\x00\x01\x00\x05\x5b\x56\ +\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x68\xf9\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x6e\x2e\ +\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x78\x03\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9f\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ -\x00\x00\x13\x14\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xfb\ +\x00\x00\x12\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x7f\xcd\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x29\x00\x00\x00\xa3\ -\x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x94\xc1\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x75\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\x9a\ -\x00\x00\x13\x9a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\x1a\ -\x00\x00\x13\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xc7\ -\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xac\x9c\ -\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xad\x8c\ -\x00\x00\x14\x02\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xb5\ -\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb7\xec\ -\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xcc\xf9\ -\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xd1\xe9\ -\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xd4\xe8\ -\x00\x00\x14\x90\x00\x00\x00\x00\x00\x01\x00\x05\xda\xf2\ -\x00\x00\x14\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xde\x41\ -\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x62\ -\x00\x00\x14\xf0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x59\ -\x00\x00\x15\x04\x00\x00\x00\x00\x00\x01\x00\x05\xea\x6a\ -\x00\x00\x15\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xee\x2d\ -\x00\x00\x15\x42\x00\x00\x00\x00\x00\x01\x00\x05\xf7\xe1\ -\x00\x00\x15\x60\x00\x00\x00\x00\x00\x01\x00\x05\xfd\xa9\ -\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x06\x00\xf7\ -\x00\x00\x15\xac\x00\x00\x00\x00\x00\x01\x00\x06\x04\x85\ -\x00\x00\x15\xd2\x00\x00\x00\x00\x00\x01\x00\x06\x09\x26\ -\x00\x00\x15\xe6\x00\x00\x00\x00\x00\x01\x00\x06\x12\xf8\ -\x00\x00\x16\x12\x00\x00\x00\x00\x00\x01\x00\x06\x18\x42\ -\x00\x00\x16\x3a\x00\x00\x00\x00\x00\x01\x00\x06\x1e\x49\ -\x00\x00\x16\x50\x00\x00\x00\x00\x00\x01\x00\x06\x1f\x2d\ -\x00\x00\x16\x7c\x00\x00\x00\x00\x00\x01\x00\x06\x21\x76\ -\x00\x00\x16\x92\x00\x00\x00\x00\x00\x01\x00\x06\x28\x26\ -\x00\x00\x16\xae\x00\x00\x00\x00\x00\x01\x00\x06\x2b\x6a\ -\x00\x00\x16\xc6\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x96\ -\x00\x00\x16\xe0\x00\x00\x00\x00\x00\x01\x00\x06\x32\x57\ -\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\x79\ -\x00\x00\x17\x20\x00\x00\x00\x00\x00\x01\x00\x06\x39\x6c\ -\x00\x00\x17\x40\x00\x00\x00\x00\x00\x01\x00\x06\x3c\x70\ -\x00\x00\x17\x62\x00\x00\x00\x00\x00\x01\x00\x06\x3d\x91\ -\x00\x00\x17\x82\x00\x00\x00\x00\x00\x01\x00\x06\x40\x65\ -\x00\x00\x17\xb0\x00\x00\x00\x00\x00\x01\x00\x06\x48\xd1\ -\x00\x00\x17\xd4\x00\x00\x00\x00\x00\x01\x00\x06\x50\x91\ -\x00\x00\x17\xf8\x00\x00\x00\x00\x00\x01\x00\x06\x55\xac\ -\x00\x00\x18\x20\x00\x00\x00\x00\x00\x01\x00\x06\x56\xff\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\xa3\ +\x00\x00\x13\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x84\x93\ +\x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x8c\x47\ +\x00\x00\x13\x4c\x00\x00\x00\x00\x00\x01\x00\x05\x8e\x6c\ +\x00\x00\x13\x84\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xec\ +\x00\x00\x13\xa0\x00\x00\x00\x00\x00\x01\x00\x05\x97\x99\ +\x00\x00\x13\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x6e\ +\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x9d\x5e\ +\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xa1\x87\ +\x00\x00\x14\x12\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xbe\ +\x00\x00\x14\x2c\x00\x00\x00\x00\x00\x01\x00\x05\xbc\xcb\ +\x00\x00\x14\x4e\x00\x00\x00\x00\x00\x01\x00\x05\xc1\xbb\ +\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xc4\xba\ +\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xca\xc4\ +\x00\x00\x14\xac\x00\x00\x00\x00\x00\x01\x00\x05\xce\x13\ +\x00\x00\x14\xc4\x00\x00\x00\x00\x00\x01\x00\x05\xd2\x34\ +\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xd8\x2b\ +\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xda\x3c\ +\x00\x00\x15\x06\x00\x00\x00\x00\x00\x01\x00\x05\xdd\xff\ +\x00\x00\x15\x2c\x00\x00\x00\x00\x00\x01\x00\x05\xe7\xb3\ +\x00\x00\x15\x56\x00\x00\x00\x00\x00\x01\x00\x05\xeb\x01\ +\x00\x00\x15\x78\x00\x00\x00\x00\x00\x01\x00\x05\xee\x8f\ +\x00\x00\x15\x9e\x00\x00\x00\x00\x00\x01\x00\x05\xf3\x30\ +\x00\x00\x15\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xfd\x02\ +\x00\x00\x15\xde\x00\x00\x00\x00\x00\x01\x00\x06\x02\x4c\ +\x00\x00\x16\x06\x00\x00\x00\x00\x00\x01\x00\x06\x08\x53\ +\x00\x00\x16\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x09\x37\ +\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x0b\x80\ +\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x12\x30\ +\x00\x00\x16\x7a\x00\x00\x00\x00\x00\x01\x00\x06\x15\x74\ +\x00\x00\x16\x92\x00\x00\x00\x00\x00\x01\x00\x06\x16\xa0\ +\x00\x00\x16\xac\x00\x00\x00\x00\x00\x01\x00\x06\x1c\x61\ +\x00\x00\x16\xce\x00\x00\x00\x00\x00\x01\x00\x06\x1d\x83\ +\x00\x00\x16\xec\x00\x00\x00\x00\x00\x01\x00\x06\x23\x76\ +\x00\x00\x17\x0c\x00\x00\x00\x00\x00\x01\x00\x06\x26\x7a\ +\x00\x00\x17\x2e\x00\x00\x00\x00\x00\x01\x00\x06\x27\x9b\ +\x00\x00\x17\x4e\x00\x00\x00\x00\x00\x01\x00\x06\x2a\x6f\ +\x00\x00\x17\x7c\x00\x00\x00\x00\x00\x01\x00\x06\x32\xdb\ +\x00\x00\x17\xa0\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x9b\ +\x00\x00\x17\xc4\x00\x00\x00\x00\x00\x01\x00\x06\x3f\xb6\ +\x00\x00\x17\xec\x00\x00\x00\x00\x00\x01\x00\x06\x41\x09\ " qt_resource_struct_v2 = b"\ @@ -27778,155 +27512,153 @@ \x00\x00\x01\x9a\x72\xe1\x94\x53\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x81\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x10\x00\x00\x00\x82\ +\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x82\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x46\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x86\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0f\x50\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xfb\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x0f\x70\x00\x00\x00\x00\x00\x01\x00\x04\xb0\x94\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x0f\x90\x00\x01\x00\x00\x00\x01\x00\x04\xb6\xbb\ +\x00\x00\x0f\x50\x00\x01\x00\x00\x00\x01\x00\x04\xaa\xfb\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x0f\xb4\x00\x00\x00\x00\x00\x01\x00\x04\xc2\x0c\ +\x00\x00\x0f\x74\x00\x00\x00\x00\x00\x01\x00\x04\xb6\x4c\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x0f\xd6\x00\x00\x00\x00\x00\x01\x00\x04\xc9\xb1\ +\x00\x00\x0f\x96\x00\x00\x00\x00\x00\x01\x00\x04\xbd\xf1\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ +\x00\x00\x0f\xb2\x00\x00\x00\x00\x00\x01\x00\x04\xce\xf1\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x0f\xd2\x00\x00\x00\x00\x00\x01\x00\x04\xd4\x8a\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ \x00\x00\x0f\xf2\x00\x00\x00\x00\x00\x01\x00\x04\xda\xb1\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x32\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x10\x36\x00\x00\x00\x00\x00\x01\x00\x04\xe9\xb6\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x10\x56\x00\x00\x00\x00\x00\x01\x00\x04\xef\x4f\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x10\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xf9\x18\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x10\x9e\x00\x00\x00\x00\x00\x01\x00\x04\xfe\xb1\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x10\xd2\x00\x00\x00\x00\x00\x01\x00\x05\x09\x25\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x10\xee\x00\x00\x00\x00\x00\x01\x00\x05\x0f\x60\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x11\x22\x00\x00\x00\x00\x00\x01\x00\x05\x19\xda\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x11\x56\x00\x00\x00\x00\x00\x01\x00\x05\x24\x39\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ -\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x84\ -\x00\x00\x01\x9c\xb3\x4d\x25\xfe\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x10\x12\x00\x00\x00\x00\x00\x01\x00\x04\xdf\x06\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x10\x32\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x8a\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x10\x52\x00\x00\x00\x00\x00\x01\x00\x04\xea\x23\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x10\x84\x00\x00\x00\x00\x00\x01\x00\x04\xef\x25\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x10\xa4\x00\x00\x00\x00\x00\x01\x00\x04\xf4\xbe\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x10\xd8\x00\x00\x00\x00\x00\x01\x00\x04\xff\x32\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x11\x0c\x00\x00\x00\x00\x00\x01\x00\x05\x09\xac\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x11\x40\x00\x00\x00\x00\x00\x01\x00\x05\x14\x0b\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ +\x00\x00\x11\x74\x00\x00\x00\x00\x00\x01\x00\x05\x1f\x56\ +\x00\x00\x01\x9c\x9f\xa4\x2d\xe9\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x94\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x11\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x39\xf8\ +\x00\x00\x11\xa8\x00\x00\x00\x00\x00\x01\x00\x05\x29\xca\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x43\x8e\ +\x00\x00\x11\xd8\x00\x00\x00\x00\x00\x01\x00\x05\x33\x60\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6a\ +\x00\x00\x12\x08\x00\x00\x00\x00\x00\x01\x00\x05\x3f\x3c\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x42\x00\x00\x00\x00\x00\x01\x00\x05\x55\xae\ +\x00\x00\x12\x2c\x00\x00\x00\x00\x00\x01\x00\x05\x45\x80\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x6c\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x37\ +\x00\x00\x12\x56\x00\x00\x00\x00\x00\x01\x00\x05\x4d\x09\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x12\x98\x00\x00\x00\x00\x00\x01\x00\x05\x63\x95\ +\x00\x00\x12\x82\x00\x00\x00\x00\x00\x01\x00\x05\x53\x67\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x12\xce\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x84\ +\x00\x00\x12\xb8\x00\x00\x00\x00\x00\x01\x00\x05\x5b\x56\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x79\x27\ +\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x68\xf9\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x7e\x5c\ +\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x6e\x2e\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x12\xec\x00\x00\x00\x00\x00\x01\x00\x05\x88\x31\ +\x00\x00\x12\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x78\x03\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9f\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x13\x14\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xfb\ +\x00\x00\x12\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x7f\xcd\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x29\x00\x00\x00\xa3\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x94\xc1\ +\x00\x00\x13\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x84\x93\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x75\ +\x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x8c\x47\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\x9a\ +\x00\x00\x13\x4c\x00\x00\x00\x00\x00\x01\x00\x05\x8e\x6c\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x13\x9a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\x1a\ +\x00\x00\x13\x84\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xec\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xc7\ +\x00\x00\x13\xa0\x00\x00\x00\x00\x00\x01\x00\x05\x97\x99\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xac\x9c\ +\x00\x00\x13\xc0\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x6e\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xad\x8c\ +\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\x9d\x5e\ \x00\x00\x01\x9c\x48\x63\x91\xdd\ -\x00\x00\x14\x02\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xb5\ +\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xa1\x87\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb7\xec\ +\x00\x00\x14\x12\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xbe\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xcc\xf9\ +\x00\x00\x14\x2c\x00\x00\x00\x00\x00\x01\x00\x05\xbc\xcb\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xd1\xe9\ +\x00\x00\x14\x4e\x00\x00\x00\x00\x00\x01\x00\x05\xc1\xbb\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xd4\xe8\ +\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xc4\xba\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x90\x00\x00\x00\x00\x00\x01\x00\x05\xda\xf2\ +\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xca\xc4\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x14\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xde\x41\ +\x00\x00\x14\xac\x00\x00\x00\x00\x00\x01\x00\x05\xce\x13\ \x00\x00\x01\x9c\x48\x63\x91\xdd\ -\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x62\ +\x00\x00\x14\xc4\x00\x00\x00\x00\x00\x01\x00\x05\xd2\x34\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x14\xf0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x59\ +\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xd8\x2b\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x04\x00\x00\x00\x00\x00\x01\x00\x05\xea\x6a\ +\x00\x00\x14\xee\x00\x00\x00\x00\x00\x01\x00\x05\xda\x3c\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x15\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xee\x2d\ +\x00\x00\x15\x06\x00\x00\x00\x00\x00\x01\x00\x05\xdd\xff\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x15\x42\x00\x00\x00\x00\x00\x01\x00\x05\xf7\xe1\ -\x00\x00\x01\x9c\xb3\x48\x8b\xb6\ -\x00\x00\x15\x60\x00\x00\x00\x00\x00\x01\x00\x05\xfd\xa9\ +\x00\x00\x15\x2c\x00\x00\x00\x00\x00\x01\x00\x05\xe7\xb3\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x8a\x00\x00\x00\x00\x00\x01\x00\x06\x00\xf7\ +\x00\x00\x15\x56\x00\x00\x00\x00\x00\x01\x00\x05\xeb\x01\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\xac\x00\x00\x00\x00\x00\x01\x00\x06\x04\x85\ +\x00\x00\x15\x78\x00\x00\x00\x00\x00\x01\x00\x05\xee\x8f\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x15\xd2\x00\x00\x00\x00\x00\x01\x00\x06\x09\x26\ +\x00\x00\x15\x9e\x00\x00\x00\x00\x00\x01\x00\x05\xf3\x30\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\xe6\x00\x00\x00\x00\x00\x01\x00\x06\x12\xf8\ +\x00\x00\x15\xb2\x00\x00\x00\x00\x00\x01\x00\x05\xfd\x02\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\x12\x00\x00\x00\x00\x00\x01\x00\x06\x18\x42\ +\x00\x00\x15\xde\x00\x00\x00\x00\x00\x01\x00\x06\x02\x4c\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x3a\x00\x00\x00\x00\x00\x01\x00\x06\x1e\x49\ +\x00\x00\x16\x06\x00\x00\x00\x00\x00\x01\x00\x06\x08\x53\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x50\x00\x00\x00\x00\x00\x01\x00\x06\x1f\x2d\ +\x00\x00\x16\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x09\x37\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x16\x7c\x00\x00\x00\x00\x00\x01\x00\x06\x21\x76\ +\x00\x00\x16\x48\x00\x00\x00\x00\x00\x01\x00\x06\x0b\x80\ \x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x16\x92\x00\x00\x00\x00\x00\x01\x00\x06\x28\x26\ +\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x12\x30\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\xae\x00\x00\x00\x00\x00\x01\x00\x06\x2b\x6a\ +\x00\x00\x16\x7a\x00\x00\x00\x00\x00\x01\x00\x06\x15\x74\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\xc6\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x96\ +\x00\x00\x16\x92\x00\x00\x00\x00\x00\x01\x00\x06\x16\xa0\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\xe0\x00\x00\x00\x00\x00\x01\x00\x06\x32\x57\ +\x00\x00\x16\xac\x00\x00\x00\x00\x00\x01\x00\x06\x1c\x61\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\x79\ +\x00\x00\x16\xce\x00\x00\x00\x00\x00\x01\x00\x06\x1d\x83\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x20\x00\x00\x00\x00\x00\x01\x00\x06\x39\x6c\ +\x00\x00\x16\xec\x00\x00\x00\x00\x00\x01\x00\x06\x23\x76\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\x40\x00\x00\x00\x00\x00\x01\x00\x06\x3c\x70\ +\x00\x00\x17\x0c\x00\x00\x00\x00\x00\x01\x00\x06\x26\x7a\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x62\x00\x00\x00\x00\x00\x01\x00\x06\x3d\x91\ +\x00\x00\x17\x2e\x00\x00\x00\x00\x00\x01\x00\x06\x27\x9b\ \x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x82\x00\x00\x00\x00\x00\x01\x00\x06\x40\x65\ +\x00\x00\x17\x4e\x00\x00\x00\x00\x00\x01\x00\x06\x2a\x6f\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\xb0\x00\x00\x00\x00\x00\x01\x00\x06\x48\xd1\ +\x00\x00\x17\x7c\x00\x00\x00\x00\x00\x01\x00\x06\x32\xdb\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x17\xd4\x00\x00\x00\x00\x00\x01\x00\x06\x50\x91\ +\x00\x00\x17\xa0\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x9b\ \x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\xf8\x00\x00\x00\x00\x00\x01\x00\x06\x55\xac\ +\x00\x00\x17\xc4\x00\x00\x00\x00\x00\x01\x00\x06\x3f\xb6\ \x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x18\x20\x00\x00\x00\x00\x00\x01\x00\x06\x56\xff\ +\x00\x00\x17\xec\x00\x00\x00\x00\x00\x01\x00\x06\x41\x09\ \x00\x00\x01\x9a\x72\xe1\x94\x4b\ " @@ -27944,4 +27676,4 @@ def qInitResources(): def qCleanupResources(): QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) -qInitResources() +qInitResources() \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/0bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/0bar_wifi.svg new file mode 100644 index 00000000..ceaff53d --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/0bar_wifi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/0bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/0bar_wifi_protected.svg new file mode 100644 index 00000000..a10ea388 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/0bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/1bar_wifi.svg similarity index 100% rename from BlocksScreen/lib/ui/resources/media/btn_icons/1bar_wifi.svg rename to BlocksScreen/lib/ui/resources/media/btn_icons/network/1bar_wifi.svg diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/1bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/1bar_wifi_protected.svg new file mode 100644 index 00000000..8793447e --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/1bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/2bar_wifi.svg similarity index 100% rename from BlocksScreen/lib/ui/resources/media/btn_icons/2bar_wifi.svg rename to BlocksScreen/lib/ui/resources/media/btn_icons/network/2bar_wifi.svg diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/2bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/2bar_wifi_protected.svg new file mode 100644 index 00000000..a9f3233b --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/2bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/3bar_wifi.svg similarity index 100% rename from BlocksScreen/lib/ui/resources/media/btn_icons/3bar_wifi.svg rename to BlocksScreen/lib/ui/resources/media/btn_icons/network/3bar_wifi.svg diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/3bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/3bar_wifi_protected.svg new file mode 100644 index 00000000..458c1ac5 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/3bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/4bar_wifi.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/4bar_wifi.svg new file mode 100644 index 00000000..9aadd8e7 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/4bar_wifi.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/4bar_wifi_protected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/4bar_wifi_protected.svg new file mode 100644 index 00000000..639762e7 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/4bar_wifi_protected.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/ethernet_connected.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/ethernet_connected.svg new file mode 100644 index 00000000..8f727437 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/ethernet_connected.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/network/static_ip.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/network/static_ip.svg new file mode 100644 index 00000000..92c07b79 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/network/static_ip.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/topbar/internet_cable.svg b/BlocksScreen/lib/ui/resources/media/topbar/internet_cable.svg deleted file mode 100644 index 0fd24fd8..00000000 --- a/BlocksScreen/lib/ui/resources/media/topbar/internet_cable.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/top_bar_resources.qrc b/BlocksScreen/lib/ui/resources/top_bar_resources.qrc index a8ff7789..06dc4e50 100644 --- a/BlocksScreen/lib/ui/resources/top_bar_resources.qrc +++ b/BlocksScreen/lib/ui/resources/top_bar_resources.qrc @@ -6,7 +6,6 @@ media/topbar/custom_filament_topbar.svg media/topbar/high_temp_printcore.svg media/topbar/hips_filament_topbar.svg - media/topbar/internet_cable.svg media/topbar/not_avaible_printcore.svg media/topbar/not_available_filament_topbar.svg media/topbar/nozzle_temp_topbar.svg @@ -15,11 +14,6 @@ media/topbar/nylon_filament_topbar.svg media/topbar/petg_filament_topbar.svg media/topbar/pla_filament_topbar.svg - media/topbar/signal_good_signal.svg - media/topbar/signal_no_signal.svg - media/topbar/signal_very_good_signal.svg - media/topbar/signal_veryweak_signal.svg - media/topbar/signal_weak_signal.svg media/topbar/standard_temp_printcore.svg media/topbar/tpu_filament_topbar.svg diff --git a/BlocksScreen/lib/ui/resources/top_bar_resources_rc.py b/BlocksScreen/lib/ui/resources/top_bar_resources_rc.py index 24e73622..47047530 100644 --- a/BlocksScreen/lib/ui/resources/top_bar_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/top_bar_resources_rc.py @@ -1,2712 +1,2254 @@ -# -*- coding: utf-8 -*- - -# Resource object code -# -# Created by: The Resource Compiler for PyQt6 (Qt v5.15.14) -# +# Resource object code (Python 3) +# Created by: object code +# Created by: The Resource Compiler for Qt version 6.8.2 # WARNING! All changes made in this file will be lost! from PyQt6 import QtCore qt_resource_data = b"\ -\x00\x00\x01\x47\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x34\x2e\x36\x39\x20\x35\x2e\x30\x34\x22\x3e\x3c\ -\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\ -\x2d\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x66\x66\x66\x3b\x7d\x3c\x2f\ -\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x67\x20\ -\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\x61\x74\ -\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x32\x22\ -\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x2d\ -\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\ -\x79\x65\x72\x20\x31\x22\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\ -\x2e\x33\x35\x2c\x35\x41\x32\x2e\x34\x32\x2c\x32\x2e\x34\x32\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x30\x2c\x32\x2e\x35\x31\x2c\x32\x2e\x34\ -\x31\x2c\x32\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2e\x33\ -\x33\x2c\x30\x2c\x32\x2e\x33\x39\x2c\x32\x2e\x33\x39\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x34\x2e\x36\x39\x2c\x32\x2e\x35\x31\x2c\x32\x2e\ -\x34\x31\x2c\x32\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2e\ -\x33\x35\x2c\x35\x5a\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\ -\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x08\x73\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x38\x38\x2e\x33\x32\x20\x33\x34\x2e\x32\x34\x22\ -\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\ -\x6c\x73\x2d\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x66\x30\x35\x61\x32\ -\x38\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\ -\x66\x36\x39\x32\x31\x65\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x66\ -\x69\x6c\x6c\x3a\x23\x65\x63\x31\x63\x32\x34\x3b\x7d\x3c\x2f\x73\ -\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x67\x20\x69\ -\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\x61\x74\x61\ -\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x32\x22\x3e\ -\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x2d\x32\ -\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\ -\x65\x72\x20\x31\x22\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x37\x38\ -\x2e\x31\x32\x2c\x32\x30\x2e\x35\x36\x68\x2d\x37\x2e\x38\x61\x32\ -\x2e\x33\x38\x2c\x32\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x32\ -\x2e\x33\x39\x2d\x32\x2e\x33\x38\x71\x30\x2d\x37\x2e\x38\x2c\x30\ -\x2d\x31\x35\x2e\x35\x38\x41\x32\x2e\x34\x32\x2c\x32\x2e\x34\x32\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x30\x2e\x33\x35\x2e\x31\x38\x48\ -\x38\x35\x2e\x37\x39\x41\x32\x2e\x35\x32\x2c\x32\x2e\x35\x32\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x38\x38\x2e\x33\x32\x2c\x32\x2e\x37\x71\ -\x30\x2c\x37\x2e\x36\x38\x2c\x30\x2c\x31\x35\x2e\x33\x36\x61\x32\ -\x2e\x35\x2c\x32\x2e\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x35\ -\x31\x2c\x32\x2e\x35\x5a\x6d\x38\x2e\x35\x37\x2d\x31\x30\x2e\x31\ -\x37\x41\x38\x2e\x36\x34\x2c\x38\x2e\x36\x34\x2c\x30\x2c\x31\x2c\ -\x30\x2c\x37\x37\x2e\x39\x2c\x31\x39\x2c\x38\x2e\x36\x36\x2c\x38\ -\x2e\x36\x36\x2c\x30\x2c\x30\x2c\x30\x2c\x38\x36\x2e\x36\x39\x2c\ -\x31\x30\x2e\x33\x39\x5a\x6d\x30\x2c\x37\x2e\x37\x32\x61\x2e\x39\ -\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x38\x32\x2c\ -\x30\x2c\x2e\x39\x32\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\ -\x39\x32\x2e\x39\x32\x41\x2e\x39\x2e\x39\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x38\x36\x2e\x36\x39\x2c\x31\x38\x2e\x31\x31\x5a\x4d\x36\x39\ -\x2e\x34\x31\x2c\x32\x2e\x36\x32\x61\x2e\x38\x38\x2e\x38\x38\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x38\x2e\x39\x32\x2e\x39\x31\x2e\ -\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x39\x33\x2d\x2e\x39\x31\ -\x2e\x39\x32\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2d\ -\x2e\x39\x31\x41\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x36\x39\x2e\x34\x31\x2c\x32\x2e\x36\x32\x5a\x6d\x31\x37\x2e\ -\x32\x38\x2c\x30\x61\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\ -\x30\x2d\x2e\x39\x2d\x2e\x39\x31\x2e\x39\x31\x2e\x39\x31\x2c\x30\ -\x2c\x31\x2c\x30\x2c\x30\x2c\x31\x2e\x38\x32\x41\x2e\x39\x2e\x39\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x38\x36\x2e\x36\x39\x2c\x32\x2e\x36\ -\x33\x5a\x4d\x37\x30\x2e\x33\x32\x2c\x31\x37\x2e\x32\x61\x2e\x39\ -\x31\x2e\x39\x31\x2c\x30\x2c\x31\x2c\x30\x2c\x30\x2c\x31\x2e\x38\ -\x31\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2d\ -\x31\x2e\x38\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ -\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\ -\x38\x34\x2e\x37\x37\x2c\x39\x2e\x38\x38\x61\x35\x2e\x31\x36\x2c\ -\x35\x2e\x31\x36\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\x35\x33\x2d\ -\x2e\x37\x38\x6c\x2d\x2e\x33\x33\x2d\x2e\x31\x36\x63\x2d\x31\x2e\ -\x36\x36\x2d\x31\x2e\x36\x36\x2c\x33\x2e\x32\x35\x2d\x33\x2e\x36\ -\x2c\x34\x2e\x36\x38\x2d\x33\x2e\x31\x31\x61\x36\x2e\x39\x32\x2c\ -\x36\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x34\x2e\x33\x37\x2d\ -\x32\x2e\x35\x35\x43\x37\x36\x2e\x38\x31\x2c\x33\x2e\x31\x37\x2c\ -\x37\x36\x2c\x36\x2e\x37\x37\x2c\x37\x36\x2e\x37\x33\x2c\x38\x2e\ -\x36\x35\x63\x2e\x32\x35\x2e\x36\x39\x2e\x32\x35\x2e\x36\x37\x2d\ -\x2e\x32\x35\x2c\x31\x2e\x32\x61\x2e\x33\x38\x2e\x33\x38\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x35\x37\x2e\x30\x35\x63\x2d\x31\x2e\x38\ -\x33\x2d\x31\x2e\x30\x37\x2d\x32\x2e\x33\x33\x2d\x33\x2e\x31\x31\ -\x2d\x32\x2e\x32\x32\x2d\x35\x2e\x31\x32\x2d\x31\x2e\x35\x32\x2c\ -\x31\x2e\x32\x32\x2d\x33\x2e\x37\x38\x2c\x34\x2e\x34\x39\x2d\x32\ -\x2c\x36\x2e\x32\x61\x34\x2e\x38\x37\x2c\x34\x2e\x38\x37\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x34\x2e\x38\x33\x2e\x38\x32\x63\x2e\x36\x35\ -\x2d\x2e\x32\x36\x2e\x36\x34\x2d\x2e\x32\x36\x2c\x31\x2e\x31\x33\ -\x2e\x32\x32\x2e\x34\x34\x2c\x32\x2d\x33\x2e\x32\x35\x2c\x33\x2e\ -\x31\x36\x2d\x34\x2e\x39\x34\x2c\x32\x2e\x38\x39\x2c\x31\x2e\x32\ -\x2c\x31\x2e\x34\x37\x2c\x34\x2e\x33\x31\x2c\x33\x2e\x36\x35\x2c\ -\x36\x2c\x31\x2e\x39\x32\x61\x34\x2e\x36\x32\x2c\x34\x2e\x36\x32\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x39\x32\x2d\x34\x2e\x36\x36\x63\ -\x2d\x2e\x34\x2d\x33\x2e\x38\x36\x2c\x33\x2e\x38\x39\x2c\x31\x2c\ -\x33\x2c\x33\x2e\x37\x33\x43\x38\x34\x2e\x30\x36\x2c\x31\x35\x2c\ -\x38\x36\x2e\x31\x38\x2c\x31\x31\x2e\x35\x31\x2c\x38\x34\x2e\x37\ -\x37\x2c\x39\x2e\x38\x38\x5a\x22\x2f\x3e\x3c\x72\x65\x63\x74\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x78\x3d\ -\x22\x36\x39\x2e\x34\x39\x22\x20\x79\x3d\x22\x32\x31\x2e\x35\x34\ -\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x37\x2e\x32\x37\x22\x20\ -\x68\x65\x69\x67\x68\x74\x3d\x22\x33\x2e\x32\x38\x22\x20\x72\x78\ -\x3d\x22\x30\x2e\x33\x38\x22\x2f\x3e\x3c\x72\x65\x63\x74\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x78\x3d\x22\ -\x37\x31\x2e\x30\x32\x22\x20\x79\x3d\x22\x32\x33\x2e\x36\x32\x22\ -\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x34\x2e\x32\x32\x22\x20\x68\ -\x65\x69\x67\x68\x74\x3d\x22\x33\x2e\x34\x32\x22\x20\x72\x78\x3d\ -\x22\x30\x2e\x33\x38\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ -\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\ -\x37\x35\x2e\x30\x37\x2c\x32\x39\x2e\x34\x37\x56\x32\x38\x2e\x32\ -\x39\x63\x30\x2d\x2e\x31\x35\x2c\x30\x2d\x2e\x32\x35\x2e\x32\x33\ -\x2d\x2e\x32\x32\x68\x32\x2e\x32\x32\x63\x2e\x31\x39\x2c\x30\x2c\ -\x2e\x32\x36\x2e\x30\x37\x2e\x32\x36\x2e\x32\x35\x76\x32\x2e\x33\ -\x61\x2e\x33\x35\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x32\ -\x2e\x33\x31\x2c\x32\x2e\x36\x31\x2c\x32\x2e\x36\x31\x2c\x30\x2c\ -\x30\x2c\x31\x2d\x32\x2e\x33\x31\x2c\x30\x2c\x2e\x33\x32\x2e\x33\ -\x32\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x32\x2d\x2e\x33\x34\x5a\x22\ -\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x37\x35\x2e\x35\x2c\x33\ -\x31\x2e\x38\x37\x41\x33\x2e\x38\x31\x2c\x33\x2e\x38\x31\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x37\x38\x2c\x33\x31\x2e\x35\x36\x61\x2e\x34\ -\x38\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x32\x39\x2c\x30\ -\x2c\x33\x2e\x37\x38\x2c\x33\x2e\x37\x38\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x32\x2e\x34\x35\x2e\x33\x34\x63\x2d\x2e\x32\x2e\x32\x31\x2d\ -\x2e\x34\x33\x2e\x34\x32\x2d\x2e\x36\x32\x2e\x36\x2d\x2e\x34\x39\ -\x2e\x34\x36\x2d\x31\x2c\x2e\x39\x32\x2d\x31\x2e\x34\x33\x2c\x31\ -\x2e\x33\x37\x61\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x31\ -\x2d\x31\x2c\x2e\x30\x39\x43\x37\x36\x2e\x39\x33\x2c\x33\x33\x2e\ -\x32\x36\x2c\x37\x36\x2e\x32\x34\x2c\x33\x32\x2e\x35\x37\x2c\x37\ -\x35\x2e\x35\x2c\x33\x31\x2e\x38\x37\x5a\x22\x2f\x3e\x3c\x70\x61\ -\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\ -\x20\x64\x3d\x22\x4d\x37\x38\x2e\x34\x38\x2c\x32\x39\x2e\x34\x37\ -\x56\x32\x38\x2e\x32\x39\x63\x30\x2d\x2e\x31\x35\x2c\x30\x2d\x2e\ -\x32\x35\x2e\x32\x33\x2d\x2e\x32\x32\x68\x32\x2e\x32\x31\x63\x2e\ -\x32\x2c\x30\x2c\x2e\x32\x36\x2e\x30\x37\x2e\x32\x36\x2e\x32\x35\ -\x76\x32\x2e\x33\x61\x2e\x33\x34\x2e\x33\x34\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x2e\x31\x39\x2e\x33\x31\x2c\x32\x2e\x36\x33\x2c\x32\x2e\ -\x36\x33\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x33\x32\x2c\x30\x2c\ -\x2e\x33\x31\x2e\x33\x31\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x31\x39\ -\x2d\x2e\x33\x34\x5a\x22\x2f\x3e\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x70\ -\x6f\x69\x6e\x74\x73\x3d\x22\x32\x32\x2e\x31\x32\x20\x31\x34\x2e\ -\x30\x38\x20\x34\x2e\x33\x35\x20\x31\x34\x2e\x30\x38\x20\x34\x2e\ -\x33\x35\x20\x30\x20\x30\x20\x30\x20\x30\x20\x33\x34\x2e\x32\x34\ -\x20\x34\x2e\x33\x35\x20\x33\x34\x2e\x32\x34\x20\x34\x2e\x33\x35\ -\x20\x31\x38\x2e\x34\x32\x20\x32\x32\x2e\x31\x32\x20\x31\x38\x2e\ -\x34\x32\x20\x32\x32\x2e\x31\x32\x20\x33\x34\x2e\x32\x34\x20\x32\ -\x36\x2e\x34\x36\x20\x33\x34\x2e\x32\x34\x20\x32\x36\x2e\x34\x36\ -\x20\x30\x20\x32\x32\x2e\x31\x32\x20\x30\x20\x32\x32\x2e\x31\x32\ -\x20\x31\x34\x2e\x30\x38\x22\x2f\x3e\x3c\x70\x6f\x6c\x79\x67\x6f\ -\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\ -\x70\x6f\x69\x6e\x74\x73\x3d\x22\x33\x30\x2e\x33\x33\x20\x30\x20\ -\x33\x30\x2e\x33\x33\x20\x34\x2e\x33\x35\x20\x34\x32\x2e\x33\x39\ -\x20\x34\x2e\x33\x35\x20\x34\x32\x2e\x33\x39\x20\x33\x34\x2e\x32\ -\x34\x20\x34\x36\x2e\x37\x34\x20\x33\x34\x2e\x32\x34\x20\x34\x36\ -\x2e\x37\x34\x20\x34\x2e\x33\x35\x20\x35\x38\x2e\x38\x20\x34\x2e\ -\x33\x35\x20\x35\x38\x2e\x38\x20\x30\x20\x33\x30\x2e\x33\x33\x20\ -\x30\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\ -\x67\x3e\ -\x00\x00\x09\xfe\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x38\x32\x2e\x32\x33\x20\x33\x36\x2e\x31\x37\x22\ -\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\ -\x6c\x73\x2d\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x62\x63\x35\x33\ -\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\ -\x30\x30\x39\x31\x34\x37\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x66\ -\x69\x6c\x6c\x3a\x23\x30\x30\x61\x35\x35\x31\x3b\x7d\x3c\x2f\x73\ -\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x67\x20\x69\ -\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\x61\x74\x61\ -\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x32\x22\x3e\ -\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x2d\x32\ -\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\ -\x65\x72\x20\x31\x22\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x37\x32\ -\x2c\x32\x30\x2e\x35\x38\x68\x2d\x37\x2e\x38\x61\x32\x2e\x33\x39\ -\x2c\x32\x2e\x33\x39\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x33\x39\ -\x2d\x32\x2e\x33\x39\x56\x32\x2e\x36\x32\x41\x32\x2e\x34\x32\x2c\ -\x32\x2e\x34\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x36\x34\x2e\x32\x37\ -\x2e\x32\x48\x37\x39\x2e\x37\x31\x61\x32\x2e\x35\x32\x2c\x32\x2e\ -\x35\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2e\x35\x32\x2c\x32\x2e\ -\x35\x32\x56\x31\x38\x2e\x30\x37\x61\x32\x2e\x35\x31\x2c\x32\x2e\ -\x35\x31\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x35\x31\x2c\x32\x2e\ -\x35\x31\x5a\x4d\x38\x30\x2e\x36\x2c\x31\x30\x2e\x34\x41\x38\x2e\ -\x36\x34\x2c\x38\x2e\x36\x34\x2c\x30\x2c\x31\x2c\x30\x2c\x37\x31\ -\x2e\x38\x32\x2c\x31\x39\x2c\x38\x2e\x36\x34\x2c\x38\x2e\x36\x34\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x38\x30\x2e\x36\x2c\x31\x30\x2e\x34\ -\x5a\x6d\x30\x2c\x37\x2e\x37\x32\x61\x2e\x38\x38\x2e\x38\x38\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2d\x2e\x39\x2e\x39\x31\x2e\x39\ -\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x32\x2e\x38\x39\x2e\x39\ -\x33\x2e\x39\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x39\x33\x2e\x39\ -\x32\x41\x2e\x39\x2e\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x38\x30\x2e\ -\x36\x2c\x31\x38\x2e\x31\x32\x5a\x4d\x36\x33\x2e\x33\x33\x2c\x32\ -\x2e\x36\x34\x61\x2e\x38\x38\x2e\x38\x38\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x2e\x38\x38\x2e\x39\x31\x2e\x38\x39\x2e\x38\x39\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x2e\x39\x32\x2d\x2e\x39\x2e\x39\x31\x2e\x39\x31\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2d\x2e\x39\x31\x41\x2e\x38\ -\x38\x2e\x38\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x36\x33\x2e\x33\x33\ -\x2c\x32\x2e\x36\x34\x5a\x6d\x31\x37\x2e\x32\x37\x2c\x30\x61\x2e\ -\x39\x2e\x39\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2d\x2e\x39\x31\ -\x2e\x39\x33\x2e\x39\x33\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x32\ -\x2e\x39\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\ -\x39\x33\x2e\x39\x31\x41\x2e\x38\x39\x2e\x38\x39\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x38\x30\x2e\x36\x2c\x32\x2e\x36\x35\x5a\x4d\x36\x34\ -\x2e\x32\x33\x2c\x31\x37\x2e\x32\x32\x61\x2e\x38\x38\x2e\x38\x38\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2e\x38\x39\x2e\x38\x39\x2e\ -\x38\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x39\x31\x2e\x39\x32\x2e\ -\x39\x2e\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x39\x2d\x2e\x39\ -\x31\x41\x2e\x38\x39\x2e\x38\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x36\ -\x34\x2e\x32\x33\x2c\x31\x37\x2e\x32\x32\x5a\x22\x2f\x3e\x3c\x70\ -\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\ -\x22\x20\x64\x3d\x22\x4d\x37\x38\x2e\x36\x38\x2c\x39\x2e\x39\x61\ -\x35\x2e\x31\x34\x2c\x35\x2e\x31\x34\x2c\x30\x2c\x30\x2c\x30\x2d\ -\x35\x2e\x35\x32\x2d\x2e\x37\x39\x4c\x37\x32\x2e\x38\x32\x2c\x39\ -\x63\x2d\x31\x2e\x36\x35\x2d\x31\x2e\x36\x37\x2c\x33\x2e\x32\x36\ -\x2d\x33\x2e\x36\x2c\x34\x2e\x36\x39\x2d\x33\x2e\x31\x31\x41\x36\ -\x2e\x39\x2c\x36\x2e\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x37\x33\x2e\ -\x31\x34\x2c\x33\x2e\x33\x63\x2d\x32\x2e\x34\x31\x2d\x2e\x31\x31\ -\x2d\x33\x2e\x32\x32\x2c\x33\x2e\x34\x38\x2d\x32\x2e\x35\x2c\x35\ -\x2e\x33\x37\x2e\x32\x35\x2e\x36\x39\x2e\x32\x36\x2e\x36\x37\x2d\ -\x2e\x32\x34\x2c\x31\x2e\x31\x39\x61\x2e\x33\x38\x2e\x33\x38\x2c\ -\x30\x2c\x30\x2c\x31\x2d\x2e\x35\x37\x2e\x30\x36\x43\x36\x38\x2c\ -\x38\x2e\x38\x34\x2c\x36\x37\x2e\x35\x2c\x36\x2e\x38\x31\x2c\x36\ -\x37\x2e\x36\x2c\x34\x2e\x38\x63\x2d\x31\x2e\x35\x31\x2c\x31\x2e\ -\x32\x31\x2d\x33\x2e\x37\x38\x2c\x34\x2e\x34\x38\x2d\x32\x2c\x36\ -\x2e\x32\x61\x34\x2e\x38\x37\x2c\x34\x2e\x38\x37\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x34\x2e\x38\x33\x2e\x38\x31\x63\x2e\x36\x35\x2d\x2e\ -\x32\x36\x2e\x36\x33\x2d\x2e\x32\x36\x2c\x31\x2e\x31\x32\x2e\x32\ -\x33\x2e\x34\x35\x2c\x32\x2d\x33\x2e\x32\x35\x2c\x33\x2e\x31\x35\ -\x2d\x34\x2e\x39\x34\x2c\x32\x2e\x38\x39\x2c\x31\x2e\x32\x2c\x31\ -\x2e\x34\x36\x2c\x34\x2e\x33\x31\x2c\x33\x2e\x36\x34\x2c\x36\x2c\ -\x31\x2e\x39\x31\x61\x34\x2e\x36\x34\x2c\x34\x2e\x36\x34\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x2e\x39\x32\x2d\x34\x2e\x36\x36\x63\x2d\x2e\ -\x34\x2d\x33\x2e\x38\x36\x2c\x33\x2e\x38\x39\x2c\x31\x2c\x33\x2c\ -\x33\x2e\x37\x33\x43\x37\x38\x2c\x31\x35\x2c\x38\x30\x2e\x31\x2c\ -\x31\x31\x2e\x35\x33\x2c\x37\x38\x2e\x36\x38\x2c\x39\x2e\x39\x5a\ -\x22\x2f\x3e\x3c\x72\x65\x63\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x32\x22\x20\x78\x3d\x22\x36\x33\x2e\x34\x22\x20\ -\x79\x3d\x22\x32\x31\x2e\x35\x36\x22\x20\x77\x69\x64\x74\x68\x3d\ -\x22\x31\x37\x2e\x32\x37\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\ -\x33\x2e\x32\x38\x22\x20\x72\x78\x3d\x22\x30\x2e\x33\x38\x22\x2f\ -\x3e\x3c\x72\x65\x63\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x32\x22\x20\x78\x3d\x22\x36\x34\x2e\x39\x33\x22\x20\x79\ -\x3d\x22\x32\x33\x2e\x36\x33\x22\x20\x77\x69\x64\x74\x68\x3d\x22\ -\x31\x34\x2e\x32\x32\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x33\ -\x2e\x34\x32\x22\x20\x72\x78\x3d\x22\x30\x2e\x33\x38\x22\x2f\x3e\ -\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\ -\x2d\x31\x22\x20\x64\x3d\x22\x4d\x36\x39\x2c\x32\x39\x2e\x34\x39\ -\x56\x32\x38\x2e\x33\x63\x30\x2d\x2e\x31\x35\x2c\x30\x2d\x2e\x32\ -\x34\x2e\x32\x33\x2d\x2e\x32\x31\x68\x32\x2e\x32\x31\x63\x2e\x32\ -\x2c\x30\x2c\x2e\x32\x36\x2e\x30\x36\x2e\x32\x36\x2e\x32\x34\x76\ -\x32\x2e\x33\x31\x61\x2e\x33\x32\x2e\x33\x32\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x2e\x31\x39\x2e\x33\x2c\x32\x2e\x35\x36\x2c\x32\x2e\x35\ -\x36\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x33\x32\x2c\x30\x2c\x2e\ -\x32\x39\x2e\x32\x39\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x31\x39\x2d\ -\x2e\x33\x33\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x36\ -\x39\x2e\x34\x31\x2c\x33\x31\x2e\x38\x38\x61\x33\x2e\x38\x33\x2c\ -\x33\x2e\x38\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x38\x2d\ -\x2e\x33\x2e\x34\x38\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\ -\x32\x39\x2c\x30\x2c\x33\x2e\x36\x39\x2c\x33\x2e\x36\x39\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x34\x2e\x33\x33\x2c\x37\x2e\x31\ -\x2c\x37\x2e\x31\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x36\x32\x2e\x36\ -\x31\x63\x2d\x2e\x34\x38\x2e\x34\x36\x2d\x31\x2c\x2e\x39\x31\x2d\ -\x31\x2e\x34\x33\x2c\x31\x2e\x33\x37\x61\x2e\x38\x39\x2e\x38\x39\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x39\x34\x2e\x30\x39\x43\x37\x30\ -\x2e\x38\x34\x2c\x33\x33\x2e\x32\x38\x2c\x37\x30\x2e\x31\x36\x2c\ -\x33\x32\x2e\x35\x38\x2c\x36\x39\x2e\x34\x31\x2c\x33\x31\x2e\x38\ -\x38\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x37\x32\x2e\ -\x33\x39\x2c\x32\x39\x2e\x34\x39\x56\x32\x38\x2e\x33\x63\x30\x2d\ -\x2e\x31\x35\x2c\x30\x2d\x2e\x32\x34\x2e\x32\x33\x2d\x2e\x32\x31\ -\x68\x32\x2e\x32\x32\x63\x2e\x31\x39\x2c\x30\x2c\x2e\x32\x36\x2e\ -\x30\x36\x2e\x32\x36\x2e\x32\x34\x76\x32\x2e\x33\x31\x61\x2e\x33\ -\x33\x2e\x33\x33\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x32\x2e\x33\x2c\ -\x32\x2e\x35\x34\x2c\x32\x2e\x35\x34\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x32\x2e\x33\x31\x2c\x30\x2c\x2e\x33\x2e\x33\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x2e\x32\x2d\x2e\x33\x33\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ -\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\ -\x64\x3d\x22\x4d\x31\x39\x2e\x33\x38\x2c\x31\x38\x2e\x34\x38\x61\ -\x31\x37\x2e\x38\x39\x2c\x31\x37\x2e\x38\x39\x2c\x30\x2c\x30\x2c\ -\x30\x2d\x36\x2e\x37\x39\x2d\x33\x2e\x33\x33\x6c\x2d\x2e\x34\x2d\ -\x2e\x31\x32\x63\x2d\x33\x2d\x2e\x39\x2d\x36\x2e\x37\x38\x2d\x32\ -\x2d\x36\x2e\x37\x38\x2d\x35\x2e\x37\x39\x2c\x30\x2d\x33\x2e\x31\ -\x33\x2c\x33\x2d\x34\x2e\x37\x36\x2c\x36\x2d\x34\x2e\x37\x36\x61\ -\x31\x33\x2e\x34\x35\x2c\x31\x33\x2e\x34\x35\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x36\x2e\x38\x39\x2c\x32\x2e\x30\x39\x6c\x2e\x35\x32\x2e\ -\x32\x39\x2c\x32\x2e\x32\x36\x2d\x33\x2e\x37\x34\x2d\x2e\x35\x34\ -\x2d\x2e\x33\x32\x41\x31\x36\x2e\x37\x35\x2c\x31\x36\x2e\x37\x35\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x31\x2e\x34\x35\x2c\x30\x2c\x31\ -\x31\x2e\x36\x31\x2c\x31\x31\x2e\x36\x31\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x33\x2e\x39\x31\x2c\x32\x2e\x36\x34\x2c\x38\x2e\x36\x32\x2c\ -\x38\x2e\x36\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x37\x34\x2c\x39\ -\x2e\x32\x34\x63\x30\x2c\x34\x2e\x38\x35\x2c\x33\x2e\x33\x32\x2c\ -\x38\x2e\x32\x39\x2c\x39\x2e\x35\x39\x2c\x31\x30\x61\x31\x37\x2c\ -\x31\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x2e\x33\x32\x2c\x32\x2e\ -\x32\x33\x2c\x35\x2e\x31\x33\x2c\x35\x2e\x31\x33\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x32\x2e\x35\x35\x2c\x34\x2e\x31\x33\x63\x30\x2c\x33\ -\x2e\x33\x39\x2d\x33\x2c\x35\x2e\x39\x35\x2d\x37\x2e\x31\x2c\x35\ -\x2e\x39\x35\x68\x30\x61\x31\x36\x2e\x31\x31\x2c\x31\x36\x2e\x31\ -\x31\x2c\x30\x2c\x30\x2c\x31\x2d\x38\x2e\x32\x32\x2d\x32\x2e\x36\ -\x31\x6c\x2d\x2e\x35\x34\x2d\x2e\x33\x35\x4c\x30\x2c\x33\x32\x2e\ -\x34\x38\x6c\x2e\x34\x39\x2e\x33\x32\x41\x31\x39\x2e\x35\x37\x2c\ -\x31\x39\x2e\x35\x37\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x31\x2e\x32\ -\x2c\x33\x36\x2e\x31\x37\x61\x31\x31\x2e\x37\x37\x2c\x31\x31\x2e\ -\x37\x37\x2c\x30\x2c\x30\x2c\x30\x2c\x38\x2e\x36\x32\x2d\x33\x2e\ -\x33\x32\x2c\x31\x30\x2e\x34\x31\x2c\x31\x30\x2e\x34\x31\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x33\x2e\x30\x35\x2d\x37\x2e\x33\x31\x76\x30\ -\x41\x39\x2e\x34\x37\x2c\x39\x2e\x34\x37\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x31\x39\x2e\x33\x38\x2c\x31\x38\x2e\x34\x38\x5a\x22\x2f\x3e\ -\x3c\x70\x6f\x6c\x79\x67\x6f\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x33\x22\x20\x70\x6f\x69\x6e\x74\x73\x3d\x22\x32\ -\x34\x2e\x32\x20\x30\x2e\x38\x31\x20\x32\x34\x2e\x32\x20\x35\x2e\ -\x32\x20\x33\x36\x2e\x33\x37\x20\x35\x2e\x32\x20\x33\x36\x2e\x33\ -\x37\x20\x33\x35\x2e\x33\x36\x20\x34\x30\x2e\x37\x35\x20\x33\x35\ -\x2e\x33\x36\x20\x34\x30\x2e\x37\x35\x20\x35\x2e\x32\x20\x35\x32\ -\x2e\x39\x32\x20\x35\x2e\x32\x20\x35\x32\x2e\x39\x32\x20\x30\x2e\ -\x38\x31\x20\x32\x34\x2e\x32\x20\x30\x2e\x38\x31\x22\x2f\x3e\x3c\ -\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x05\x0e\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x33\x32\x2e\x39\x32\x20\x32\x38\x2e\x39\x22\x3e\ -\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\ -\x73\x2d\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\ -\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x39\ -\x32\x39\x34\x39\x37\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\ -\x2f\x64\x65\x66\x73\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\ -\x65\x72\x5f\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\ -\x22\x4c\x61\x79\x65\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\ -\x22\x4c\x61\x79\x65\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\ -\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\ -\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\ -\x2d\x31\x22\x20\x64\x3d\x22\x4d\x31\x35\x2e\x38\x37\x2c\x32\x41\ -\x32\x32\x2e\x31\x37\x2c\x32\x32\x2e\x31\x37\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x33\x32\x2e\x33\x2c\x39\x61\x32\x2c\x32\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x2e\x31\x39\x2c\x32\x2e\x36\x35\x2c\x31\x2e\x36\x35\ -\x2c\x31\x2e\x36\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x34\x39\ -\x2c\x30\x2c\x33\x32\x2e\x34\x32\x2c\x33\x32\x2e\x34\x32\x2c\x30\ -\x2c\x30\x2c\x30\x2d\x32\x2e\x37\x39\x2d\x32\x2e\x34\x37\x41\x31\ -\x37\x2e\x33\x35\x2c\x31\x37\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x31\x39\x2e\x35\x34\x2c\x36\x2c\x31\x38\x2c\x31\x38\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x31\x30\x2c\x36\x2e\x38\x36\x61\x31\x38\x2e\ -\x38\x38\x2c\x31\x38\x2e\x38\x38\x2c\x30\x2c\x30\x2c\x30\x2d\x37\ -\x2e\x30\x39\x2c\x34\x2e\x38\x2c\x31\x2e\x35\x37\x2c\x31\x2e\x35\ -\x37\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x38\x31\x2e\x34\x38\x2c\ -\x31\x2e\x38\x36\x2c\x31\x2e\x38\x36\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x2e\x35\x39\x2d\x33\x41\x32\x32\x2e\x35\x2c\x32\x32\x2e\x35\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x34\x2c\x36\x2e\x30\x38\x61\x32\x31\x2c\ -\x32\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x38\x2e\x34\x35\x2d\x33\x2e\ -\x36\x33\x43\x31\x33\x2e\x37\x36\x2c\x32\x2e\x32\x31\x2c\x31\x35\ -\x2e\x31\x2c\x32\x2e\x31\x33\x2c\x31\x35\x2e\x38\x37\x2c\x32\x5a\ -\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x36\x2e\x36\x35\ -\x2c\x31\x37\x2e\x33\x35\x61\x31\x2e\x37\x2c\x31\x2e\x37\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x31\x2e\x34\x31\x2d\x2e\x35\x35\x2c\x31\x31\ -\x2e\x38\x37\x2c\x31\x31\x2e\x38\x37\x2c\x30\x2c\x30\x2c\x30\x2d\ -\x31\x37\x2e\x35\x2d\x2e\x30\x36\x2c\x31\x2e\x36\x35\x2c\x31\x2e\ -\x36\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x38\x38\x2d\x2e\x36\ -\x2c\x31\x2e\x38\x2c\x31\x2e\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\ -\x34\x32\x2d\x31\x2e\x38\x39\x2c\x31\x35\x2e\x31\x33\x2c\x31\x35\ -\x2e\x31\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x38\x2e\x38\x31\x2d\x34\ -\x2e\x37\x36\x41\x31\x34\x2e\x38\x34\x2c\x31\x34\x2e\x38\x34\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x32\x36\x2e\x33\x32\x2c\x31\x33\x61\x31\ -\x32\x2e\x39\x34\x2c\x31\x32\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x31\x2e\x34\x2c\x31\x2e\x33\x37\x41\x31\x2e\x37\x36\x2c\x31\ -\x2e\x37\x36\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x38\x2c\x31\x36\x2e\ -\x33\x2c\x31\x2e\x35\x37\x2c\x31\x2e\x35\x37\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x32\x36\x2e\x36\x35\x2c\x31\x37\x2e\x33\x35\x5a\x22\x2f\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x39\x2e\x35\x33\x2c\x32\x30\ -\x2e\x36\x36\x61\x31\x2e\x39\x2c\x31\x2e\x39\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x2e\x35\x37\x2d\x31\x2e\x33\x37\x2c\x38\x2e\x37\x2c\x38\ -\x2e\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x2e\x30\x38\x2d\x32\x2e\ -\x36\x36\x2c\x38\x2e\x35\x38\x2c\x38\x2e\x35\x38\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x37\x2e\x36\x2c\x32\x2e\x36\x34\x2c\x31\x2e\x39\x31\ -\x2c\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x35\x2c\ -\x32\x2e\x31\x36\x2c\x31\x2e\x36\x33\x2c\x31\x2e\x36\x33\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x32\x2e\x37\x32\x2e\x35\x32\x2c\x35\x2e\x34\ -\x37\x2c\x35\x2e\x34\x37\x2c\x30\x2c\x30\x2c\x30\x2d\x32\x2e\x38\ -\x33\x2d\x31\x2e\x36\x35\x2c\x35\x2e\x33\x33\x2c\x35\x2e\x33\x33\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\x32\x32\x2c\x31\x2e\x36\x2c\ -\x31\x2e\x36\x35\x2c\x31\x2e\x36\x35\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x32\x2e\x38\x37\x2d\x2e\x37\x39\x41\x33\x2e\x34\x37\x2c\x33\x2e\ -\x34\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x39\x2e\x35\x33\x2c\x32\x30\ -\x2e\x36\x36\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x31\ -\x36\x2e\x34\x36\x2c\x32\x38\x2e\x38\x61\x32\x2e\x35\x32\x2c\x32\ -\x2e\x35\x32\x2c\x30\x2c\x31\x2c\x31\x2c\x32\x2e\x33\x35\x2d\x32\ -\x2e\x35\x33\x41\x32\x2e\x34\x32\x2c\x32\x2e\x34\x32\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x31\x36\x2e\x34\x36\x2c\x32\x38\x2e\x38\x5a\x22\ -\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x37\x2e\x38\x2c\x30\x6c\ -\x38\x2e\x36\x36\x2c\x31\x32\x4c\x32\x35\x2e\x31\x32\x2c\x30\x68\ -\x33\x2e\x35\x31\x4c\x31\x38\x2e\x32\x31\x2c\x31\x34\x2e\x34\x35\ -\x2c\x32\x38\x2e\x36\x33\x2c\x32\x38\x2e\x39\x48\x32\x35\x2e\x31\ -\x32\x6c\x2d\x38\x2e\x36\x36\x2d\x31\x32\x4c\x37\x2e\x38\x2c\x32\ -\x38\x2e\x39\x48\x34\x2e\x32\x39\x4c\x31\x34\x2e\x37\x31\x2c\x31\ -\x34\x2e\x34\x35\x2c\x34\x2e\x32\x39\x2c\x30\x5a\x22\x2f\x3e\x3c\ -\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x07\xa4\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x34\x30\x2e\x38\x36\x20\x34\x30\x2e\x38\x36\x22\ -\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\ -\x6c\x73\x2d\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x66\x66\x66\x3b\x7d\ -\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\ -\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\ -\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\ -\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\ -\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\ -\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x70\x61\x74\x68\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\ -\x4d\x33\x37\x2e\x36\x36\x2c\x30\x48\x33\x2e\x32\x41\x33\x2e\x32\ -\x2c\x33\x2e\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\x33\x2e\x32\ -\x56\x33\x37\x2e\x36\x36\x61\x33\x2e\x32\x2c\x33\x2e\x32\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x33\x2e\x32\x2c\x33\x2e\x32\x48\x33\x37\x2e\ -\x36\x36\x61\x33\x2e\x32\x31\x2c\x33\x2e\x32\x31\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x33\x2e\x32\x2d\x33\x2e\x32\x56\x33\x2e\x32\x41\x33\ -\x2e\x32\x2c\x33\x2e\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x33\x37\x2e\ -\x36\x36\x2c\x30\x5a\x6d\x2d\x2e\x33\x33\x2c\x34\x2e\x36\x33\x76\ -\x33\x30\x2e\x31\x61\x2e\x38\x31\x2e\x38\x31\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x2e\x38\x31\x2e\x38\x31\x48\x34\x2e\x33\x33\x61\x2e\x38\ -\x31\x2e\x38\x31\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x38\x31\x2d\x2e\ -\x38\x31\x56\x34\x2e\x36\x33\x61\x2e\x38\x31\x2e\x38\x31\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x2e\x38\x31\x2d\x2e\x38\x31\x48\x33\x36\x2e\ -\x35\x32\x41\x2e\x38\x31\x2e\x38\x31\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x33\x37\x2e\x33\x33\x2c\x34\x2e\x36\x33\x5a\x22\x2f\x3e\x3c\x70\ -\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\ -\x22\x20\x64\x3d\x22\x4d\x31\x31\x2c\x31\x32\x2e\x33\x35\x6c\x2e\ -\x36\x39\x2c\x31\x2e\x31\x31\x63\x31\x2c\x31\x2e\x35\x36\x2c\x31\ -\x2e\x39\x33\x2c\x33\x2e\x31\x32\x2c\x32\x2e\x39\x2c\x34\x2e\x36\ -\x37\x6c\x30\x2c\x2e\x30\x37\x48\x31\x32\x63\x30\x2c\x2e\x30\x38\ -\x2e\x30\x38\x2e\x31\x33\x2e\x31\x31\x2e\x31\x39\x61\x33\x2e\x33\ -\x31\x2c\x33\x2e\x33\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x38\ -\x2c\x31\x2e\x39\x2c\x33\x2e\x37\x37\x2c\x33\x2e\x37\x37\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x34\x37\x2c\x31\x2e\x35\x37\x2c\x31\x33\ -\x2e\x38\x36\x2c\x31\x33\x2e\x38\x36\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x31\x2c\x31\x2e\x34\x34\x2c\x35\x2e\x38\x34\x2c\x35\x2e\x38\x34\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x35\x39\x2c\x31\x2c\x31\x2e\x38\ -\x39\x2c\x31\x2e\x38\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x33\x35\ -\x2c\x32\x2e\x30\x37\x2c\x33\x2e\x37\x31\x2c\x33\x2e\x37\x31\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x37\x38\x63\x2e\x30\x37\x2e\ -\x30\x35\x2e\x30\x37\x2e\x30\x38\x2c\x30\x2c\x2e\x31\x34\x6c\x2d\ -\x2e\x37\x38\x2c\x31\x2e\x32\x32\x4c\x31\x31\x2c\x32\x38\x2e\x34\ -\x34\x61\x35\x2e\x35\x2c\x35\x2e\x35\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x31\x2e\x35\x34\x2d\x31\x2e\x33\x39\x2c\x33\x2e\x34\x34\x2c\x33\ -\x2e\x34\x34\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x37\x2d\x32\x2c\x33\ -\x2e\x34\x39\x2c\x33\x2e\x34\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\ -\x33\x36\x2d\x31\x2e\x35\x39\x2c\x38\x2e\x37\x33\x2c\x38\x2e\x37\ -\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x39\x2d\x31\x2e\x34\x32\x2c\ -\x37\x2e\x32\x2c\x37\x2e\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x37\ -\x34\x2d\x31\x2e\x31\x39\x2c\x31\x2e\x38\x35\x2c\x31\x2e\x38\x35\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x30\x36\x2d\x31\x2e\x37\x37\x2c\ -\x32\x2e\x37\x33\x2c\x32\x2e\x37\x33\x2c\x30\x2c\x30\x2c\x30\x2d\ -\x2e\x37\x38\x2d\x2e\x38\x34\x2e\x33\x34\x2e\x33\x34\x2c\x30\x2c\ -\x30\x2c\x30\x2d\x2e\x31\x39\x2d\x2e\x30\x37\x48\x37\x2e\x33\x39\ -\x73\x2e\x36\x33\x2d\x31\x2e\x30\x35\x2e\x39\x33\x2d\x31\x2e\x35\ -\x33\x4c\x31\x31\x2c\x31\x32\x2e\x33\x35\x5a\x22\x2f\x3e\x3c\x70\ -\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\ -\x22\x20\x64\x3d\x22\x4d\x32\x30\x2e\x34\x34\x2c\x31\x32\x2e\x33\ -\x35\x6c\x2e\x36\x38\x2c\x31\x2e\x31\x31\x4c\x32\x34\x2c\x31\x38\ -\x2e\x31\x33\x6c\x30\x2c\x2e\x30\x37\x48\x32\x31\x2e\x33\x36\x6c\ -\x2e\x31\x31\x2e\x31\x39\x61\x33\x2e\x33\x32\x2c\x33\x2e\x33\x32\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x39\x2c\x31\x2e\x39\x2c\x33\ -\x2e\x37\x37\x2c\x33\x2e\x37\x37\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\ -\x34\x37\x2c\x31\x2e\x35\x37\x2c\x31\x35\x2e\x36\x36\x2c\x31\x35\ -\x2e\x36\x36\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2c\x31\x2e\x34\x34\ -\x2c\x34\x2e\x37\x38\x2c\x34\x2e\x37\x38\x2c\x30\x2c\x30\x2c\x30\ -\x2d\x2e\x35\x39\x2c\x31\x2c\x31\x2e\x38\x37\x2c\x31\x2e\x38\x37\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x33\x34\x2c\x32\x2e\x30\x37\x2c\ -\x33\x2e\x38\x37\x2c\x33\x2e\x38\x37\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x2e\x37\x38\x63\x2e\x30\x38\x2e\x30\x35\x2e\x30\x37\x2e\ -\x30\x38\x2c\x30\x2c\x2e\x31\x34\x2d\x2e\x32\x35\x2e\x33\x37\x2d\ -\x2e\x37\x38\x2c\x31\x2e\x32\x32\x2d\x2e\x37\x38\x2c\x31\x2e\x32\ -\x32\x6c\x2d\x2e\x31\x33\x2d\x2e\x30\x37\x61\x35\x2e\x36\x34\x2c\ -\x35\x2e\x36\x34\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x34\x2d\ -\x31\x2e\x33\x39\x2c\x33\x2e\x33\x36\x2c\x33\x2e\x33\x36\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x36\x39\x2d\x32\x2c\x33\x2e\x34\x39\x2c\ -\x33\x2e\x34\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x33\x35\x2d\x31\ -\x2e\x35\x39\x2c\x31\x30\x2e\x33\x34\x2c\x31\x30\x2e\x33\x34\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x2e\x39\x2d\x31\x2e\x34\x32\x2c\x36\x2e\ -\x35\x39\x2c\x36\x2e\x35\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x37\ -\x34\x2d\x31\x2e\x31\x39\x2c\x31\x2e\x38\x38\x2c\x31\x2e\x38\x38\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x37\x37\x2c\x32\x2e\ -\x37\x36\x2c\x32\x2e\x37\x36\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x37\ -\x39\x2d\x2e\x38\x34\x2e\x33\x34\x2e\x33\x34\x2c\x30\x2c\x30\x2c\ -\x30\x2d\x2e\x31\x39\x2d\x2e\x30\x37\x48\x31\x36\x2e\x37\x39\x73\ -\x2e\x36\x34\x2d\x31\x2e\x30\x35\x2e\x39\x33\x2d\x31\x2e\x35\x33\ -\x6c\x32\x2e\x37\x2d\x34\x2e\x33\x33\x5a\x22\x2f\x3e\x3c\x70\x61\ -\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\ -\x20\x64\x3d\x22\x4d\x32\x39\x2e\x38\x34\x2c\x31\x32\x2e\x33\x35\ -\x6c\x2e\x36\x39\x2c\x31\x2e\x31\x31\x63\x31\x2c\x31\x2e\x35\x36\ -\x2c\x31\x2e\x39\x33\x2c\x33\x2e\x31\x32\x2c\x32\x2e\x39\x2c\x34\ -\x2e\x36\x37\x6c\x30\x2c\x2e\x30\x37\x68\x2d\x32\x2e\x37\x61\x31\ -\x2e\x36\x2c\x31\x2e\x36\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x31\x31\ -\x2e\x31\x39\x2c\x33\x2e\x33\x31\x2c\x33\x2e\x33\x31\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x2e\x34\x38\x2c\x31\x2e\x39\x2c\x33\x2e\x37\x37\ -\x2c\x33\x2e\x37\x37\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x34\x37\x2c\ -\x31\x2e\x35\x37\x2c\x31\x33\x2e\x38\x36\x2c\x31\x33\x2e\x38\x36\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2c\x31\x2e\x34\x34\x2c\x35\x2e\ -\x38\x34\x2c\x35\x2e\x38\x34\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x35\ -\x39\x2c\x31\x2c\x31\x2e\x38\x39\x2c\x31\x2e\x38\x39\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x2e\x33\x35\x2c\x32\x2e\x30\x37\x2c\x33\x2e\x37\ -\x31\x2c\x33\x2e\x37\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\ -\x37\x38\x63\x2e\x30\x37\x2e\x30\x35\x2e\x30\x37\x2e\x30\x38\x2c\ -\x30\x2c\x2e\x31\x34\x6c\x2d\x2e\x37\x38\x2c\x31\x2e\x32\x32\x2d\ -\x2e\x31\x32\x2d\x2e\x30\x37\x61\x35\x2e\x35\x2c\x35\x2e\x35\x2c\ -\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x34\x2d\x31\x2e\x33\x39\x2c\ -\x33\x2e\x34\x34\x2c\x33\x2e\x34\x34\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x2e\x37\x2d\x32\x2c\x33\x2e\x34\x39\x2c\x33\x2e\x34\x39\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x2e\x33\x36\x2d\x31\x2e\x35\x39\x2c\x38\x2e\ -\x37\x33\x2c\x38\x2e\x37\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x39\ -\x2d\x31\x2e\x34\x32\x2c\x37\x2e\x32\x2c\x37\x2e\x32\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x2e\x37\x34\x2d\x31\x2e\x31\x39\x2c\x31\x2e\x38\ -\x35\x2c\x31\x2e\x38\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x30\x36\ -\x2d\x31\x2e\x37\x37\x2c\x32\x2e\x37\x33\x2c\x32\x2e\x37\x33\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x2e\x37\x38\x2d\x2e\x38\x34\x2e\x33\x35\ -\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x32\x2d\x2e\x30\x37\ -\x48\x32\x36\x2e\x31\x39\x73\x2e\x36\x34\x2d\x31\x2e\x30\x35\x2e\ -\x39\x34\x2d\x31\x2e\x35\x33\x6c\x32\x2e\x36\x39\x2d\x34\x2e\x33\ -\x33\x5a\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\ -\x76\x67\x3e\ -\x00\x00\x07\x3b\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x34\x30\x2e\x35\x20\x33\x33\x2e\x35\x32\x22\x3e\ -\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\ -\x73\x2d\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x66\x66\x66\x3b\x7d\x3c\ -\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x67\ -\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\x61\ -\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x32\ -\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\ -\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\ -\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ -\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\ -\x33\x32\x2e\x36\x33\x2c\x33\x33\x2e\x35\x32\x48\x2e\x39\x34\x61\ -\x2e\x39\x34\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x36\x38\ -\x2d\x31\x2e\x35\x38\x6c\x36\x2e\x39\x32\x2d\x37\x2e\x33\x35\x61\ -\x2e\x39\x32\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x36\x38\ -\x2d\x2e\x32\x39\x68\x33\x31\x2e\x37\x61\x2e\x39\x34\x2e\x39\x34\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x36\x38\x2c\x31\x2e\x35\x38\x6c\ -\x2d\x36\x2e\x39\x32\x2c\x37\x2e\x33\x35\x41\x31\x2c\x31\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x33\x32\x2e\x36\x33\x2c\x33\x33\x2e\x35\x32\ -\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x31\x30\x2e\x33\ -\x32\x2c\x30\x63\x2e\x33\x2e\x34\x39\x2e\x36\x2c\x31\x2c\x2e\x39\ -\x31\x2c\x31\x2e\x34\x37\x6c\x33\x2e\x38\x33\x2c\x36\x2e\x31\x36\ -\x2c\x30\x2c\x2e\x30\x39\x48\x31\x31\x2e\x35\x35\x6c\x2e\x31\x34\ -\x2e\x32\x34\x61\x34\x2e\x33\x37\x2c\x34\x2e\x33\x37\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x2e\x36\x34\x2c\x32\x2e\x35\x31\x2c\x34\x2e\x39\ -\x34\x2c\x34\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x36\x32\ -\x2c\x32\x2e\x30\x36\x2c\x31\x39\x2c\x31\x39\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x31\x2e\x32\x37\x2c\x31\x2e\x39\x2c\x38\x2e\x33\x33\x2c\ -\x38\x2e\x33\x33\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x37\x38\x2c\x31\ -\x2e\x33\x32\x2c\x32\x2e\x35\x32\x2c\x32\x2e\x35\x32\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x2e\x34\x36\x2c\x32\x2e\x37\x34\x2c\x35\x2e\x34\ -\x32\x2c\x35\x2e\x34\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2e\x33\ -\x31\x2c\x31\x63\x2e\x31\x2e\x30\x35\x2e\x30\x38\x2e\x30\x39\x2c\ -\x30\x2c\x2e\x31\x38\x6c\x2d\x31\x2c\x31\x2e\x36\x2d\x2e\x31\x36\ -\x2d\x2e\x30\x38\x61\x37\x2e\x35\x36\x2c\x37\x2e\x35\x36\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x32\x2d\x31\x2e\x38\x34\x2c\x34\x2e\x34\x33\ -\x2c\x34\x2e\x34\x33\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x39\x32\x2d\ -\x32\x2e\x35\x39\x2c\x34\x2e\x35\x38\x2c\x34\x2e\x35\x38\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x2e\x34\x37\x2d\x32\x2e\x31\x41\x31\x32\x2e\ -\x31\x33\x2c\x31\x32\x2e\x31\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x39\ -\x2c\x31\x32\x2e\x38\x31\x61\x38\x2e\x39\x32\x2c\x38\x2e\x39\x32\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x36\x2c\x32\x2e\ -\x35\x2c\x32\x2e\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x30\x37\x2d\ -\x32\x2e\x33\x33\x2c\x33\x2e\x36\x38\x2c\x33\x2e\x36\x38\x2c\x30\ -\x2c\x30\x2c\x30\x2d\x31\x2d\x31\x2e\x31\x31\x2e\x34\x2e\x34\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x2e\x32\x35\x2d\x2e\x30\x39\x48\x35\x2e\ -\x35\x32\x73\x2e\x38\x33\x2d\x31\x2e\x33\x38\x2c\x31\x2e\x32\x33\ -\x2d\x32\x43\x37\x2e\x39\x33\x2c\x33\x2e\x38\x31\x2c\x39\x2e\x31\ -\x31\x2c\x31\x2e\x39\x2c\x31\x30\x2e\x33\x2c\x30\x5a\x22\x2f\x3e\ -\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\ -\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x32\x2e\x37\x32\x2c\x30\x6c\ -\x2e\x39\x2c\x31\x2e\x34\x37\x71\x31\x2e\x39\x32\x2c\x33\x2e\x30\ -\x38\x2c\x33\x2e\x38\x33\x2c\x36\x2e\x31\x36\x61\x2e\x32\x34\x2e\ -\x32\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x30\x35\x2e\x30\x39\x48\ -\x32\x33\x2e\x39\x34\x63\x2e\x30\x36\x2e\x30\x39\x2e\x31\x2e\x31\ -\x37\x2e\x31\x34\x2e\x32\x34\x61\x34\x2e\x32\x39\x2c\x34\x2e\x32\ -\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x36\x34\x2c\x32\x2e\x35\x31\ -\x2c\x34\x2e\x38\x2c\x34\x2e\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\ -\x36\x32\x2c\x32\x2e\x30\x36\x2c\x31\x37\x2e\x33\x37\x2c\x31\x37\ -\x2e\x33\x37\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x32\x36\x2c\x31\ -\x2e\x39\x2c\x37\x2e\x36\x33\x2c\x37\x2e\x36\x33\x2c\x30\x2c\x30\ -\x2c\x30\x2d\x2e\x37\x38\x2c\x31\x2e\x33\x32\x2c\x32\x2e\x35\x2c\ -\x32\x2e\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x36\x2c\x32\x2e\ -\x37\x34\x2c\x35\x2e\x33\x37\x2c\x35\x2e\x33\x37\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x31\x2e\x33\x2c\x31\x63\x2e\x31\x2e\x30\x35\x2e\x30\ -\x39\x2e\x30\x39\x2c\x30\x2c\x2e\x31\x38\x2d\x2e\x33\x33\x2e\x34\ -\x39\x2d\x31\x2c\x31\x2e\x36\x2d\x31\x2c\x31\x2e\x36\x6c\x2d\x2e\ -\x31\x36\x2d\x2e\x30\x38\x61\x37\x2e\x37\x31\x2c\x37\x2e\x37\x31\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2d\x31\x2e\x38\x34\x2c\x34\x2e\ -\x35\x2c\x34\x2e\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x39\x32\x2d\ -\x32\x2e\x35\x39\x2c\x34\x2e\x35\x38\x2c\x34\x2e\x35\x38\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x2e\x34\x37\x2d\x32\x2e\x31\x2c\x31\x33\x2c\ -\x31\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x31\x39\x2d\x31\x2e\ -\x38\x38\x2c\x38\x2e\x39\x32\x2c\x38\x2e\x39\x32\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x36\x2c\x32\x2e\x34\x37\x2c\x32\ -\x2e\x34\x37\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x30\x38\x2d\x32\x2e\ -\x33\x33\x2c\x33\x2e\x36\x35\x2c\x33\x2e\x36\x35\x2c\x30\x2c\x30\ -\x2c\x30\x2d\x31\x2d\x31\x2e\x31\x31\x41\x2e\x34\x31\x2e\x34\x31\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x31\x2c\x37\x2e\x37\x32\x48\x31\ -\x37\x2e\x39\x31\x73\x2e\x38\x34\x2d\x31\x2e\x33\x38\x2c\x31\x2e\ -\x32\x33\x2d\x32\x43\x32\x30\x2e\x33\x33\x2c\x33\x2e\x38\x31\x2c\ -\x32\x31\x2e\x35\x31\x2c\x31\x2e\x39\x2c\x32\x32\x2e\x36\x39\x2c\ -\x30\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x35\x2e\ -\x31\x32\x2c\x30\x2c\x33\x36\x2c\x31\x2e\x34\x37\x6c\x33\x2e\x38\ -\x33\x2c\x36\x2e\x31\x36\x2c\x30\x2c\x2e\x30\x39\x48\x33\x36\x2e\ -\x33\x34\x63\x2e\x30\x36\x2e\x30\x39\x2e\x31\x2e\x31\x37\x2e\x31\ -\x34\x2e\x32\x34\x61\x34\x2e\x33\x37\x2c\x34\x2e\x33\x37\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x2e\x36\x34\x2c\x32\x2e\x35\x31\x2c\x34\x2e\ -\x38\x2c\x34\x2e\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x36\x32\x2c\ -\x32\x2e\x30\x36\x2c\x31\x39\x2c\x31\x39\x2c\x30\x2c\x30\x2c\x31\ -\x2d\x31\x2e\x32\x36\x2c\x31\x2e\x39\x2c\x37\x2c\x37\x2c\x30\x2c\ -\x30\x2c\x30\x2d\x2e\x37\x38\x2c\x31\x2e\x33\x32\x2c\x32\x2e\x35\ -\x2c\x32\x2e\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x35\x2c\x32\ -\x2e\x37\x34\x2c\x35\x2e\x34\x32\x2c\x35\x2e\x34\x32\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x31\x2e\x33\x31\x2c\x31\x63\x2e\x31\x2e\x30\x35\ -\x2e\x30\x39\x2e\x30\x39\x2c\x30\x2c\x2e\x31\x38\x6c\x2d\x31\x2c\ -\x31\x2e\x36\x2d\x2e\x31\x36\x2d\x2e\x30\x38\x61\x37\x2e\x35\x36\ -\x2c\x37\x2e\x35\x36\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2d\x31\x2e\ -\x38\x34\x2c\x34\x2e\x34\x33\x2c\x34\x2e\x34\x33\x2c\x30\x2c\x30\ -\x2c\x31\x2d\x2e\x39\x32\x2d\x32\x2e\x35\x39\x2c\x34\x2e\x35\x38\ -\x2c\x34\x2e\x35\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x37\x2d\ -\x32\x2e\x31\x2c\x31\x32\x2e\x39\x33\x2c\x31\x32\x2e\x39\x33\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x31\x38\x2d\x31\x2e\x38\x38\x2c\ -\x38\x2e\x33\x36\x2c\x38\x2e\x33\x36\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x31\x2e\x35\x36\x2c\x32\x2e\x34\x34\x2c\x32\x2e\x34\x34\ -\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x30\x37\x2d\x32\x2e\x33\x33\x2c\ -\x33\x2e\x35\x36\x2c\x33\x2e\x35\x36\x2c\x30\x2c\x30\x2c\x30\x2d\ -\x31\x2d\x31\x2e\x31\x31\x2e\x33\x38\x2e\x33\x38\x2c\x30\x2c\x30\ -\x2c\x30\x2d\x2e\x32\x35\x2d\x2e\x30\x39\x48\x33\x30\x2e\x33\x31\ -\x73\x2e\x38\x34\x2d\x31\x2e\x33\x38\x2c\x31\x2e\x32\x33\x2d\x32\ -\x4c\x33\x35\x2e\x30\x39\x2c\x30\x5a\x22\x2f\x3e\x3c\x2f\x67\x3e\ -\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x0a\x60\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x31\x37\x36\x2e\x30\x32\x20\x35\x38\x2e\x34\x31\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\ -\x36\x35\x2e\x37\x38\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ -\x69\x6c\x79\x3a\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\ -\x2c\x20\x4d\x6f\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\ -\x65\x69\x67\x68\x74\x3a\x32\x30\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x31\x2c\x2e\x63\x6c\x73\x2d\x34\x7b\x66\x69\x6c\x6c\x3a\x23\x66\ -\x66\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x6c\x65\x74\x74\x65\ -\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x32\x65\ -\x6d\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x6c\x65\x74\x74\x65\x72\ -\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x35\x65\x6d\ -\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\ -\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\ -\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\ -\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x74\x65\x78\x74\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x74\ -\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\ -\x61\x74\x65\x28\x34\x37\x2e\x39\x31\x20\x34\x39\x2e\x39\x33\x29\ -\x22\x3e\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x32\x22\x3e\x61\x3c\x2f\x74\x73\x70\x61\x6e\x3e\ -\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x33\x22\x20\x78\x3d\x22\x33\x32\x2e\x36\x39\x22\x20\x79\ -\x3d\x22\x30\x22\x3e\x62\x3c\x2f\x74\x73\x70\x61\x6e\x3e\x3c\x74\ -\x73\x70\x61\x6e\x20\x78\x3d\x22\x35\x38\x2e\x34\x31\x22\x20\x79\ -\x3d\x22\x30\x22\x3e\x73\x3c\x2f\x74\x73\x70\x61\x6e\x3e\x3c\x2f\ -\x74\x65\x78\x74\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x32\x34\x2e\ -\x37\x38\x2c\x31\x38\x2e\x31\x34\x63\x32\x2e\x35\x36\x2c\x31\x2e\ -\x33\x35\x2c\x33\x2e\x37\x31\x2c\x36\x2e\x34\x34\x2c\x34\x2e\x31\ -\x38\x2c\x39\x2e\x30\x36\x2c\x31\x2e\x32\x2c\x36\x2e\x35\x36\x2c\ -\x31\x2e\x31\x34\x2c\x31\x39\x2e\x31\x2d\x33\x2e\x30\x39\x2c\x32\ -\x34\x2e\x35\x31\x2d\x2e\x38\x31\x2c\x31\x2d\x31\x2e\x38\x32\x2c\ -\x31\x2e\x32\x32\x2d\x32\x2e\x37\x34\x2e\x32\x33\x61\x39\x2e\x31\ -\x31\x2c\x39\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x38\ -\x35\x2d\x33\x2e\x31\x31\x63\x2d\x32\x2e\x38\x33\x2d\x37\x2e\x36\ -\x2d\x32\x2e\x38\x38\x2d\x31\x39\x2e\x32\x39\x2c\x30\x2d\x32\x36\ -\x2e\x39\x2e\x35\x38\x2d\x31\x2e\x34\x32\x2c\x31\x2e\x33\x31\x2d\ -\x33\x2c\x32\x2e\x37\x35\x2d\x33\x2e\x37\x39\x5a\x4d\x32\x33\x2e\ -\x35\x33\x2c\x34\x39\x2e\x37\x32\x61\x2e\x38\x38\x2e\x38\x38\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x36\x2e\x36\x2e\x39\x31\x2e\x39\ -\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x39\x2d\x2e\x36\x32\x63\ -\x33\x2e\x30\x39\x2d\x36\x2e\x35\x34\x2c\x33\x2e\x31\x33\x2d\x31\ -\x36\x2e\x35\x35\x2c\x31\x2e\x36\x39\x2d\x32\x33\x2e\x35\x37\x41\ -\x32\x30\x2e\x32\x37\x2c\x32\x30\x2e\x32\x37\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x32\x35\x2e\x32\x37\x2c\x32\x31\x61\x2e\x39\x2e\x39\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x2e\x38\x35\x2d\x2e\x36\x2e\x39\x32\x2e\ -\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2e\x36\x31\x2c\x31\ -\x34\x2e\x39\x34\x2c\x31\x34\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x30\ -\x2d\x31\x2e\x33\x2c\x33\x2e\x34\x38\x43\x32\x30\x2e\x35\x32\x2c\ -\x33\x31\x2e\x36\x34\x2c\x31\x39\x2e\x34\x31\x2c\x34\x30\x2e\x34\ -\x31\x2c\x32\x33\x2e\x35\x33\x2c\x34\x39\x2e\x37\x32\x5a\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\ -\x22\x4d\x35\x2e\x36\x34\x2c\x31\x38\x2e\x31\x34\x61\x35\x2e\x31\ -\x33\x2c\x35\x2e\x31\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2c\x32\ -\x2e\x32\x37\x63\x2d\x2e\x37\x32\x2c\x30\x2d\x31\x2e\x33\x39\x2c\ -\x30\x2d\x32\x2e\x30\x36\x2c\x30\x73\x2d\x31\x2c\x2e\x31\x2d\x31\ -\x2e\x32\x39\x2e\x37\x34\x43\x31\x2c\x32\x38\x2e\x34\x38\x2c\x31\ -\x2c\x34\x31\x2e\x31\x39\x2c\x33\x2e\x39\x32\x2c\x34\x38\x2e\x37\ -\x63\x2e\x31\x35\x2e\x33\x36\x2e\x33\x33\x2e\x37\x31\x2e\x34\x39\ -\x2c\x31\x2e\x30\x36\x61\x31\x2c\x31\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x2e\x35\x37\x48\x37\x2e\x36\x37\x41\x35\x2e\x30\x37\x2c\ -\x35\x2e\x30\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x2e\x39\x32\x2c\ -\x35\x32\x2e\x34\x63\x2d\x31\x2e\x32\x34\x2e\x37\x34\x2d\x32\x2e\ -\x34\x2d\x2e\x38\x38\x2d\x32\x2e\x39\x33\x2d\x31\x2e\x38\x31\x43\ -\x2d\x2e\x38\x32\x2c\x34\x33\x2e\x35\x33\x2d\x2e\x37\x34\x2c\x33\ -\x30\x2e\x32\x38\x2c\x31\x2e\x38\x33\x2c\x32\x32\x2e\x37\x34\x63\ -\x2e\x35\x39\x2d\x31\x2e\x36\x34\x2c\x31\x2e\x34\x2d\x33\x2e\x36\ -\x39\x2c\x33\x2e\x30\x35\x2d\x34\x2e\x36\x5a\x22\x20\x74\x72\x61\ -\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\ -\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x33\ -\x2e\x35\x32\x2c\x33\x35\x2e\x37\x37\x63\x30\x2d\x35\x2c\x2e\x33\ -\x37\x2d\x39\x2e\x32\x33\x2c\x32\x2e\x31\x37\x2d\x31\x33\x2e\x36\ -\x31\x61\x2e\x34\x32\x2e\x34\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\ -\x34\x38\x2d\x2e\x32\x39\x63\x2e\x36\x2c\x30\x2c\x31\x2e\x32\x2c\ -\x30\x2c\x31\x2e\x38\x2c\x30\x2c\x2e\x32\x39\x2c\x30\x2c\x2e\x33\ -\x35\x2c\x30\x2c\x2e\x32\x35\x2e\x33\x61\x32\x37\x2e\x33\x35\x2c\ -\x32\x37\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x33\x37\ -\x2c\x35\x63\x2d\x31\x2e\x31\x37\x2c\x36\x2e\x39\x2d\x31\x2e\x30\ -\x38\x2c\x31\x34\x2e\x37\x35\x2c\x31\x2e\x33\x37\x2c\x32\x31\x2e\ -\x33\x38\x2e\x31\x2e\x32\x34\x2e\x30\x39\x2e\x33\x34\x2d\x2e\x32\ -\x35\x2e\x33\x33\x2d\x2e\x36\x2c\x30\x2d\x31\x2e\x32\x31\x2c\x30\ -\x2d\x31\x2e\x38\x31\x2c\x30\x61\x2e\x34\x33\x2e\x34\x33\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x34\x37\x2d\x2e\x33\x43\x34\x2c\x34\x34\ -\x2e\x36\x38\x2c\x33\x2e\x35\x32\x2c\x33\x39\x2e\x36\x38\x2c\x33\ -\x2e\x35\x32\x2c\x33\x35\x2e\x37\x37\x5a\x22\x20\x74\x72\x61\x6e\ -\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\ -\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x31\x38\ -\x2e\x33\x38\x2c\x32\x31\x2e\x38\x37\x63\x2e\x32\x38\x2e\x30\x35\ -\x2e\x38\x31\x2d\x2e\x31\x36\x2c\x31\x2c\x2e\x30\x39\x73\x2d\x2e\ -\x31\x33\x2e\x36\x31\x2d\x2e\x32\x32\x2e\x39\x32\x61\x34\x35\x2e\ -\x36\x39\x2c\x34\x35\x2e\x36\x39\x2c\x30\x2c\x30\x2c\x30\x2d\x31\ -\x2e\x35\x32\x2c\x31\x37\x2e\x32\x39\x2c\x33\x39\x2e\x36\x31\x2c\ -\x33\x39\x2e\x36\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2e\x37\x32\ -\x2c\x38\x2e\x33\x31\x63\x2e\x31\x31\x2e\x33\x32\x2e\x30\x35\x2e\ -\x34\x2d\x2e\x33\x33\x2e\x33\x38\x73\x2d\x31\x2c\x30\x2d\x31\x2e\ -\x35\x33\x2c\x30\x61\x2e\x34\x39\x2e\x34\x39\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x2e\x35\x34\x2d\x2e\x33\x33\x2c\x32\x33\x2e\x38\x35\x2c\ -\x32\x33\x2e\x38\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x31\ -\x2d\x35\x2e\x33\x63\x2d\x31\x2e\x30\x39\x2d\x36\x2e\x35\x38\x2d\ -\x31\x2d\x31\x34\x2e\x37\x34\x2c\x31\x2e\x34\x39\x2d\x32\x31\x61\ -\x2e\x35\x2e\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x36\x2d\x2e\x33\ -\x37\x41\x37\x2e\x34\x31\x2c\x37\x2e\x34\x31\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x31\x38\x2e\x33\x38\x2c\x32\x31\x2e\x38\x37\x5a\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\ -\x22\x4d\x37\x2e\x38\x37\x2c\x33\x36\x2e\x35\x34\x61\x31\x31\x2e\ -\x32\x36\x2c\x31\x31\x2e\x32\x36\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\ -\x30\x36\x2d\x32\x2e\x33\x33\x63\x2e\x32\x37\x2d\x34\x2e\x30\x38\ -\x2e\x35\x37\x2d\x38\x2e\x32\x38\x2c\x32\x2e\x32\x35\x2d\x31\x32\ -\x2e\x31\x31\x61\x2e\x33\x36\x2e\x33\x36\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x2e\x33\x38\x2d\x2e\x32\x33\x63\x31\x2e\x33\x39\x2d\x2e\x30\ -\x38\x2c\x31\x2e\x33\x38\x2d\x2e\x30\x38\x2c\x31\x2c\x31\x43\x39\ -\x2c\x33\x30\x2e\x34\x35\x2c\x38\x2e\x39\x33\x2c\x34\x31\x2c\x31\ -\x31\x2e\x36\x38\x2c\x34\x38\x2e\x35\x63\x2e\x31\x32\x2e\x33\x31\ -\x2e\x30\x35\x2e\x33\x37\x2d\x2e\x33\x2e\x33\x36\x2d\x31\x2e\x32\ -\x35\x2c\x30\x2d\x31\x2e\x32\x36\x2c\x30\x2d\x31\x2e\x36\x34\x2d\ -\x31\x41\x33\x36\x2e\x31\x34\x2c\x33\x36\x2e\x31\x34\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x37\x2e\x38\x37\x2c\x33\x36\x2e\x35\x34\x5a\x22\ -\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\ -\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\ -\x3d\x22\x4d\x31\x31\x2e\x33\x35\x2c\x33\x35\x2e\x36\x36\x63\x30\ -\x2d\x34\x2e\x38\x36\x2e\x33\x38\x2d\x39\x2e\x31\x37\x2c\x32\x2e\ -\x31\x37\x2d\x31\x33\x2e\x34\x39\x61\x2e\x34\x2e\x34\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x2e\x34\x37\x2d\x2e\x33\x63\x2e\x34\x2c\x30\x2c\ -\x31\x2d\x2e\x31\x35\x2c\x31\x2e\x31\x38\x2e\x30\x37\x73\x2d\x2e\ -\x31\x37\x2e\x36\x33\x2d\x2e\x32\x38\x2c\x31\x63\x2d\x32\x2e\x34\ -\x31\x2c\x37\x2e\x36\x31\x2d\x32\x2e\x35\x31\x2c\x31\x38\x2c\x2e\ -\x32\x37\x2c\x32\x35\x2e\x35\x36\x2e\x31\x34\x2e\x33\x36\x2c\x30\ -\x2c\x2e\x34\x2d\x2e\x33\x35\x2e\x33\x39\x2d\x31\x2e\x31\x37\x2c\ -\x30\x2d\x31\x2e\x31\x39\x2c\x30\x2d\x31\x2e\x35\x37\x2d\x31\x41\ -\x33\x35\x2e\x37\x34\x2c\x33\x35\x2e\x37\x34\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x31\x31\x2e\x33\x35\x2c\x33\x35\x2e\x36\x36\x5a\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\ -\x22\x4d\x31\x39\x2e\x31\x32\x2c\x32\x30\x2e\x37\x37\x63\x2e\x39\ -\x32\x2c\x30\x2c\x2e\x39\x32\x2c\x30\x2c\x31\x2e\x31\x33\x2d\x2e\ -\x37\x38\x2e\x38\x36\x2d\x32\x2e\x38\x36\x2d\x2e\x32\x35\x2d\x35\ -\x2e\x34\x35\x2d\x33\x2e\x35\x2d\x35\x2e\x38\x37\x2d\x31\x2e\x35\ -\x31\x2d\x2e\x32\x33\x2d\x31\x2e\x35\x2d\x2e\x32\x31\x2d\x31\x2e\ -\x34\x38\x2c\x31\x2e\x31\x2c\x30\x2c\x2e\x32\x35\x2e\x30\x39\x2e\ -\x33\x33\x2e\x33\x37\x2e\x33\x33\x61\x35\x2c\x35\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x31\x2e\x34\x39\x2e\x32\x33\x2c\x32\x2e\x31\x31\x2c\ -\x32\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x35\x38\x2c\ -\x31\x2e\x37\x37\x2c\x35\x2e\x37\x34\x2c\x35\x2e\x37\x34\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x33\x38\x2c\x32\x2e\x37\x38\x63\x2d\x2e\ -\x31\x35\x2e\x34\x34\x2d\x2e\x31\x35\x2e\x34\x34\x2e\x34\x31\x2e\ -\x34\x34\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\ -\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\ -\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x34\x22\x20\x63\x78\x3d\x22\x32\x34\x2e\x34\x32\x22\ -\x20\x63\x79\x3d\x22\x33\x35\x2e\x35\x34\x22\x20\x72\x78\x3d\x22\ -\x31\x2e\x30\x31\x22\x20\x72\x79\x3d\x22\x35\x2e\x39\x39\x22\x2f\ -\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x0a\x25\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x31\x37\x36\x2e\x30\x32\x20\x35\x38\x2e\x34\x31\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\ -\x36\x35\x2e\x37\x38\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ -\x69\x6c\x79\x3a\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\ -\x2c\x20\x4d\x6f\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\ -\x65\x69\x67\x68\x74\x3a\x32\x30\x30\x3b\x6c\x65\x74\x74\x65\x72\ -\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x31\x65\x6d\ -\x3b\x7d\x2e\x63\x6c\x73\x2d\x31\x2c\x2e\x63\x6c\x73\x2d\x33\x7b\ -\x66\x69\x6c\x6c\x3a\x23\x66\x66\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x32\x7b\x6c\x65\x74\x74\x65\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\ -\x3a\x2d\x30\x2e\x30\x33\x65\x6d\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\ -\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x67\x20\x69\x64\x3d\x22\ -\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\ -\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x32\x22\x3e\x3c\x67\x20\ -\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x2d\x32\x22\x20\x64\ -\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\ -\x31\x22\x3e\x3c\x74\x65\x78\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x31\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\ -\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x34\x37\x2e\x39\ -\x31\x20\x34\x39\x2e\x39\x33\x29\x22\x3e\x70\x6c\x3c\x74\x73\x70\ -\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\ -\x20\x78\x3d\x22\x35\x35\x2e\x31\x39\x22\x20\x79\x3d\x22\x30\x22\ -\x3e\x61\x3c\x2f\x74\x73\x70\x61\x6e\x3e\x3c\x2f\x74\x65\x78\x74\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x32\x34\x2e\x37\x38\x2c\x31\ -\x38\x2e\x31\x32\x63\x32\x2e\x35\x36\x2c\x31\x2e\x33\x34\x2c\x33\ -\x2e\x37\x31\x2c\x36\x2e\x34\x34\x2c\x34\x2e\x31\x38\x2c\x39\x2e\ -\x30\x36\x2c\x31\x2e\x32\x2c\x36\x2e\x35\x36\x2c\x31\x2e\x31\x34\ -\x2c\x31\x39\x2e\x31\x2d\x33\x2e\x30\x39\x2c\x32\x34\x2e\x35\x31\ -\x2d\x2e\x38\x31\x2c\x31\x2d\x31\x2e\x38\x32\x2c\x31\x2e\x32\x31\ -\x2d\x32\x2e\x37\x34\x2e\x32\x32\x61\x39\x2c\x39\x2c\x30\x2c\x30\ -\x2c\x31\x2d\x31\x2e\x38\x35\x2d\x33\x2e\x31\x63\x2d\x32\x2e\x38\ -\x33\x2d\x37\x2e\x36\x31\x2d\x32\x2e\x38\x38\x2d\x31\x39\x2e\x33\ -\x2c\x30\x2d\x32\x36\x2e\x39\x31\x2e\x35\x38\x2d\x31\x2e\x34\x31\ -\x2c\x31\x2e\x33\x31\x2d\x33\x2c\x32\x2e\x37\x35\x2d\x33\x2e\x37\ -\x38\x5a\x4d\x32\x33\x2e\x35\x33\x2c\x34\x39\x2e\x36\x39\x61\x2e\ -\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x36\x2e\ -\x36\x31\x2e\x39\x34\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\ -\x38\x39\x2d\x2e\x36\x32\x63\x33\x2e\x30\x39\x2d\x36\x2e\x35\x34\ -\x2c\x33\x2e\x31\x33\x2d\x31\x36\x2e\x35\x35\x2c\x31\x2e\x36\x39\ -\x2d\x32\x33\x2e\x35\x37\x41\x32\x30\x2e\x32\x37\x2c\x32\x30\x2e\ -\x32\x37\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x35\x2e\x32\x37\x2c\x32\ -\x31\x61\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\ -\x38\x35\x2d\x2e\x36\x31\x2e\x39\x34\x2e\x39\x34\x2c\x30\x2c\x30\ -\x2c\x30\x2d\x2e\x39\x2e\x36\x32\x2c\x31\x34\x2e\x39\x34\x2c\x31\ -\x34\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x33\x2c\x33\ -\x2e\x34\x38\x43\x32\x30\x2e\x35\x32\x2c\x33\x31\x2e\x36\x32\x2c\ -\x31\x39\x2e\x34\x31\x2c\x34\x30\x2e\x33\x39\x2c\x32\x33\x2e\x35\ -\x33\x2c\x34\x39\x2e\x36\x39\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\ -\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\ -\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x35\x2e\x36\x34\ -\x2c\x31\x38\x2e\x31\x32\x61\x35\x2e\x31\x36\x2c\x35\x2e\x31\x36\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2c\x32\x2e\x32\x36\x48\x35\x2e\ -\x36\x31\x63\x2d\x2e\x38\x2c\x30\x2d\x31\x2c\x2e\x31\x31\x2d\x31\ -\x2e\x32\x39\x2e\x37\x34\x43\x31\x2c\x32\x38\x2e\x34\x35\x2c\x31\ -\x2c\x34\x31\x2e\x31\x37\x2c\x33\x2e\x39\x32\x2c\x34\x38\x2e\x36\ -\x38\x63\x2e\x31\x35\x2e\x33\x36\x2e\x33\x33\x2e\x37\x2e\x34\x39\ -\x2c\x31\x2e\x30\x36\x61\x31\x2c\x31\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x2e\x35\x37\x63\x2e\x37\x37\x2c\x30\x2c\x31\x2e\x34\x31\ -\x2c\x30\x2c\x32\x2e\x32\x32\x2c\x30\x61\x35\x2c\x35\x2c\x30\x2c\ -\x30\x2c\x31\x2d\x31\x2e\x37\x35\x2c\x32\x2e\x30\x37\x63\x2d\x31\ -\x2e\x32\x34\x2e\x37\x34\x2d\x32\x2e\x34\x2d\x2e\x38\x37\x2d\x32\ -\x2e\x39\x33\x2d\x31\x2e\x38\x43\x2d\x2e\x38\x32\x2c\x34\x33\x2e\ -\x35\x2d\x2e\x37\x34\x2c\x33\x30\x2e\x32\x35\x2c\x31\x2e\x38\x33\ -\x2c\x32\x32\x2e\x37\x32\x63\x2e\x35\x39\x2d\x31\x2e\x36\x35\x2c\ -\x31\x2e\x34\x2d\x33\x2e\x36\x39\x2c\x33\x2e\x30\x35\x2d\x34\x2e\ -\x36\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\ -\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\ -\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\ -\x22\x20\x64\x3d\x22\x4d\x33\x2e\x35\x32\x2c\x33\x35\x2e\x37\x35\ -\x63\x30\x2d\x35\x2c\x2e\x33\x37\x2d\x39\x2e\x32\x34\x2c\x32\x2e\ -\x31\x37\x2d\x31\x33\x2e\x36\x32\x61\x2e\x34\x34\x2e\x34\x34\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x38\x2d\x2e\x32\x39\x63\x2e\x36\ -\x2c\x30\x2c\x31\x2e\x32\x2c\x30\x2c\x31\x2e\x38\x2c\x30\x2c\x2e\ -\x32\x39\x2c\x30\x2c\x2e\x33\x35\x2c\x30\x2c\x2e\x32\x35\x2e\x32\ -\x39\x61\x32\x37\x2e\x34\x35\x2c\x32\x37\x2e\x34\x35\x2c\x30\x2c\ -\x30\x2c\x30\x2d\x31\x2e\x33\x37\x2c\x35\x43\x35\x2e\x36\x38\x2c\ -\x33\x34\x2c\x35\x2e\x37\x37\x2c\x34\x31\x2e\x38\x39\x2c\x38\x2e\ -\x32\x32\x2c\x34\x38\x2e\x35\x32\x63\x2e\x31\x2e\x32\x34\x2e\x30\ -\x39\x2e\x33\x33\x2d\x2e\x32\x35\x2e\x33\x32\x2d\x2e\x36\x2c\x30\ -\x2d\x31\x2e\x32\x31\x2c\x30\x2d\x31\x2e\x38\x31\x2c\x30\x61\x2e\ -\x34\x32\x2e\x34\x32\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x34\x37\x2d\ -\x2e\x32\x39\x43\x34\x2c\x34\x34\x2e\x36\x36\x2c\x33\x2e\x35\x32\ -\x2c\x33\x39\x2e\x36\x35\x2c\x33\x2e\x35\x32\x2c\x33\x35\x2e\x37\ -\x35\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\ -\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\ -\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\ -\x22\x20\x64\x3d\x22\x4d\x31\x38\x2e\x33\x38\x2c\x32\x31\x2e\x38\ -\x35\x63\x2e\x32\x38\x2c\x30\x2c\x2e\x38\x31\x2d\x2e\x31\x36\x2c\ -\x31\x2c\x2e\x30\x39\x73\x2d\x2e\x31\x33\x2e\x36\x2d\x2e\x32\x32\ -\x2e\x39\x32\x61\x34\x35\x2e\x36\x39\x2c\x34\x35\x2e\x36\x39\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x35\x32\x2c\x31\x37\x2e\x32\x39\ -\x2c\x33\x39\x2e\x38\x37\x2c\x33\x39\x2e\x38\x37\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x31\x2e\x37\x32\x2c\x38\x2e\x33\x31\x63\x2e\x31\x31\ -\x2e\x33\x32\x2e\x30\x35\x2e\x34\x2d\x2e\x33\x33\x2e\x33\x38\x73\ -\x2d\x31\x2c\x30\x2d\x31\x2e\x35\x33\x2c\x30\x61\x2e\x34\x37\x2e\ -\x34\x37\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x35\x34\x2d\x2e\x33\x33\ -\x2c\x32\x33\x2e\x36\x32\x2c\x32\x33\x2e\x36\x32\x2c\x30\x2c\x30\ -\x2c\x31\x2d\x31\x2e\x35\x31\x2d\x35\x2e\x32\x39\x63\x2d\x31\x2e\ -\x30\x39\x2d\x36\x2e\x35\x39\x2d\x31\x2d\x31\x34\x2e\x37\x34\x2c\ -\x31\x2e\x34\x39\x2d\x32\x31\x61\x2e\x35\x2e\x35\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x2e\x36\x2d\x2e\x33\x38\x41\x37\x2e\x31\x33\x2c\x37\ -\x2e\x31\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x38\x2e\x33\x38\x2c\ -\x32\x31\x2e\x38\x35\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\ -\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\ -\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x37\x2e\x38\x37\x2c\x33\ -\x36\x2e\x35\x32\x61\x31\x31\x2e\x32\x36\x2c\x31\x31\x2e\x32\x36\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x30\x36\x2d\x32\x2e\x33\x33\x63\ -\x2e\x32\x37\x2d\x34\x2e\x30\x38\x2e\x35\x37\x2d\x38\x2e\x32\x39\ -\x2c\x32\x2e\x32\x35\x2d\x31\x32\x2e\x31\x31\x61\x2e\x33\x38\x2e\ -\x33\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x33\x38\x2d\x2e\x32\x34\ -\x63\x31\x2e\x33\x39\x2d\x2e\x30\x37\x2c\x31\x2e\x33\x38\x2d\x2e\ -\x30\x38\x2c\x31\x2c\x31\x2e\x30\x35\x43\x39\x2c\x33\x30\x2e\x34\ -\x33\x2c\x38\x2e\x39\x33\x2c\x34\x31\x2c\x31\x31\x2e\x36\x38\x2c\ -\x34\x38\x2e\x34\x38\x63\x2e\x31\x32\x2e\x33\x31\x2e\x30\x35\x2e\ -\x33\x36\x2d\x2e\x33\x2e\x33\x36\x2d\x31\x2e\x32\x35\x2c\x30\x2d\ -\x31\x2e\x32\x36\x2c\x30\x2d\x31\x2e\x36\x34\x2d\x31\x41\x33\x36\ -\x2e\x31\x39\x2c\x33\x36\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x37\x2e\x38\x37\x2c\x33\x36\x2e\x35\x32\x5a\x22\x20\x74\x72\x61\ -\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\ -\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x31\ -\x31\x2e\x33\x35\x2c\x33\x35\x2e\x36\x34\x63\x30\x2d\x34\x2e\x38\ -\x36\x2e\x33\x38\x2d\x39\x2e\x31\x37\x2c\x32\x2e\x31\x37\x2d\x31\ -\x33\x2e\x35\x61\x2e\x34\x31\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x2e\x34\x37\x2d\x2e\x32\x39\x63\x2e\x34\x2c\x30\x2c\x31\x2d\ -\x2e\x31\x35\x2c\x31\x2e\x31\x38\x2e\x30\x36\x73\x2d\x2e\x31\x37\ -\x2e\x36\x34\x2d\x2e\x32\x38\x2c\x31\x63\x2d\x32\x2e\x34\x31\x2c\ -\x37\x2e\x36\x31\x2d\x32\x2e\x35\x31\x2c\x31\x38\x2c\x2e\x32\x37\ -\x2c\x32\x35\x2e\x35\x37\x2e\x31\x34\x2e\x33\x36\x2c\x30\x2c\x2e\ -\x33\x39\x2d\x2e\x33\x35\x2e\x33\x39\x2d\x31\x2e\x31\x37\x2c\x30\ -\x2d\x31\x2e\x31\x39\x2c\x30\x2d\x31\x2e\x35\x37\x2d\x31\x41\x33\ -\x35\x2e\x37\x37\x2c\x33\x35\x2e\x37\x37\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x31\x31\x2e\x33\x35\x2c\x33\x35\x2e\x36\x34\x5a\x22\x20\x74\ -\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\ -\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\ -\x4d\x31\x39\x2e\x31\x32\x2c\x32\x30\x2e\x37\x35\x63\x2e\x39\x32\ -\x2c\x30\x2c\x2e\x39\x32\x2c\x30\x2c\x31\x2e\x31\x33\x2d\x2e\x37\ -\x38\x2e\x38\x36\x2d\x32\x2e\x38\x37\x2d\x2e\x32\x35\x2d\x35\x2e\ -\x34\x36\x2d\x33\x2e\x35\x2d\x35\x2e\x38\x37\x2d\x31\x2e\x35\x31\ -\x2d\x2e\x32\x33\x2d\x31\x2e\x35\x2d\x2e\x32\x31\x2d\x31\x2e\x34\ -\x38\x2c\x31\x2e\x31\x2c\x30\x2c\x2e\x32\x35\x2e\x30\x39\x2e\x33\ -\x33\x2e\x33\x37\x2e\x33\x33\x61\x35\x2e\x33\x32\x2c\x35\x2e\x33\ -\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x34\x39\x2e\x32\x32\x2c\ -\x32\x2e\x31\x32\x2c\x32\x2e\x31\x32\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x31\x2e\x35\x38\x2c\x31\x2e\x37\x38\x2c\x35\x2e\x37\x38\x2c\x35\ -\x2e\x37\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x33\x38\x2c\x32\x2e\ -\x37\x38\x63\x2d\x2e\x31\x35\x2e\x34\x34\x2d\x2e\x31\x35\x2e\x34\ -\x34\x2e\x34\x31\x2e\x34\x34\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\ -\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\ -\x29\x22\x2f\x3e\x3c\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x63\x78\x3d\x22\x32\ -\x34\x2e\x34\x32\x22\x20\x63\x79\x3d\x22\x33\x35\x2e\x35\x32\x22\ -\x20\x72\x78\x3d\x22\x31\x2e\x30\x31\x22\x20\x72\x79\x3d\x22\x35\ -\x2e\x39\x39\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\ -\x73\x76\x67\x3e\ -\x00\x00\x0a\x54\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x32\x35\x32\x2e\x31\x33\x20\x35\x38\x2e\x34\x31\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\ -\x36\x35\x2e\x37\x38\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ -\x69\x6c\x79\x3a\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\ -\x2c\x20\x4d\x6f\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\ -\x65\x69\x67\x68\x74\x3a\x32\x30\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x31\x2c\x2e\x63\x6c\x73\x2d\x34\x7b\x66\x69\x6c\x6c\x3a\x23\x66\ -\x66\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x6c\x65\x74\x74\x65\ -\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x35\x65\ -\x6d\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x6c\x65\x74\x74\x65\x72\ -\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x34\x65\x6d\ -\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\ -\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\ -\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\ -\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x74\x65\x78\x74\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x74\ -\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\ -\x61\x74\x65\x28\x34\x37\x2e\x39\x31\x20\x34\x39\x2e\x39\x33\x29\ -\x22\x3e\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x32\x22\x3e\x63\x3c\x2f\x74\x73\x70\x61\x6e\x3e\ -\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x33\x22\x20\x78\x3d\x22\x33\x31\x2e\x39\x22\x20\x79\x3d\ -\x22\x30\x22\x3e\x6f\x73\x3c\x2f\x74\x73\x70\x61\x6e\x3e\x3c\x74\ -\x73\x70\x61\x6e\x20\x78\x3d\x22\x39\x34\x2e\x37\x39\x22\x20\x79\ -\x3d\x22\x30\x22\x3e\x74\x75\x6d\x3c\x2f\x74\x73\x70\x61\x6e\x3e\ -\x3c\x2f\x74\x65\x78\x74\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x32\ -\x34\x2e\x37\x38\x2c\x31\x38\x2e\x38\x34\x63\x32\x2e\x35\x36\x2c\ -\x31\x2e\x33\x35\x2c\x33\x2e\x37\x31\x2c\x36\x2e\x34\x34\x2c\x34\ -\x2e\x31\x38\x2c\x39\x2e\x30\x37\x2c\x31\x2e\x32\x2c\x36\x2e\x35\ -\x36\x2c\x31\x2e\x31\x34\x2c\x31\x39\x2e\x31\x2d\x33\x2e\x30\x39\ -\x2c\x32\x34\x2e\x35\x31\x2d\x2e\x38\x31\x2c\x31\x2d\x31\x2e\x38\ -\x32\x2c\x31\x2e\x32\x31\x2d\x32\x2e\x37\x34\x2e\x32\x32\x61\x39\ -\x2e\x31\x31\x2c\x39\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2d\x31\ -\x2e\x38\x35\x2d\x33\x2e\x31\x31\x63\x2d\x32\x2e\x38\x33\x2d\x37\ -\x2e\x36\x2d\x32\x2e\x38\x38\x2d\x31\x39\x2e\x32\x39\x2c\x30\x2d\ -\x32\x36\x2e\x39\x41\x37\x2e\x30\x36\x2c\x37\x2e\x30\x36\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x32\x34\x2c\x31\x38\x2e\x38\x34\x5a\x4d\x32\ -\x33\x2e\x35\x33\x2c\x35\x30\x2e\x34\x32\x61\x2e\x38\x38\x2e\x38\ -\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x36\x2e\x36\x2e\x39\x31\ -\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x39\x2d\x2e\x36\ -\x32\x63\x33\x2e\x30\x39\x2d\x36\x2e\x35\x34\x2c\x33\x2e\x31\x33\ -\x2d\x31\x36\x2e\x35\x35\x2c\x31\x2e\x36\x39\x2d\x32\x33\x2e\x35\ -\x37\x61\x32\x30\x2e\x32\x37\x2c\x32\x30\x2e\x32\x37\x2c\x30\x2c\ -\x30\x2c\x30\x2d\x31\x2e\x37\x2d\x35\x2e\x31\x32\x2e\x39\x31\x2e\ -\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x38\x35\x2d\x2e\x36\x2e\ -\x39\x32\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2e\x36\ -\x31\x2c\x31\x35\x2c\x31\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\ -\x33\x2c\x33\x2e\x34\x39\x43\x32\x30\x2e\x35\x32\x2c\x33\x32\x2e\ -\x33\x34\x2c\x31\x39\x2e\x34\x31\x2c\x34\x31\x2e\x31\x31\x2c\x32\ -\x33\x2e\x35\x33\x2c\x35\x30\x2e\x34\x32\x5a\x22\x20\x74\x72\x61\ -\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\ -\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x35\ -\x2e\x36\x34\x2c\x31\x38\x2e\x38\x34\x61\x35\x2e\x31\x39\x2c\x35\ -\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2c\x32\x2e\x32\x37\ -\x48\x35\x2e\x36\x31\x63\x2d\x2e\x38\x2c\x30\x2d\x31\x2c\x2e\x31\ -\x31\x2d\x31\x2e\x32\x39\x2e\x37\x34\x43\x31\x2c\x32\x39\x2e\x31\ -\x38\x2c\x31\x2c\x34\x31\x2e\x39\x2c\x33\x2e\x39\x32\x2c\x34\x39\ -\x2e\x34\x63\x2e\x31\x35\x2e\x33\x36\x2e\x33\x33\x2e\x37\x31\x2e\ -\x34\x39\x2c\x31\x2e\x30\x36\x61\x31\x2c\x31\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x2e\x35\x37\x48\x37\x2e\x36\x37\x41\x35\x2c\x35\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x2e\x39\x32\x2c\x35\x33\x2e\x31\ -\x63\x2d\x31\x2e\x32\x34\x2e\x37\x34\x2d\x32\x2e\x34\x2d\x2e\x38\ -\x37\x2d\x32\x2e\x39\x33\x2d\x31\x2e\x38\x43\x2d\x2e\x38\x32\x2c\ -\x34\x34\x2e\x32\x33\x2d\x2e\x37\x34\x2c\x33\x31\x2c\x31\x2e\x38\ -\x33\x2c\x32\x33\x2e\x34\x34\x63\x2e\x35\x39\x2d\x31\x2e\x36\x34\ -\x2c\x31\x2e\x34\x2d\x33\x2e\x36\x38\x2c\x33\x2e\x30\x35\x2d\x34\ -\x2e\x36\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\ -\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x34\x22\x20\x64\x3d\x22\x4d\x33\x2e\x35\x32\x2c\x33\x36\x2e\x34\ -\x37\x63\x30\x2d\x35\x2c\x2e\x33\x37\x2d\x39\x2e\x32\x33\x2c\x32\ -\x2e\x31\x37\x2d\x31\x33\x2e\x36\x31\x61\x2e\x34\x33\x2e\x34\x33\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x38\x2d\x2e\x32\x39\x71\x2e\ -\x39\x2c\x30\x2c\x31\x2e\x38\x2c\x30\x63\x2e\x32\x39\x2c\x30\x2c\ -\x2e\x33\x35\x2e\x30\x35\x2e\x32\x35\x2e\x33\x61\x32\x37\x2e\x33\ -\x35\x2c\x32\x37\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\ -\x33\x37\x2c\x35\x63\x2d\x31\x2e\x31\x37\x2c\x36\x2e\x39\x2d\x31\ -\x2e\x30\x38\x2c\x31\x34\x2e\x37\x36\x2c\x31\x2e\x33\x37\x2c\x32\ -\x31\x2e\x33\x38\x2e\x31\x2e\x32\x35\x2e\x30\x39\x2e\x33\x34\x2d\ -\x2e\x32\x35\x2e\x33\x33\x2d\x2e\x36\x2c\x30\x2d\x31\x2e\x32\x31\ -\x2c\x30\x2d\x31\x2e\x38\x31\x2c\x30\x61\x2e\x34\x32\x2e\x34\x32\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x34\x37\x2d\x2e\x33\x43\x34\x2c\ -\x34\x35\x2e\x33\x39\x2c\x33\x2e\x35\x32\x2c\x34\x30\x2e\x33\x38\ -\x2c\x33\x2e\x35\x32\x2c\x33\x36\x2e\x34\x37\x5a\x22\x20\x74\x72\ -\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\ -\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ -\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\ -\x31\x38\x2e\x33\x38\x2c\x32\x32\x2e\x35\x38\x63\x2e\x32\x38\x2c\ -\x30\x2c\x2e\x38\x31\x2d\x2e\x31\x36\x2c\x31\x2c\x2e\x30\x38\x73\ -\x2d\x2e\x31\x33\x2e\x36\x31\x2d\x2e\x32\x32\x2e\x39\x32\x61\x34\ -\x35\x2e\x36\x39\x2c\x34\x35\x2e\x36\x39\x2c\x30\x2c\x30\x2c\x30\ -\x2d\x31\x2e\x35\x32\x2c\x31\x37\x2e\x32\x39\x2c\x33\x39\x2e\x36\ -\x31\x2c\x33\x39\x2e\x36\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2e\ -\x37\x32\x2c\x38\x2e\x33\x31\x63\x2e\x31\x31\x2e\x33\x33\x2e\x30\ -\x35\x2e\x34\x31\x2d\x2e\x33\x33\x2e\x33\x39\x61\x31\x33\x2c\x31\ -\x33\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x35\x33\x2c\x30\x2c\x2e\ -\x34\x38\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x35\x34\x2d\ -\x2e\x33\x33\x2c\x32\x33\x2e\x38\x35\x2c\x32\x33\x2e\x38\x35\x2c\ -\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x31\x2d\x35\x2e\x33\x63\x2d\ -\x31\x2e\x30\x39\x2d\x36\x2e\x35\x38\x2d\x31\x2d\x31\x34\x2e\x37\ -\x34\x2c\x31\x2e\x34\x39\x2d\x32\x31\x61\x2e\x35\x2e\x35\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x2e\x36\x2d\x2e\x33\x37\x41\x37\x2e\x31\x33\ -\x2c\x37\x2e\x31\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x38\x2e\x33\ -\x38\x2c\x32\x32\x2e\x35\x38\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\ -\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\ -\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x37\x2e\x38\x37\ -\x2c\x33\x37\x2e\x32\x35\x61\x31\x31\x2e\x32\x38\x2c\x31\x31\x2e\ -\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x30\x36\x2d\x32\x2e\x33\ -\x33\x63\x2e\x32\x37\x2d\x34\x2e\x30\x38\x2e\x35\x37\x2d\x38\x2e\ -\x32\x39\x2c\x32\x2e\x32\x35\x2d\x31\x32\x2e\x31\x32\x61\x2e\x33\ -\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x33\x38\x2d\x2e\ -\x32\x33\x63\x31\x2e\x33\x39\x2d\x2e\x30\x37\x2c\x31\x2e\x33\x38\ -\x2d\x2e\x30\x38\x2c\x31\x2c\x31\x43\x39\x2c\x33\x31\x2e\x31\x35\ -\x2c\x38\x2e\x39\x33\x2c\x34\x31\x2e\x37\x33\x2c\x31\x31\x2e\x36\ -\x38\x2c\x34\x39\x2e\x32\x63\x2e\x31\x32\x2e\x33\x31\x2e\x30\x35\ -\x2e\x33\x37\x2d\x2e\x33\x2e\x33\x37\x2d\x31\x2e\x32\x35\x2c\x30\ -\x2d\x31\x2e\x32\x36\x2c\x30\x2d\x31\x2e\x36\x34\x2d\x31\x41\x33\ -\x36\x2e\x30\x38\x2c\x33\x36\x2e\x30\x38\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x37\x2e\x38\x37\x2c\x33\x37\x2e\x32\x35\x5a\x22\x20\x74\x72\ -\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\ -\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ -\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\ -\x31\x31\x2e\x33\x35\x2c\x33\x36\x2e\x33\x37\x63\x30\x2d\x34\x2e\ -\x38\x36\x2e\x33\x38\x2d\x39\x2e\x31\x38\x2c\x32\x2e\x31\x37\x2d\ -\x31\x33\x2e\x35\x61\x2e\x34\x2e\x34\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x2e\x34\x37\x2d\x2e\x33\x63\x2e\x34\x2c\x30\x2c\x31\x2d\x2e\x31\ -\x35\x2c\x31\x2e\x31\x38\x2e\x30\x37\x73\x2d\x2e\x31\x37\x2e\x36\ -\x34\x2d\x2e\x32\x38\x2c\x31\x63\x2d\x32\x2e\x34\x31\x2c\x37\x2e\ -\x36\x31\x2d\x32\x2e\x35\x31\x2c\x31\x38\x2c\x2e\x32\x37\x2c\x32\ -\x35\x2e\x35\x37\x2e\x31\x34\x2e\x33\x36\x2c\x30\x2c\x2e\x33\x39\ -\x2d\x2e\x33\x35\x2e\x33\x39\x2d\x31\x2e\x31\x37\x2c\x30\x2d\x31\ -\x2e\x31\x39\x2c\x30\x2d\x31\x2e\x35\x37\x2d\x31\x41\x33\x35\x2e\ -\x37\x32\x2c\x33\x35\x2e\x37\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x31\ -\x31\x2e\x33\x35\x2c\x33\x36\x2e\x33\x37\x5a\x22\x20\x74\x72\x61\ -\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\ -\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x31\ -\x39\x2e\x31\x32\x2c\x32\x31\x2e\x34\x37\x63\x2e\x39\x32\x2c\x30\ -\x2c\x2e\x39\x32\x2c\x30\x2c\x31\x2e\x31\x33\x2d\x2e\x37\x38\x2e\ -\x38\x36\x2d\x32\x2e\x38\x36\x2d\x2e\x32\x35\x2d\x35\x2e\x34\x35\ -\x2d\x33\x2e\x35\x2d\x35\x2e\x38\x36\x2d\x31\x2e\x35\x31\x2d\x2e\ -\x32\x33\x2d\x31\x2e\x35\x2d\x2e\x32\x31\x2d\x31\x2e\x34\x38\x2c\ -\x31\x2e\x30\x39\x2c\x30\x2c\x2e\x32\x35\x2e\x30\x39\x2e\x33\x33\ -\x2e\x33\x37\x2e\x33\x34\x61\x34\x2e\x36\x38\x2c\x34\x2e\x36\x38\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x34\x39\x2e\x32\x32\x2c\x32\ -\x2e\x31\x31\x2c\x32\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x31\ -\x2e\x35\x38\x2c\x31\x2e\x37\x38\x41\x35\x2e\x37\x38\x2c\x35\x2e\ -\x37\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x38\x2e\x33\x33\x2c\x32\ -\x31\x63\x2d\x2e\x31\x35\x2e\x34\x33\x2d\x2e\x31\x35\x2e\x34\x33\ -\x2e\x34\x31\x2e\x34\x33\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\ -\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\ -\x22\x2f\x3e\x3c\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x63\x78\x3d\x22\x32\x34\ -\x2e\x34\x32\x22\x20\x63\x79\x3d\x22\x33\x36\x2e\x32\x35\x22\x20\ -\x72\x78\x3d\x22\x31\x2e\x30\x31\x22\x20\x72\x79\x3d\x22\x35\x2e\ -\x39\x39\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\ -\x76\x67\x3e\ +\x00\x00\x0aT\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 252.13 58.41\ +\x22>c\ +ostum\ +<\ +path class=\x22cls-\ +4\x22 d=\x22M3.52,36.4\ +7c0-5,.37-9.23,2\ +.17-13.61a.43.43\ +,0,0,1,.48-.29q.\ +9,0,1.8,0c.29,0,\ +.35.05.25.3a27.3\ +5,27.35,0,0,0-1.\ +37,5c-1.17,6.9-1\ +.08,14.76,1.37,2\ +1.38.1.25.09.34-\ +.25.33-.6,0-1.21\ +,0-1.81,0a.42.42\ +,0,0,1-.47-.3C4,\ +45.39,3.52,40.38\ +,3.52,36.47Z\x22 tr\ +ansform=\x22transla\ +te(0)\x22/>\ +\x00\x00\x0ab\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 196.37 58.41\ +\x22>p\ +e\ +tg\ +\x00\x00\x0a%\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 176.02 58.41\ +\x22>hips<\ +path class=\x22cls-\ +3\x22 d=\x22M5.64,18.1\ +4a5.13,5.13,0,0,\ +1,2,2.27c-.72,0-\ +1.39,0-2.06,0s-1\ +,.1-1.29.74C1,28\ +.48,1,41.19,3.92\ +,48.7c.15.36.33.\ +71.49,1.06a1,1,0\ +,0,0,1,.57H7.67A\ +5.07,5.07,0,0,1,\ +5.92,52.4c-1.24.\ +74-2.4-.88-2.93-\ +1.81C-.82,43.53-\ +.74,30.28,1.83,2\ +2.74c.59-1.64,1.\ +4-3.69,3.05-4.6Z\ +\x22 transform=\x22tra\ +nslate(0)\x22/><\ +path class=\x22cls-\ +3\x22 d=\x22M7.87,36.5\ +4a11.26,11.26,0,\ +0,1-.06-2.33c.27\ +-4.08.57-8.28,2.\ +25-12.11a.36.36,\ +0,0,1,.38-.23c1.\ +39-.08,1.38-.08,\ +1,1C9,30.45,8.93\ +,41,11.68,48.5c.\ +12.31.05.37-.3.3\ +6-1.25,0-1.26,0-\ +1.64-1A36.14,36.\ +14,0,0,1,7.87,36\ +.54Z\x22 transform=\ +\x22translate(0)\x22/>\ +<\ +path class=\x22cls-\ +3\x22 d=\x22M19.12,20.\ +77c.92,0,.92,0,1\ +.13-.78.86-2.86-\ +.25-5.45-3.5-5.8\ +7-1.51-.23-1.5-.\ +21-1.48,1.1,0,.2\ +5.09.33.37.33a5,\ +5,0,0,1,1.49.23,\ +2.11,2.11,0,0,1,\ +1.58,1.77,5.74,5\ +.74,0,0,1-.38,2.\ +78c-.15.44-.15.4\ +4.41.44Z\x22 transf\ +orm=\x22translate(0\ +)\x22/>\ \x00\x00\x0a\x14\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x31\x37\x36\x2e\x30\x32\x20\x35\x38\x2e\x34\x31\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\ -\x36\x35\x2e\x37\x38\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ -\x69\x6c\x79\x3a\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\ -\x2c\x20\x4d\x6f\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\ -\x65\x69\x67\x68\x74\x3a\x32\x30\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x31\x2c\x2e\x63\x6c\x73\x2d\x33\x7b\x66\x69\x6c\x6c\x3a\x23\x66\ -\x66\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x6c\x65\x74\x74\x65\ -\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x31\x65\ -\x6d\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\ -\x73\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\ -\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\ -\x65\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\ -\x65\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\ -\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x74\x65\x78\ -\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x34\x37\x2e\x39\x31\x20\x34\x39\x2e\x39\x33\ -\x29\x22\x3e\x74\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x78\x3d\x22\x33\x33\x2e\x33\ -\x35\x22\x20\x79\x3d\x22\x30\x22\x3e\x70\x3c\x2f\x74\x73\x70\x61\ -\x6e\x3e\x3c\x74\x73\x70\x61\x6e\x20\x78\x3d\x22\x36\x31\x2e\x33\ -\x37\x22\x20\x79\x3d\x22\x30\x22\x3e\x75\x3c\x2f\x74\x73\x70\x61\ -\x6e\x3e\x3c\x2f\x74\x65\x78\x74\x3e\x3c\x70\x61\x74\x68\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\ -\x4d\x32\x34\x2e\x37\x38\x2c\x31\x38\x2e\x38\x34\x63\x32\x2e\x35\ -\x36\x2c\x31\x2e\x33\x35\x2c\x33\x2e\x37\x31\x2c\x36\x2e\x34\x34\ -\x2c\x34\x2e\x31\x38\x2c\x39\x2e\x30\x37\x2c\x31\x2e\x32\x2c\x36\ -\x2e\x35\x36\x2c\x31\x2e\x31\x34\x2c\x31\x39\x2e\x31\x2d\x33\x2e\ -\x30\x39\x2c\x32\x34\x2e\x35\x31\x2d\x2e\x38\x31\x2c\x31\x2d\x31\ -\x2e\x38\x32\x2c\x31\x2e\x32\x31\x2d\x32\x2e\x37\x34\x2e\x32\x32\ -\x61\x39\x2e\x31\x31\x2c\x39\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\ -\x2d\x31\x2e\x38\x35\x2d\x33\x2e\x31\x31\x63\x2d\x32\x2e\x38\x33\ -\x2d\x37\x2e\x36\x2d\x32\x2e\x38\x38\x2d\x31\x39\x2e\x32\x39\x2c\ -\x30\x2d\x32\x36\x2e\x39\x41\x37\x2e\x30\x36\x2c\x37\x2e\x30\x36\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x34\x2c\x31\x38\x2e\x38\x34\x5a\ -\x4d\x32\x33\x2e\x35\x33\x2c\x35\x30\x2e\x34\x32\x61\x2e\x38\x38\ -\x2e\x38\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x36\x2e\x36\x2e\ -\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x39\x2d\ -\x2e\x36\x32\x63\x33\x2e\x30\x39\x2d\x36\x2e\x35\x34\x2c\x33\x2e\ -\x31\x33\x2d\x31\x36\x2e\x35\x35\x2c\x31\x2e\x36\x39\x2d\x32\x33\ -\x2e\x35\x37\x61\x32\x30\x2e\x32\x37\x2c\x32\x30\x2e\x32\x37\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x37\x2d\x35\x2e\x31\x32\x2e\x39\ -\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x38\x35\x2d\x2e\ -\x36\x2e\x39\x32\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\ -\x2e\x36\x31\x2c\x31\x35\x2c\x31\x35\x2c\x30\x2c\x30\x2c\x30\x2d\ -\x31\x2e\x33\x2c\x33\x2e\x34\x39\x43\x32\x30\x2e\x35\x32\x2c\x33\ -\x32\x2e\x33\x34\x2c\x31\x39\x2e\x34\x31\x2c\x34\x31\x2e\x31\x31\ -\x2c\x32\x33\x2e\x35\x33\x2c\x35\x30\x2e\x34\x32\x5a\x22\x20\x74\ -\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\ -\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\ -\x4d\x35\x2e\x36\x34\x2c\x31\x38\x2e\x38\x34\x61\x35\x2e\x31\x39\ -\x2c\x35\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2c\x32\x2e\ -\x32\x37\x48\x35\x2e\x36\x31\x63\x2d\x2e\x38\x2c\x30\x2d\x31\x2c\ -\x2e\x31\x31\x2d\x31\x2e\x32\x39\x2e\x37\x34\x43\x31\x2c\x32\x39\ -\x2e\x31\x38\x2c\x31\x2c\x34\x31\x2e\x39\x2c\x33\x2e\x39\x32\x2c\ -\x34\x39\x2e\x34\x63\x2e\x31\x35\x2e\x33\x36\x2e\x33\x33\x2e\x37\ -\x31\x2e\x34\x39\x2c\x31\x2e\x30\x36\x61\x31\x2c\x31\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x2e\x35\x37\x48\x37\x2e\x36\x37\x41\x35\ -\x2c\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x2e\x39\x32\x2c\x35\x33\ -\x2e\x31\x63\x2d\x31\x2e\x32\x34\x2e\x37\x34\x2d\x32\x2e\x34\x2d\ -\x2e\x38\x37\x2d\x32\x2e\x39\x33\x2d\x31\x2e\x38\x43\x2d\x2e\x38\ -\x32\x2c\x34\x34\x2e\x32\x33\x2d\x2e\x37\x34\x2c\x33\x31\x2c\x31\ -\x2e\x38\x33\x2c\x32\x33\x2e\x34\x34\x63\x2e\x35\x39\x2d\x31\x2e\ -\x36\x34\x2c\x31\x2e\x34\x2d\x33\x2e\x36\x38\x2c\x33\x2e\x30\x35\ -\x2d\x34\x2e\x36\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\ -\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x33\x2e\x35\x32\x2c\x33\x36\ -\x2e\x34\x37\x63\x30\x2d\x35\x2c\x2e\x33\x37\x2d\x39\x2e\x32\x33\ -\x2c\x32\x2e\x31\x37\x2d\x31\x33\x2e\x36\x31\x61\x2e\x34\x33\x2e\ -\x34\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x38\x2d\x2e\x32\x39\ -\x71\x2e\x39\x2c\x30\x2c\x31\x2e\x38\x2c\x30\x63\x2e\x32\x39\x2c\ -\x30\x2c\x2e\x33\x35\x2e\x30\x35\x2e\x32\x35\x2e\x33\x61\x32\x37\ -\x2e\x33\x35\x2c\x32\x37\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\ -\x31\x2e\x33\x37\x2c\x35\x63\x2d\x31\x2e\x31\x37\x2c\x36\x2e\x39\ -\x2d\x31\x2e\x30\x38\x2c\x31\x34\x2e\x37\x36\x2c\x31\x2e\x33\x37\ -\x2c\x32\x31\x2e\x33\x38\x2e\x31\x2e\x32\x35\x2e\x30\x39\x2e\x33\ -\x34\x2d\x2e\x32\x35\x2e\x33\x33\x2d\x2e\x36\x2c\x30\x2d\x31\x2e\ -\x32\x31\x2c\x30\x2d\x31\x2e\x38\x31\x2c\x30\x61\x2e\x34\x32\x2e\ -\x34\x32\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x34\x37\x2d\x2e\x33\x43\ -\x34\x2c\x34\x35\x2e\x33\x39\x2c\x33\x2e\x35\x32\x2c\x34\x30\x2e\ -\x33\x38\x2c\x33\x2e\x35\x32\x2c\x33\x36\x2e\x34\x37\x5a\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\ -\x22\x4d\x31\x38\x2e\x33\x38\x2c\x32\x32\x2e\x35\x38\x63\x2e\x32\ -\x38\x2c\x30\x2c\x2e\x38\x31\x2d\x2e\x31\x36\x2c\x31\x2c\x2e\x30\ -\x38\x73\x2d\x2e\x31\x33\x2e\x36\x31\x2d\x2e\x32\x32\x2e\x39\x32\ -\x61\x34\x35\x2e\x37\x32\x2c\x34\x35\x2e\x37\x32\x2c\x30\x2c\x30\ -\x2c\x30\x2d\x31\x2e\x35\x32\x2c\x31\x37\x2e\x33\x2c\x33\x39\x2e\ -\x36\x35\x2c\x33\x39\x2e\x36\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x31\ -\x2e\x37\x32\x2c\x38\x2e\x33\x63\x2e\x31\x31\x2e\x33\x33\x2e\x30\ -\x35\x2e\x34\x31\x2d\x2e\x33\x33\x2e\x33\x39\x61\x31\x33\x2c\x31\ -\x33\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x35\x33\x2c\x30\x2c\x2e\ -\x34\x38\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x35\x34\x2d\ -\x2e\x33\x33\x2c\x32\x33\x2e\x38\x35\x2c\x32\x33\x2e\x38\x35\x2c\ -\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x31\x2d\x35\x2e\x33\x63\x2d\ -\x31\x2e\x30\x39\x2d\x36\x2e\x35\x38\x2d\x31\x2d\x31\x34\x2e\x37\ -\x34\x2c\x31\x2e\x34\x39\x2d\x32\x31\x61\x2e\x35\x2e\x35\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x2e\x36\x2d\x2e\x33\x37\x41\x37\x2e\x31\x33\ -\x2c\x37\x2e\x31\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x38\x2e\x33\ -\x38\x2c\x32\x32\x2e\x35\x38\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\ -\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\ -\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x37\x2e\x38\x37\ -\x2c\x33\x37\x2e\x32\x35\x61\x31\x31\x2e\x32\x38\x2c\x31\x31\x2e\ -\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x30\x36\x2d\x32\x2e\x33\ -\x33\x63\x2e\x32\x37\x2d\x34\x2e\x30\x38\x2e\x35\x37\x2d\x38\x2e\ -\x32\x39\x2c\x32\x2e\x32\x35\x2d\x31\x32\x2e\x31\x32\x61\x2e\x33\ -\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x33\x38\x2d\x2e\ -\x32\x33\x63\x31\x2e\x33\x39\x2d\x2e\x30\x37\x2c\x31\x2e\x33\x38\ -\x2d\x2e\x30\x38\x2c\x31\x2c\x31\x43\x39\x2c\x33\x31\x2e\x31\x35\ -\x2c\x38\x2e\x39\x33\x2c\x34\x31\x2e\x37\x33\x2c\x31\x31\x2e\x36\ -\x38\x2c\x34\x39\x2e\x32\x63\x2e\x31\x32\x2e\x33\x31\x2e\x30\x35\ -\x2e\x33\x37\x2d\x2e\x33\x2e\x33\x37\x2d\x31\x2e\x32\x35\x2c\x30\ -\x2d\x31\x2e\x32\x36\x2c\x30\x2d\x31\x2e\x36\x34\x2d\x31\x41\x33\ -\x36\x2e\x30\x38\x2c\x33\x36\x2e\x30\x38\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x37\x2e\x38\x37\x2c\x33\x37\x2e\x32\x35\x5a\x22\x20\x74\x72\ -\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\ -\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ -\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\ -\x31\x31\x2e\x33\x35\x2c\x33\x36\x2e\x33\x37\x63\x30\x2d\x34\x2e\ -\x38\x36\x2e\x33\x38\x2d\x39\x2e\x31\x38\x2c\x32\x2e\x31\x37\x2d\ -\x31\x33\x2e\x35\x61\x2e\x34\x2e\x34\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x2e\x34\x37\x2d\x2e\x33\x63\x2e\x34\x2c\x30\x2c\x31\x2d\x2e\x31\ -\x35\x2c\x31\x2e\x31\x38\x2e\x30\x37\x73\x2d\x2e\x31\x37\x2e\x36\ -\x34\x2d\x2e\x32\x38\x2c\x31\x63\x2d\x32\x2e\x34\x31\x2c\x37\x2e\ -\x36\x31\x2d\x32\x2e\x35\x31\x2c\x31\x38\x2c\x2e\x32\x37\x2c\x32\ -\x35\x2e\x35\x37\x2e\x31\x34\x2e\x33\x36\x2c\x30\x2c\x2e\x33\x39\ -\x2d\x2e\x33\x35\x2e\x33\x39\x2d\x31\x2e\x31\x37\x2c\x30\x2d\x31\ -\x2e\x31\x39\x2c\x30\x2d\x31\x2e\x35\x37\x2d\x31\x41\x33\x35\x2e\ -\x37\x32\x2c\x33\x35\x2e\x37\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x31\ -\x31\x2e\x33\x35\x2c\x33\x36\x2e\x33\x37\x5a\x22\x20\x74\x72\x61\ -\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\ -\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x31\ -\x39\x2e\x31\x32\x2c\x32\x31\x2e\x34\x37\x63\x2e\x39\x32\x2c\x30\ -\x2c\x2e\x39\x32\x2c\x30\x2c\x31\x2e\x31\x33\x2d\x2e\x37\x38\x2e\ -\x38\x36\x2d\x32\x2e\x38\x36\x2d\x2e\x32\x35\x2d\x35\x2e\x34\x35\ -\x2d\x33\x2e\x35\x2d\x35\x2e\x38\x36\x2d\x31\x2e\x35\x31\x2d\x2e\ -\x32\x33\x2d\x31\x2e\x35\x2d\x2e\x32\x31\x2d\x31\x2e\x34\x38\x2c\ -\x31\x2e\x30\x39\x2c\x30\x2c\x2e\x32\x35\x2e\x30\x39\x2e\x33\x33\ -\x2e\x33\x37\x2e\x33\x34\x61\x34\x2e\x36\x38\x2c\x34\x2e\x36\x38\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x34\x39\x2e\x32\x32\x2c\x32\ -\x2e\x31\x31\x2c\x32\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x31\ -\x2e\x35\x38\x2c\x31\x2e\x37\x38\x41\x35\x2e\x37\x38\x2c\x35\x2e\ -\x37\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x38\x2e\x33\x33\x2c\x32\ -\x31\x63\x2d\x2e\x31\x35\x2e\x34\x33\x2d\x2e\x31\x35\x2e\x34\x33\ -\x2e\x34\x31\x2e\x34\x33\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\ -\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\ -\x22\x2f\x3e\x3c\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x63\x78\x3d\x22\x32\x34\ -\x2e\x34\x32\x22\x20\x63\x79\x3d\x22\x33\x36\x2e\x32\x35\x22\x20\ -\x72\x78\x3d\x22\x31\x2e\x30\x31\x22\x20\x72\x79\x3d\x22\x35\x2e\ -\x39\x39\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\ -\x76\x67\x3e\ -\x00\x00\x01\x21\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x33\x38\x2e\x32\x32\x20\x33\x34\x2e\x35\x37\x22\ -\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\ -\x6c\x73\x2d\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x66\x66\x66\x3b\x7d\ -\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\ -\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\ -\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\ -\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\ -\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\ -\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x70\x61\x74\x68\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\ -\x4d\x30\x2c\x30\x56\x32\x31\x2e\x36\x6c\x31\x39\x2e\x31\x31\x2c\ -\x31\x33\x2c\x31\x39\x2e\x31\x31\x2d\x31\x33\x56\x30\x5a\x4d\x33\ -\x35\x2e\x32\x2c\x31\x39\x2e\x34\x34\x6c\x2d\x39\x2e\x35\x35\x2c\ -\x36\x2e\x34\x39\x56\x38\x2e\x36\x34\x48\x33\x35\x2e\x32\x5a\x22\ -\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\x3e\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 176.02 58.41\ +\x22>tpu\ +\x00\x00\x07\xa4\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 40.86 40.86\x22\ +><\ +g id=\x22Layer_2\x22 d\ +ata-name=\x22Layer \ +2\x22>\ +\x00\x00\x08s\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 88.32 34.24\x22\ +>a\ +bs<\ +ellipse class=\x22c\ +ls-4\x22 cx=\x2224.42\x22\ + cy=\x2235.54\x22 rx=\x22\ +1.01\x22 ry=\x225.99\x22/\ +>\ +\x00\x00\x01a\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 79.53 48.16\x22\ +>\ \ \x00\x00\x0a\x9a\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x31\x38\x34\x2e\x31\x36\x20\x35\x37\x2e\x37\x36\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x2c\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\ -\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x32\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\x36\x35\x2e\x30\ -\x35\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\x69\x6c\x79\x3a\ -\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\x2c\x20\x4d\x6f\ -\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\x65\x69\x67\x68\ -\x74\x3a\x32\x30\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x6c\x65\ -\x74\x74\x65\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\ -\x30\x32\x65\x6d\x3b\x7d\x2e\x63\x6c\x73\x2d\x34\x7b\x6c\x65\x74\ -\x74\x65\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\ -\x33\x65\x6d\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ -\x65\x66\x73\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\ -\x5f\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\ -\x61\x79\x65\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\ -\x61\x79\x65\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\ -\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x70\ -\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\ -\x22\x20\x64\x3d\x22\x4d\x34\x2e\x37\x35\x2c\x32\x32\x2e\x33\x35\ -\x63\x31\x2e\x35\x35\x2d\x32\x2e\x39\x31\x2c\x37\x2e\x34\x2d\x34\ -\x2e\x32\x31\x2c\x31\x30\x2e\x34\x32\x2d\x34\x2e\x37\x35\x2c\x37\ -\x2e\x35\x35\x2d\x31\x2e\x33\x36\x2c\x32\x32\x2d\x31\x2e\x33\x2c\ -\x32\x38\x2e\x31\x39\x2c\x33\x2e\x35\x31\x2c\x31\x2e\x31\x33\x2e\ -\x39\x32\x2c\x31\x2e\x34\x2c\x32\x2e\x30\x37\x2e\x32\x36\x2c\x33\ -\x2e\x31\x32\x41\x31\x30\x2e\x33\x36\x2c\x31\x30\x2e\x33\x36\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x34\x30\x2c\x32\x36\x2e\x33\x33\x63\x2d\ -\x38\x2e\x37\x34\x2c\x33\x2e\x32\x32\x2d\x32\x32\x2e\x31\x38\x2c\ -\x33\x2e\x32\x38\x2d\x33\x30\x2e\x39\x34\x2c\x30\x2d\x31\x2e\x36\ -\x33\x2d\x2e\x36\x35\x2d\x33\x2e\x34\x35\x2d\x31\x2e\x34\x39\x2d\ -\x34\x2e\x33\x35\x2d\x33\x2e\x31\x33\x5a\x6d\x33\x36\x2e\x33\x31\ -\x2c\x31\x2e\x34\x33\x61\x31\x2c\x31\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x2e\x37\x2d\x31\x2c\x31\x2e\x30\x38\x2c\x31\x2e\x30\x38\x2c\x30\ -\x2c\x30\x2c\x30\x2d\x2e\x37\x31\x2d\x31\x63\x2d\x37\x2e\x35\x33\ -\x2d\x33\x2e\x35\x31\x2d\x31\x39\x2d\x33\x2e\x35\x36\x2d\x32\x37\ -\x2e\x31\x31\x2d\x31\x2e\x39\x32\x61\x32\x33\x2e\x36\x34\x2c\x32\ -\x33\x2e\x36\x34\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\x38\x39\x2c\ -\x31\x2e\x39\x33\x2c\x31\x2c\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\ -\x36\x39\x2c\x31\x2c\x31\x2e\x30\x36\x2c\x31\x2e\x30\x36\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x2e\x37\x2c\x31\x2c\x31\x37\x2e\x39\x34\x2c\ -\x31\x37\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x2c\x31\x2e\ -\x34\x38\x43\x32\x30\x2e\x32\x37\x2c\x32\x37\x2e\x32\x2c\x33\x30\ -\x2e\x33\x36\x2c\x32\x38\x2e\x34\x36\x2c\x34\x31\x2e\x30\x36\x2c\ -\x32\x33\x2e\x37\x38\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\ -\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\ -\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x2e\x37\x35\x2c\x34\ -\x34\x2e\x31\x32\x61\x35\x2e\x38\x38\x2c\x35\x2e\x38\x38\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x32\x2e\x36\x2d\x32\x2e\x33\x31\x76\x32\x2e\ -\x33\x35\x63\x30\x2c\x2e\x39\x2e\x31\x33\x2c\x31\x2e\x31\x32\x2e\ -\x38\x35\x2c\x31\x2e\x34\x36\x2c\x38\x2e\x34\x33\x2c\x33\x2e\x38\ -\x2c\x32\x33\x2e\x30\x36\x2c\x33\x2e\x37\x38\x2c\x33\x31\x2e\x36\ -\x39\x2e\x34\x35\x2e\x34\x32\x2d\x2e\x31\x36\x2e\x38\x32\x2d\x2e\ -\x33\x36\x2c\x31\x2e\x32\x32\x2d\x2e\x35\x35\x61\x31\x2e\x31\x34\ -\x2c\x31\x2e\x31\x34\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x36\x36\x2d\ -\x31\x2e\x31\x38\x63\x30\x2d\x2e\x38\x38\x2c\x30\x2d\x31\x2e\x36\ -\x2c\x30\x2d\x32\x2e\x35\x33\x61\x35\x2e\x38\x2c\x35\x2e\x38\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x32\x2e\x33\x39\x2c\x32\x63\x2e\x38\x35\ -\x2c\x31\x2e\x34\x31\x2d\x31\x2c\x32\x2e\x37\x33\x2d\x32\x2e\x30\ -\x38\x2c\x33\x2e\x33\x32\x2d\x38\x2e\x31\x33\x2c\x34\x2e\x33\x34\ -\x2d\x32\x33\x2e\x33\x36\x2c\x34\x2e\x32\x34\x2d\x33\x32\x2c\x31\ -\x2e\x33\x32\x43\x38\x2e\x31\x35\x2c\x34\x37\x2e\x37\x39\x2c\x35\ -\x2e\x38\x2c\x34\x36\x2e\x38\x36\x2c\x34\x2e\x37\x35\x2c\x34\x35\ -\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\ -\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\ -\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\ -\x20\x64\x3d\x22\x4d\x32\x35\x2c\x34\x36\x2e\x35\x33\x63\x2d\x35\ -\x2e\x37\x36\x2d\x2e\x30\x35\x2d\x31\x30\x2e\x36\x31\x2d\x2e\x34\ -\x32\x2d\x31\x35\x2e\x36\x35\x2d\x32\x2e\x34\x37\x41\x2e\x34\x37\ -\x2e\x34\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x39\x2c\x34\x33\x2e\x35\ -\x32\x63\x30\x2d\x2e\x36\x38\x2c\x30\x2d\x31\x2e\x33\x37\x2c\x30\ -\x2d\x32\x2e\x30\x35\x2c\x30\x2d\x2e\x33\x33\x2e\x30\x35\x2d\x2e\ -\x34\x2e\x33\x34\x2d\x2e\x32\x39\x61\x33\x30\x2e\x34\x39\x2c\x33\ -\x30\x2e\x34\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x35\x2e\x37\x34\x2c\ -\x31\x2e\x35\x36\x63\x37\x2e\x39\x33\x2c\x31\x2e\x33\x33\x2c\x31\ -\x37\x2c\x31\x2e\x32\x33\x2c\x32\x34\x2e\x35\x39\x2d\x31\x2e\x35\ -\x36\x2e\x32\x38\x2d\x2e\x31\x2e\x33\x39\x2d\x2e\x31\x2e\x33\x38\ -\x2e\x32\x39\x2c\x30\x2c\x2e\x36\x39\x2c\x30\x2c\x31\x2e\x33\x37\ -\x2c\x30\x2c\x32\x2e\x30\x36\x61\x2e\x34\x37\x2e\x34\x37\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x33\x35\x2e\x35\x33\x43\x33\x35\x2e\x32\ -\x38\x2c\x34\x36\x2c\x32\x39\x2e\x35\x32\x2c\x34\x36\x2e\x35\x33\ -\x2c\x32\x35\x2c\x34\x36\x2e\x35\x33\x5a\x22\x20\x74\x72\x61\x6e\ -\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\ -\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x39\x2c\ -\x32\x39\x2e\x36\x34\x63\x2e\x30\x35\x2d\x2e\x33\x33\x2d\x2e\x31\ -\x38\x2d\x2e\x39\x33\x2e\x31\x2d\x31\x2e\x31\x36\x73\x2e\x37\x2e\ -\x31\x34\x2c\x31\x2e\x30\x36\x2e\x32\x34\x61\x35\x32\x2e\x37\x38\ -\x2c\x35\x32\x2e\x37\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x39\x2e\ -\x38\x39\x2c\x31\x2e\x37\x33\x2c\x34\x35\x2e\x36\x35\x2c\x34\x35\ -\x2e\x36\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x39\x2e\x35\x35\x2d\x32\ -\x63\x2e\x33\x37\x2d\x2e\x31\x32\x2e\x34\x36\x2d\x2e\x30\x36\x2e\ -\x34\x34\x2e\x33\x38\x61\x31\x36\x2e\x36\x32\x2c\x31\x36\x2e\x36\ -\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2e\x37\x33\x2e\x35\ -\x35\x2e\x35\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x33\x38\x2e\x36\ -\x32\x2c\x32\x37\x2e\x34\x37\x2c\x32\x37\x2e\x34\x37\x2c\x30\x2c\ -\x30\x2c\x31\x2d\x36\x2e\x31\x2c\x31\x2e\x37\x32\x43\x32\x36\x2c\ -\x33\x34\x2e\x31\x38\x2c\x31\x36\x2e\x36\x36\x2c\x33\x34\x2e\x30\ -\x39\x2c\x39\x2e\x34\x36\x2c\x33\x31\x2e\x32\x34\x41\x2e\x35\x35\ -\x2e\x35\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x39\x2c\x33\x30\x2e\x35\ -\x36\x43\x39\x2e\x30\x37\x2c\x33\x30\x2e\x33\x2c\x39\x2c\x33\x30\ -\x2c\x39\x2c\x32\x39\x2e\x36\x34\x5a\x22\x20\x74\x72\x61\x6e\x73\ -\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\ -\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x35\x2e\ -\x39\x31\x2c\x34\x31\x2e\x35\x38\x61\x31\x32\x2e\x35\x34\x2c\x31\ -\x32\x2e\x35\x34\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x36\x38\x2e\ -\x30\x37\x63\x2d\x34\x2e\x36\x39\x2d\x2e\x33\x2d\x39\x2e\x35\x33\ -\x2d\x2e\x36\x35\x2d\x31\x33\x2e\x39\x33\x2d\x32\x2e\x35\x36\x41\ -\x2e\x34\x31\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x39\x2c\x33\ -\x38\x2e\x36\x36\x63\x2d\x2e\x30\x39\x2d\x31\x2e\x35\x38\x2d\x2e\ -\x31\x2d\x31\x2e\x35\x37\x2c\x31\x2e\x31\x39\x2d\x31\x2e\x31\x32\ -\x2c\x38\x2e\x36\x37\x2c\x32\x2e\x38\x31\x2c\x32\x30\x2e\x38\x34\ -\x2c\x32\x2e\x38\x34\x2c\x32\x39\x2e\x34\x34\x2d\x2e\x32\x39\x2e\ -\x33\x35\x2d\x2e\x31\x34\x2e\x34\x31\x2d\x2e\x30\x36\x2e\x34\x31\ -\x2e\x33\x35\x2c\x30\x2c\x31\x2e\x34\x31\x2c\x30\x2c\x31\x2e\x34\ -\x32\x2d\x31\x2e\x31\x36\x2c\x31\x2e\x38\x36\x41\x34\x31\x2e\x38\ -\x36\x2c\x34\x31\x2e\x38\x36\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x35\ -\x2e\x39\x31\x2c\x34\x31\x2e\x35\x38\x5a\x22\x20\x74\x72\x61\x6e\ -\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\ -\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x34\ -\x2e\x39\x2c\x33\x37\x2e\x36\x33\x63\x2d\x35\x2e\x35\x39\x2c\x30\ -\x2d\x31\x30\x2e\x35\x35\x2d\x2e\x34\x34\x2d\x31\x35\x2e\x35\x32\ -\x2d\x32\x2e\x34\x37\x2d\x2e\x32\x34\x2d\x2e\x31\x31\x2d\x2e\x33\ -\x37\x2d\x2e\x32\x32\x2d\x2e\x33\x34\x2d\x2e\x35\x33\x2c\x30\x2d\ -\x2e\x34\x37\x2d\x2e\x31\x37\x2d\x31\x2e\x31\x32\x2e\x30\x38\x2d\ -\x31\x2e\x33\x35\x73\x2e\x37\x33\x2e\x32\x2c\x31\x2e\x31\x31\x2e\ -\x33\x32\x43\x31\x39\x2c\x33\x36\x2e\x33\x34\x2c\x33\x31\x2c\x33\ -\x36\x2e\x34\x36\x2c\x33\x39\x2e\x36\x33\x2c\x33\x33\x2e\x32\x39\ -\x63\x2e\x34\x32\x2d\x2e\x31\x36\x2e\x34\x36\x2c\x30\x2c\x2e\x34\ -\x35\x2e\x34\x2c\x30\x2c\x31\x2e\x33\x34\x2c\x30\x2c\x31\x2e\x33\ -\x35\x2d\x31\x2e\x31\x32\x2c\x31\x2e\x37\x38\x41\x34\x31\x2e\x35\ -\x31\x2c\x34\x31\x2e\x35\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x34\ -\x2e\x39\x2c\x33\x37\x2e\x36\x33\x5a\x22\x20\x74\x72\x61\x6e\x73\ -\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\ -\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x37\x2e\x37\ -\x37\x2c\x32\x38\x2e\x37\x39\x63\x30\x2d\x31\x2c\x30\x2d\x31\x2e\ -\x30\x35\x2d\x2e\x38\x39\x2d\x31\x2e\x32\x39\x2d\x33\x2e\x33\x2d\ -\x31\x2d\x36\x2e\x32\x38\x2e\x32\x39\x2d\x36\x2e\x37\x35\x2c\x34\ -\x2d\x2e\x32\x37\x2c\x31\x2e\x37\x31\x2d\x2e\x32\x34\x2c\x31\x2e\ -\x37\x31\x2c\x31\x2e\x32\x36\x2c\x31\x2e\x36\x38\x2e\x32\x39\x2c\ -\x30\x2c\x2e\x33\x38\x2d\x2e\x31\x2e\x33\x38\x2d\x2e\x34\x32\x41\ -\x35\x2e\x36\x33\x2c\x35\x2e\x36\x33\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x32\x2c\x33\x31\x2e\x30\x36\x61\x32\x2e\x34\x2c\x32\x2e\x34\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x32\x2d\x31\x2e\x38\x2c\x36\x2e\x37\x33\ -\x2c\x36\x2e\x37\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x2e\x32\x2e\ -\x34\x32\x63\x2e\x35\x2e\x31\x38\x2e\x35\x2e\x31\x38\x2e\x35\x2d\ -\x2e\x34\x35\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\ -\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\ -\x3c\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x31\x22\x20\x63\x78\x3d\x22\x32\x34\x2e\x37\x36\ -\x22\x20\x63\x79\x3d\x22\x32\x32\x2e\x37\x37\x22\x20\x72\x78\x3d\ -\x22\x36\x2e\x38\x39\x22\x20\x72\x79\x3d\x22\x31\x2e\x31\x35\x22\ -\x2f\x3e\x3c\x74\x65\x78\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x32\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\ -\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x35\x36\x2e\x30\x34\ -\x20\x34\x39\x2e\x33\x37\x29\x20\x73\x63\x61\x6c\x65\x28\x31\x2e\ -\x30\x31\x20\x31\x29\x22\x3e\x6e\x3c\x74\x73\x70\x61\x6e\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x78\x3d\x22\ -\x33\x32\x2e\x39\x31\x22\x20\x79\x3d\x22\x30\x22\x3e\x2f\x3c\x2f\ -\x74\x73\x70\x61\x6e\x3e\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x78\x3d\x22\x35\x30\ -\x2e\x34\x38\x22\x20\x79\x3d\x22\x30\x22\x3e\x61\x3c\x2f\x74\x73\ -\x70\x61\x6e\x3e\x3c\x2f\x74\x65\x78\x74\x3e\x3c\x2f\x67\x3e\x3c\ -\x2f\x67\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x02\x53\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x31\x33\x2e\x38\x36\x20\x31\x32\x2e\x32\x37\x22\ -\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\ -\x6c\x73\x2d\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x66\x66\x66\x3b\x7d\ -\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\ -\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\ -\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\ -\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\ -\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\ -\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x70\x61\x74\x68\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\ -\x4d\x30\x2c\x34\x2e\x31\x33\x41\x31\x2e\x38\x37\x2c\x31\x2e\x38\ -\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x35\x38\x2c\x32\x2e\x37\x36\ -\x2c\x38\x2e\x36\x34\x2c\x38\x2e\x36\x34\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x35\x2e\x36\x36\x2e\x31\x61\x38\x2e\x35\x37\x2c\x38\x2e\x35\ -\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x35\x39\x2c\x32\x2e\x36\ -\x33\x41\x31\x2e\x39\x31\x2c\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x31\x33\x2e\x37\x2c\x34\x2e\x39\x2c\x31\x2e\x36\x33\x2c\ -\x31\x2e\x36\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x31\x2c\x35\x2e\ -\x34\x32\x2c\x35\x2e\x35\x35\x2c\x35\x2e\x35\x35\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x38\x2e\x31\x35\x2c\x33\x2e\x37\x36\x2c\x35\x2e\x33\ -\x34\x2c\x35\x2e\x33\x34\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x2e\x39\ -\x34\x2c\x35\x2e\x33\x37\x61\x31\x2e\x36\x36\x2c\x31\x2e\x36\x36\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x32\x2e\x38\x38\x2d\x2e\x38\x41\x33\ -\x2e\x31\x31\x2c\x33\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x30\ -\x2c\x34\x2e\x31\x33\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\ -\x4d\x36\x2e\x39\x33\x2c\x31\x32\x2e\x32\x37\x41\x32\x2e\x34\x2c\ -\x32\x2e\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x2e\x35\x39\x2c\x39\ -\x2e\x37\x34\x2c\x32\x2e\x34\x31\x2c\x32\x2e\x34\x31\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x36\x2e\x39\x32\x2c\x37\x2e\x32\x33\x2c\x32\x2e\ -\x34\x31\x2c\x32\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x39\x2e\ -\x32\x38\x2c\x39\x2e\x37\x34\x2c\x32\x2e\x34\x2c\x32\x2e\x34\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x36\x2e\x39\x33\x2c\x31\x32\x2e\x32\x37\ -\x5a\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\ -\x67\x3e\ -\x00\x00\x0a\x25\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x31\x37\x36\x2e\x30\x32\x20\x35\x38\x2e\x34\x31\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\ -\x36\x35\x2e\x37\x38\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ -\x69\x6c\x79\x3a\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\ -\x2c\x20\x4d\x6f\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\ -\x65\x69\x67\x68\x74\x3a\x32\x30\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x31\x2c\x2e\x63\x6c\x73\x2d\x33\x7b\x66\x69\x6c\x6c\x3a\x23\x66\ -\x66\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x6c\x65\x74\x74\x65\ -\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x33\x65\ -\x6d\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\ -\x73\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\ -\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\ -\x65\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\ -\x65\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\ -\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x74\x65\x78\ -\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x34\x37\x2e\x39\x31\x20\x34\x39\x2e\x39\x33\ -\x29\x22\x3e\x68\x69\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x78\x3d\x22\x33\x39\x2e\ -\x37\x33\x22\x20\x79\x3d\x22\x30\x22\x3e\x70\x3c\x2f\x74\x73\x70\ -\x61\x6e\x3e\x3c\x74\x73\x70\x61\x6e\x20\x78\x3d\x22\x36\x36\x2e\ -\x34\x33\x22\x20\x79\x3d\x22\x30\x22\x3e\x73\x3c\x2f\x74\x73\x70\ -\x61\x6e\x3e\x3c\x2f\x74\x65\x78\x74\x3e\x3c\x70\x61\x74\x68\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\ -\x22\x4d\x32\x34\x2e\x37\x38\x2c\x31\x38\x2e\x31\x34\x63\x32\x2e\ -\x35\x36\x2c\x31\x2e\x33\x35\x2c\x33\x2e\x37\x31\x2c\x36\x2e\x34\ -\x34\x2c\x34\x2e\x31\x38\x2c\x39\x2e\x30\x36\x2c\x31\x2e\x32\x2c\ -\x36\x2e\x35\x36\x2c\x31\x2e\x31\x34\x2c\x31\x39\x2e\x31\x2d\x33\ -\x2e\x30\x39\x2c\x32\x34\x2e\x35\x31\x2d\x2e\x38\x31\x2c\x31\x2d\ -\x31\x2e\x38\x32\x2c\x31\x2e\x32\x32\x2d\x32\x2e\x37\x34\x2e\x32\ -\x33\x61\x39\x2e\x31\x31\x2c\x39\x2e\x31\x31\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x31\x2e\x38\x35\x2d\x33\x2e\x31\x31\x63\x2d\x32\x2e\x38\ -\x33\x2d\x37\x2e\x36\x2d\x32\x2e\x38\x38\x2d\x31\x39\x2e\x32\x39\ -\x2c\x30\x2d\x32\x36\x2e\x39\x2e\x35\x38\x2d\x31\x2e\x34\x32\x2c\ -\x31\x2e\x33\x31\x2d\x33\x2c\x32\x2e\x37\x35\x2d\x33\x2e\x37\x39\ -\x5a\x4d\x32\x33\x2e\x35\x33\x2c\x34\x39\x2e\x37\x32\x61\x2e\x38\ -\x38\x2e\x38\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x36\x2e\x36\ -\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x39\ -\x2d\x2e\x36\x32\x63\x33\x2e\x30\x39\x2d\x36\x2e\x35\x34\x2c\x33\ -\x2e\x31\x33\x2d\x31\x36\x2e\x35\x35\x2c\x31\x2e\x36\x39\x2d\x32\ -\x33\x2e\x35\x37\x41\x32\x30\x2e\x32\x37\x2c\x32\x30\x2e\x32\x37\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x35\x2e\x32\x37\x2c\x32\x31\x61\ -\x2e\x39\x2e\x39\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x38\x35\x2d\x2e\ -\x36\x2e\x39\x32\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\ -\x2e\x36\x31\x2c\x31\x34\x2e\x39\x34\x2c\x31\x34\x2e\x39\x34\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x33\x2c\x33\x2e\x34\x38\x43\x32\ -\x30\x2e\x35\x32\x2c\x33\x31\x2e\x36\x34\x2c\x31\x39\x2e\x34\x31\ -\x2c\x34\x30\x2e\x34\x31\x2c\x32\x33\x2e\x35\x33\x2c\x34\x39\x2e\ -\x37\x32\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\ -\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x33\x22\x20\x64\x3d\x22\x4d\x35\x2e\x36\x34\x2c\x31\x38\x2e\x31\ -\x34\x61\x35\x2e\x31\x33\x2c\x35\x2e\x31\x33\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x32\x2c\x32\x2e\x32\x37\x63\x2d\x2e\x37\x32\x2c\x30\x2d\ -\x31\x2e\x33\x39\x2c\x30\x2d\x32\x2e\x30\x36\x2c\x30\x73\x2d\x31\ -\x2c\x2e\x31\x2d\x31\x2e\x32\x39\x2e\x37\x34\x43\x31\x2c\x32\x38\ -\x2e\x34\x38\x2c\x31\x2c\x34\x31\x2e\x31\x39\x2c\x33\x2e\x39\x32\ -\x2c\x34\x38\x2e\x37\x63\x2e\x31\x35\x2e\x33\x36\x2e\x33\x33\x2e\ -\x37\x31\x2e\x34\x39\x2c\x31\x2e\x30\x36\x61\x31\x2c\x31\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x35\x37\x48\x37\x2e\x36\x37\x41\ -\x35\x2e\x30\x37\x2c\x35\x2e\x30\x37\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x35\x2e\x39\x32\x2c\x35\x32\x2e\x34\x63\x2d\x31\x2e\x32\x34\x2e\ -\x37\x34\x2d\x32\x2e\x34\x2d\x2e\x38\x38\x2d\x32\x2e\x39\x33\x2d\ -\x31\x2e\x38\x31\x43\x2d\x2e\x38\x32\x2c\x34\x33\x2e\x35\x33\x2d\ -\x2e\x37\x34\x2c\x33\x30\x2e\x32\x38\x2c\x31\x2e\x38\x33\x2c\x32\ -\x32\x2e\x37\x34\x63\x2e\x35\x39\x2d\x31\x2e\x36\x34\x2c\x31\x2e\ -\x34\x2d\x33\x2e\x36\x39\x2c\x33\x2e\x30\x35\x2d\x34\x2e\x36\x5a\ -\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\ -\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\ -\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\ -\x64\x3d\x22\x4d\x33\x2e\x35\x32\x2c\x33\x35\x2e\x37\x37\x63\x30\ -\x2d\x35\x2c\x2e\x33\x37\x2d\x39\x2e\x32\x33\x2c\x32\x2e\x31\x37\ -\x2d\x31\x33\x2e\x36\x31\x61\x2e\x34\x32\x2e\x34\x32\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x2e\x34\x38\x2d\x2e\x32\x39\x63\x2e\x36\x2c\x30\ -\x2c\x31\x2e\x32\x2c\x30\x2c\x31\x2e\x38\x2c\x30\x2c\x2e\x32\x39\ -\x2c\x30\x2c\x2e\x33\x35\x2c\x30\x2c\x2e\x32\x35\x2e\x33\x61\x32\ -\x37\x2e\x33\x35\x2c\x32\x37\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\ -\x2d\x31\x2e\x33\x37\x2c\x35\x63\x2d\x31\x2e\x31\x37\x2c\x36\x2e\ -\x39\x2d\x31\x2e\x30\x38\x2c\x31\x34\x2e\x37\x35\x2c\x31\x2e\x33\ -\x37\x2c\x32\x31\x2e\x33\x38\x2e\x31\x2e\x32\x34\x2e\x30\x39\x2e\ -\x33\x34\x2d\x2e\x32\x35\x2e\x33\x33\x2d\x2e\x36\x2c\x30\x2d\x31\ -\x2e\x32\x31\x2c\x30\x2d\x31\x2e\x38\x31\x2c\x30\x61\x2e\x34\x33\ -\x2e\x34\x33\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x34\x37\x2d\x2e\x33\ -\x43\x34\x2c\x34\x34\x2e\x36\x38\x2c\x33\x2e\x35\x32\x2c\x33\x39\ -\x2e\x36\x38\x2c\x33\x2e\x35\x32\x2c\x33\x35\x2e\x37\x37\x5a\x22\ -\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\ -\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\ -\x3d\x22\x4d\x31\x38\x2e\x33\x38\x2c\x32\x31\x2e\x38\x37\x63\x2e\ -\x32\x38\x2e\x30\x35\x2e\x38\x31\x2d\x2e\x31\x36\x2c\x31\x2c\x2e\ -\x30\x39\x73\x2d\x2e\x31\x33\x2e\x36\x31\x2d\x2e\x32\x32\x2e\x39\ -\x32\x61\x34\x35\x2e\x36\x39\x2c\x34\x35\x2e\x36\x39\x2c\x30\x2c\ -\x30\x2c\x30\x2d\x31\x2e\x35\x32\x2c\x31\x37\x2e\x32\x39\x2c\x33\ -\x39\x2e\x36\x31\x2c\x33\x39\x2e\x36\x31\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x31\x2e\x37\x32\x2c\x38\x2e\x33\x31\x63\x2e\x31\x31\x2e\x33\ -\x33\x2e\x30\x35\x2e\x34\x2d\x2e\x33\x33\x2e\x33\x38\x73\x2d\x31\ -\x2c\x30\x2d\x31\x2e\x35\x33\x2c\x30\x61\x2e\x34\x39\x2e\x34\x39\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x35\x34\x2d\x2e\x33\x33\x2c\x32\ -\x33\x2e\x38\x35\x2c\x32\x33\x2e\x38\x35\x2c\x30\x2c\x30\x2c\x31\ -\x2d\x31\x2e\x35\x31\x2d\x35\x2e\x33\x63\x2d\x31\x2e\x30\x39\x2d\ -\x36\x2e\x35\x38\x2d\x31\x2d\x31\x34\x2e\x37\x34\x2c\x31\x2e\x34\ -\x39\x2d\x32\x31\x61\x2e\x35\x2e\x35\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x2e\x36\x2d\x2e\x33\x37\x41\x37\x2e\x34\x31\x2c\x37\x2e\x34\x31\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x38\x2e\x33\x38\x2c\x32\x31\x2e\ -\x38\x37\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\ -\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x33\x22\x20\x64\x3d\x22\x4d\x37\x2e\x38\x37\x2c\x33\x36\x2e\x35\ -\x34\x61\x31\x31\x2e\x32\x36\x2c\x31\x31\x2e\x32\x36\x2c\x30\x2c\ -\x30\x2c\x31\x2d\x2e\x30\x36\x2d\x32\x2e\x33\x33\x63\x2e\x32\x37\ -\x2d\x34\x2e\x30\x38\x2e\x35\x37\x2d\x38\x2e\x32\x38\x2c\x32\x2e\ -\x32\x35\x2d\x31\x32\x2e\x31\x31\x61\x2e\x33\x36\x2e\x33\x36\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x2e\x33\x38\x2d\x2e\x32\x33\x63\x31\x2e\ -\x33\x39\x2d\x2e\x30\x38\x2c\x31\x2e\x33\x38\x2d\x2e\x30\x38\x2c\ -\x31\x2c\x31\x43\x39\x2c\x33\x30\x2e\x34\x35\x2c\x38\x2e\x39\x33\ -\x2c\x34\x31\x2c\x31\x31\x2e\x36\x38\x2c\x34\x38\x2e\x35\x63\x2e\ -\x31\x32\x2e\x33\x31\x2e\x30\x35\x2e\x33\x37\x2d\x2e\x33\x2e\x33\ -\x36\x2d\x31\x2e\x32\x35\x2c\x30\x2d\x31\x2e\x32\x36\x2c\x30\x2d\ -\x31\x2e\x36\x34\x2d\x31\x41\x33\x36\x2e\x31\x34\x2c\x33\x36\x2e\ -\x31\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x38\x37\x2c\x33\x36\ -\x2e\x35\x34\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\ -\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\ -\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\ -\x2d\x33\x22\x20\x64\x3d\x22\x4d\x31\x31\x2e\x33\x35\x2c\x33\x35\ -\x2e\x36\x36\x63\x30\x2d\x34\x2e\x38\x36\x2e\x33\x38\x2d\x39\x2e\ -\x31\x37\x2c\x32\x2e\x31\x37\x2d\x31\x33\x2e\x34\x39\x61\x2e\x34\ -\x2e\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x37\x2d\x2e\x33\x63\ -\x2e\x34\x2c\x30\x2c\x31\x2d\x2e\x31\x35\x2c\x31\x2e\x31\x38\x2e\ -\x30\x37\x73\x2d\x2e\x31\x37\x2e\x36\x33\x2d\x2e\x32\x38\x2c\x31\ -\x63\x2d\x32\x2e\x34\x31\x2c\x37\x2e\x36\x31\x2d\x32\x2e\x35\x31\ -\x2c\x31\x38\x2c\x2e\x32\x37\x2c\x32\x35\x2e\x35\x36\x2e\x31\x34\ -\x2e\x33\x36\x2c\x30\x2c\x2e\x34\x2d\x2e\x33\x35\x2e\x33\x39\x2d\ -\x31\x2e\x31\x37\x2c\x30\x2d\x31\x2e\x31\x39\x2c\x30\x2d\x31\x2e\ -\x35\x37\x2d\x31\x41\x33\x35\x2e\x37\x34\x2c\x33\x35\x2e\x37\x34\ -\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x31\x2e\x33\x35\x2c\x33\x35\x2e\ -\x36\x36\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\ -\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x33\x22\x20\x64\x3d\x22\x4d\x31\x39\x2e\x31\x32\x2c\x32\x30\x2e\ -\x37\x37\x63\x2e\x39\x32\x2c\x30\x2c\x2e\x39\x32\x2c\x30\x2c\x31\ -\x2e\x31\x33\x2d\x2e\x37\x38\x2e\x38\x36\x2d\x32\x2e\x38\x36\x2d\ -\x2e\x32\x35\x2d\x35\x2e\x34\x35\x2d\x33\x2e\x35\x2d\x35\x2e\x38\ -\x37\x2d\x31\x2e\x35\x31\x2d\x2e\x32\x33\x2d\x31\x2e\x35\x2d\x2e\ -\x32\x31\x2d\x31\x2e\x34\x38\x2c\x31\x2e\x31\x2c\x30\x2c\x2e\x32\ -\x35\x2e\x30\x39\x2e\x33\x33\x2e\x33\x37\x2e\x33\x33\x61\x35\x2c\ -\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x34\x39\x2e\x32\x33\x2c\ -\x32\x2e\x31\x31\x2c\x32\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x31\x2e\x35\x38\x2c\x31\x2e\x37\x37\x2c\x35\x2e\x37\x34\x2c\x35\ -\x2e\x37\x34\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x33\x38\x2c\x32\x2e\ -\x37\x38\x63\x2d\x2e\x31\x35\x2e\x34\x34\x2d\x2e\x31\x35\x2e\x34\ -\x34\x2e\x34\x31\x2e\x34\x34\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\ -\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\ -\x29\x22\x2f\x3e\x3c\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x63\x78\x3d\x22\x32\ -\x34\x2e\x34\x32\x22\x20\x63\x79\x3d\x22\x33\x35\x2e\x35\x34\x22\ -\x20\x72\x78\x3d\x22\x31\x2e\x30\x31\x22\x20\x72\x79\x3d\x22\x35\ -\x2e\x39\x39\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\ -\x73\x76\x67\x3e\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 184.16 57.76\ +\x22>\ +n/a<\ +/g>\ \x00\x00\x0a\x12\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x31\x37\x36\x2e\x30\x32\x20\x35\x38\x2e\x34\x31\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\ -\x36\x35\x2e\x37\x38\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ -\x69\x6c\x79\x3a\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\ -\x2c\x20\x4d\x6f\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\ -\x65\x69\x67\x68\x74\x3a\x32\x30\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x31\x2c\x2e\x63\x6c\x73\x2d\x33\x7b\x66\x69\x6c\x6c\x3a\x23\x66\ -\x66\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x6c\x65\x74\x74\x65\ -\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x31\x65\ -\x6d\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\ -\x73\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\ -\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\ -\x65\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\ -\x65\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\ -\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x74\x65\x78\ -\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x34\x37\x2e\x39\x31\x20\x34\x39\x2e\x39\x33\ -\x29\x22\x3e\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x32\x22\x3e\x6e\x79\x3c\x2f\x74\x73\x70\x61\ -\x6e\x3e\x3c\x74\x73\x70\x61\x6e\x20\x78\x3d\x22\x36\x34\x2e\x34\ -\x22\x20\x79\x3d\x22\x30\x22\x3e\x6c\x3c\x2f\x74\x73\x70\x61\x6e\ -\x3e\x3c\x2f\x74\x65\x78\x74\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ -\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\ -\x32\x34\x2e\x37\x38\x2c\x31\x38\x2e\x31\x32\x63\x32\x2e\x35\x36\ -\x2c\x31\x2e\x33\x34\x2c\x33\x2e\x37\x31\x2c\x36\x2e\x34\x34\x2c\ -\x34\x2e\x31\x38\x2c\x39\x2e\x30\x36\x2c\x31\x2e\x32\x2c\x36\x2e\ -\x35\x36\x2c\x31\x2e\x31\x34\x2c\x31\x39\x2e\x31\x2d\x33\x2e\x30\ -\x39\x2c\x32\x34\x2e\x35\x31\x2d\x2e\x38\x31\x2c\x31\x2d\x31\x2e\ -\x38\x32\x2c\x31\x2e\x32\x31\x2d\x32\x2e\x37\x34\x2e\x32\x32\x61\ -\x39\x2c\x39\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x38\x35\x2d\x33\ -\x2e\x31\x63\x2d\x32\x2e\x38\x33\x2d\x37\x2e\x36\x31\x2d\x32\x2e\ -\x38\x38\x2d\x31\x39\x2e\x33\x2c\x30\x2d\x32\x36\x2e\x39\x31\x2e\ -\x35\x38\x2d\x31\x2e\x34\x31\x2c\x31\x2e\x33\x31\x2d\x33\x2c\x32\ -\x2e\x37\x35\x2d\x33\x2e\x37\x38\x5a\x4d\x32\x33\x2e\x35\x33\x2c\ -\x34\x39\x2e\x36\x39\x61\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x2e\x38\x36\x2e\x36\x31\x2e\x39\x34\x2e\x39\x34\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x2e\x38\x39\x2d\x2e\x36\x32\x63\x33\x2e\ -\x30\x39\x2d\x36\x2e\x35\x34\x2c\x33\x2e\x31\x33\x2d\x31\x36\x2e\ -\x35\x35\x2c\x31\x2e\x36\x39\x2d\x32\x33\x2e\x35\x37\x41\x32\x30\ -\x2e\x32\x37\x2c\x32\x30\x2e\x32\x37\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x32\x35\x2e\x32\x37\x2c\x32\x31\x61\x2e\x38\x38\x2e\x38\x38\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x2e\x38\x35\x2d\x2e\x36\x2e\x39\x32\x2e\ -\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x39\x2e\x36\x31\x2c\x31\ -\x34\x2e\x39\x34\x2c\x31\x34\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x30\ -\x2d\x31\x2e\x33\x2c\x33\x2e\x34\x38\x43\x32\x30\x2e\x35\x32\x2c\ -\x33\x31\x2e\x36\x32\x2c\x31\x39\x2e\x34\x31\x2c\x34\x30\x2e\x33\ -\x39\x2c\x32\x33\x2e\x35\x33\x2c\x34\x39\x2e\x36\x39\x5a\x22\x20\ -\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\ -\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\ -\x22\x4d\x35\x2e\x36\x34\x2c\x31\x38\x2e\x31\x32\x61\x35\x2e\x31\ -\x36\x2c\x35\x2e\x31\x36\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x2c\x32\ -\x2e\x32\x36\x48\x35\x2e\x36\x31\x63\x2d\x2e\x38\x2c\x30\x2d\x31\ -\x2c\x2e\x31\x31\x2d\x31\x2e\x32\x39\x2e\x37\x34\x43\x31\x2c\x32\ -\x38\x2e\x34\x35\x2c\x31\x2c\x34\x31\x2e\x31\x37\x2c\x33\x2e\x39\ -\x32\x2c\x34\x38\x2e\x36\x38\x63\x2e\x31\x35\x2e\x33\x36\x2e\x33\ -\x33\x2e\x37\x2e\x34\x39\x2c\x31\x2e\x30\x36\x61\x31\x2c\x31\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x35\x37\x48\x37\x2e\x36\x37\ -\x61\x35\x2c\x35\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x37\x35\x2c\ -\x32\x2e\x30\x37\x63\x2d\x31\x2e\x32\x34\x2e\x37\x35\x2d\x32\x2e\ -\x34\x2d\x2e\x38\x37\x2d\x32\x2e\x39\x33\x2d\x31\x2e\x38\x43\x2d\ -\x2e\x38\x32\x2c\x34\x33\x2e\x35\x2d\x2e\x37\x34\x2c\x33\x30\x2e\ -\x32\x35\x2c\x31\x2e\x38\x33\x2c\x32\x32\x2e\x37\x32\x63\x2e\x35\ -\x39\x2d\x31\x2e\x36\x35\x2c\x31\x2e\x34\x2d\x33\x2e\x36\x39\x2c\ -\x33\x2e\x30\x35\x2d\x34\x2e\x36\x5a\x22\x20\x74\x72\x61\x6e\x73\ -\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\ -\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x33\x2e\x35\ -\x32\x2c\x33\x35\x2e\x37\x35\x63\x30\x2d\x35\x2c\x2e\x33\x37\x2d\ -\x39\x2e\x32\x34\x2c\x32\x2e\x31\x37\x2d\x31\x33\x2e\x36\x32\x61\ -\x2e\x34\x34\x2e\x34\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\x38\ -\x2d\x2e\x32\x39\x63\x2e\x36\x2c\x30\x2c\x31\x2e\x32\x2c\x30\x2c\ -\x31\x2e\x38\x2c\x30\x2c\x2e\x32\x39\x2c\x30\x2c\x2e\x33\x35\x2c\ -\x30\x2c\x2e\x32\x35\x2e\x32\x39\x61\x32\x37\x2e\x34\x35\x2c\x32\ -\x37\x2e\x34\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x33\x37\x2c\ -\x35\x43\x35\x2e\x36\x38\x2c\x33\x34\x2c\x35\x2e\x37\x37\x2c\x34\ -\x31\x2e\x38\x39\x2c\x38\x2e\x32\x32\x2c\x34\x38\x2e\x35\x32\x63\ -\x2e\x31\x2e\x32\x34\x2e\x30\x39\x2e\x33\x34\x2d\x2e\x32\x35\x2e\ -\x33\x32\x2d\x2e\x36\x2c\x30\x2d\x31\x2e\x32\x31\x2c\x30\x2d\x31\ -\x2e\x38\x31\x2c\x30\x61\x2e\x34\x32\x2e\x34\x32\x2c\x30\x2c\x30\ -\x2c\x31\x2d\x2e\x34\x37\x2d\x2e\x32\x39\x43\x34\x2c\x34\x34\x2e\ -\x36\x36\x2c\x33\x2e\x35\x32\x2c\x33\x39\x2e\x36\x35\x2c\x33\x2e\ -\x35\x32\x2c\x33\x35\x2e\x37\x35\x5a\x22\x20\x74\x72\x61\x6e\x73\ -\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\ -\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x31\x38\x2e\ -\x33\x38\x2c\x32\x31\x2e\x38\x35\x63\x2e\x32\x38\x2c\x30\x2c\x2e\ -\x38\x31\x2d\x2e\x31\x36\x2c\x31\x2c\x2e\x30\x39\x73\x2d\x2e\x31\ -\x33\x2e\x36\x2d\x2e\x32\x32\x2e\x39\x32\x61\x34\x35\x2e\x36\x39\ -\x2c\x34\x35\x2e\x36\x39\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x35\ -\x32\x2c\x31\x37\x2e\x32\x39\x2c\x33\x39\x2e\x38\x37\x2c\x33\x39\ -\x2e\x38\x37\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2e\x37\x32\x2c\x38\ -\x2e\x33\x31\x63\x2e\x31\x31\x2e\x33\x32\x2e\x30\x35\x2e\x34\x2d\ -\x2e\x33\x33\x2e\x33\x38\x73\x2d\x31\x2c\x30\x2d\x31\x2e\x35\x33\ -\x2c\x30\x61\x2e\x34\x37\x2e\x34\x37\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x2e\x35\x34\x2d\x2e\x33\x33\x2c\x32\x33\x2e\x36\x32\x2c\x32\x33\ -\x2e\x36\x32\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\x31\x2d\x35\ -\x2e\x32\x39\x63\x2d\x31\x2e\x30\x39\x2d\x36\x2e\x35\x39\x2d\x31\ -\x2d\x31\x34\x2e\x37\x34\x2c\x31\x2e\x34\x39\x2d\x32\x31\x61\x2e\ -\x34\x38\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x36\x2d\x2e\ -\x33\x37\x43\x31\x37\x2e\x38\x2c\x32\x31\x2e\x38\x37\x2c\x31\x38\ -\x2c\x32\x31\x2e\x38\x35\x2c\x31\x38\x2e\x33\x38\x2c\x32\x31\x2e\ -\x38\x35\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\ -\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x33\x22\x20\x64\x3d\x22\x4d\x37\x2e\x38\x37\x2c\x33\x36\x2e\x35\ -\x32\x61\x31\x31\x2e\x32\x36\x2c\x31\x31\x2e\x32\x36\x2c\x30\x2c\ -\x30\x2c\x31\x2d\x2e\x30\x36\x2d\x32\x2e\x33\x33\x63\x2e\x32\x37\ -\x2d\x34\x2e\x30\x38\x2e\x35\x37\x2d\x38\x2e\x32\x39\x2c\x32\x2e\ -\x32\x35\x2d\x31\x32\x2e\x31\x31\x61\x2e\x33\x38\x2e\x33\x38\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x2e\x33\x38\x2d\x2e\x32\x34\x63\x31\x2e\ -\x33\x39\x2d\x2e\x30\x37\x2c\x31\x2e\x33\x38\x2d\x2e\x30\x38\x2c\ -\x31\x2c\x31\x2e\x30\x35\x43\x39\x2c\x33\x30\x2e\x34\x33\x2c\x38\ -\x2e\x39\x33\x2c\x34\x31\x2c\x31\x31\x2e\x36\x38\x2c\x34\x38\x2e\ -\x34\x38\x63\x2e\x31\x32\x2e\x33\x31\x2e\x30\x35\x2e\x33\x36\x2d\ -\x2e\x33\x2e\x33\x36\x2d\x31\x2e\x32\x35\x2c\x30\x2d\x31\x2e\x32\ -\x36\x2c\x30\x2d\x31\x2e\x36\x34\x2d\x31\x41\x33\x36\x2e\x31\x39\ -\x2c\x33\x36\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x38\ -\x37\x2c\x33\x36\x2e\x35\x32\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\ -\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\ -\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x31\x31\x2e\x33\ -\x35\x2c\x33\x35\x2e\x36\x34\x63\x30\x2d\x34\x2e\x38\x36\x2e\x33\ -\x38\x2d\x39\x2e\x31\x37\x2c\x32\x2e\x31\x37\x2d\x31\x33\x2e\x35\ -\x61\x2e\x34\x31\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x34\ -\x37\x2d\x2e\x32\x39\x63\x2e\x34\x2c\x30\x2c\x31\x2d\x2e\x31\x35\ -\x2c\x31\x2e\x31\x38\x2e\x30\x36\x73\x2d\x2e\x31\x37\x2e\x36\x34\ -\x2d\x2e\x32\x38\x2c\x31\x63\x2d\x32\x2e\x34\x31\x2c\x37\x2e\x36\ -\x31\x2d\x32\x2e\x35\x31\x2c\x31\x38\x2c\x2e\x32\x37\x2c\x32\x35\ -\x2e\x35\x37\x2e\x31\x34\x2e\x33\x36\x2c\x30\x2c\x2e\x33\x39\x2d\ -\x2e\x33\x35\x2e\x33\x39\x2d\x31\x2e\x31\x37\x2c\x30\x2d\x31\x2e\ -\x31\x39\x2c\x30\x2d\x31\x2e\x35\x37\x2d\x31\x41\x33\x35\x2e\x37\ -\x37\x2c\x33\x35\x2e\x37\x37\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x31\ -\x2e\x33\x35\x2c\x33\x35\x2e\x36\x34\x5a\x22\x20\x74\x72\x61\x6e\ -\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\ -\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x31\x39\ -\x2e\x31\x32\x2c\x32\x30\x2e\x37\x35\x63\x2e\x39\x32\x2c\x30\x2c\ -\x2e\x39\x32\x2c\x30\x2c\x31\x2e\x31\x33\x2d\x2e\x37\x38\x2e\x38\ -\x36\x2d\x32\x2e\x38\x37\x2d\x2e\x32\x35\x2d\x35\x2e\x34\x36\x2d\ -\x33\x2e\x35\x2d\x35\x2e\x38\x37\x2d\x31\x2e\x35\x31\x2d\x2e\x32\ -\x33\x2d\x31\x2e\x35\x2d\x2e\x32\x31\x2d\x31\x2e\x34\x38\x2c\x31\ -\x2e\x31\x2c\x30\x2c\x2e\x32\x35\x2e\x30\x39\x2e\x33\x33\x2e\x33\ -\x37\x2e\x33\x33\x61\x35\x2e\x33\x32\x2c\x35\x2e\x33\x32\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x31\x2e\x34\x39\x2e\x32\x32\x2c\x32\x2e\x31\ -\x32\x2c\x32\x2e\x31\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x35\ -\x38\x2c\x31\x2e\x37\x38\x2c\x35\x2e\x37\x38\x2c\x35\x2e\x37\x38\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x33\x38\x2c\x32\x2e\x37\x38\x63\ -\x2d\x2e\x31\x35\x2e\x34\x34\x2d\x2e\x31\x35\x2e\x34\x34\x2e\x34\ -\x31\x2e\x34\x34\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\ -\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\ -\x3e\x3c\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x33\x22\x20\x63\x78\x3d\x22\x32\x34\x2e\x34\ -\x32\x22\x20\x63\x79\x3d\x22\x33\x35\x2e\x35\x32\x22\x20\x72\x78\ -\x3d\x22\x31\x2e\x30\x31\x22\x20\x72\x79\x3d\x22\x35\x2e\x39\x39\ -\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\ -\x3e\ -\x00\x00\x0a\x62\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x31\x39\x36\x2e\x33\x37\x20\x35\x38\x2e\x34\x31\ -\x22\x3e\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\ -\x63\x6c\x73\x2d\x31\x7b\x66\x6f\x6e\x74\x2d\x73\x69\x7a\x65\x3a\ -\x36\x35\x2e\x37\x38\x70\x78\x3b\x66\x6f\x6e\x74\x2d\x66\x61\x6d\ -\x69\x6c\x79\x3a\x4d\x6f\x6d\x63\x61\x6b\x65\x2d\x54\x68\x69\x6e\ -\x2c\x20\x4d\x6f\x6d\x63\x61\x6b\x65\x3b\x66\x6f\x6e\x74\x2d\x77\ -\x65\x69\x67\x68\x74\x3a\x32\x30\x30\x3b\x7d\x2e\x63\x6c\x73\x2d\ -\x31\x2c\x2e\x63\x6c\x73\x2d\x34\x7b\x66\x69\x6c\x6c\x3a\x23\x66\ -\x66\x66\x3b\x7d\x2e\x63\x6c\x73\x2d\x32\x7b\x6c\x65\x74\x74\x65\ -\x72\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x31\x65\ -\x6d\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x6c\x65\x74\x74\x65\x72\ -\x2d\x73\x70\x61\x63\x69\x6e\x67\x3a\x2d\x30\x2e\x30\x35\x65\x6d\ -\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\ -\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x32\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\ -\x72\x5f\x31\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\ -\x3d\x22\x4c\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x74\x65\x78\x74\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x74\ -\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\ -\x61\x74\x65\x28\x34\x37\x2e\x39\x31\x20\x34\x39\x2e\x39\x33\x29\ -\x22\x3e\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x32\x22\x3e\x70\x3c\x2f\x74\x73\x70\x61\x6e\x3e\ -\x3c\x74\x73\x70\x61\x6e\x20\x78\x3d\x22\x32\x38\x2e\x30\x32\x22\ -\x20\x79\x3d\x22\x30\x22\x3e\x65\x3c\x2f\x74\x73\x70\x61\x6e\x3e\ -\x3c\x74\x73\x70\x61\x6e\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x33\x22\x20\x78\x3d\x22\x35\x34\x2e\x38\x36\x22\x20\x79\ -\x3d\x22\x30\x22\x3e\x74\x3c\x2f\x74\x73\x70\x61\x6e\x3e\x3c\x74\ -\x73\x70\x61\x6e\x20\x78\x3d\x22\x38\x34\x2e\x39\x32\x22\x20\x79\ -\x3d\x22\x30\x22\x3e\x67\x3c\x2f\x74\x73\x70\x61\x6e\x3e\x3c\x2f\ -\x74\x65\x78\x74\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x32\x34\x2e\ -\x37\x38\x2c\x31\x38\x2e\x31\x31\x63\x32\x2e\x35\x36\x2c\x31\x2e\ -\x33\x35\x2c\x33\x2e\x37\x31\x2c\x36\x2e\x34\x34\x2c\x34\x2e\x31\ -\x38\x2c\x39\x2e\x30\x37\x2c\x31\x2e\x32\x2c\x36\x2e\x35\x36\x2c\ -\x31\x2e\x31\x34\x2c\x31\x39\x2e\x31\x2d\x33\x2e\x30\x39\x2c\x32\ -\x34\x2e\x35\x31\x2d\x2e\x38\x31\x2c\x31\x2d\x31\x2e\x38\x32\x2c\ -\x31\x2e\x32\x31\x2d\x32\x2e\x37\x34\x2e\x32\x32\x61\x39\x2c\x39\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x38\x35\x2d\x33\x2e\x31\x31\ -\x63\x2d\x32\x2e\x38\x33\x2d\x37\x2e\x36\x2d\x32\x2e\x38\x38\x2d\ -\x31\x39\x2e\x32\x39\x2c\x30\x2d\x32\x36\x2e\x39\x41\x37\x2e\x30\ -\x36\x2c\x37\x2e\x30\x36\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x34\x2c\ -\x31\x38\x2e\x31\x31\x5a\x4d\x32\x33\x2e\x35\x33\x2c\x34\x39\x2e\ -\x36\x39\x61\x2e\x38\x38\x2e\x38\x38\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x2e\x38\x36\x2e\x36\x2e\x39\x32\x2e\x39\x32\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x2e\x38\x39\x2d\x2e\x36\x31\x63\x33\x2e\x30\x39\x2d\x36\ -\x2e\x35\x35\x2c\x33\x2e\x31\x33\x2d\x31\x36\x2e\x35\x36\x2c\x31\ -\x2e\x36\x39\x2d\x32\x33\x2e\x35\x37\x41\x32\x30\x2e\x32\x31\x2c\ -\x32\x30\x2e\x32\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x35\x2e\x32\ -\x37\x2c\x32\x31\x61\x2e\x39\x31\x2e\x39\x31\x2c\x30\x2c\x30\x2c\ -\x30\x2d\x2e\x38\x35\x2d\x2e\x36\x2c\x31\x2c\x31\x2c\x30\x2c\x30\ -\x2c\x30\x2d\x2e\x39\x2e\x36\x31\x2c\x31\x35\x2c\x31\x35\x2c\x30\ -\x2c\x30\x2c\x30\x2d\x31\x2e\x33\x2c\x33\x2e\x34\x39\x43\x32\x30\ -\x2e\x35\x32\x2c\x33\x31\x2e\x36\x31\x2c\x31\x39\x2e\x34\x31\x2c\ -\x34\x30\x2e\x33\x39\x2c\x32\x33\x2e\x35\x33\x2c\x34\x39\x2e\x36\ -\x39\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\ -\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\ -\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\ -\x22\x20\x64\x3d\x22\x4d\x35\x2e\x36\x34\x2c\x31\x38\x2e\x31\x31\ -\x61\x35\x2e\x32\x34\x2c\x35\x2e\x32\x34\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x32\x2c\x32\x2e\x32\x37\x48\x35\x2e\x36\x31\x63\x2d\x2e\x38\ -\x2c\x30\x2d\x31\x2c\x2e\x31\x31\x2d\x31\x2e\x32\x39\x2e\x37\x34\ -\x43\x31\x2c\x32\x38\x2e\x34\x35\x2c\x31\x2c\x34\x31\x2e\x31\x37\ -\x2c\x33\x2e\x39\x32\x2c\x34\x38\x2e\x36\x37\x63\x2e\x31\x35\x2e\ -\x33\x36\x2e\x33\x33\x2e\x37\x31\x2e\x34\x39\x2c\x31\x2e\x30\x36\ -\x61\x31\x2c\x31\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x35\x37\ -\x48\x37\x2e\x36\x37\x61\x35\x2c\x35\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x31\x2e\x37\x35\x2c\x32\x2e\x30\x37\x63\x2d\x31\x2e\x32\x34\x2e\ -\x37\x34\x2d\x32\x2e\x34\x2d\x2e\x38\x37\x2d\x32\x2e\x39\x33\x2d\ -\x31\x2e\x38\x43\x2d\x2e\x38\x32\x2c\x34\x33\x2e\x35\x2d\x2e\x37\ -\x34\x2c\x33\x30\x2e\x32\x35\x2c\x31\x2e\x38\x33\x2c\x32\x32\x2e\ -\x37\x32\x63\x2e\x35\x39\x2d\x31\x2e\x36\x35\x2c\x31\x2e\x34\x2d\ -\x33\x2e\x36\x39\x2c\x33\x2e\x30\x35\x2d\x34\x2e\x36\x31\x5a\x22\ -\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\ -\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\ -\x3d\x22\x4d\x33\x2e\x35\x32\x2c\x33\x35\x2e\x37\x34\x63\x30\x2d\ -\x35\x2c\x2e\x33\x37\x2d\x39\x2e\x32\x33\x2c\x32\x2e\x31\x37\x2d\ -\x31\x33\x2e\x36\x31\x61\x2e\x34\x33\x2e\x34\x33\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x2e\x34\x38\x2d\x2e\x32\x39\x71\x2e\x39\x2c\x30\x2c\ -\x31\x2e\x38\x2c\x30\x63\x2e\x32\x39\x2c\x30\x2c\x2e\x33\x35\x2e\ -\x30\x35\x2e\x32\x35\x2e\x33\x61\x32\x37\x2e\x33\x35\x2c\x32\x37\ -\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x33\x37\x2c\x35\ -\x43\x35\x2e\x36\x38\x2c\x33\x34\x2c\x35\x2e\x37\x37\x2c\x34\x31\ -\x2e\x38\x39\x2c\x38\x2e\x32\x32\x2c\x34\x38\x2e\x35\x32\x63\x2e\ -\x31\x2e\x32\x34\x2e\x30\x39\x2e\x33\x33\x2d\x2e\x32\x35\x2e\x33\ -\x32\x2d\x2e\x36\x2c\x30\x2d\x31\x2e\x32\x31\x2c\x30\x2d\x31\x2e\ -\x38\x31\x2c\x30\x61\x2e\x34\x32\x2e\x34\x32\x2c\x30\x2c\x30\x2c\ -\x31\x2d\x2e\x34\x37\x2d\x2e\x33\x43\x34\x2c\x34\x34\x2e\x36\x36\ -\x2c\x33\x2e\x35\x32\x2c\x33\x39\x2e\x36\x35\x2c\x33\x2e\x35\x32\ -\x2c\x33\x35\x2e\x37\x34\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\ -\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\ -\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\ -\x63\x6c\x73\x2d\x34\x22\x20\x64\x3d\x22\x4d\x31\x38\x2e\x33\x38\ -\x2c\x32\x31\x2e\x38\x35\x63\x2e\x32\x38\x2c\x30\x2c\x2e\x38\x31\ -\x2d\x2e\x31\x36\x2c\x31\x2c\x2e\x30\x38\x73\x2d\x2e\x31\x33\x2e\ -\x36\x31\x2d\x2e\x32\x32\x2e\x39\x32\x61\x34\x35\x2e\x37\x32\x2c\ -\x34\x35\x2e\x37\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x2e\x35\x32\ -\x2c\x31\x37\x2e\x33\x2c\x33\x39\x2e\x36\x35\x2c\x33\x39\x2e\x36\ -\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2e\x37\x32\x2c\x38\x2e\x33\ -\x63\x2e\x31\x31\x2e\x33\x33\x2e\x30\x35\x2e\x34\x31\x2d\x2e\x33\ -\x33\x2e\x33\x39\x61\x31\x33\x2c\x31\x33\x2c\x30\x2c\x30\x2c\x30\ -\x2d\x31\x2e\x35\x33\x2c\x30\x2c\x2e\x34\x38\x2e\x34\x38\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x35\x34\x2d\x2e\x33\x33\x2c\x32\x33\x2e\ -\x37\x2c\x32\x33\x2e\x37\x2c\x30\x2c\x30\x2c\x31\x2d\x31\x2e\x35\ -\x31\x2d\x35\x2e\x33\x63\x2d\x31\x2e\x30\x39\x2d\x36\x2e\x35\x38\ -\x2d\x31\x2d\x31\x34\x2e\x37\x34\x2c\x31\x2e\x34\x39\x2d\x32\x31\ -\x61\x2e\x35\x2e\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x2e\x36\x2d\x2e\ -\x33\x37\x41\x37\x2e\x31\x33\x2c\x37\x2e\x31\x33\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x31\x38\x2e\x33\x38\x2c\x32\x31\x2e\x38\x35\x5a\x22\ -\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\ -\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\ -\x3d\x22\x4d\x37\x2e\x38\x37\x2c\x33\x36\x2e\x35\x32\x61\x31\x31\ -\x2e\x32\x38\x2c\x31\x31\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x2e\x30\x36\x2d\x32\x2e\x33\x33\x63\x2e\x32\x37\x2d\x34\x2e\x30\ -\x38\x2e\x35\x37\x2d\x38\x2e\x32\x39\x2c\x32\x2e\x32\x35\x2d\x31\ -\x32\x2e\x31\x31\x61\x2e\x33\x37\x2e\x33\x37\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x2e\x33\x38\x2d\x2e\x32\x34\x63\x31\x2e\x33\x39\x2d\x2e\ -\x30\x37\x2c\x31\x2e\x33\x38\x2d\x2e\x30\x38\x2c\x31\x2c\x31\x43\ -\x39\x2c\x33\x30\x2e\x34\x32\x2c\x38\x2e\x39\x33\x2c\x34\x31\x2c\ -\x31\x31\x2e\x36\x38\x2c\x34\x38\x2e\x34\x38\x63\x2e\x31\x32\x2e\ -\x33\x2e\x30\x35\x2e\x33\x36\x2d\x2e\x33\x2e\x33\x36\x2d\x31\x2e\ -\x32\x35\x2c\x30\x2d\x31\x2e\x32\x36\x2c\x30\x2d\x31\x2e\x36\x34\ -\x2d\x31\x41\x33\x36\x2e\x30\x38\x2c\x33\x36\x2e\x30\x38\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x37\x2e\x38\x37\x2c\x33\x36\x2e\x35\x32\x5a\ -\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\ -\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\ -\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\ -\x64\x3d\x22\x4d\x31\x31\x2e\x33\x35\x2c\x33\x35\x2e\x36\x34\x63\ -\x30\x2d\x34\x2e\x38\x36\x2e\x33\x38\x2d\x39\x2e\x31\x38\x2c\x32\ -\x2e\x31\x37\x2d\x31\x33\x2e\x35\x61\x2e\x34\x2e\x34\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x2e\x34\x37\x2d\x2e\x33\x63\x2e\x34\x2c\x30\x2c\ -\x31\x2d\x2e\x31\x35\x2c\x31\x2e\x31\x38\x2e\x30\x37\x73\x2d\x2e\ -\x31\x37\x2e\x36\x34\x2d\x2e\x32\x38\x2c\x31\x63\x2d\x32\x2e\x34\ -\x31\x2c\x37\x2e\x36\x31\x2d\x32\x2e\x35\x31\x2c\x31\x38\x2c\x2e\ -\x32\x37\x2c\x32\x35\x2e\x35\x37\x2e\x31\x34\x2e\x33\x36\x2c\x30\ -\x2c\x2e\x33\x39\x2d\x2e\x33\x35\x2e\x33\x39\x2d\x31\x2e\x31\x37\ -\x2c\x30\x2d\x31\x2e\x31\x39\x2c\x30\x2d\x31\x2e\x35\x37\x2d\x31\ -\x41\x33\x35\x2e\x36\x38\x2c\x33\x35\x2e\x36\x38\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x31\x31\x2e\x33\x35\x2c\x33\x35\x2e\x36\x34\x5a\x22\ -\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\x61\x6e\ -\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x34\x22\x20\x64\ -\x3d\x22\x4d\x31\x39\x2e\x31\x32\x2c\x32\x30\x2e\x37\x34\x63\x2e\ -\x39\x32\x2c\x30\x2c\x2e\x39\x32\x2c\x30\x2c\x31\x2e\x31\x33\x2d\ -\x2e\x37\x38\x2e\x38\x36\x2d\x32\x2e\x38\x36\x2d\x2e\x32\x35\x2d\ -\x35\x2e\x34\x35\x2d\x33\x2e\x35\x2d\x35\x2e\x38\x36\x2d\x31\x2e\ -\x35\x31\x2d\x2e\x32\x33\x2d\x31\x2e\x35\x2d\x2e\x32\x31\x2d\x31\ -\x2e\x34\x38\x2c\x31\x2e\x30\x39\x2c\x30\x2c\x2e\x32\x35\x2e\x30\ -\x39\x2e\x33\x33\x2e\x33\x37\x2e\x33\x34\x61\x35\x2c\x35\x2c\x30\ -\x2c\x30\x2c\x31\x2c\x31\x2e\x34\x39\x2e\x32\x32\x2c\x32\x2e\x31\ -\x31\x2c\x32\x2e\x31\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x35\ -\x38\x2c\x31\x2e\x37\x38\x2c\x35\x2e\x37\x38\x2c\x35\x2e\x37\x38\ -\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x33\x38\x2c\x32\x2e\x37\x38\x63\ -\x2d\x2e\x31\x35\x2e\x34\x33\x2d\x2e\x31\x35\x2e\x34\x33\x2e\x34\ -\x31\x2e\x34\x33\x5a\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\ -\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x30\x29\x22\x2f\ -\x3e\x3c\x65\x6c\x6c\x69\x70\x73\x65\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x34\x22\x20\x63\x78\x3d\x22\x32\x34\x2e\x34\ -\x32\x22\x20\x63\x79\x3d\x22\x33\x35\x2e\x35\x32\x22\x20\x72\x78\ -\x3d\x22\x31\x2e\x30\x31\x22\x20\x72\x79\x3d\x22\x35\x2e\x39\x39\ -\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\x67\ -\x3e\ -\x00\x00\x05\xf3\ -\x3c\ -\x73\x76\x67\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\x3a\ -\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\x30\ -\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\x22\ -\x30\x20\x30\x20\x31\x31\x2e\x32\x34\x20\x32\x36\x2e\x38\x22\x3e\ -\x3c\x64\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\ -\x73\x2d\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x66\x66\x66\x3b\x7d\x3c\ -\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x67\ -\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x32\x22\x20\x64\x61\ -\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\x72\x20\x32\ -\x22\x3e\x3c\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\ -\x2d\x32\x22\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\ -\x61\x79\x65\x72\x20\x31\x22\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ -\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\ -\x38\x2e\x36\x34\x2c\x31\x39\x76\x31\x2e\x31\x32\x63\x30\x2c\x2e\ -\x34\x38\x2d\x2e\x31\x33\x2e\x36\x2d\x2e\x36\x31\x2e\x36\x31\x48\ -\x37\x2e\x37\x38\x56\x32\x36\x2e\x32\x63\x30\x2c\x2e\x34\x36\x2d\ -\x2e\x31\x34\x2e\x36\x2d\x2e\x35\x39\x2e\x36\x48\x34\x63\x2d\x2e\ -\x34\x33\x2c\x30\x2d\x2e\x35\x37\x2d\x2e\x31\x35\x2d\x2e\x35\x37\ -\x2d\x2e\x35\x37\x56\x32\x30\x2e\x37\x36\x48\x33\x2e\x32\x31\x63\ -\x2d\x2e\x35\x2c\x30\x2d\x2e\x36\x32\x2d\x2e\x31\x33\x2d\x2e\x36\ -\x32\x2d\x2e\x36\x33\x56\x31\x39\x48\x32\x2e\x32\x32\x61\x2e\x34\ -\x34\x2e\x34\x34\x2c\x30\x2c\x30\x2c\x31\x2d\x2e\x34\x39\x2d\x2e\ -\x35\x31\x63\x30\x2d\x31\x2c\x30\x2d\x32\x2c\x30\x2d\x33\x2e\x30\ -\x35\x61\x31\x2c\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x31\x2d\x2e\ -\x34\x4c\x2e\x31\x34\x2c\x31\x32\x2e\x34\x35\x41\x31\x2e\x31\x34\ -\x2c\x31\x2e\x31\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x30\x2c\x31\x31\ -\x2e\x39\x34\x51\x30\x2c\x36\x2e\x36\x36\x2c\x30\x2c\x31\x2e\x33\ -\x38\x41\x31\x2e\x32\x39\x2c\x31\x2e\x32\x39\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x31\x2e\x33\x39\x2c\x30\x48\x39\x2e\x38\x34\x61\x31\x2e\ -\x33\x31\x2c\x31\x2e\x33\x31\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\ -\x34\x2c\x31\x2e\x34\x71\x30\x2c\x35\x2e\x32\x38\x2c\x30\x2c\x31\ -\x30\x2e\x35\x34\x61\x31\x2e\x31\x32\x2c\x31\x2e\x31\x32\x2c\x30\ -\x2c\x30\x2c\x31\x2d\x2e\x31\x35\x2e\x35\x33\x63\x2d\x2e\x34\x37\ -\x2e\x38\x36\x2d\x31\x2c\x31\x2e\x37\x2d\x31\x2e\x34\x35\x2c\x32\ -\x2e\x35\x36\x61\x31\x2c\x31\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x31\ -\x32\x2e\x34\x35\x63\x30\x2c\x31\x2c\x30\x2c\x31\x2e\x39\x33\x2c\ -\x30\x2c\x32\x2e\x38\x39\x2c\x30\x2c\x2e\x35\x33\x2d\x2e\x31\x32\ -\x2e\x36\x34\x2d\x2e\x36\x33\x2e\x36\x35\x5a\x4d\x2e\x38\x36\x2c\ -\x37\x2e\x37\x39\x63\x30\x2c\x31\x2e\x33\x38\x2c\x30\x2c\x32\x2e\ -\x37\x33\x2c\x30\x2c\x34\x2e\x30\x38\x61\x2e\x36\x31\x2e\x36\x31\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x30\x39\x2e\x32\x38\x63\x2e\x34\ -\x35\x2e\x37\x39\x2e\x39\x2c\x31\x2e\x35\x37\x2c\x31\x2e\x33\x34\ -\x2c\x32\x2e\x33\x36\x61\x2e\x33\x32\x2e\x33\x32\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x2e\x33\x32\x2e\x31\x39\x68\x36\x61\x2e\x33\x32\x2e\ -\x33\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x33\x32\x2d\x2e\x31\x38\ -\x63\x2e\x34\x34\x2d\x2e\x37\x39\x2e\x39\x2d\x31\x2e\x35\x38\x2c\ -\x31\x2e\x33\x34\x2d\x32\x2e\x33\x37\x61\x2e\x38\x33\x2e\x38\x33\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x31\x2d\x2e\x33\x38\x76\x2d\x34\ -\x68\x2d\x33\x76\x33\x41\x31\x2e\x33\x2c\x31\x2e\x33\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x36\x2c\x31\x32\x2e\x31\x48\x35\x2e\x32\x36\x61\ -\x31\x2e\x32\x39\x2c\x31\x2e\x32\x39\x2c\x30\x2c\x30\x2c\x31\x2d\ -\x31\x2e\x33\x37\x2d\x31\x2e\x33\x37\x56\x37\x2e\x37\x39\x5a\x4d\ -\x31\x2e\x37\x33\x2e\x38\x39\x43\x31\x2e\x31\x31\x2e\x38\x31\x2e\ -\x38\x35\x2e\x38\x38\x2e\x38\x36\x2c\x31\x2e\x36\x32\x63\x30\x2c\ -\x31\x2e\x36\x37\x2c\x30\x2c\x33\x2e\x33\x33\x2c\x30\x2c\x35\x76\ -\x2e\x33\x68\x33\x41\x2e\x33\x33\x2e\x33\x33\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x34\x2c\x36\x2e\x37\x34\x61\x31\x2e\x33\x32\x2c\x31\x2e\ -\x33\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x31\x35\x2d\x2e\x36\ -\x39\x68\x2e\x38\x36\x61\x31\x2e\x32\x34\x2c\x31\x2e\x32\x34\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x31\x2e\x31\x32\x2e\x36\x38\x2e\x33\x32\ -\x2e\x33\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x33\x34\x2e\x31\x39\ -\x68\x32\x2e\x38\x36\x56\x31\x2e\x32\x39\x41\x2e\x34\x2e\x34\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x31\x30\x2c\x2e\x38\x38\x61\x32\x2e\x37\ -\x37\x2c\x32\x2e\x37\x37\x2c\x30\x2c\x30\x2c\x30\x2d\x2e\x35\x2c\ -\x30\x76\x32\x2e\x39\x41\x31\x2e\x33\x31\x2c\x31\x2e\x33\x31\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x38\x2e\x31\x2c\x35\x2e\x31\x39\x48\x33\ -\x2e\x31\x36\x41\x31\x2e\x33\x31\x2c\x31\x2e\x33\x31\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x31\x2e\x37\x33\x2c\x33\x2e\x37\x36\x5a\x6d\x2e\ -\x38\x36\x2c\x30\x56\x33\x2e\x37\x38\x63\x30\x2c\x2e\x33\x39\x2e\ -\x31\x36\x2e\x35\x34\x2e\x35\x34\x2e\x35\x34\x68\x35\x63\x2e\x34\ -\x2c\x30\x2c\x2e\x35\x34\x2d\x2e\x31\x35\x2e\x35\x34\x2d\x2e\x35\ -\x36\x56\x31\x2e\x30\x39\x63\x30\x2d\x2e\x30\x37\x2c\x30\x2d\x2e\ -\x31\x34\x2c\x30\x2d\x2e\x32\x32\x5a\x4d\x34\x2e\x33\x34\x2c\x32\ -\x30\x2e\x37\x36\x76\x35\x2e\x31\x35\x48\x36\x2e\x39\x56\x32\x30\ -\x2e\x37\x36\x5a\x4d\x34\x2e\x37\x35\x2c\x39\x2e\x30\x35\x63\x30\ -\x2c\x2e\x35\x37\x2c\x30\x2c\x31\x2e\x31\x33\x2c\x30\x2c\x31\x2e\ -\x37\x61\x2e\x34\x34\x2e\x34\x34\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\ -\x34\x38\x2e\x34\x39\x48\x36\x61\x2e\x34\x33\x2e\x34\x33\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x2e\x34\x36\x2d\x2e\x34\x36\x56\x37\x2e\x33\ -\x37\x41\x2e\x34\x32\x2e\x34\x32\x2c\x30\x2c\x30\x2c\x30\x2c\x36\ -\x2c\x36\x2e\x39\x32\x48\x35\x2e\x32\x37\x63\x2d\x2e\x33\x36\x2c\ -\x30\x2d\x2e\x35\x31\x2e\x31\x36\x2d\x2e\x35\x32\x2e\x35\x31\x5a\ -\x6d\x33\x2e\x38\x38\x2c\x36\x2e\x35\x33\x68\x2d\x36\x76\x32\x2e\ -\x35\x35\x68\x36\x5a\x4d\x33\x2e\x34\x37\x2c\x31\x39\x76\x2e\x38\ -\x33\x48\x37\x2e\x37\x36\x56\x31\x39\x5a\x22\x2f\x3e\x3c\x70\x61\ -\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\ -\x20\x64\x3d\x22\x4d\x34\x2e\x33\x31\x2c\x31\x2e\x37\x35\x56\x33\ -\x2e\x34\x34\x48\x33\x2e\x34\x37\x56\x31\x2e\x37\x35\x5a\x22\x2f\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x35\x2e\x32\x31\x2c\x31\x2e\ -\x37\x34\x48\x36\x76\x31\x2e\x37\x48\x35\x2e\x32\x31\x5a\x22\x2f\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x37\x2e\x37\x36\x2c\x33\x2e\ -\x34\x35\x48\x36\x2e\x39\x33\x56\x31\x2e\x37\x34\x68\x2e\x38\x33\ -\x5a\x22\x2f\x3e\x3c\x2f\x67\x3e\x3c\x2f\x67\x3e\x3c\x2f\x73\x76\ -\x67\x3e\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 176.02 58.41\ +\x22>nyl<\ +path class=\x22cls-\ +3\x22 d=\x22M7.87,36.5\ +2a11.26,11.26,0,\ +0,1-.06-2.33c.27\ +-4.08.57-8.29,2.\ +25-12.11a.38.38,\ +0,0,1,.38-.24c1.\ +39-.07,1.38-.08,\ +1,1.05C9,30.43,8\ +.93,41,11.68,48.\ +48c.12.31.05.36-\ +.3.36-1.25,0-1.2\ +6,0-1.64-1A36.19\ +,36.19,0,0,1,7.8\ +7,36.52Z\x22 transf\ +orm=\x22translate(0\ +)\x22/>\ +\x00\x00\x07;\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 40.5 33.52\x22>\ +\ +\x00\x00\x01!\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 38.22 34.57\x22\ +><\ +g id=\x22Layer_2\x22 d\ +ata-name=\x22Layer \ +2\x22>\ \ +\x00\x00\x09\xfe\ +<\ +svg xmlns=\x22http:\ +//www.w3.org/200\ +0/svg\x22 viewBox=\x22\ +0 0 82.23 36.17\x22\ +> + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/arrow_right.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/arrow_right.svg new file mode 100644 index 00000000..8abacdf2 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/arrow_right.svg @@ -0,0 +1,14 @@ + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/blower.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/blower.svg index 6de49748..027ea01c 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/blower.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/blower.svg @@ -1 +1,15 @@ - \ No newline at end of file + + + + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/error.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/error.svg index 0bb1f19f..27d84d05 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/error.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/error.svg @@ -7,7 +7,6 @@ } - - - + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/fan.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/fan.svg index 9c0023ab..46bccde3 100644 --- a/BlocksScreen/lib/ui/resources/media/btn_icons/fan.svg +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/fan.svg @@ -1 +1,11 @@ - \ No newline at end of file + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/notification.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/notification.svg new file mode 100644 index 00000000..ac306fb6 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/notification.svg @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/ui/resources/media/btn_icons/notification_active.svg b/BlocksScreen/lib/ui/resources/media/btn_icons/notification_active.svg new file mode 100644 index 00000000..4c3915e2 --- /dev/null +++ b/BlocksScreen/lib/ui/resources/media/btn_icons/notification_active.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/BlocksScreen/lib/utils/list_model.py b/BlocksScreen/lib/utils/list_model.py index 4946f737..287d0b1c 100644 --- a/BlocksScreen/lib/utils/list_model.py +++ b/BlocksScreen/lib/utils/list_model.py @@ -1,5 +1,5 @@ import typing -from dataclasses import dataclass +from dataclasses import dataclass, field from PyQt6 import QtCore, QtGui, QtWidgets # pylint: disable=import-error @@ -10,23 +10,42 @@ class ListItem: text: str right_text: str = "" + _rfontsize: int = 0 + _lfontsize: int = 0 + + callback: typing.Optional[typing.Callable] = None + + color: str = "#dfdfdf" + color_left_icon: bool = False right_icon: typing.Optional[QtGui.QPixmap] = None left_icon: typing.Optional[QtGui.QPixmap] = None - callback: typing.Optional[typing.Callable] = None + selected: bool = False allow_check: bool = True - _lfontsize: int = 0 - _rfontsize: int = 0 - height: int = 60 # Change has needed - notificate: bool = False # render red dot + not_clickable: bool = False + allow_expand: bool = False + needs_expansion: bool = False + is_expanded: bool = False + + height: int = 60 + notificate: bool = False + + # stores width and heitgh of the button so we dont need to recalculate it every time + _cache: typing.Dict[int, int] = field(default_factory=dict) + + def clear_cache(self): + """Call this if text or font size changes dynamically""" + self._cache.clear() + class EntryListModel(QtCore.QAbstractListModel): """List model Subclassed QAbstractListModel""" EnableRole = QtCore.Qt.ItemDataRole.UserRole + 1 NotificateRole = QtCore.Qt.ItemDataRole.UserRole + 2 + ExpandRole = QtCore.Qt.ItemDataRole.UserRole + 3 def __init__(self, entries=None) -> None: """Initialise the model with an optional pre-populated list of ``ListItem``s.""" @@ -41,6 +60,37 @@ def deleteLater(self) -> None: """subclass for deleting the object""" return super().deleteLater() + def remove_item(self, item: ListItem) -> None: + """Removes one row item from the model""" + if item in self.entries: + index = self.entries.index(item) + self.beginRemoveRows(QtCore.QModelIndex(), index, index) + self.entries.pop(index) + self.endRemoveRows() + + def delete_duplicates(self) -> None: + """ + Removes items that have identical text, color, and + last time entry (get(-1)). + """ + seen_identifiers: set[tuple[str, str, str]] = set() + unique_entries: list[ListItem] = [] + + for item in self.entries: + text_val = item.text + color_val = item.color + time_val = item._cache.get(-1) + + identifier = (text_val, color_val, time_val) + + if identifier not in seen_identifiers: + unique_entries.append(item) + seen_identifiers.add(identifier) + + self.beginResetModel() + self.entries = unique_entries + self.endResetModel() + def clear(self) -> None: """Clear model rows""" self.beginResetModel() @@ -211,6 +261,11 @@ def setData(self, index: QtCore.QModelIndex, value: typing.Any, role: int) -> bo item.notificate = value self.dataChanged.emit(index, index, [EntryListModel.NotificateRole]) return True + if role == EntryListModel.ExpandRole: + item = self.entries[index.row()] + item.is_expanded = value + self.layoutChanged.emit() + self.dataChanged.emit(index, index, [EntryListModel.ExpandRole]) if role == QtCore.Qt.ItemDataRole.UserRole: self.dataChanged.emit(index, index, [QtCore.Qt.ItemDataRole.UserRole]) return True @@ -225,6 +280,8 @@ def data(self, index: QtCore.QModelIndex, role: int) -> typing.Any: return item.selected if role == EntryListModel.NotificateRole: return item.notificate + if role == EntryListModel.ExpandRole: + return item.is_expanded if role == QtCore.Qt.ItemDataRole.UserRole: return item return None @@ -281,19 +338,61 @@ def clear(self) -> None: def sizeHint( self, option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex ): - """Returns base size for items, re-implemented method""" - base = super().sizeHint(option, index) - base.setHeight(self.height) - return QtCore.QSize(base.width(), int(self.height + self.height * 0.20)) + """ + Calculates size AND determines if expansion is needed. + """ + item: ListItem = index.data(QtCore.Qt.ItemDataRole.UserRole) + target_width = option.rect.width() - def updateEditorGeometry( - self, - editor: QtWidgets.QWidget | None, - option: QtWidgets.QStyleOptionViewItem, - index: QtCore.QModelIndex, - ) -> None: - """re-implemented method""" - return super().updateEditorGeometry(editor, option, index) + base_h = item.height + ellipse_size = base_h * 0.8 + + right_reserved = base_h + + left_reserved = 10 + if item.left_icon: + left_reserved = (base_h * 0.1) + ellipse_size + 8 + + if item._lfontsize > 0 and item._lfontsize != option.font.pointSize(): + f = QtGui.QFont(option.font) + f.setPointSize(item._lfontsize) + fm = QtGui.QFontMetrics(f) + else: + fm = option.fontMetrics + + if item.right_text: + if item._rfontsize > 0 and item._rfontsize != option.font.pointSize(): + fr = QtGui.QFont(option.font) + fr.setPointSize(item._rfontsize) + fmr = QtGui.QFontMetrics(fr) + else: + fmr = option.fontMetrics + right_reserved += fmr.horizontalAdvance(item.right_text) + 10 + + if item.right_icon: + right_reserved += ellipse_size + + text_avail_width = target_width - left_reserved - right_reserved + if text_avail_width < 50: + text_avail_width = 50 + + single_line_width = fm.horizontalAdvance(item.text) + + item.needs_expansion = single_line_width > text_avail_width + + if not item.is_expanded: + return QtCore.QSize(target_width, int(item.height * 1.1)) + + text_rect = fm.boundingRect( + QtCore.QRect(0, 0, int(text_avail_width), 0), + QtCore.Qt.AlignmentFlag.AlignLeft | QtCore.Qt.TextFlag.TextWordWrap, + item.text, + ) + + final_height = max(item.height, text_rect.height() - 1) + # Cache it + item._cache[target_width] = final_height + 20 + return QtCore.QSize(target_width, int(final_height * 1.2)) def paint( self, @@ -301,174 +400,178 @@ def paint( option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, ): - """Renders each item, re-implemented method""" - super().paint(painter, option, index) - item = index.data(QtCore.Qt.ItemDataRole.UserRole) + """Renders each item""" painter.save() - rect = option.rect - rect.setHeight(item.height) - button = QtWidgets.QStyleOptionButton() - button.rect = rect painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) painter.setRenderHint(QtGui.QPainter.RenderHint.SmoothPixmapTransform, True) - radius = rect.height() / 5.0 - # Main rounded rectangle path (using the adjusted rect) + item = index.data(QtCore.Qt.ItemDataRole.UserRole) + rect = option.rect.adjusted(2, 2, -2, -2) + path = QtGui.QPainterPath() - path.addRoundedRect(QtCore.QRectF(rect), radius, radius) + path.addRoundedRect(QtCore.QRectF(rect), 12, 12) if item.not_clickable: painter.restore() return + if item.allow_expand and item.needs_expansion: + item.right_icon = ( + QtGui.QPixmap(":/arrow_icons/media/btn_icons/arrow_down.svg") + if item.is_expanded + else QtGui.QPixmap(":/arrow_icons/media/btn_icons/arrow_right.svg") + ) + + # Background Color pressed_color = QtGui.QColor("#1A8FBF") pressed_color.setAlpha(90 if item.selected else 20) + painter.setPen(QtCore.Qt.PenStyle.NoPen) painter.setBrush(pressed_color) painter.fillPath(path, pressed_color) - # Ellipse ("hole") for the icon on the right - ellipse_margin = rect.height() * 0.05 - ellipse_size = rect.height() * 0.90 + # Geometry Calc + + # ICON SPACEEE + ellipse_size = item.height * 0.8 + ellipse_margin = (item.height - ellipse_size) / 2 ellipse_rect = QtCore.QRectF( rect.right() - ellipse_margin - ellipse_size, rect.top() + ellipse_margin, ellipse_size, ellipse_size, ) - icon_margin = ellipse_size * 0.10 - # Draw icon inside the ellipse "hole" (on the right) + if item.right_icon: - icon_rect = QtCore.QRectF( - ellipse_rect.left() + icon_margin / 2, - ellipse_rect.top() + icon_margin / 2, - ellipse_rect.width() - icon_margin, - ellipse_rect.height() - icon_margin, - ) - icon_scaled = self._get_scaled(item.right_icon, icon_rect.size().toSize()) - # Center the icon in the ellipse - adjusted_x = ( - icon_rect.x() + (icon_rect.width() - icon_scaled.width()) // 2.0 - ) - adjusted_y = rect.y() + (rect.height() - icon_scaled.height()) // 2.0 - adjusted_icon_rect = QtCore.QRectF( - adjusted_x, - adjusted_y, - icon_scaled.width(), - icon_scaled.height(), + icon_scaled = item.right_icon.scaled( + ellipse_rect.size().toSize(), + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, ) painter.drawPixmap( - adjusted_icon_rect, icon_scaled, icon_scaled.rect().toRectF() + ellipse_rect.toRect(), + icon_scaled, ) - # Ellipse ("hole") for the icon on the left (only if present) - left_icon_margin = rect.height() * 0.05 - left_icon_size = rect.height() * 0.70 + left_margin = 10 left_icon_rect = QtCore.QRectF( - rect.left() + left_icon_margin, - rect.top() + left_icon_margin, - left_icon_size, - left_icon_size, + rect.left() + ellipse_margin, + rect.top() + ellipse_margin, + ellipse_size, + ellipse_size, ) - left_margin = 10 # default left margin - # Draw second icon (on the left, if present) + if item.left_icon: - left_icon_scaled = self._get_scaled( - item.left_icon, left_icon_rect.size().toSize() - ) - # Center the icon in the rect - adjusted_x = ( - left_icon_rect.x() - + (left_icon_rect.width() - left_icon_scaled.width()) // 2.0 - ) - adjusted_y = rect.y() + (rect.height() - left_icon_scaled.height()) // 2.0 - adjusted_left_icon_rect = QtCore.QRectF( - adjusted_x, - adjusted_y, - left_icon_scaled.width(), - left_icon_scaled.height(), - ) - painter.drawPixmap( - adjusted_left_icon_rect, - left_icon_scaled, - left_icon_scaled.rect().toRectF(), + l_icon_scaled = item.left_icon.scaled( + int(left_icon_rect.width()), + int(left_icon_rect.height()), + QtCore.Qt.AspectRatioMode.KeepAspectRatio, + QtCore.Qt.TransformationMode.SmoothTransformation, ) - left_margin = left_icon_margin + left_icon_size + 8 # 8px gap after icon - # Draw text, area before the ellipse (adjusted for left icon) + if item.color_left_icon: + tinted = QtGui.QPixmap(l_icon_scaled.size()) + tinted.fill(QtCore.Qt.GlobalColor.transparent) + p2 = QtGui.QPainter(tinted) + p2.drawPixmap(0, 0, l_icon_scaled) + p2.setCompositionMode( + QtGui.QPainter.CompositionMode.CompositionMode_SourceIn + ) + p2.fillRect(tinted.rect(), QtGui.QColor(item.color)) + p2.end() + painter.drawPixmap( + left_icon_rect.toRect(), + tinted, + ) + else: + painter.drawPixmap( + left_icon_rect.toRect(), + l_icon_scaled, + ) + text_margin = int( rect.right() - ellipse_size - ellipse_margin - rect.height() * 0.10 ) + text_rect = QtCore.QRectF( - rect.left() + left_margin, + rect.left() + + left_margin + + (left_icon_rect.width() if item.left_icon else 0), rect.top(), - text_margin - rect.left() - left_margin, + text_margin + - rect.left() + - left_margin + - (left_icon_rect.width() if item.left_icon else 0), rect.height(), ) - # Draw main text (left-aligned) painter.setPen(QtGui.QColor(255, 255, 255)) + _font = painter.font() - _font.setPointSize(item._lfontsize) + if item._lfontsize > 0: + _font.setPointSize(item._lfontsize) painter.setFont(_font) - metrics = QtGui.QFontMetrics(_font) - main_text_height = metrics.height() - # Vertically center text - text_y = rect.top() + (rect.height() + main_text_height) / 2 - metrics.descent() + metrics = QtGui.QFontMetrics(_font) - # Calculate where to start the right text: just left of the right icon ellipse - gap = 10 # gap between right text and icon ellipse right_font = QtGui.QFont(_font) - right_font.setPointSize(item._rfontsize) - right_metrics = QtGui.QFontMetrics(right_font) - right_text_width = right_metrics.horizontalAdvance(item.right_text) + if item._rfontsize > 0: + right_font.setPointSize(item._rfontsize) - # The right text should end at ellipse_rect.left() - gap - right_text_x = ellipse_rect.left() - gap - right_text_width + right_metrics = QtGui.QFontMetrics(right_font) - # Draw main text (left-aligned, but don't overlap right text) - max_main_text_width = ( - right_text_x - text_rect.left() - 10 - ) # 10px gap between main and right text - elided_main_text = metrics.elidedText( - item.text, - QtCore.Qt.TextElideMode.ElideRight, - int(max_main_text_width), + right_text_x = ( + ellipse_rect.right() + - right_metrics.horizontalAdvance(item.right_text) + - left_icon_rect.width() + - left_margin ) - painter.setFont(_font) - painter.drawText( - int(text_rect.left()), - int(text_y), - elided_main_text, - ) + text = item.text.replace("\n", "") + # Logic: If not expanded, OR if expansion is not needed, draw single line + if not item.is_expanded: + max_main_text_width = right_text_x - left_margin + text = metrics.elidedText( + text, + QtCore.Qt.TextElideMode.ElideRight, + int(max_main_text_width), + ) + painter.drawText( + text_rect, + QtCore.Qt.AlignmentFlag.AlignVCenter, + text, + ) + else: + # Expanded mode + painter.drawText( + text_rect, + QtCore.Qt.AlignmentFlag.AlignLeft + | QtCore.Qt.AlignmentFlag.AlignVCenter + | QtCore.Qt.TextFlag.TextWordWrap, + text, + ) - # Draw right text (smaller, grey, just left of the icon) if item.right_text: painter.setFont(right_font) - painter.setPen(QtGui.QColor(160, 160, 160)) # grey color - right_text_height = right_metrics.height() - right_text_y = ( - rect.top() - + (rect.height() + right_text_height) / 2 - - right_metrics.descent() - ) + painter.setPen(QtGui.QColor(160, 160, 160)) painter.drawText( int(right_text_x), - int(right_text_y), + int( + ellipse_rect.top() + + (ellipse_rect.height() + right_metrics.ascent()) / 2 + ), item.right_text, ) + if item.notificate: dot_diameter = rect.height() * 0.3 dot_x = rect.width() - dot_diameter - 5 - notification_color = QtGui.QColor(226, 31, 31) painter.setBrush(notification_color) painter.setPen(QtCore.Qt.PenStyle.NoPen) - dot_rect = QtCore.QRectF(dot_x, rect.top(), dot_diameter, dot_diameter) painter.drawEllipse(dot_rect) + painter.restore() def editorEvent( # pylint: disable=invalid-name @@ -478,20 +581,45 @@ def editorEvent( # pylint: disable=invalid-name option: QtWidgets.QStyleOptionViewItem, index: QtCore.QModelIndex, ): - """Capture view model events, re-implemented method""" + """Capture view model events""" item = index.data(QtCore.Qt.ItemDataRole.UserRole) if event.type() == QtCore.QEvent.Type.MouseButtonPress: if item and item.not_clickable: return True - if item.callback: - if callable(item.callback): - item.callback() + + if item.callback and callable(item.callback): + item.callback() + if self.prev_index is None: return False + + ellipse_size = item.height * 0.8 + ellipse_margin = (item.height - ellipse_size) / 2 + ellipse_rect = QtCore.QRectF( + option.rect.right() - ellipse_margin - ellipse_size, + option.rect.top() + ellipse_margin, + ellipse_size, + ellipse_size, + ) + pos = event.position() + + # --- Logic Check --- + # Only allow toggle if allow_expand AND text actually needs expansion + if ( + ellipse_rect.contains(pos) + and item.allow_expand + and item.needs_expansion + ): + new_state = not item.is_expanded + model.setData(index, new_state, EntryListModel.ExpandRole) + return True + if self.prev_index != index.row(): prev_index: QtCore.QModelIndex = model.index(self.prev_index) - model.setData(prev_index, False, EntryListModel.EnableRole) + if prev_index.isValid(): + model.setData(prev_index, False, EntryListModel.EnableRole) self.prev_index = index.row() + model.setData(index, True, EntryListModel.EnableRole) self.item_selected.emit(item) return True From f9db18bce421a8c63f6d2619fcb0ca9e772615e2 Mon Sep 17 00:00:00 2001 From: HugoCLSC Date: Tue, 3 Mar 2026 15:54:12 +0000 Subject: [PATCH 56/70] Del extras directory not needed --- extras/k_extras/bed_custom_bound.py | 171 ---- .../k_extras/bo_idex_xy_offset_calibration.py | 379 -------- extras/k_extras/bucket.py | 93 -- extras/k_extras/cutter_sensor.py | 403 --------- extras/k_extras/load_filament.py | 372 -------- extras/k_extras/toolhead_fan_duct_sensor.py | 29 - extras/k_extras/unload_filament.py | 210 ----- extras/k_macros/XYOffsetCalibration.cfg | 810 ------------------ extras/k_macros/macros_essentials.cfg | 340 -------- extras/k_macros/speed_test_macro.cfg | 126 --- 10 files changed, 2933 deletions(-) delete mode 100644 extras/k_extras/bed_custom_bound.py delete mode 100644 extras/k_extras/bo_idex_xy_offset_calibration.py delete mode 100644 extras/k_extras/bucket.py delete mode 100644 extras/k_extras/cutter_sensor.py delete mode 100644 extras/k_extras/load_filament.py delete mode 100644 extras/k_extras/toolhead_fan_duct_sensor.py delete mode 100644 extras/k_extras/unload_filament.py delete mode 100644 extras/k_macros/XYOffsetCalibration.cfg delete mode 100644 extras/k_macros/macros_essentials.cfg delete mode 100644 extras/k_macros/speed_test_macro.cfg diff --git a/extras/k_extras/bed_custom_bound.py b/extras/k_extras/bed_custom_bound.py deleted file mode 100644 index fe823935..00000000 --- a/extras/k_extras/bed_custom_bound.py +++ /dev/null @@ -1,171 +0,0 @@ -import logging -import typing -from collections import OrderedDict - - -class BedCustomBound: - def __init__(self, config): - self.printer = config.get_printer() - self.reactor = self.printer.get_reactor() - self.gcode = self.printer.lookup_object("gcode") - - # * Register event handlers - self.printer.register_event_handler("klippy:ready", self.handle_ready) - - # * Get module configs - self.custom_boundary_x = None - if config.getfloatlist("custom_boundary_x", None, count=2) is not None: - self.custom_boundary_x = config.getfloatlist("custom_boundary_x", count=2) - self.custom_boundary_y = None - if config.getfloatlist("custom_boundary_y", None, count=2) is not None: - self.custom_boundary_y = config.getfloatlist("custom_boundary_y", count=2) - self.park = None - if config.getfloatlist("park_xy", None, count=2) is not None: - self.park = config.getfloatlist("park_xy", count=2) - - self.travel_speed = config.getfloat( - "travel_speed", 100.0, above=1.0, minval=1.0, maxval=300.0 - ) - - # * Variables - self.min_event_systime = self.reactor.NEVER - self.default_limits_x = self.default_limits_y = None - # * Register new gcode commands - self.gcode.register_command( - "SET_CUSTOM_BOUNDARY", - self.cmd_SET_CUSTOM_BOUNDARY, - "Sets a custom boundary for the bed", - ) - self.gcode.register_command( - "RESTORE_DEFAULT_BOUNDARY", - self.cmd_RESTORE_DEFAULT_BOUNDARY, - "Restores the axis boundaries to the ones found when homing.", - ) - - self.current_boundary: str = "default" - - def handle_ready(self): - self.toolhead = self.printer.lookup_object("toolhead") - - def cmd_SET_CUSTOM_BOUNDARY(self, gcmd): - if self.toolhead is None: - return - if self.custom_boundary_x is None or self.custom_boundary_y is None: - return - - move_to_custom_pos = gcmd.get("MOVE_TO_PARK", False, parser=bool) - - self.set_custom_boundary() - if move_to_custom_pos and self.park is not None: - self.toolhead.manual_move( - [self.park[0], self.park[1]], - self.travel_speed, - ) - - def cmd_RESTORE_DEFAULT_BOUNDARY(self, gcmd): - self.restore_default_boundary() - - def restore_default_boundary(self): - if self.toolhead is None: - return - if self.default_limits_x is None or self.default_limits_y is None: - return - self.gcode.respond_info( - "[CUSTOM BED BOUNDARY] -> Restoring printer boundary limits." - ) - kin = self.toolhead.get_kinematics() - kin.limits[0] = ( - self.default_limits_x[0], - self.default_limits_y[1], - ) # X min, X max - kin.limits[1] = ( - self.default_limits_y[0], - self.default_limits_y[1], - ) # Y min , Y max - self.current_boundary = "default" - - def set_custom_boundary(self): - if self.toolhead is None: - return - if self.custom_boundary_x is None or self.custom_boundary_y is None: - return - self.gcode.respond_info("Setting specified custom boundary.") - kin = self.toolhead.get_kinematics() - self.default_limits_x, self.default_limits_y = kin.limits[0], kin.limits[1] - - kin.limits[0] = ( - self.custom_boundary_x[0], - self.custom_boundary_x[1], - # kin.limits[0][1], - ) # X min, X max - kin.limits[1] = ( - self.custom_boundary_y[0], - self.custom_boundary_y[1], - # kin.limits[1][1], - ) # Y min , Y max - self.current_boundary = "custom" - - def move_to_park(self): - if self.park is not None: - self.toolhead.manual_move([self.park[0], self.park[1]], self.travel_speed) - - def check_boundary_limits( - self, position: typing.Tuple[float, float], type: str = "default" - ): - if self.toolhead is None or position is None: - return - - _limits = { - "x": True, - "y": True, - } - - if ( - type == "default" - and self.default_limits_x is not None - and self.default_limits_y is not None - ): - min_limit_x, max_limit_x = ( - self.default_limits_x[0], - self.default_limits_x[1], - ) - min_limit_y, max_limit_y = ( - self.default_limits_y[0], - self.default_limits_y[1], - ) - - if type == "current": - kin = self.toolhead.get_kinematics() - min_limit_x, max_limit_x = kin.limits[0][0], kin.limits[0][1] - min_limit_y, max_limit_y = kin.limits[1][0], kin.limits[1][1] - if ( - type == "custom" - and self.custom_boundary_x is not None - and self.custom_boundary_y is not None - ): - min_limit_x, max_limit_x = ( - self.custom_boundary_x[0], - self.custom_boundary_x[1], - ) - min_limit_y, max_limit_y = ( - self.custom_boundary_y[0], - self.custom_boundary_y[1], - ) - - if min_limit_x < position[0] or max_limit_x < position[0]: - _limits.update({"x": False}) - - if min_limit_y < position[1] or max_limit_y < position[1]: - _limits.update({"y": False}) - - - - return _limits - - def get_status(self, eventtime=None): - """Get the status of the current boundary""" - return {"status": self.current_boundary} - - -def load_config(config): - return BedCustomBound(config) diff --git a/extras/k_extras/bo_idex_xy_offset_calibration.py b/extras/k_extras/bo_idex_xy_offset_calibration.py deleted file mode 100644 index 6cbcb0ff..00000000 --- a/extras/k_extras/bo_idex_xy_offset_calibration.py +++ /dev/null @@ -1,379 +0,0 @@ -from __future__ import annotations - - -class XYOffsetCalibrationTool: - """Tool that helps the calibration of the XY offsets for Idex printers.""" - - def __init__(self, config): - self.printer = config.get_printer() - self.reactor = self.printer.get_reactor() - - # self.name = config.get_name().split()[-1] - - # * Register event handlers - self.printer.register_event_handler("klippy:connect", self.handle_connect) - self.printer.register_event_handler("klippy:ready", self.handle_ready) - - self.gcode = self.printer.lookup_object("gcode") - self.enable = False - self.last_state = False - - # * Control variables - self.prime: bool = False - self.line_number: int = 0 - self.line_spacing: float = 0.0 - self.line_height: float = 0.0 - self.line_top: float = 0.0 - self.initial_x_h: float = 0.0 - self.initial_y_h: float = 0.0 - self.initial_x_h_2: float = 0.0 - self.initial_y_h_2: float = 0.0 - self.initial_x_v: float = 0.0 - self.initial_y_v: float = 0.0 - self.initial_x_v_2: float = 0.0 - self.initial_y_v_2: float = 0.0 - self.extrude_per_mm: float = 0.0 - - self.horizontal_line_spacing: float = 0.0 - self.horizontal_line_height: float = 0.0 - self.horizontal_line_top: float = 0.0 - - self.vertical_line_spacing: float = 0.0 - self.vertical_line_height: float = 0.0 - self.vertical_line_top: float = 0.0 - - self.tool_2_x_offset: float = 0.0 - self.tool_2_y_offset: float = 0.0 - - self.filament_temperature: int = 190 - # * Register gcode commands - self.gcode.register_command( - "XYCALIBRATION", - self.cmd_XYCALIBRATION, - desc=self.cmd_XYCALIBRATION_help, - ) - - # self.gcode.register_mux_command( - # "XYNOZZLEPRIME", - # "XYOffsetCalibrationTool", - # self.name, - # self.cmd_XYNOZZLEPRIME, - # self.cmd_XYNOZZLEPRIME_help, - # ) - - def handle_connect(self): - """Event handler method for when klippy connects""" - self.toolhead = self.printer.lookup_object("toolhead") - # self.configfile = self.printer.lookup_object("configfile") - # self.manual_probe = self.printer.lookup_object("manual_probe") - # self.stepper_enable = self.printer.lookup_object("stepper_enable") - # self.mcu = self.printer.lookup_object("mcu") - # printer.register_event_handler("toolhead:set_position", - # self.reset_last_position) - # printer.register_event_handler("toolhead:manual_move", - # self.reset_last_position) - # printer.register_event_handler("gcode:command_error", - # self.reset_last_position) - # printer.register_event_handler("extruder:activate_extruder", - # self._handle_activate_extruder) - # printer.register_event_handler("homing:home_rails_end", - # self._handle_home_rails_end) - # printer.register_event_handler("gcode:request_restart", self._handle_request_restart) - - # self.printer.send_event("stepper_enable:motor_off", print_time) - - # gcmd.get_int("ENABLE", 1) - - # cur_time = self.printer.get_reactor().monotonic() - # kin_status = toolhead.get_kinematics().get_status(curtime) - - # toolhead.set_position(pos, homing_axes=[2]) - # toolhead.manual_move([None, None, self.z_hop], self.z_hop_speed) - # toolhead.get_kinematics().note_z_homed() - # need_x, need_y, need_z = [gcmd.get(axis, None) is not None for axis in "XYZ"] - # g28_cmd = self.gcode.create_gcode_command("G28", "G28", new_params) - # self.prev_G28(g28_cmd) - - # toolhead.get_position() - - def handle_ready(self): - """Event handler method for when klippy is enabled""" - self.toolhead = self.printer.lookup_object("toolhead") - self.kin = self.printer.lookup_object("toolhead").get_kinematics() - - - - # self.enable = True - # self.reactor.update_timer( - # self.update_direction_timer, self.reactor.Never - # ) - # self.enable = False - - cmd_XYCALIBRATION_help = "Handles the calibration of xy axes on Idex printers." - def cmd_XYCALIBRATION(self, gcmd): - # * First home axes - self.printer.send_event( - "stepper_enable:motor_on", self.printer.get_reactor().monotonic() - ) - gcmd.respond_info("XY calibration tool for Idex printers loaded.") - gcmd.respond_info(f"{self.toolhead.get_position()}") - - gcmd.respond_info(f"{self.get_homed_axes()}") - if self.get_homed_axes() is None or self.get_homed_axes() != "xy": - gcmd.respond_info("Must home axes before running the XY offset calibration for Idex printers.") - # return # I cannot continue if there is no homing - - - - _dual_carriage_module = self.kin.dc_module - - if _dual_carriage_module is None: - gcmd.respond_info("No dual carriage config section defined. Cannot run tool") - return - - - # Set extruder 0 - _dual_carriage_module.activate_dc_mode( - index= 0, - mode= "PRIMARY" #This is the default - ) - - _cur_extruder = self.toolhead.get_extruder() - _cur_extruder_name = _cur_extruder.get_name() - - - _available_printer_objects = self.printer.objects - gcmd.respond_info(f"Available Objects : {_available_printer_objects}") - gcmd.respond_info(f"Current extruder name : {_cur_extruder_name,}") - # Check that the kin type is cartesian - - # * Maybe home axes - # _homexy = "G28" - # # * Force usage of toolhead 0 T0 - # * Get extruder names, and activate the main toolhead - # gcmd.run_script_from_command("G28") - - # # * Horizontal Print in tool 0 - _horizontal_print_tool_0 = [ - "G90", - "M83", - f"G1 X{self.initial_x_h_2} Y{self.initial_y_h - 2} E{-0.1} F{12000}", # Brim - "G1 Z0.2 E0.5 F2000", # Brim - f"G1 X{self.initial_x_h} E{(self.initial_x_h_2 - self.initial_x_h) * self.extrude_per_mm} F3000", # U turn of the brim - f"G1 Y{self.initial_y_h} E{2 * self.extrude_per_mm}", # Start of the calibration - "G91", - ] - - # # self.print_horizontal_line - # # Raise the nozze G1 Z2 E-0.1 - - # # * Vertical Print in tool 0 - # _horizontal_print_tool_0 = [ - # "G90", - # "M83 ", - # f"G1 X{self.initial_x_v - 2} Y{self.initial_y_v} E{-0.1} F{12000}", # Brim - # "G1 Z0.2 E0.5 F2000", # Brim - # f"G1 X{self.initial_y_v} E{(self.initial_y_h_2 - self.initial_x_h) * self.extrude_per_mm} F3000", # U turn of the brim - # f"G1 Y{self.initial_y_h} E{2 * self.extrude_per_mm}", # Start of the calibration - # "G91", - # ] - - # # self.print_vertical_line - - # # Park the nozzle - # # G91 - # # park_nozzle - - # # Change to tool 1 - # # prime - # # * Horizontal print in tool 1 - - # _horizontal_print_tool_1 = [ - # "G90", - # "M83", - # f"G1 X{self.initial_x_h} Y{self.initial_y_h_2 - 2 + 2} E{-0.1} F{12000}", # Brim - # "G1 Z0.2 E0.5 F2000", # Lower nozzle for brim - # f"G1 X{self.initial_x_h_2} E{(self.initial_x_h_2 - self.initial_x_h) * self.extrude_per_mm} F3000", # U turn of the brim - # f"G1 Y{self.initial_y_h_2} E{2 * self.extrude_per_mm}", # Start of the calibration - # "G91", - # ] - # # self.print_horizontal_line - - # # Raise nozzle G1 Z2 E-0.1 - - # # * Vertical Print in tool 1 - # _horizontal_print_tool_0 = [ - # "G90", - # "M83 ", - # f"G1 X{self.initial_x_v_2 + 2} Y{self.initial_y_v} E{-0.1} F{12000}", # Brim - # "G1 Z0.2 E0.5 F2000", # Brim - # f"G1 X{self.initial_y_v_2} E{(self.initial_y_h_2 - self.initial_x_h) * self.extrude_per_mm} F3000", # U turn of the brim - # f"G1 Y{self.initial_x_v_2} E{2 * self.extrude_per_mm}", # Start of the calibration - # "G91", - # ] - # # self.print_vertical_line - - # # * Park the nozzle - # # G90 - # # G1 Y150 # So the user can see the result and select the line - - # # Change to tool 0 - # # T0 - - # # Permit the user to choose the line - # pass - - # cmd_OFFSETCALIBRATION_helpe = ( - # "calls a ui to select the ranges or something i don't know" - # ) - - # def cmd_OFFSETCALIBRATION(self, gcmd): - # pass - - # cmd_XYNOZZLEPRIME_help = "Primes the nozzle that it's going to be used" - - # def cmd_XYNOZZLEPRIME(self, gcmd): - # commands = [ - # f"M109 S{self.filament_temperature}", - # "G92 E0", # Specify that Extruder is at position 0 - # "M83", # Relative coordinates - # "G1 E50 F300", # Prime the nozzle - # "G92 E0", # Specify that Extruder is at position 0 - # ] - # for command_line in commands: - # gcmd.run_script_as_command(command_line) - - def get_status(self, eventtime): - return {"last_state": self.last_state, "enabled": self.enable} - - ################################################################################ - # Helper function that are useful for performing the XY calibration - ################################################################################ - - - def print_horizontal_line(self, gcmd, callback): - gcode_lines = [ - f"G1 X{self.horizontal_line_spacing} E{ abs(self.horizontal_line_spacing * self.extrude_per_mm)}", - f"G1 Y{self.horizontal_line_height} E{abs(self.horizontal_line_height * self.extrude_per_mm)}", - f"G1 X{self.horizontal_line_top} E{ abs(self.horizontal_line_top * self.extrude_per_mm)}", - f"G1 Y{int(self.horizontal_line_height) * -1} E{abs(self.horizontal_line_height * self.extrude_per_mm)}", - ] - for line in range(self.line_number + 1): - gcmd.run_command( - next(iter(gcode_lines)) - ) # Something like this to run all the lines - - def print_vertical_lines(self, gcmd, callback): - gcode_lines = [ - f"G1 Y{self.vertical_line_spacing} E{abs(self.vertical_line_spacing * self.extrude_per_mm)}", - f"G1 X{self.vertical_line_height} E{abs(self.vertical_line_height * self.extrude_per_mm)}", - f"G1 Y{self.vertical_line_top} E{abs(self.vertical_line_top * self.extrude_per_mm)}", - f"G1 X{int(self.vertical_line_height * -1)} E{abs(self.vertical_line_height * self.extrude_per_mm)}", - ] - for line in range(self.line_number + 1): - gcmd.run_command( - next(iter(gcode_lines)) - ) # Something like this to run all the lines - - def update_offsets(self): - pass - - def get_homed_axes(self) -> str | None: - cur_time = self.reactor.monotonic() - _kin_status = self.kin.get_status(cur_time) - - if not isinstance(_kin_status, dict): - return None - - if "homed_axes" in _kin_status.keys(): - return _kin_status["homed_axes"] - - return None - - # gcmd respond_info #Displays stuff on the console - # gcode run_script #Don't know what this one does Same as run_script_from_command but with mutex - # gcode run_script_from_command #Runs a command from a string - # toolhead move #Moves the toolhead - # toohead wait_moves() #Waits for the toolhead to finish its movements - - # When i have the tool head i can use the move command move(newpos, speed) - # There is also on toolhead a method called check_busy - # also a get_kinematics - # - - -def load_config(config): - return XYOffsetCalibrationTool(config) - - -# def load_config_prefix(config): -# return XYOffsetCalibrationTool(config) - - - -""" -From the kinmatics i can run - -get_steppers() -calc_position(self, stepper_positions) -set_position(self, newpos, homign_axes) -note_z_not_homed(self) -home(homign_state) -_motor_off(slef, pritne_time) -_check_positions(slef, move) -check_move(self, move) -get_status - - -variables: -self.rails -max_velocity, max_accel -max_z_velocity -max_z_accel -limits -ranges -axes_min -axes_max - - -From the main mcu class - - -canbus_uuid -setup_pin -create_oid(self) -get_printer -get_name -register_response(self, cb, msg, oid=None) -get_query_slot(self, oid) -seconds_to_clock(self, time) -get_max_stepper_error(self) -alloc_command_queue(self) - -lookup_command(self, msgformat, cp=None) -lookup_query_command(sef, msgformat, respformat, oid=None, cp=None, is_async=None) -estimate_print_time -get_contatns() -get_enumerations() -get_constants_float() -register_stepqueue(self, stepqueue) -request_move_queue_slot(self) -register_flush_callback(self, callback) -flush_move(self, printe_time, clear_history_time) -check_active() -is_fileoutput(self) -is_shutdown() -stats() - - -can also go get the extruder object so i can run some specific methods - - - - - - - -No final quando tiver-mos os valores, colocamos um gcode offset na segunda cabeça com o valor obtido -cada vez que cada cabeca e activada colocamos um offse, na cabela um e 0,0 e a segunda cabeca e o valor obtido -""" \ No newline at end of file diff --git a/extras/k_extras/bucket.py b/extras/k_extras/bucket.py deleted file mode 100644 index 821e2d8a..00000000 --- a/extras/k_extras/bucket.py +++ /dev/null @@ -1,93 +0,0 @@ -import logging -import typing - - -class Bucket: - def __init__(self, config): - self.printer = config.get_printer() - self.reactor = self.printer.get_reactor() - self.gcode = self.printer.lookup_object("gcode") - self.toolhead = None - - self.printer.register_event_handler("klippy:ready", self.handle_ready) - self.printer.register_event_handler("klippy:connect", self.handle_connect) - - self.has_custom_bed_bound = config.getboolean("has_custom_boundary", False) - self.bucket_position = config.getfloatlist("bucket_position_xy", None, count=2) - self.travel_speed = config.getfloat( - "travel_speed", default=50.0, minval=20.0, maxval=10000.0 - ) - - self.custom_bed_bound_object = None - - self.gcode.register_command( - "MOVE_TO_BUCKET", - self.cmd_MOVE_TO_BUCKET, - "Gcode for moving the toolhead to the bucket position. Takes into account if there is a custom bed boundary", - ) - - def handle_ready(self): - if self.has_custom_bed_bound: - self.custom_bed_bound_object = self.printer.lookup_object( - "bed_custom_bound" - ) - - def handle_connect(self): - self.toolhead = self.printer.lookup_object("toolhead") - - def move_to_bucket(self, split: typing.Optional["bool"] = False): - """Moves to bucket position""" - if self.toolhead is None: - return - try: - # * Check if the bucket is outside the printer boundary - if self.custom_bed_bound_object is not None: - _conf_bound = self.custom_bed_bound_object.check_boundary_limits( - position=(self.bucket_position[0], self.bucket_position[1]) - ) - if ( - not _conf_bound["x"] or not _conf_bound["y"] - ) and self.custom_bed_bound_object.get_status()["status"] == "custom": - self.custom_bed_bound_object.restore_default_boundary() - - if not split: - self.toolhead.manual_move( - [self.bucket_position[0], self.bucket_position[1]], - self.travel_speed, - ) - else: - self.toolhead.manual_move([self.bucket_position[0]], self.travel_speed) - self.toolhead.wait_moves() - self.toolhead.manual_move( - [self.bucket_position[0], self.bucket_position[1]], - self.travel_speed, - ) - - self.toolhead.wait_moves() - - if ( - self.custom_bed_bound_object is not None - and self.custom_bed_bound_object.get_status()["status"] == "default" - ): - self.custom_bed_bound_object.set_custom_boundary() - except Exception as e: - # self.gcode.respond_error( - # "Exception encountered while trying to move to bucket position" - # ) - logging.info("Exception occurred when moving toolhead to bucket position.") - - def cmd_MOVE_TO_BUCKET(self, gcmd): - _split = gcmd.get("SPLIT", False, parser=bool) - self.move_to_bucket() - - -class BucketMoveError(Exception): - "Raised when there is an exception moving the toolhead to the bucket" - - def __init__(self, message, errors): - super(BucketMoveError, self).__init__(message) - self.errors = errors - - -def load_config(config): - return Bucket(config) diff --git a/extras/k_extras/cutter_sensor.py b/extras/k_extras/cutter_sensor.py deleted file mode 100644 index 219eb045..00000000 --- a/extras/k_extras/cutter_sensor.py +++ /dev/null @@ -1,403 +0,0 @@ -import logging -import typing - - -class CutterSensor: - def __init__(self, config): - self.name = config.get_name().split()[-1] - self.printer = config.get_printer() - self.reactor = self.printer.get_reactor() - self.gcode = self.printer.lookup_object("gcode") - - # * Register Event handlers - self.printer.register_event_handler("klippy:connect", self.handle_connect) - self.printer.register_event_handler("klippy:ready", self.handle_ready) - # self.printer.register_event_handler( - # "idle_timeout:printing", self.handle_printing - # ) - - # * Get Cutter Module parameters / define variables - self.extrude_length_mm = config.getfloat( - "extrude_length_mm", 5.0, minval=0.0, maxval=50.0 - ) - self.retract_length_mm = config.getfloat( - "retract_length_mm", -5.0, minval=-50.0, maxval=-0.5 - ) - self.retract_to_cutter_sensor = config.getfloat( - "retract_to_sensor_mm", -10.0, minval=-50.0, maxval=-0.5 - ) - self.extrude_speed = config.getfloat( - "extrude_speed", 2.0, above=0.0, minval=1.0, maxval=50.0 - ) - self.travel_speed = config.getfloat( - "travel_speed", 100.0, above=0.0, minval=30.0, maxval=600.0 - ) - self.cut_speed = config.getfloat( - "cut_speed", 100.0, above=50.0, minval=50.0, maxval=300.0 - ) - self.cutter_position = config.getfloatlist("cutter_position_xy", count=2) - self.pre_cutter_position = config.getfloatlist( - "pre_cutter_position_xy", count=2 - ) - self.bucked_position_xy = config.getfloatlist( - "bucket_position_xy", default=None, count=2 - ) - - self.runout_gcode = self.insert_gcode = None - gcode_macro = self.printer.load_object(config, "gcode_macro") - if config.get("runout_gcode", None) is not None: - self.runout_gcode = gcode_macro.load_template(config, "runout_gcode", "") - if config.get("insert_gcode", None) is not None: - self.insert_gcode = gcode_macro.load_template(config, "insert_gcode") - self.event_delay = config.getfloat("event_delay", 0.3, above=0.0) - - self.pause_delay = config.getfloat("pause_delay", 0.5, above=0) - self.runout_pause = config.getboolean("pause_on_runout", False) - - if self.bucked_position_xy is not None: - self.bucked_position_x, self.bucked_position_y = self.bucked_position_xy - self.cutter_position_x, self.cutter_position_y = self.cutter_position - self.pre_cutter_position_x, self.pre_cutter_position_y = ( - self.pre_cutter_position - ) - - self.filament_present: bool = False - self.sensor_enabled = True - self.min_event_systime = self.reactor.NEVER - - # * Register button sensor for the cutter filament sensor - cutter_sensor_pin = config.get("cutter_sensor_pin") - buttons = self.printer.load_object(config, "buttons") - buttons.register_buttons([cutter_sensor_pin], self.cutter_sensor_callback) - - # * Register Gcode Commands - - self.gcode.register_mux_command( - "CUT", "SENSOR", self.name, self.cmd_CUT, self.cmd_CUT_helper - ) - self.gcode.register_mux_command( - "QUERY_FILAMENT_SENSOR", - "SENSOR", - self.name, - self.cmd_QUERY_FILAMENT_SENSOR, - self.cmd_QUERY_FILAMENT_SENSOR_helper, - ) - self.gcode.register_mux_command( - "SET_FILAMENT_SENSOR", - "SENSOR", - self.name, - self.cmd_SET_FILAMENT_SENSOR, - self.cmd_SET_FILAMENT_SENSOR_helper, - ) - - - cmd_QUERY_FILAMENT_SENSOR_helper = "Query the status of the cutter sensor" - cmd_SET_FILAMENT_SENSOR_helper = "Query the status of the cutter sensor" - - def handle_connect(self): - self.toolhead = self.printer.lookup_object("toolhead") - - def handle_ready(self): - self.min_event_systime = self.reactor.monotonic() + 2.0 - self.custom_boundary_object = self.printer.lookup_object("bed_custom_bound") - self.load_filament_object = self.printer.lookup_object("load_filament") - - def cmd_QUERY_FILAMENT_SENSOR(self, gcmd): - if self.filament_present: - msg = "Cutter Filament Sensor: filament Detected" - else: - msg = "Cutter Filament Sensor: filament not detected" - gcmd.respond_info(msg) - - def cmd_SET_FILAMENT_SENSOR(self, gcmd): - self.sensor_enabled = gcmd.get_int("ENABLE", 1) - - cmd_CUT_helper = "Routine that handles a cutter on the printer toolhead" - - def cmd_CUT(self, gcmd): - """Gcode command for the Cutter module - - Call CUT gcode command to perform the filament cutting - """ - self.home_if_needed() - self.toolhead.wait_moves() - eventtime = self.reactor.monotonic() - kin_status = self.toolhead.get_kinematics().get_status(eventtime) - return_to_last_pos = gcmd.get("MOVE_TO_LAST_POS", False, parser=bool) - turn_off_heater = gcmd.get("TURN_OFF_HEATER", False, parser=bool) - temp = gcmd.get("TEMP", 220.0, parser=float, minval=200, maxval=250) - - if "xyz" not in kin_status["homed_axes"]: - # Require the printer to be homed - gcmd.respond_info("Cut requires printer to be homed.", log=True) - return - - self.prev_pos = ( - self.toolhead.get_position() - ) # The position where the cutter was called - - # * Heat the extruder - gcmd.respond_info("Heating Extruder. Waiting.") - self.heat_and_wait(temp, wait=True) - - if self.bucked_position_xy is not None: - self.move_to_bucket() - - # * Extrude - gcmd.respond_info(f"Extruder {self.extrude_length_mm}") - self.move_extruder_mm(self.extrude_length_mm) - gcmd.respond_info(f"Extruder {self.retract_length_mm}") - self.move_extruder_mm(self.retract_length_mm) - - # * Move to cutting pos - gcmd.respond_info(f"Moving to cutter position: {self.pre_cutter_position}") - self.move_to_cutter_pos() - gcmd.respond_info(f"Performing cut: {self.cutter_position}") - self.cut_move() - - if self.bucked_position_xy is not None: - self.move_to_bucket() - - # * Push the filament a little down do not to - gcmd.respond_info(f"Extruding {self.extrude_length_mm} to push filament out.") - self.move_extruder_mm(-2.0) # Aliviate the pressure on the blade - - self.move_extruder_mm(self.extrude_length_mm + 2) - - self.move_extruder_mm(float(self.retract_to_cutter_sensor)) - - # * Push the filament out of the cutter pos - if self.prev_pos is not None and return_to_last_pos: - gcmd.respond_info( - f"Filament cutting done, moving back to initial position: {self.prev_pos}." - ) - self.move_back() - self.toolhead.wait_moves() - if self.custom_boundary_object is not None: - self.custom_boundary_object.set_custom_boundary() - - self.toolhead.wait_moves() - # Cooldown extruder - gcmd.respond_info("Cut done. Turning off Extruder heater.") - - if turn_off_heater: - self.heat_and_wait(0, wait=False) - - def move_extruder_mm(self, dist): - """Move the extruder. - - Args: - dist (float in mm): The distance in a certain amount. - """ - curtime = self.reactor.monotonic() - gcode_move = self.printer.lookup_object("gcode_move") - # last_move_time = self.toolhead.get_last_move_time() - v = dist * gcode_move.get_status(curtime)["extrude_factor"] - new_dist = v + gcode_move.get_status(curtime)["position"][3] - prev_pos = self.toolhead.get_position() - self.toolhead.move( - [prev_pos[0], prev_pos[1], prev_pos[2], new_dist], self.extrude_speed - ) - self.toolhead.wait_moves() - - def home_if_needed(self): - if self.toolhead is None: - return - try: - eventtime = self.reactor.monotonic() - - kin = self.toolhead.get_kinematics() - _homed_axes = kin.get_status(eventtime)["homed_axes"] - - if "xyz" in _homed_axes.lower(): - return - else: - self.gcode.respond_info("Homing machine.") - # completion = self.reactor.register_callback(self._exec_gcode("G28")) - self.gcode.run_script_from_command("G28") - - self.gcode.respond_info("Waiting for homing.") - # self.toolhead.wait_moves() - except Exception as e: - logging.error(f"Unable to home for somereason on load filament: {e}") - - def heat_and_wait(self, temp, wait: typing.Optional["bool"] = True): - """Heats the extruder and wait. - - Method returns when temperature is [temp - 5 ; temp + 5]. - Args: - temp (float): - Target temperature in Celsius. - wait (bool, optional): - Weather to wait or not for the temperature to reach the interval . Defaults to True - """ - eventtime = self.reactor.monotonic() - extruder = self.toolhead.get_extruder() - pheaters = self.printer.lookup_object("heaters") - pheaters.set_temperature(extruder.get_heater(), temp, False) - - extruder_heater = extruder.get_heater() - - while not self.printer.is_shutdown() and wait: - self.gcode.respond_info("Waiting for temperature to stabilize.") - heater_temp, target = extruder_heater.get_temp(eventtime) - if heater_temp >= (temp - 5) and heater_temp <= (temp + 5): - return - - print_time = self.toolhead.get_last_move_time() - eventtime = self.reactor.pause(eventtime + 1.0) - - def cut_move(self): - """Performs the cut movement""" - self.toolhead.manual_move( - [self.cutter_position_x, self.cutter_position_y], self.travel_speed - ) - self.toolhead.manual_move( - [self.pre_cutter_position_x, self.pre_cutter_position_y], self.cut_speed - ) - self.toolhead.wait_moves() - - def move_to_cutter_pos(self): - """Moves the toolhead to the pre cutting position""" - curtime = self.reactor.monotonic() - kin_status = self.toolhead.get_kinematics().get_status(curtime) - - if "xyz" not in kin_status["homed_axes"]: - # need to home - return - self.toolhead.manual_move( - [self.pre_cutter_position_x, self.pre_cutter_position_y], self.travel_speed - ) - self.toolhead.wait_moves() - - def move_to_home(self): - """Moves to the homing position""" - gcode_move = self.printer.lookup_object("gcode_move") - homing_origin = gcode_move.get_status()["homing_origin"] - self.toolhead.manual_move(homing_origin, self.travel_speed) - - def move_to_bucket(self, split=False): - """Moves to the bucket position""" - if self.custom_boundary_object is not None: - # * Restore original - self.gcode.respond_info("Restoring original printer Boundaries.") - self.custom_boundary_object.restore_default_boundary() - - if not split: - self.toolhead.manual_move( - [self.bucked_position_x, self.bucked_position_y], self.travel_speed - ) - else: - self.toolhead.manual_move([self.bucked_position_x], self.travel_speed) - self.toolhead.wait_moves() - self.toolhead.manual_move([self.bucked_position_y], self.travel_speed) - - self.toolhead.wait_moves() - - def move_back(self): - """Moves back to the original position where the CUT gcode command was called""" - if self.prev_pos is None: - return - - self.toolhead.manual_move( - [self.prev_pos[0], self.prev_pos[1], self.prev_pos[2]], self.travel_speed - ) - self.toolhead.wait_moves() - - def cutter_sensor_callback(self, eventtime, state): - """Callback for the change state""" - # if the state is true, the callback - if state == self.filament_present: - return - - self.filament_present = state - eventtime = self.reactor.monotonic() - - if eventtime < self.min_event_systime or not self.sensor_enabled: - return - - if self.insert_gcode is None or self.insert_gcode is None: - return - - # * Determine the printing status - idle_timeout = self.printer.lookup_object("idle_timeout") - is_printing = idle_timeout.get_status(eventtime)["state"] == "Printing" - - # * Perform filament action associated with status change (if any) - self.gcode.respond_info("[CUTTTTTER] IN CUTTER SENSOR CALLBACK") - if ( - self.load_filament_object is not None - and self.load_filament_object.load_started - ): - if state: - self.printer.send_event("cutter_sensor:filament_present") - else: - self.printer.send_event("cutter_sensor:no_filament") - - elif state: - if ( - not is_printing and self.insert_gcode is not None - ): # Not printing and there is an insert gcode - self.printer.send_event("cutter_sensor:filament_present") - # filament inserted detected - self.min_event_systime = self.reactor.NEVER - - logging.info( - f"Cutter filament sensor insert detected, time : {eventtime}" - ) - self.reactor.register_callback(self._insert_event_handler) - - - elif ( - not is_printing and self.runout_gcode is not None - ): # When not printing and there is a runout gcode - self.printer.send_event("cutter_sensor:no_filament") - # Act During printing - # self.min_event_systime = self.reactor.NEVER - self.min_event_systime = eventtime - logging.info( - f"Cutter filament sensor runout detected, while not printing, time: {eventtime}" - ) - self.reactor.register_callback(self._runout_event_handler) - - elif ( - is_printing and self.runout_gcode is not None - ): # When printing and there is a runout gcode - self.printer.send_event("cutter_sensor:no_filament") - # self.min_event_systime = self.reactor.NEVER - self.min_event_systime = eventtime - logging.info(f"Cutter filament sensor runout detected, time: {eventtime}") - self.reactor.register_callback(self._runout_event_handler) - - - def _insert_event_handler(self, eventtime): - self._exec_gcode("", self.insert_gcode) - - def _runout_event_handler(self, eventtime): - pause_prefix = "" - if self.runout_pause: - pause_resume = self.printer.lookup_object("pause_resume") - pause_resume.send_pause_command() - pause_prefix = "PAUSE\n" - self.printer.get_reactor().pause(eventtime + self.pause_delay) - self._exec_gcode(pause_prefix, self.runout_gcode) - - def _exec_gcode(self, prefix, template): - """Internal Executes a gcode just like what's in the klipper filament_switch_sensor.py""" - - try: - self.gcode.run_script(prefix + template.render() + "\nM400") - except Exception: - logging.exception("Script running error") - self.min_event_systime = self.reactor.monotonic() + self.event_delay - - def get_status(self, eventtime): - """Gets the status of the sensor of the cutter""" - return { - "filament_detect": self.filament_present, - "enabled": bool(self.sensor_enabled), - } - - -def load_config_prefix(config): - return CutterSensor(config) diff --git a/extras/k_extras/load_filament.py b/extras/k_extras/load_filament.py deleted file mode 100644 index 124884c8..00000000 --- a/extras/k_extras/load_filament.py +++ /dev/null @@ -1,372 +0,0 @@ -import logging -import typing - - -class LoadFilamentError(Exception): - """Raised when there was an error during filament loading""" - - def __init__(self, message, errors): - super(LoadFilamentError, self).__init__(message) - self.errors = errors - - -class LoadFilament: - def __init__(self, config): - # self.name = config.get_name().split()[-1] - self.printer = config.get_printer() - self.reactor = self.printer.get_reactor() - self.gcode = self.printer.lookup_object("gcode") - - # * Register Event handlers - self.printer.register_event_handler("klippy:connect", self.handle_connect) - self.printer.register_event_handler("klippy:ready", self.handle_ready) - - # * Get configs - self.has_cutter = config.getboolean("cutter_present", False) - if self.has_cutter: - self.cutter_name = config.get("cutter_name") - self.has_custom_boundary = config.getboolean("has_custom_boundary", False) - - self.min_dist_to_nozzle = config.getfloat( - "minimum_distance_to_nozzle", 0.1, minval=0.1, maxval=5000.0 - ) - - self.park = config.getfloatlist("park_xy", None, count=2) - - self.bucket_position = config.getfloatlist("bucket_position", count=2) - self.travel_speed = config.getfloat( - "travel_speed", default=50.0, minval=20.0, maxval=500.0 - ) - self.extrude_speed = config.getfloat( - "extrude_speed", default=10.0, minval=5.0, maxval=100.0 - ) - self.purge_speed = config.getfloat( - "purge_speed", default=5.0, minval=2.0, maxval=50.0 - ) - self.purge_distance = config.getfloat( - "purge_distance", default=1.5, minval=0.5, maxval=20.0 - ) - self.purge_max_retries = config.getint( - "purge_max_count", default=10, minval=2, maxval=30 - ) - self.belay_present = config.getboolean("belay_present", default=False) - self.cutter_handles_rest = config.getboolean( - "cutter_handles_rest", default=False - ) - self.purge_interval = config.getfloat( - "purge_timeout", default=3.0, minval=0.1, maxval=5.5 - ) - self.pre_load_gcode = self.post_load_gcode = None - gcode_macro = self.printer.load_object(config, "gcode_macro") - if config.get("pre_load_gcode", None) is not None: - self.pre_load_gcode = gcode_macro.load_template( - config, "pre_load_gcode", "" - ) - if config.get("post_load_gcode", None) is not None: - self.post_load_gcode = gcode_macro.load_template(config, "post_load_gcode") - - self.mainsail_prompt_gcode = ( - "\n//action:prompt_begin Load Filament\n" - + "//action:prompt_text Purging filament, click button to stop.\n" - + "//action:prompt_button Stop Purge|PURGE_STOP|error\n" - + "//action:prompt_show\n" - ) - - self.load_started: bool = False - if self.has_cutter: - self.printer.register_event_handler( - "cutter_sensor:filament_present", self.handle_cutter_filament_present - ) - self.printer.register_event_handler( - "cutter_sensor:no_filament", self.handle_cutter_no_filament - ) - - self.current_purge_index: int = 0 - self.extrude_purge_timer = self.reactor.register_timer( - self.purge_extrude, self.reactor.NEVER - ) - # self.timer_handler_extrude_to_cutter = self.reactor.register_timer( - # self.extruder_alot, self.reactor.NEVER - # ) - self.extrude_to_cutter_sensor_timer = self.reactor.register_timer( - self.extrude_to_cutter_sensor, self.reactor.NEVER - ) - - self.gcode.register_command( - "LOAD_FILAMENT", - self.cmd_LOAD_FILAMENT, - "GCODE MACRO to load filament, takes into account if there is a belay and or a filament cutter with a sensor.", - ) - self.gcode.register_command( - "PURGE_STOP", - self.cmd_PURGE_STOP, - "Helper gcode command that stop filament purging", - ) - - def handle_connect(self): - self.toolhead = self.printer.lookup_object("toolhead") - - def handle_ready(self): - self.min_event_systime = self.reactor.monotonic() + 2.0 - if self.has_cutter and self.cutter_name is not None: - self.cutter_object = self.printer.lookup_object( - f"cutter_sensor {self.cutter_name}" - ) - if self.has_custom_boundary: - self.custom_boundary_object = self.printer.lookup_object("bed_custom_bound") - - def handle_cutter_filament_present(self, eventtime=None): - # * The cutter sensor detected the filament, i can now extrude to the nozzle and perform the purge loop - if self.load_started: - self.gcode.respond_info( - "Cutter sensor on loading -> filament present, stopping extrude to cutter sensor" - ) - # TODO: ! Experimental - # self.toolhead.lookahead.flush() - # self.toolhead.lookahead.reset() - # self.toolhead.lookahead.set_flush_time(self.reactor.NOW) - - # * Filament is present here so i can extrude a good amount first - self.reactor.update_timer( - self.extrude_to_cutter_sensor_timer, self.reactor.NEVER - ) - - self.toolhead.wait_moves() - # * Purge little by little for convenience - self.gcode.respond_info("Starting purge loop") - self.reactor.update_timer(self.extrude_purge_timer, self.reactor.NOW) - - def handle_cutter_no_filament(self, eventtime=None): - if self.load_started: - self.gcode.respond_info("Cutter sensor on loading -> starting purge loop") - self.reactor.update_timer(self.extrude_purge_timer, self.reactor.NOW) - - def extrude_to_cutter_sensor(self, eventtime): - if self.cutter_object is not None and not self.cutter_object.filament_present: - self.move_extruder_mm(distance=10, speed=self.extrude_speed, wait=False) - return eventtime + float((10 / self.extrude_speed)) - - def purge_extrude(self, eventtime): - if self.current_purge_index > self.purge_max_retries: - self.current_purge_index = 0 - self.gcode.respond_info("Purge limit reached, ending load routine.") - self.reactor.register_callback(self._purge_end) - return eventtime + self.reactor.NEVER - # * Extrude continuously until someone says to stop it - self.move_extruder_mm(distance=self.purge_distance, speed=self.purge_speed) - self.current_purge_index += 1 - return eventtime + float(self.purge_interval) - - def create_mainsail_prompt(self): - # * create gcode template - self._exec_gcode(self.mainsail_prompt_gcode) - - def cmd_PURGE_STOP(self, gcmd): - self._purge_end() - - def _purge_end(self, eventtime=None): - self.reactor.update_timer( - self.extrude_purge_timer, self.reactor.NEVER - ) # Stop purging - self.load_started = False - # * Move to zero position - if self.park is not None: - self.toolhead.manual_move([self.park[0], self.park[1]], self.travel_speed) - self.toolhead.wait_moves() - - self.heat_and_wait(0, wait=False) - if ( - self.has_custom_boundary - and self.custom_boundary_object is not None - and self.custom_boundary_object.get_status()["status"] == "default" - ): - # * Restore the boundary to the custom one - self.custom_boundary_object.set_custom_boundary() - - # * stop mainsail prompt - self._exec_gcode("//action:prompt_end") - self._exec_gcode("RESTORE_GCODE_STATE NAME=LOAD_FILAMENT_state") - # * Run post load filament gcode - if self.post_load_gcode is not None: - self._exec_gcode(self.post_load_gcode) - self.toolhead.wait_moves() - - if self.printer.lookup_object("gcode_macro CLEAN_NOZZLE") is not None: - # * Clean the nozzle after the loading if there is gcode macro for that - self.gcode.run_script_from_command("CLEAN_NOZZLE") - - def cmd_LOAD_FILAMENT(self, gcmd): - # * Save printer gcode state - temp = gcmd.get("TEMPERATURE", 220.0, parser=float, minval=210, maxval=250) - try: - self.home_if_needed() - self._exec_gcode("SAVE_GCODE_STATE NAME=LOAD_FILAMENT_state\nM400") - self.load_started = True - - self.printer.send_event("load_filament:start") - - if self.has_custom_boundary: - self.gcode.respond_info("has custom boundary restoring default ") - # * The limits might be set and there is no way to reach the bucket, restore the default boundary here then set the custom one later - self.custom_boundary_object.restore_default_boundary() - - - # * Run pre load filament gcode commands - if self.pre_load_gcode is not None: - self._exec_gcode(self.pre_load_gcode) - # self.toolhead.wait_moves() - - # * Start heating the extruder. - self.heat_and_wait(temp, wait=False) - # * Home if needed - - # * Go to bucket position - self.move_to_bucket() - - # * Actually wait for the temperature here - self.heat_and_wait(temp, wait=True) - self.toolhead.wait_moves() - - # * Increase max extrude distance if needed - extruder = self.toolhead.get_extruder() - _old_extrude_min_dist = None - if extruder.max_e_dist < self.min_dist_to_nozzle: - _old_extrude_min_dist = extruder.max_e_dist - extruder.max_e_dist = self.min_dist_to_nozzle + 10.0 - self.gcode.respond_info( - f"Changed extrude distance to {self.min_dist_to_nozzle + 10.0}" - ) - - if self.cutter_handles_rest: - self.reactor.update_timer( - self.extrude_to_cutter_sensor_timer, self.reactor.NOW - ) - self.toolhead.wait_moves() - self.create_mainsail_prompt() - - if not self.cutter_handles_rest: - # * Restore printer gcode state - self._exec_gcode("RESTORE_GCODE_STATE NAME=LOAD_FILAMENT_state") - - # * Run post load filament gcode - if self.post_load_gcode is not None: - self._exec_gcode(self.post_load_gcode) - self.toolhead.wait_moves() - - # * Restore extruder min extrude distance - if _old_extrude_min_dist is not None: - extruder.max_e_dist = _old_extrude_min_dist - - except LoadFilamentError as e: - logging.error(f"Error loading filament : {e}") - - def move_extruder_mm(self, distance=10.0, speed=30.0, wait=True): - """Move the extruder - - Args: - distance (float): The distance in mm to move the extruder. - """ - try: - eventtime = self.reactor.monotonic() - gcode_move = self.printer.lookup_object("gcode_move") - - prev_pos = self.toolhead.get_position() - - v = distance * gcode_move.get_status(eventtime)["extrude_factor"] - # new_distance = v + gcode_move.get_status(eventtime)["position"][3] - new_distance = v + prev_pos[3] - - self.toolhead.move( - [prev_pos[0], prev_pos[1], prev_pos[2], new_distance], speed - ) - self.gcode.respond_info(f"Extruding -> {distance} mm. Speed -> {speed}") - if wait: - self.toolhead.wait_moves() - except Exception: - raise Exception("fucked up extrusion") - return False - return True - - def move_to_bucket(self, split: typing.Optional["bool"] = False): - """Moves to bucket position""" - if self.toolhead is None: - return - - # * Maybe check if the move is within the printers boundaries - - if not split: - self.toolhead.manual_move( - [self.bucket_position[0], self.bucket_position[1]], self.travel_speed - ) - else: - self.toolhead.manual_move([self.bucket_position[0]], self.travel_speed) - self.toolhead.wait_moves() - self.toolhead.manual_move([self.bucket_position[1]], self.travel_speed) - - self.toolhead.wait_moves() - - def move_to_home_pos(self): - if self.toolhead is None: - return - - self.toolhead.manual_move([self.park[0], self.park[1]], self.travel_speed) - self.toolhead.wait_moves() - - def home_if_needed(self): - if self.toolhead is None: - return - try: - eventtime = self.reactor.monotonic() - - kin = self.toolhead.get_kinematics() - _homed_axes = kin.get_status(eventtime)["homed_axes"] - - if "xyz" in _homed_axes.lower(): - return - else: - self.gcode.respond_info("Homing machine.") - # completion = self.reactor.register_callback(self._exec_gcode("G28")) - self.gcode.run_script_from_command("G28") - - self.gcode.respond_info("Waiting for homing.") - except Exception as e: - logging.error(f"Unable to home for somereason on load filament: {e}") - - def heat_and_wait(self, temp, wait: typing.Optional["bool"] = False): - """Heats the extruder and wait. - - Method returns when temperature is [temp - 5 ; temp + 5]. - Args: - temp (float): - Target temperature in Celsios. - wait (bool, optional): - Weather to wait or not for the temperature to reach the interval . Defaults to True - """ - - eventtime = self.reactor.monotonic() - extruder = self.toolhead.get_extruder() - pheaters = self.printer.lookup_object("heaters") - self.gcode.respond_info(f"Setting hotend temperature to : {temp}ºC") - pheaters.set_temperature(extruder.get_heater(), temp, False) - - extruder_heater = extruder.get_heater() - - while not self.printer.is_shutdown() and wait: - self.gcode.respond_info("Waiting for temperature to stabilize.") - heater_temp, target = extruder_heater.get_temp(eventtime) - if heater_temp >= (temp - 5) and heater_temp <= (temp + 5): - return - - print_time = self.toolhead.get_last_move_time() - eventtime = self.reactor.pause(eventtime + 1.0) - - def _exec_gcode(self, template): - try: - self.gcode.run_script(template.render() + "\nM400") - except Exception: - logging.exception("Error running gcode script on load_filament.py") - self.min_event_systime = self.reactor.monotonic() + 2.0 - - -def load_config(config): - return LoadFilament(config) diff --git a/extras/k_extras/toolhead_fan_duct_sensor.py b/extras/k_extras/toolhead_fan_duct_sensor.py deleted file mode 100644 index a92d4656..00000000 --- a/extras/k_extras/toolhead_fan_duct_sensor.py +++ /dev/null @@ -1,29 +0,0 @@ -import logging -import typing - - -class ToolheadFanDuctSensor: - def __init__(self, config): - self.printer = config.get_printer() - self.name = config.get_name().split()[-1] - self.reactor = self.printer.get_reactor() - self.gcode = self.printer.lookup_object("gcode") - - # * Register fan duct sensor - duct_sensor_pin = config.get("sensor_pin") - buttons = self.printer.load_object(config, "buttons") - buttons.register_buttons([duct_sensor_pin], self.fan_duct_sensor_handler) - - def fan_duct_sensor_handler(self, eventtime, state): - if not state: - return - - if state: - # * Stop the printer - self.printer.invoke_shutdown( - msg="Toolhead Fan duct is currently open, close the fan duct and restart" - ) - - -def load_config_prefix(config): - return ToolheadFanDuctSensor(config) diff --git a/extras/k_extras/unload_filament.py b/extras/k_extras/unload_filament.py deleted file mode 100644 index 7b4ab50e..00000000 --- a/extras/k_extras/unload_filament.py +++ /dev/null @@ -1,210 +0,0 @@ -import logging -import typing - -class UnloadFilament: - - def __init__(self, config): - - self.printer = config.get_printer() - self.reactor = self.printer.get_reactor() - self.cutter_object = None - self.custom_boundary_object = None - self.belay_object = None - self.min_event_systime = None - self.toolhead = None - self.bucket_object = None - - self.printer.register_event_handler("klippy:connect", self.handle_connect) - self.printer.register_event_handler("klippy:ready", self.handle_ready) - - self.has_cutter = config.getboolean("has_cutter", False) - self.has_custom_boundary = config.getboolean("has_custom_boundary", False) - self.has_belay = config.getboolean("has_belay", False) - self.sensor_name = config.get("filament_sensor_name", default=None) - self.cutter_name = config.get("cutter_name", default=None) - self.park = config.getfloatlist("park_xy", None, count=2) - self.extrude_speed = config.getfloat("extrude_speed", default=10., minval=5., maxval=50.) - self.purge_speed = config.getfloat("purge_speed", default=5., minval=5., maxval=50.) - self.purge_max_count = config.getint("purge_max_count", default=2, minval=1, maxval=30) - self.purge_interval = config.getfloat("purge_interval", default=2., minval=0.1, maxval=5.5) - self.min_dist_to_nozzle = config.getfloat("minimum_dist_to_nozzle", default=30., minval=20., maxval=3000.) - self.extrude_speed = config.getfloat("unextrude_speed", default=30., minval=1., maxval=100.) - - self.pre_unload_gcode = self.post_unload_gcode = None - gcode_macro = self.printer.load_object(config, "gcode_macro") - if config.get("pre_unload_gcode", None) is not None: - self.pre_unload_gcode = gcode_macro.load_template( - config, "pre_unload_gcode", "" - ) - if config.get("post_unload_gcode", None) is not None: - self.post_unload_gcode = gcode_macro.load_template( - config, "post_unload_gcode", "" - ) - - self.mainsail_prompt_gcode = ( - "\n//action:prompt_begin Load Filament\n" - + "//action:prompt_text Purging filament, click button to stop.\n" - + "//action:prompt_button Stop Purge|PURGE_STOP|error\n" - + "//action:prompt_show\n" - ) - - - self.unload_started: bool = False - self.current_purge_index: int = 0 - - - self.unextrude_timer = self.reactor.register_timer( - self.unextrude, self.reactor.NEVER - ) - self.extrude_purge_timer = self.reactor.register_timer( - self.purge, self.reactor.NEVER - ) - - - if self.has_cutter: - self.printer.register_event_handler( - "cutter_sensor:no-filament", self.handle_cutter_fnp - ) - - self.gcode.register_command( - "UNLOAD_FILAMENT", - self.cmd_UNLOAD_FILAMENT, - "GCODE Macro to unload filament, takes into account if there is a belay and or a filament cutter with a sensor" - ) - def handle_connect(self): - self.toolhead = self.printer.lookup_object("toolhead") - self.gcode = self.printer.lookup_object("gcode") - - def handle_ready(self): - self.min_event_systime = self.reactor.monotonic() + 2. - if self.has_custom_boundary: - self.custom_boundary_object = self.printer.lookup_object("bed_custom_bound") - logging.debug("Unload module using custom bed boundary.") - if self.has_cutter and self.cutter_name is not None: - self.cutter_object = self.printer.lookup_object(f"cutter_sensor {self.cutter_name}") - logging.debug(f"Unload module using cutter -> {self.cutter_name}.") - - if self.printer.lookup_object("bucket") is not None: - self.bucket_object = self.printer.lookup_object("bucket") - logging.debug("There is a bucket object, using it") - - if self.sensor_name is not None: - self.sensor_object = self.printer.lookup_object(f"filament_switch_sensor {self.sensor_name}") - logging.debug(f"Unload using filament switch sensor {self.sensor_name} to detect when filament is out of the printer.") - - def handle_cutter_fnp(self, eventtime = None): - # * The cutter sensor does not report filament here - # * Can extrude to the other sensor - if self.unload_started: - self.reactor.update_timer(self.unextrude_timer, self.reactor.NOW) - - - def cmd_UNLOAD_FILAMENT(self, gcmd): - if self.toolhead is None: - return - try: - if self.pre_unload_gcode is not None: - self._exec_gcode(self.pre_unload_gcode) - - self.unload_started = True - # * Increase the max extrude distance if needed - extruder = self.toolhead.get_extruder() - _old_extruder_dist = None - if extruder.max_e_dist < self.min_dist_to_nozzle: - _old_extruder_dist = extruder.max_e_dist - extruder.max_e_dist = self.min_dist_to_nozzle + 10. - self.gcode.respond_info( - f"Changed extrude distance to {self.min_dist_to_nozzle + 10.} for unload procedure." - ) - - if self.cutter_object is not None: - # * Just need to perform a cut first, then pull out the filament, - # * Cutter additionally indicates when the filament can be pulled. - - pass - except Exception as e: - logging.error(f"Unexpected error while trying to unload filament: {e}.") - - def unextrude(self, eventtime): - """Move the extruder to unload""" - try: - self.move_extruder_mm(distance= -10, speed= self.extrude_speed, wait = False) - return eventtime + float((10/self.extrude_speed)) - except Exception: - logging.error("Error pulling the filament back") - finally: - return self.reactor.NEVER - - def move_extruder_mm(self, distance=10.0, speed=30.0, wait=True): - """Move the extruder - - Args: - distance (float): The distance in mm to move the extruder. - """ - if self.toolhead is None: - return - - try: - eventtime = self.reactor.monotonic() - gcode_move = self.printer.lookup_object("gcode_move") - - prev_pos = self.toolhead.get_position() - - v = distance * gcode_move.get_status(eventtime)["extrude_factor"] - # new_distance = v + gcode_move.get_status(eventtime)["position"][3] - new_distance = v + prev_pos[3] - - self.toolhead.move( - [prev_pos[0], prev_pos[1], prev_pos[2], new_distance], speed - ) - self.gcode.respond_info(f"Extruding -> {distance} mm. Speed -> {speed}") - if wait: - self.toolhead.wait_moves() - except Exception as e: - logging.error(f"Exception moving extruder while on Unloading procedure: {e}.") - raise Exception("fucked up extrusion") - return False - return True - - def home_if_needed(self): - if self.toolhead is None: - return - try: - eventtime = self.reactor.monotonic() - - kin = self.toolhead.get_kinematics() - _homed_axes = kin.get_status(eventtime)["homed_axes"] - - if "xyz" in _homed_axes.lower(): - return - else: - self.gcode.respond_info("Homing machine.") - # completion = self.reactor.register_callback(self._exec_gcode("G28")) - self.gcode.run_script_from_command("G28") - - self.gcode.respond_info("Waiting for homing.") - # self.toolhead.wait_moves() - except Exception as e: - logging.error(f"Unable to home for somereason on load filament: {e}") - - def purge(self, eventtime): - if self.current_purge_index > self.purge_max_count: - self.current_purge_index = 0 - self.gcode.respond_info("Purge limit reached, ending purge.") - self.reactor.register_callback(self._purge_end) - return eventtime + self.reactor.NEVER - - - def _purge_end(self): - pass - - - def _exec_gcode(self, template): - try: - self.gcode.run_script(template.render() + "\nM400") - except Exception: - logging.exception("Error running gcode script on load_filament.py") - self.min_event_systime = self.reactor.monotonic() + 2.0 - -def load_config(config): - return UnloadFilament(config) \ No newline at end of file diff --git a/extras/k_macros/XYOffsetCalibration.cfg b/extras/k_macros/XYOffsetCalibration.cfg deleted file mode 100644 index 3166e0b4..00000000 --- a/extras/k_macros/XYOffsetCalibration.cfg +++ /dev/null @@ -1,810 +0,0 @@ -[gcode_macro _prime_Nozzle] -gcode: - {% set psv = printer.save_variables.variables %} - {% if psv.current_extruder == 1 %} - M109 S{psv.filament_temp_1} - {% elif psv.current_extruder == 2 %} - M109 S{psv.filament_temp_2} - {% endif %} - - G92 E0 # Specify Extruder is at position 0 - M83 # Relative Coordinates - G1 E50 F300 - G92 E0 - -[gcode_macro _print_Horizontal] -variable_line_number: 0 -variable_line_spacing: 0 -variable_line_height: 0 -variable_line_top: 0 -variable_extrude_per_mm: 0 -gcode: - #M118 _print_Horiz line number:{line_number} - #M118 _print_Horiz line spacing:{line_spacing} - #M118 _print_Horiz line height:{line_height} - #M118 _print_Horiz line top:{line_top} - #M118 _print_Horiz extrude per mm:{extrude_per_mm} - - {% for l in range(line_number + 1) %} - G1 X{line_spacing} E{ (line_spacing * extrude_per_mm) | abs} - G1 Y{line_height} E{ (line_height * extrude_per_mm) | abs} - G1 X{line_top} E{ (line_top * extrude_per_mm) | abs} - G1 Y{line_height|int * -1} E{ (line_height * extrude_per_mm) | abs} - {% endfor %} - -[gcode_macro _print_Vertical] -variable_line_number: 0 -variable_line_spacing: 0 -variable_line_height: 0 -variable_line_top: 0 -variable_extrude_per_mm: 0 -gcode: - #M118 _print_Vert line number:{line_number} - #M118 _print_Vert line spacing:{line_spacing} - #M118 _print_Vert line height:{line_height} - #M118 _print_Vert line top:{line_top} - #M118 _print_Vert extrude per mm:{extrude_per_mm} - - {% for l in range(line_number + 1) %} - G1 Y{line_spacing} E{ (line_spacing * extrude_per_mm) | abs} - G1 X{line_height} E{ (line_height * extrude_per_mm) | abs} - G1 Y{line_top} E{ (line_top * extrude_per_mm) | abs} - G1 X{line_height|int * -1} E{ (line_height * extrude_per_mm) | abs} - {% endfor %} - -[gcode_macro _park_Nozzle] -variable_x_home_position: 0 #X{printer.toolhead.axis_minimum.x + 5} -gcode: - G91 #Relative Coordinates - G1 Z2 E-0.1 F500 - G90 #Absolute Coordinates - G1 X{x_home_position} F12000 - -[gcode_macro _XY_Calibration] -variable_prime: False -variable_line_number: 0 -variable_line_spacing: 0 -variable_line_height: 0 -variable_line_top: 0 -variable_extrude_per_mm: 0 -variable_line_spacing_2: 0 -variable_initial_x_horizontal: 0 -variable_initial_y_horizontal: 0 -variable_initial_x_horizontal_2: 0 -variable_initial_y_horizontal_2: 0 -variable_initial_x_vertical: 0 -variable_initial_y_vertical: 0 -variable_initial_x_vertical_2: 0 -variable_initial_y_vertical_2: 0 -gcode: - M118 _XY_Calib. line number:{line_number} - M118 _XY_Calib. line spacing:{line_spacing} - M118 _XY_Calib. line height:{line_height} - M118 _XY_Calib. line top:{line_top} - M118 _XY_Calib. extrude per mm:{extrude_per_mm} - M118 _XY_Calib. line spacing 2:{line_spacing_2} - M118 _XY_Calib. initial x horizontal:{initial_x_horizontal} - M118 _XY_Calib. initial y horizontal:{initial_y_horizontal} - M118 _XY_Calib. initial x horizontal 2:{initial_x_horizontal_2} - M118 _XY_Calib. initial y horizontal 2:{initial_y_horizontal_2} - M118 _XY_Calib. initial x vertical:{initial_x_vertical} - M118 _XY_Calib. initial y vertical:{initial_y_vertical} - M118 _XY_Calib. initial x vertical 2:{initial_x_vertical_2} - M118 _XY_Calib. initial y vertical 2:{initial_y_vertical_2} - - G28 X Y - - {% if prime %} - _prime_Nozzle - {% else %} - M83 # Relative Coordinates for extruder only - G1 E0.1 - {% endif %} - - # - # Horizontal Print in tool 0 - # - G90 # Absolute Coordinates for all axes - M83 # Relative Coordinates for extruder only - - # Going to the start of the "brim" - G1 X{initial_x_horizontal_2} Y{initial_y_horizontal - 2} E-0.1 F12000 - G1 Z0.2 E0.5 F2000 - - # Making the U turn of the "brim" - G1 X{initial_x_horizontal} E{(initial_x_horizontal_2 - initial_x_horizontal) * extrude_per_mm} F3000 - - # Going to start of calibration - G1 Y{initial_y_horizontal} E{2 * extrude_per_mm} #this 2* could be updated to a variable - - # Printing the Zig zag pattern - G91 #Relative Coordinates - SET_GCODE_VARIABLE MACRO=_print_Horizontal VARIABLE=line_number VALUE={line_number} - SET_GCODE_VARIABLE MACRO=_print_Horizontal VARIABLE=line_spacing VALUE={line_spacing} - SET_GCODE_VARIABLE MACRO=_print_Horizontal VARIABLE=line_height VALUE={line_height} - SET_GCODE_VARIABLE MACRO=_print_Horizontal VARIABLE=line_top VALUE={line_top} - SET_GCODE_VARIABLE MACRO=_print_Horizontal VARIABLE=extrude_per_mm VALUE={extrude_per_mm} - _print_Horizontal - - # Raising the nozzle - G1 Z2 E-0.1 - - # - # Vertical Print in tool 0 - # - G90 # Absolute Coordinates for all axes - M83 # Relative Coordinates for extruder only - - # Going to the start of the "brim" - G1 X{initial_x_vertical - 2} Y{initial_y_vertical_2} E-0.1 F12000 - - # Lowering the Nozzle - G1 Z0.2 E0.5 F2000 - - # Making the U turn of the "brim" - G1 Y{initial_y_vertical} E{(initial_x_horizontal_2 - initial_x_horizontal) * extrude_per_mm} F3000 - - # Going to start of calibration - G1 X{initial_x_vertical} E{2 * extrude_per_mm} #this 2* could be updated to a variable - - # Printing the Zig zag pattern - G91 #Relative Coordinates - SET_GCODE_VARIABLE MACRO=_print_Vertical VARIABLE=line_number VALUE={line_number} - SET_GCODE_VARIABLE MACRO=_print_Vertical VARIABLE=line_spacing VALUE={line_spacing} - SET_GCODE_VARIABLE MACRO=_print_Vertical VARIABLE=line_height VALUE={line_height} - SET_GCODE_VARIABLE MACRO=_print_Vertical VARIABLE=line_top VALUE={line_top} - SET_GCODE_VARIABLE MACRO=_print_Vertical VARIABLE=extrude_per_mm VALUE={extrude_per_mm} - _print_Vertical - - # Parking the nozzle - G91 #Relative Coordinates - SET_GCODE_VARIABLE MACRO=_park_Nozzle VARIABLE=x_home_position VALUE={printer.configfile.config["stepper_x"].position_min|int + 5} - _park_Nozzle - - # - # Changing Tool - # - T1 - - {% if prime %} - _prime_Nozzle - {% else %} - M83 # Relative Coordinates for extruder only - G1 E0.1 - {% endif %} - - # - # Horizontal Print in tool 1 - # - G90 # Absolute Coordinates for all axes - M83 # Relative Coordinates for extruder only - - # Going to the start of the "brim" - G1 X{initial_x_horizontal} Y{initial_y_horizontal_2 + 2} E-0.1 F12000 - - # Lowering the nozzle - G1 Z0.2 E0.5 F2000 - - # Making the U turn of the "brim" - G1 X{initial_x_horizontal_2} E{(initial_x_horizontal_2 - initial_x_horizontal) * extrude_per_mm} F3000 - - # Going to start of calibration - G1 Y{initial_y_horizontal_2} E{2 * extrude_per_mm} #this 2* could be updated to a variable - - # Printing the Zig zag pattern - G91 #Relative Coordinates - SET_GCODE_VARIABLE MACRO=_print_Horizontal VARIABLE=line_number VALUE={line_number} - SET_GCODE_VARIABLE MACRO=_print_Horizontal VARIABLE=line_spacing VALUE=-{line_spacing_2} - SET_GCODE_VARIABLE MACRO=_print_Horizontal VARIABLE=line_height VALUE=-{line_height} - SET_GCODE_VARIABLE MACRO=_print_Horizontal VARIABLE=line_top VALUE=-{line_top} - SET_GCODE_VARIABLE MACRO=_print_Horizontal VARIABLE=extrude_per_mm VALUE={extrude_per_mm} - _print_Horizontal - - # Raising the nozzle - G1 Z2 E-0.1 - - # - # Vertical Print in tool 1 - # - G90 # Absolute Coordinates for all axes - M83 # Relative Coordinates for extruder only - - # Going to the start of the "brim" - G1 X{initial_x_vertical_2 + 2} Y{initial_y_vertical} E-0.1 F12000 - - # Lowering the Nozzle - G1 Z0.2 E0.5 F2000 - - # Making the U turn of the "brim" - G1 Y{initial_y_vertical_2} E{(initial_x_horizontal_2 - initial_x_horizontal) * extrude_per_mm} F3000 - - # Going to start of calibration - G1 X{initial_x_vertical_2} E{2 * extrude_per_mm} #this 2* could be updated to a variable - - # Printing the Zig zag pattern - G91 #Relative Coordinates - SET_GCODE_VARIABLE MACRO=_print_Vertical VARIABLE=line_number VALUE={line_number} - SET_GCODE_VARIABLE MACRO=_print_Vertical VARIABLE=line_spacing VALUE=-{line_spacing_2} - SET_GCODE_VARIABLE MACRO=_print_Vertical VARIABLE=line_height VALUE=-{line_height} - SET_GCODE_VARIABLE MACRO=_print_Vertical VARIABLE=line_top VALUE=-{line_top} - SET_GCODE_VARIABLE MACRO=_print_Vertical VARIABLE=extrude_per_mm VALUE={extrude_per_mm} - _print_Vertical - - # Parking the nozzle - G90 # Absolute Coordinates - SET_GCODE_VARIABLE MACRO=_park_Nozzle VARIABLE=x_home_position VALUE={printer.configfile.config["dual_carriage"].position_max|int - 5} - _park_Nozzle - G1 Y150 # So the user can see the lines properly - - # - # Changing Tool - # - T0 - - _select_X_Line - -[delayed_gcode _wait_For_User_X] -gcode: - M118 Wait for user X - {% set offset = printer['gcode_macro _select_X_Line'].line_selected %} - {% if not offset %} # If the line still hasn't been selected, loop this delayed g_code - UPDATE_DELAYED_GCODE ID=_wait_For_User_X duration=2 - {% else %} # When a Line has been selected, advance to the selection of the vertical line - {% if offset < 20 %} # If the selected line is the middle one, selected_line value is set to 20, but the offset is 0. Setting it to 0 would keep this code on a loop... - SAVE_VARIABLE VARIABLE=tool_2_x_offset VALUE={offset} # When a line is selected, a macro changing the line_selected variable is called. - #{% else %} - #SAVE_VARIABLE VARIABLE=tool_2_x_offset VALUE={0|float} - {% endif %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE=0 - - UPDATE_DELAYED_GCODE id=_wait_For_User_Y duration=2 - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE=0 - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=detailed VALUE=False - _select_Y_Line - {% endif %} - -[delayed_gcode _wait_For_User_Y] -gcode: - M118 Wait for user Y - {% set offset = printer['gcode_macro _select_Y_Line'].line_selected %} - {% if not offset %} # If the line still hasn't been selected, loop this delayed g_code - UPDATE_DELAYED_GCODE ID=_wait_For_User_Y duration=2 - {% else %} - {% if offset < 20 %} # If the selected line is the middle one, selected_line value is set to 20, but the offset is 0. Setting it to 0 would keep this code on a loop... - SAVE_VARIABLE VARIABLE=tool_2_y_offset VALUE={offset} # When a line is selected, a macro changing the line_selected variable is called. - #{% else %} - #SAVE_VARIABLE VARIABLE=tool_2_y_offset VALUE={0|float} - {% endif %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE=0 - - _XY_Calibration_Print_detailed - {% endif %} - -[gcode_macro _XY_Calibration_Print_detailed] -gcode: - - {% set line_number = printer["gcode_macro XY_Calibration_Print"].line_number %} - {% set line_spacing = printer["gcode_macro XY_Calibration_Print"].line_spacing %} - {% set line_height = printer["gcode_macro XY_Calibration_Print"].line_height %} - {% set line_top = printer["gcode_macro XY_Calibration_Print"].line_top %} - {% set extrude_per_mm = printer["gcode_macro XY_Calibration_Print"].extrude_per_mm %} - {% set line_spacing_2_detailed = printer["gcode_macro XY_Calibration_Print"].line_spacing_2_detailed %} - {% set initial_detailed_x_horizontal = printer["gcode_macro XY_Calibration_Print"].initial_detailed_x_horizontal %} - {% set initial_detailed_y_horizontal = printer["gcode_macro XY_Calibration_Print"].initial_detailed_y_horizontal %} - {% set initial_detailed_x_horizontal_2 = printer["gcode_macro XY_Calibration_Print"].initial_detailed_x_horizontal_2 %} - {% set initial_detailed_y_horizontal_2 = printer["gcode_macro XY_Calibration_Print"].initial_detailed_y_horizontal_2 %} - {% set initial_detailed_x_vertical = printer["gcode_macro XY_Calibration_Print"].initial_detailed_x_vertical %} - {% set initial_detailed_y_vertical = printer["gcode_macro XY_Calibration_Print"].initial_detailed_y_vertical %} - {% set initial_detailed_x_vertical_2 = printer["gcode_macro XY_Calibration_Print"].initial_detailed_x_vertical_2 %} - {% set initial_detailed_y_vertical_2 = printer["gcode_macro XY_Calibration_Print"].initial_detailed_y_vertical_2 %} - - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=prime VALUE=True - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_number VALUE={line_number} #TODO ITS NOT SETTING THESE VALUES - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_spacing VALUE={line_spacing} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_height VALUE={line_height} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_top VALUE={line_top} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=extrude_per_mm VALUE={extrude_per_mm} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_spacing_2 VALUE={line_spacing_2_detailed} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_x_horizontal VALUE={initial_detailed_x_horizontal} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_y_horizontal VALUE={initial_detailed_y_horizontal} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_x_horizontal_2 VALUE={initial_detailed_x_horizontal_2} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_y_horizontal_2 VALUE={initial_detailed_y_horizontal_2} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_x_vertical VALUE={initial_detailed_x_vertical} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_y_vertical VALUE={initial_detailed_y_vertical} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_x_vertical_2 VALUE={initial_detailed_x_vertical_2} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_y_vertical_2 VALUE={initial_detailed_y_vertical_2} - - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=prime VALUE=True - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_number VALUE={printer["gcode_macro XY_Calibration_Print"].line_number} #TODO ITS NOT SETTING THESE VALUES - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_spacing VALUE={printer["gcode_macro XY_Calibration_Print"].line_spacing} - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_height VALUE={printer["gcode_macro XY_Calibration_Print"].line_height} - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_top VALUE={printer["gcode_macro XY_Calibration_Print"].line_top} - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=extrude_per_mm VALUE={printer["gcode_macro XY_Calibration_Print"].extrude_per_mm} - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_spacing_2 VALUE={printer["gcode_macro XY_Calibration_Print"].line_spacing_2_detailed} - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_x_horizontal VALUE={printer["gcode_macro XY_Calibration_Print"].initial_detailed_x_horizontal} - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_y_horizontal VALUE={printer["gcode_macro XY_Calibration_Print"].initial_detailed_y_horizontal} - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_x_horizontal_2 VALUE={printer["gcode_macro XY_Calibration_Print"].initial_detailed_x_horizontal_2} - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_y_horizontal_2 VALUE={printer["gcode_macro XY_Calibration_Print"].initial_detailed_y_horizontal_2} - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_x_vertical VALUE={printer["gcode_macro XY_Calibration_Print"].initial_detailed_x_vertical} - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_y_vertical VALUE={printer["gcode_macro XY_Calibration_Print"].initial_detailed_y_vertical} - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_x_vertical_2 VALUE={printer["gcode_macro XY_Calibration_Print"].initial_detailed_x_vertical_2} - #SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_y_vertical_2 VALUE={printer["gcode_macro XY_Calibration_Print"].initial_detailed_y_vertical_2} - - #M118 Before Print Detailed line number:{printer["gcode_macro XY_Calibration_Print"].line_number} - #M118 Before Print Detailed line spacing:{printer["gcode_macro XY_Calibration_Print"].line_spacing} - #M118 Before Print Detailed line height:{printer["gcode_macro XY_Calibration_Print"].line_height} - #M118 Before Print Detailed line top:{printer["gcode_macro XY_Calibration_Print"].line_top} - #M118 Before Print Detailed extrude per mm:{printer["gcode_macro XY_Calibration_Print"].extrude_per_mm} - #M118 Before Print Detailed line spacing 2:{printer["gcode_macro XY_Calibration_Print"].line_spacing_2} - #M118 Before Print Detailed initial x horizontal:{printer["gcode_macro XY_Calibration_Print"].initial_x_horizontal} - #M118 Before Print Detailed initial y horizontal:{printer["gcode_macro XY_Calibration_Print"].initial_y_horizontal} - #M118 Before Print Detailed initial x horizontal 2:{printer["gcode_macro XY_Calibration_Print"].initial_x_horizontal_2} - #M118 Before Print Detailed initial y horizontal 2:{printer["gcode_macro XY_Calibration_Print"].initial_y_horizontal_2} - #M118 Before Print Detailed initial x vertical:{printer["gcode_macro XY_Calibration_Print"].initial_x_vertical} - #M118 Before Print Detailed initial y vertical:{printer["gcode_macro XY_Calibration_Print"].initial_y_vertical} - #M118 Before Print Detailed initial x vertical 2:{printer["gcode_macro XY_Calibration_Print"].initial_x_vertical_2} - #M118 Before Print Detailed initial y vertical 2:{printer["gcode_macro XY_Calibration_Print"].initial_y_vertical_2} - - RESPOND TYPE=command MSG="action:prompt_begin Printing Detailed Calibration Print" - RESPOND TYPE=command MSG="action:prompt_text When print ends, select which horizontal line is the most aligned." - RESPOND TYPE=command MSG="action:prompt_footer_button OK|RESPOND TYPE=command MSG=action:prompt_end|info" - RESPOND TYPE=command MSG="action:prompt_show" - - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE=0 - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=detailed VALUE=True - UPDATE_DELAYED_GCODE ID=_wait_For_User_X_Detailed duration=70 - - _XY_Calibration - -[delayed_gcode _wait_For_User_X_Detailed] -gcode: - M118 Wait for user X detailed - {% set offset = printer['gcode_macro _select_X_Line'].line_selected %} - {% if not offset %} #If the line still hasn't been selected, loop this delayed g_code - UPDATE_DELAYED_GCODE ID=_wait_For_User_X_Detailed duration=2 - {% else %} # When a Line has been selected, advance to the selection of the vertical line - {% if offset < 20 %} # If the selected line is the middle one, selected_line value is set to 20, but the offset is 0. Setting it to 0 would keep this code on a loop... - SAVE_VARIABLE VARIABLE=tool_2_x_offset VALUE={offset} # When a line is selected, a macro changing the line_selected variable is called. - #{% else %} - #SAVE_VARIABLE VARIABLE=tool_2_x_offset VALUE={0|float} - {% endif %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE=0 - UPDATE_DELAYED_GCODE ID=_wait_For_User_X_Detailed duration=0 - - UPDATE_DELAYED_GCODE id=_wait_For_User_Y_Detailed duration=3 - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE=0 - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=detailed VALUE=True - _select_Y_Line - {% endif %} - -[delayed_gcode _wait_For_User_Y_Detailed] -gcode: - M118 Wait for user Y detailed - {% set offset = printer['gcode_macro _select_Y_Line'].line_selected %} - {% if not offset %} #If the line still hasn't been selected, loop this delayed g_code - UPDATE_DELAYED_GCODE ID=_wait_For_User_Y_Detailed duration=2 - {% else %} - {% if offset < 20 %} - SAVE_VARIABLE VARIABLE=tool_2_y_offset VALUE={offset} # When a line is selected, a macro changing the line_selected variable is called. - #{% else %} - #SAVE_VARIABLE VARIABLE=tool_2_y_offset VALUE={0|float} - {% endif %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE=0 - UPDATE_DELAYED_GCODE ID=_wait_For_User_Y_Detailed duration=0 - - M118 XY Calibration Finished - {% endif %} - -[gcode_macro XY_Calibration_Print] -variable_printing_feedrate : 50 -variable_extrude_per_mm : 0.035 -variable_line_number : 10 # Must be an even number. Number of vertical calibration lines excluding center line. -variable_line_height : 20 # Line height -variable_line_top : 1 # Length of line top -variable_line_spacing : 8 # Distance between lines -variable_line_spacing_2 : 9 -variable_line_spacing_2_detailed : 8.1 -variable_deviation : 0 # line_spacing_2 - line_spacing -variable_deviation_detailed : 0 # line_spacing_2_detailed - line_spacing -variable_initial_x_horizontal : 230 -variable_initial_y_horizontal : 30 -variable_initial_x_horizontal_2 : 0 # initial_x_horizontal + ((line_spacing + line_top) * (line_number + 1)) + (line_number/2 * deviation) + line_spacing_2 -variable_initial_y_horizontal_2 : 0 # initial_y_horizontal + (line_height * 2) + 2 -variable_initial_x_vertical : 0 # initial_x_horizontal_2 + 20 -variable_initial_y_vertical : 0 # initial_y_horizontal -variable_initial_x_vertical_2 : 0 # initial_x_vertical + (line_height * 2) + 2 -variable_initial_y_vertical_2 : 0 # initial_y_vertical + ((line_spacing + line_top) * (line_number + 1)) + (line_number/2 * deviation) + line_spacing_2 -variable_initial_detailed_x_horizontal : 0 # initial_x_horizontal -variable_initial_detailed_y_horizontal : 0 # initial_y_horizontal_2 + line_height -variable_initial_detailed_x_horizontal_2 : 0 # initial_detailed_x_horizontal + ((line_spacing + line_top) * (line_number + 1)) + (line_number/2 * deviation_detailed) + line_spacing_2_detailed -variable_initial_detailed_y_horizontal_2 : 0 # initial_detailed_y_horizontal + (line_height * 2) + 2 -variable_initial_detailed_x_vertical : 0 # initial_x_vertical_2 + line_height -variable_initial_detailed_y_vertical : 0 # initial_y_horizontal -variable_initial_detailed_x_vertical_2 : 0 # initial_detailed_x_vertical + (line_height * 2) + 2 -variable_initial_detailed_y_vertical_2 : 0 # initial_detailed_y_vertical + ((line_spacing + line_top) * (line_number + 1)) + (line_number/2 * deviation_detailed) + line_spacing_2_detailed -gcode: - #SAVE_VARIABLE VARIABLE=tool_2_x_offset VALUE={0|float} # Reseting Values - #SAVE_VARIABLE VARIABLE=tool_2_y_offset VALUE={0|float} # Reseting Values - - {% set deviation = line_spacing_2 - line_spacing %} - - {% set initial_x_horizontal_2 = initial_x_horizontal + ((line_spacing + line_top) * (line_number + 1)) + (line_number/2 * deviation) + line_spacing_2 %} - {% set initial_y_horizontal_2 = initial_y_horizontal + (line_height * 2) + 2 %} - - {% set initial_x_vertical = initial_x_horizontal_2 + 20 %} - {% set initial_y_vertical = initial_y_horizontal %} - {% set initial_x_vertical_2 = initial_x_vertical + (line_height * 2) + 2 %} - {% set initial_y_vertical_2 = initial_y_vertical + ((line_spacing + line_top) * (line_number + 1)) + (line_number/2 * deviation) + line_spacing_2 %} - - {% set deviation_detailed = line_spacing_2_detailed - line_spacing %} - - {% set initial_detailed_x_horizontal = initial_x_horizontal %} - {% set initial_detailed_y_horizontal = initial_y_horizontal_2 + line_height %} - {% set initial_detailed_x_horizontal_2 = initial_detailed_x_horizontal + ((line_spacing + line_top) * (line_number + 1)) + (line_number/2 * deviation_detailed) + line_spacing_2_detailed %} - {% set initial_detailed_y_horizontal_2 = initial_detailed_y_horizontal + (line_height * 2) + 2 %} - - {% set initial_detailed_x_vertical = initial_x_vertical_2 + line_height %} - {% set initial_detailed_y_vertical = initial_y_horizontal %} - {% set initial_detailed_x_vertical_2 = initial_detailed_x_vertical + (line_height * 2) + 2 %} - {% set initial_detailed_y_vertical_2 = initial_detailed_y_vertical + ((line_spacing + line_top) * (line_number + 1)) + (line_number/2 * deviation_detailed) + line_spacing_2_detailed %} - - RESPOND TYPE=command MSG="action:prompt_begin XY Calibration Print" - RESPOND TYPE=command MSG="action:prompt_text Hotends heating to begin calibration print." - RESPOND TYPE=command MSG="action:prompt_show" - - {% set psv = printer.save_variables.variables %} - _SET_HEATER_TEMPERATURE HEATER=extruder TARGET={psv.filament_temp_1} - _SET_HEATER_TEMPERATURE HEATER=extruder1 TARGET={psv.filament_temp_2} - g28 #go to min positions - #TEMPERATURE_WAIT SENSOR=extruder MINIMUM={psv.filament_temp_1} - #TEMPERATURE_WAIT SENSOR=extruder1 MINIMUM={psv.filament_temp_2} - - SET_GCODE_VARIABLE MACRO=XY_Calibration_Print VARIABLE=initial_x_horizontal_2 VALUE={initial_x_horizontal_2} - SET_GCODE_VARIABLE MACRO=XY_Calibration_Print VARIABLE=initial_y_horizontal_2 VALUE={initial_y_horizontal_2} - SET_GCODE_VARIABLE MACRO=XY_Calibration_Print VARIABLE=initial_x_vertical VALUE={initial_x_vertical} - SET_GCODE_VARIABLE MACRO=XY_Calibration_Print VARIABLE=initial_y_vertical VALUE={initial_y_vertical} - SET_GCODE_VARIABLE MACRO=XY_Calibration_Print VARIABLE=initial_x_vertical_2 VALUE={initial_x_vertical_2} - SET_GCODE_VARIABLE MACRO=XY_Calibration_Print VARIABLE=initial_y_vertical_2 VALUE={initial_y_vertical_2} - - SET_GCODE_VARIABLE MACRO=XY_Calibration_Print VARIABLE=initial_detailed_x_horizontal VALUE={initial_detailed_x_horizontal} - SET_GCODE_VARIABLE MACRO=XY_Calibration_Print VARIABLE=initial_detailed_y_horizontal VALUE={initial_detailed_y_horizontal} - SET_GCODE_VARIABLE MACRO=XY_Calibration_Print VARIABLE=initial_detailed_x_horizontal_2 VALUE={initial_detailed_x_horizontal_2} - SET_GCODE_VARIABLE MACRO=XY_Calibration_Print VARIABLE=initial_detailed_y_horizontal_2 VALUE={initial_detailed_y_horizontal_2} - SET_GCODE_VARIABLE MACRO=XY_Calibration_Print VARIABLE=initial_detailed_x_vertical VALUE={initial_detailed_x_vertical} - SET_GCODE_VARIABLE MACRO=XY_Calibration_Print VARIABLE=initial_detailed_y_vertical VALUE={initial_detailed_y_vertical} - SET_GCODE_VARIABLE MACRO=XY_Calibration_Print VARIABLE=initial_detailed_x_vertical_2 VALUE={initial_detailed_x_vertical_2} - SET_GCODE_VARIABLE MACRO=XY_Calibration_Print VARIABLE=initial_detailed_y_vertical_2 VALUE={initial_detailed_y_vertical_2} - - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=prime VALUE=True - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_number VALUE={line_number} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_spacing VALUE={line_spacing} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_height VALUE={line_height} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_top VALUE={line_top} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=extrude_per_mm VALUE={extrude_per_mm} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=line_spacing_2 VALUE={line_spacing_2} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_x_horizontal VALUE={initial_x_horizontal} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_y_horizontal VALUE={initial_y_horizontal} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_x_horizontal_2 VALUE={initial_x_horizontal_2} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_y_horizontal_2 VALUE={initial_y_horizontal_2} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_x_vertical VALUE={initial_x_vertical} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_y_vertical VALUE={initial_y_vertical} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_x_vertical_2 VALUE={initial_x_vertical_2} - SET_GCODE_VARIABLE MACRO=_XY_Calibration VARIABLE=initial_y_vertical_2 VALUE={initial_y_vertical_2} - - RESPOND TYPE=command MSG="action:prompt_begin Printing Calibrating Print" - RESPOND TYPE=command MSG="action:prompt_text Wait for print to end and then select which horizontal line is the most aligned." - RESPOND TYPE=command MSG="action:prompt_footer_button OK|RESPOND TYPE=command MSG=action:prompt_end|info" - RESPOND TYPE=command MSG="action:prompt_show" - - M118 line number:{line_number} - M118 line height:{line_height} - M118 line top:{line_top} - M118 line spacing:{line_spacing} - M118 line spacing 2:{line_spacing_2} - M118 Detailed line spacing 2 :{line_spacing_2_detailed} - M118 deviation:{deviation} - M118 Detailed deviation:{deviation_detailed} - M118 initial x horizontal:{initial_x_horizontal} - M118 initial y horizontal:{initial_y_horizontal} - M118 initial x horizontal 2:{initial_x_horizontal_2} - M118 initial y horizontal 2:{initial_y_horizontal_2} - M118 initial x vertical:{initial_x_vertical} - M118 initial y vertical:{initial_y_vertical} - M118 initial x vertical 2:{initial_x_vertical_2} - M118 initial y vertical 2:{initial_y_vertical_2} - M118 Detailed initial x horizontal:{initial_detailed_x_horizontal} - M118 Detailed initial y horizontal:{initial_detailed_y_horizontal} - M118 Detailed initial x horizontal 2:{initial_detailed_x_horizontal_2} - M118 Detailed initial y horizontal 2:{initial_detailed_y_horizontal_2} - M118 Detailed initial x vertical:{initial_detailed_x_vertical} - M118 Detailed initial y vertical:{initial_detailed_y_vertical} - M118 Detailed initial x vertical 2:{initial_detailed_x_vertical_2} - M118 Detailed initial y vertical 2:{initial_detailed_y_vertical_2} - - #SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE=0 - #SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE=0 - #SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=detailed VALUE=False - #SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=detailed VALUE=False - #UPDATE_DELAYED_GCODE ID=_wait_For_User_X duration=70 - - #_XY_Calibration - - #G91 # Relative Coordinates - -[gcode_macro print_shit] -variable_prime: False -variable_line_number: 0 -variable_line_spacing: 0 -variable_line_height: 0 -variable_line_top: 0 -variable_extrude_per_mm: 0 -variable_line_spacing_2: 0 -variable_initial_x_horizontal: 0 -variable_initial_y_horizontal: 50 -variable_initial_x_horizontal_2: 50 -variable_initial_y_horizontal_2: 0 -variable_initial_x_vertical: 0 -variable_initial_y_vertical: 0 -variable_initial_x_vertical_2: 0 -variable_initial_y_vertical_2: 0 -gcode: - UPDATE_DELAYED_GCODE ID=_wait_For_User_X duration=1 - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=detailed VALUE=False - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE=0 - _select_X_Line - - -[gcode_macro _select_X_Line] -variable_line_selected: 0 -variable_detailed: False -gcode: - RESPOND TYPE=command MSG="action:prompt_begin Select Best Matching Line" - RESPOND TYPE=command MSG="action:prompt_text When print ends, select which horizontal line is the most aligned." - RESPOND TYPE=command MSG="action:prompt_button_group_start" - RESPOND TYPE=command MSG="action:prompt_button 1|_horizontal_1|info" - RESPOND TYPE=command MSG="action:prompt_button 2|_horizontal_2|info" - RESPOND TYPE=command MSG="action:prompt_button 3|_horizontal_3|info" - RESPOND TYPE=command MSG="action:prompt_button 4|_horizontal_4|info" - RESPOND TYPE=command MSG="action:prompt_button_group_end" - RESPOND TYPE=command MSG="action:prompt_button_group_start" - RESPOND TYPE=command MSG="action:prompt_button 5|_horizontal_5|info" - RESPOND TYPE=command MSG="action:prompt_button 6|_horizontal_6|info" - RESPOND TYPE=command MSG="action:prompt_button 7|_horizontal_7|info" - RESPOND TYPE=command MSG="action:prompt_button 8|_horizontal_8|info" - RESPOND TYPE=command MSG="action:prompt_button_group_end" - RESPOND TYPE=command MSG="action:prompt_button_group_start" - RESPOND TYPE=command MSG="action:prompt_button 9|_horizontal_9|info" - RESPOND TYPE=command MSG="action:prompt_button 10|_horizontal_10|info" - RESPOND TYPE=command MSG="action:prompt_button 11|_horizontal_11|info" - RESPOND TYPE=command MSG="action:prompt_button_group_end" - RESPOND TYPE=command MSG="action:prompt_show" - -[gcode_macro _select_Y_Line] -variable_line_selected: 0 -variable_detailed: False -gcode: - RESPOND TYPE=command MSG="action:prompt_begin Select Best Matching Line" - RESPOND TYPE=command MSG="action:prompt_text When print ends, select which vertical line is the most aligned." - RESPOND TYPE=command MSG="action:prompt_button_group_start" - RESPOND TYPE=command MSG="action:prompt_button 1|_vertical_1|info" - RESPOND TYPE=command MSG="action:prompt_button 2|_vertical_2|info" - RESPOND TYPE=command MSG="action:prompt_button 3|_vertical_3|info" - RESPOND TYPE=command MSG="action:prompt_button 4|_vertical_4|info" - RESPOND TYPE=command MSG="action:prompt_button_group_end" - RESPOND TYPE=command MSG="action:prompt_button_group_start" - RESPOND TYPE=command MSG="action:prompt_button 5|_vertical_5|info" - RESPOND TYPE=command MSG="action:prompt_button 6|_vertical_6|info" - RESPOND TYPE=command MSG="action:prompt_button 7|_vertical_7|info" - RESPOND TYPE=command MSG="action:prompt_button 8|_vertical_8|info" - RESPOND TYPE=command MSG="action:prompt_button_group_end" - RESPOND TYPE=command MSG="action:prompt_button_group_start" - RESPOND TYPE=command MSG="action:prompt_button 9|_vertical_9|info" - RESPOND TYPE=command MSG="action:prompt_button 10|_vertical_10|info" - RESPOND TYPE=command MSG="action:prompt_button 11|_vertical_11|info" - RESPOND TYPE=command MSG="action:prompt_button_group_end" - RESPOND TYPE=command MSG="action:prompt_show" - -################################################################################################################### -# -# Macros for selecting the best matching lines -# -################################################################################################################### - - -[gcode_macro _horizontal_1] # It would be better to pass an argument than have a ton of similar functions, but it is what it is. -gcode: - {% if not printer['gcode_macro _select_X_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float - 5|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float - 0.5|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _horizontal_2] -gcode: - {% if not printer['gcode_macro _select_X_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float - 4|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float - 0.4|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _horizontal_3] -gcode: - {% if not printer['gcode_macro _select_X_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float - 3|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float - 0.3|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _horizontal_4] -gcode: - {% if not printer['gcode_macro _select_X_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float - 2|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float - 0.2|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _horizontal_5] -gcode: - {% if not printer['gcode_macro _select_X_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float - 1|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float - 0.1|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _horizontal_6] -gcode: - {% if not printer['gcode_macro _select_X_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={20|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={20|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _horizontal_7] -gcode: - {% if not printer['gcode_macro _select_X_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float + 1|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float + 0.1|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _horizontal_8] -gcode: - {% if not printer['gcode_macro _select_X_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float + 2|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float + 0.2|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _horizontal_9] -gcode: - {% if not printer['gcode_macro _select_X_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float + 3|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float + 0.3|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _horizontal_10] -gcode: - {% if not printer['gcode_macro _select_X_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float + 4|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float + 0.4|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _horizontal_11] -gcode: - {% if not printer['gcode_macro _select_X_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float + 5|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_X_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_x_offset|float + 0.5|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _vertical_1] # It would be better to pass an argument than have a ton of similar functions, but it is what it is. -gcode: - {% if not printer['gcode_macro _select_Y_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float + 5|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float + 0.5|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _vertical_2] -gcode: - {% if not printer['gcode_macro _select_Y_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float + 4|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float + 0.4|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _vertical_3] -gcode: - {% if not printer['gcode_macro _select_Y_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float + 3|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float + 0.3|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _vertical_4] -gcode: - {% if not printer['gcode_macro _select_Y_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float + 2|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float + 0.2|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _vertical_5] -gcode: - {% if not printer['gcode_macro _select_Y_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float + 1|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float + 0.1|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _vertical_6] -gcode: - {% if not printer['gcode_macro _select_Y_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={20|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={20|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _vertical_7] -gcode: - {% if not printer['gcode_macro _select_Y_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float - 1|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float - 0.1|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _vertical_8] -gcode: - {% if not printer['gcode_macro _select_Y_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float - 2|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float - 0.2|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _vertical_9] -gcode: - {% if not printer['gcode_macro _select_Y_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float - 3|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float - 0.3|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _vertical_10] -gcode: - {% if not printer['gcode_macro _select_Y_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float - 4|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float - 0.4|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" - -[gcode_macro _vertical_11] -gcode: - {% if not printer['gcode_macro _select_Y_Line'].detailed %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float - 5|float} - {% else %} - SET_GCODE_VARIABLE MACRO=_select_Y_Line VARIABLE=line_selected VALUE={printer.save_variables.variables.tool_2_y_offset|float - 0.5|float} - {% endif %} - RESPOND TYPE=command MSG="action:prompt_end" diff --git a/extras/k_macros/macros_essentials.cfg b/extras/k_macros/macros_essentials.cfg deleted file mode 100644 index c1900257..00000000 --- a/extras/k_macros/macros_essentials.cfg +++ /dev/null @@ -1,340 +0,0 @@ -[gcode_macro TEMP_CHECK] -description: Check temperature on the nozzle, heat the nozzle if needed -gcode: - {% set T = params.T|default(200)|float %} - {% set garantee = params.G|default(0)|float %} - {% if printer.extruder.target !=0 and garantee == 1 %} - {% if printer.extruder.temperature < printer.extruder.target %} - M109 S{printer.extruder.target|float} - {action_respond_info("Waiting to reach target temperature: %d."%(printer.extruder.target|float))} - {% endif %} - {% else %} - {% if printer.extruder.target < T %} - M109 S{T} - {% endif %} - {% endif %} - {% if garantee == 1 %} - M109 S{T} - {% endif %} - -####################################################################################### -[gcode_macro GO_TO_BUCKET] -gcode: - RESTORE_DEFAULT_BOUNDARY - G90 - G1 X-20 Y80 F12000 - M400 - SET_CUSTOM_BOUNDARY MOTE_TO_PARK=False - -####################################################################################### - -[gcode_macro HOME_IF_NEEDED] -gcode: - {% if "xyz" not in printer.toolhead.homed_axes %} - G28 - {% endif %} - -####################################################################################### - -[gcode_macro CLEAN_NOZZLE] -variable_start_x: -20 -variable_start_y: 80 -variable_start_y_2: 100 -variable_wipe_dist: 20 -variable_wipe_dist_2: 45 -variable_wipe_qty: 8 -variable_wipe_spd: 500 -variable_raise_distance: 20 - -gcode: - #TODO: Reagir consoante o filamento introduzido - RESTORE_DEFAULT_BOUNDARY - SET_HEATER_TEMPERATURE HEATER=extruder TARGET=190 - TEMPERATURE_WAIT SENSOR=extruder MINIMUM=190 - G90 ; absolute positioning - ## Move nozzle to start position - G1 X{start_x-(start_x)} Y{start_y} F12000 - G1 X{start_x} Y{start_y} F12000 - - - ## Wipe nozzle - {% for wipes in range(1, (wipe_qty + 1)) %} - G1 X{start_x} Y{start_y + wipe_dist} F{wipe_spd * 60} - G1 Y{start_y} F{wipe_spd * 60} - {% endfor %} - {% for wipes in range(1, (wipe_qty + 1)) %} - G1 X{start_x} Y{start_y_2 + wipe_dist_2} F{wipe_spd * 60} - G1 Y{start_y_2} F{wipe_spd * 60} - {% endfor %} - G92 E0 - G1 E-1 - G92 E0 - - SET_CUSTOM_BOUNDARY MOVE_TO_PARK=False - M400 #Wait for movements to finish - -####################################################################################### - -[gcode_macro PRIME_EXTRUDER] -# Do nozzle priming -gcode: - RESTORE_DEFAULT_BOUNDARY - - M117 Priming - G92 E0 - G1 X-3 Y10 F18000 - G1 Z1.0 F600 - G1 X-3 Y200 Z0.3 F2400.0 E20; Draw the first line - G1 X-2.6 Y200 Z0.3 F4800.0; Move to the side a little - G1 X-2.6 Y10.0 Z0.3 F2400.0 E40; Draw the second line - G92 E0 - G1 F2400 E-0.7 # Small retract to remove pressure - G1 Z2.0 F600 ;Move Z Axis up - SET_CUSTOM_BOUNDARY MOVE_TO_PARK=False - - -####################################################################################### - -[gcode_macro PRINT_START] -description: Machine Print start macro. -variable_bed_temperature:0 -variable_extruder_temperature:0 -#variable_material_type: "XXX" -gcode: - #TODO: Messages are always displayed instantly, need to conditionally send the messages, or else the temperature messages will be displayed and sent all at the same time - - - {% set BED = params.BED| default(60) |int %} - {% set EXTRUDER = params.EXTRUDER| default(190) | int %} - - SET_GCODE_VARIABLE MACRO=PRINT_START VARIABLE=bed_temperature VALUE={BED} - SET_GCODE_VARIABLE MACRO=PRINT_START VARIABLE=extruder_temperature VALUE={EXTRUDER} - - CLEAR_PAUSE - # Set the belay to desired values - #BELAY_SET_MULTIPLIER BELAY=belay_extruder HIGH=1.1 LOW=0.90 - RESPOND MSG="Starting print...." - SET_HEATER_TEMPERATURE HEATER=heater_bed TARGET={BED} ; set bed temp - SET_HEATER_TEMPERATURE HEATER=extruder TARGET=190 ; set nozzle temp for any filament - {action_respond_info("Heating extruder to 190ºC")} - TEMPERATURE_WAIT SENSOR=extruder MINIMUM=190 - RESPOND MSG="Homing..." - G21 ; set to mm - M220 S100 ; set print speed to 100% - M221 S100 ; set flow rate to 100% - M107 ; disable fans - G90 ; absolute positioning - HOME_IF_NEEDED - - - RESPOND MSG="Leveling gantry..." - #QUAD_GANTRY_LEVEL - Z_TILT_ADJUST - RESPOND MSG="Cleaning nozzle..." - CLEAN_NOZZLE - G28 Z - - M400 ## Wait for moves to finish - - {action_respond_info("Heating bed to %dºC" % (BED))} - TEMPERATURE_WAIT SENSOR=heater_bed MINIMUM={BED} ; wait for bed temp - - RESPOND MSG="Building mesh." - BED_MESH_CALIBRATE - - {action_respond_info("Heating extruder to %dºC" % (EXTRUDER))} - SET_HEATER_TEMPERATURE HEATER=extruder TARGET={EXTRUDER} - TEMPERATURE_WAIT SENSOR=extruder MINIMUM={EXTRUDER} ; wait for extruder temp - #SET_FILAMENT_SENSOR SENSOR=ToolHeadFilamentSensor ENABLE=1 - #SET_FILAMENT_SENSOR SENSOR=Spool_filament_sensor ENABLE=1 - - M117 Purge Line - RESPOND MSG="Purge Line" - M83 ; extruder to relative mode - G92 E0 ; Reset extruder - PRIME_EXTRUDER - G92 E0 - M400 ; clear buffer - M117 Printing - RESPOND MSG="Printing." -####################################################################################### - -[gcode_macro CANCEL_PRINT] -description: Cancel a running print job. -rename_existing: BASE_CANCEL_PRINT -gcode: - {% set park_x = printer.toolhead.axis_minimum.x |default(100)|int %} - {% set park_y = printer.toolhead.axis_minimum.y + 1|default(0)|int %} - {% set current_print_z = printer.gcode_move.position.z|float %} - # Raise z axis so to not hit the printed piece. - # And Get how much we can raise the z axis without hitting the z maximum - - {% if ((current_print_z + 15) <= (printer.toolhead.axis_maximum.z )) %} - {action_respond_info("Raising z height by 15")} - G91 - G1 Z15 F900 - {% else %} - G91 - G1 Z{printer.toolhead.axis_maximum.z - current_print_z} F900 - {action_respond_info("Raising z height by %d"%(printer.toolhead.axis.maximum.z - current_print_z))} - {% endif %} - # Move the toolhead to the defined park positions - G90 - G1 X{park_x} Y{park_y} F12000 - # If a cancel is made after a pause print, we need to clear the pause. - CLEAR_PAUSE - SET_IDLE_TIMEOUT TIMEOUT={printer.configfile.settings.idle_timeout.timeout} - SDCARD_RESET_FILE - SET_FILAMENT_SENSOR SENSOR=ToolHeadFilamentSensor ENABLE=0 - SET_FILAMENT_SENSOR SENSOR=Spool_filament_sensor ENABLE=0 - BASE_CANCEL_PRINT - DISABLE_ALL - M104 S0 - M140 S0 - -####################################################################################### - -[gcode_macro PRINT_END] -gcode: - - # Retract a little bit - G91 - G1 E-2 F1800 - # Move with abosilute positions - G90 # Absolute positioning - # Raise the toohead - G1 Z{ printer.gcode_move.position.z + 2 if (printer.gcode_move.position.z + 2 ) <= (printer.toolhead.axis_maximum.z) else 0 } F1300 - - G1 X0 Y1000 F12000 - # Turn off bed, extruder, and fan - SET_FILAMENT_SENSOR SENSOR=ToolHeadFilamentSensor ENABLE=0 - SET_FILAMENT_SENSOR SENSOR=Spool_filament_sensor ENABLE=0 - - BELAY_SET_MULTIPLIER BELAY=belay_extruder HIGH=1.0 LOW=1.0 - - M140 S0 - M104 S0 - M106 S0 - # Disable steppers and others - DISABLE_ALL - M117 Print complete - REPSOND MSG="Print complete" - -###################################################################################### - - -###################################################################################### -###################################################################################### -###################################LOAD/UNLOAD######################################## -###################################################################################### -###################################################################################### -# START HEATING ON THE BEGGINING ONLY WAIT WHEN THE FILAMENT IS AT THE TOOLHEAD EXTRUDER - -[gcode_macro LOAD_FILAMENT] -description: Loads filament. -gcode: - {% set speed = params.SPEED|default(600)|int %} - RESPOND MSG="Heating the extruder for loading." - HOME_IF_NEEDED - - {action_respond_info("Loading Filament")} - - - - SAVE_VARIABLE VARIABLE=loading VALUE=True - - SAVE_GCODE_STATE NAME=LOAD_FILAMENT_state - # Control variable to indicate that we are on a loading prodedure - RESTORE_DEFAULT_BOUNDARY - GO_TO_BUCKET - M400 - - # Check nozzle temperature - TEMP_CHECK T=250 - - RESPOND MSG="Loading...." - G91 - G92 E0 - - FORCE_MOVE STEPPER="extruder_stepper feeder_extruder" VELOCITY=100 DISTANCE=1050 - - # * Extrude until it its the filament cutter sensor - G1 E38 F900 - - - # G1 E50 F9i00 - G1 E20 F300 - G1 E30 F120 - G1 E30 F120 - - - #SET_FILAMENT_SENSOR SENSOR=ToolHeadFilamentSensor ENABLE=1 - #SET_FILAMENT_SENSOR SENSOR=Spool_filament_sensor ENABLE=1 - # dwell for 5 seconds - G4 P5000 - - G92 E0 - - M400 - - RESTORE_GCODE_STATE NAME=LOAD_FILAMENT_state - - # * Enable filament sensors again - - #SET_FILAMENT_SENSOR SENSOR=Spool_filament_sensor ENABLE=0 - #SET_FILAMENT_SENSOR SENSOR=ToolHeadFilamentSensor ENABLE=0 - SET_CUSTOM_BOUNDARY MOVE_TO_PARK=False - - M104 S0 - SAVE_VARIABLE VARIABLE=loading VALUE=False -###################################################################################### - -[gcode_macro UNLOAD_FILAMENT] -description: Unload the filament -gcode: - {% set speed = params.SPEED|default(100)|int %} - {% set temp = params.TEMP|default(220)|int %} - {action_respond_info("Unloading Filament")} - # Control vairable to indicate that we are in an unloading procedure - SAVE_VARIABLE VARIABLE=unloading VALUE=True - # Disable the filament motion sensor for - #SET_FILAMENT_SENSOR SENSOR=ToolHeadFilamentSensor ENABLE=0 - #SET_FILAMENT_SENSOR SENSOR=Spool_filament_sensor ENABLE=0 - #check the nozzle temperature - SET_HEATER_TEMPERATURE HEATER=extruder TARGET={temp} - TEMPERATURE_WAIT SENSOR=extruder MINIMUM={temp} - #TEMP_CHECK T=250 - HOME_IF_NEEDED - M400 - SAVE_GCODE_STATE NAME=UNLOAD_FILAMENT_state - RESTORE_DEFAULT_BOUNDARY - # Make the movements for unloading save previous state first - CUT TEMP={temp} SENSOR=extruder_cutter MOTE_TO_LAST_POS=False TURN_OFF_HEATER=False - M400 - - - G91 - G92 E0 -# G1 E30 F480 - G1 E-100 F2000 - G4 P5000 - - FORCE_MOVE STEPPER="extruder_stepper feeder_extruder" VELOCITY=100 DISTANCE=-1300 - - #Flush klippers buffer to ensure the unload is done before continuing - M400 - G92 E0 - - RESTORE_GCODE_STATE NAME=UNLOAD_FILAMENT_state - - #SET_FILAMENT_SENSOR SENSOR=ToolHeadFilamentSensor ENABLE=1 - #SET_FILAMENT_SENSOR SENSOR=Spool_filament_sensor ENABLE=1 - #Disable the temperature after the unloading - SET_CUSTOM_BOUNDARY MOVE_TO_PARK=False - M104 S0 - - - -###################################################################################### -###################################################################################### -###################################################################################### diff --git a/extras/k_macros/speed_test_macro.cfg b/extras/k_macros/speed_test_macro.cfg deleted file mode 100644 index 992e4d5c..00000000 --- a/extras/k_macros/speed_test_macro.cfg +++ /dev/null @@ -1,126 +0,0 @@ - -[gcode_macro TEST_SPEED] -# Home, get position, throw around toolhead, home again. -# If MCU stepper positions (first line in GET_POSITION) are greater than a full step different (your number of microsteps), then skipping occured. -# We only measure to a full step to accomodate for endstop variance. -# Example: TEST_SPEED SPEED=300 ACCEL=5000 ITERATIONS=10 - -description: Test for max speed and acceleration parameters for the printer. Procedure: Home -> ReadPositionFromMCU -> MovesToolhead@Vel&Accel -> Home -> ReadPositionfromMCU - -gcode: - # Speed - {% set speed = params.SPEED|default(printer.configfile.settings.printer.max_velocity)|int %} - # Iterations - {% set iterations = params.ITERATIONS|default(5)|int %} - # Acceleration - {% set accel = params.ACCEL|default(printer.configfile.settings.printer.max_accel)|int %} - # Minimum Cruise Ratio - {% set min_cruise_ratio = params.MIN_CRUISE_RATIO|default(0)|float %} # Possible location in printer object stack: printer.configfile.settings.printer.minimum_cruise_ratio - # Bounding inset for large pattern (helps prevent slamming the toolhead into the sides after small skips, and helps to account for machines with imperfectly set dimensions) - {% set bound = params.BOUND|default(20)|int %} - # Size for small pattern box - {% set smallpatternsize = SMALLPATTERNSIZE|default(20)|int %} - - # Large pattern - # Max positions, inset by BOUND - {% set x_min = printer.toolhead.axis_minimum.x + bound %} - {% set x_max = printer.toolhead.axis_maximum.x - bound %} - {% set y_min = printer.toolhead.axis_minimum.y + bound %} - {% set y_max = printer.toolhead.axis_maximum.y - bound %} - - # Small pattern at center - # Find X/Y center point - {% set x_center = (printer.toolhead.axis_minimum.x|float + printer.toolhead.axis_maximum.x|float ) / 2 %} - {% set y_center = (printer.toolhead.axis_minimum.y|float + printer.toolhead.axis_maximum.y|float ) / 2 %} - - # Set small pattern box around center point - {% set x_center_min = x_center - (smallpatternsize/2) %} - {% set x_center_max = x_center + (smallpatternsize/2) %} - {% set y_center_min = y_center - (smallpatternsize/2) %} - {% set y_center_max = y_center + (smallpatternsize/2) %} - - # Save current gcode state (absolute/relative, etc) - SAVE_GCODE_STATE NAME=TEST_SPEED - - # Output parameters to g-code terminal - { action_respond_info("TEST_SPEED: starting %d iterations at speed %d, accel %d" % (iterations, speed, accel)) } - - # Home and get position for comparison later: - M400 # Finish moves - https://github.com/AndrewEllis93/Print-Tuning-Guide/issues/66 - G28 X Y - RESTORE_DEFAULT_BOUNDARY - # QGL if not already QGLd (only if QGL section exists in config) - #{% if printer.configfile.settings.quad_gantry_level %} - # {% if printer.quad_gantry_level.applied == False %} - # QUAD_GANTRY_LEVEL - # G28 Z - # {% endif %} - #{% endif %} - # Move 50mm away from max position and home again (to help with hall effect endstop accuracy - https://github.com/AndrewEllis93/Print-Tuning-Guide/issues/24) - G90 - G1 X{printer.toolhead.axis_maximum.x-50} Y{printer.toolhead.axis_maximum.y-50} F{30*60} - M400 # Finish moves - https://github.com/AndrewEllis93/Print-Tuning-Guide/issues/66 - G28 X Y - RESTORE_DEFAULT_BOUNDARY - - G0 X{printer.toolhead.axis_maximum.x-1} Y{printer.toolhead.axis_maximum.y-1} F{30*60} - G4 P1000 - GET_POSITION - - # Go to starting position - G0 X{x_min} Y{y_min} F{speed*60} #Z{bound + 10} - - # Set new limits - # SET_VELOCITY_LIMIT VELOCITY={speed} ACCEL={accel} ACCEL_TO_DECEL={accel / 2} # DEPRECATED | Due to deprecation of accel_to_decel parameter in Klipper - SET_VELOCITY_LIMIT VELOCITY={speed} ACCEL={accel} MIN_CRUISE_RATIO={minCruiseRatio} - - {% for i in range(iterations) %} - # Large pattern diagonals - G0 X{x_min} Y{y_min} F{speed*60} - G0 X{x_max} Y{y_max} F{speed*60} - G0 X{x_min} Y{y_min} F{speed*60} - G0 X{x_max} Y{y_min} F{speed*60} - G0 X{x_min} Y{y_max} F{speed*60} - G0 X{x_max} Y{y_min} F{speed*60} - - # Large pattern box - G0 X{x_min} Y{y_min} F{speed*60} - G0 X{x_min} Y{y_max} F{speed*60} - G0 X{x_max} Y{y_max} F{speed*60} - G0 X{x_max} Y{y_min} F{speed*60} - - # Small pattern diagonals - G0 X{x_center_min} Y{y_center_min} F{speed*60} - G0 X{x_center_max} Y{y_center_max} F{speed*60} - G0 X{x_center_min} Y{y_center_min} F{speed*60} - G0 X{x_center_max} Y{y_center_min} F{speed*60} - G0 X{x_center_min} Y{y_center_max} F{speed*60} - G0 X{x_center_max} Y{y_center_min} F{speed*60} - - # Small patternbox - G0 X{x_center_min} Y{y_center_min} F{speed*60} - G0 X{x_center_min} Y{y_center_max} F{speed*60} - G0 X{x_center_max} Y{y_center_max} F{speed*60} - G0 X{x_center_max} Y{y_center_min} F{speed*60} - {% endfor %} - - # Restore max speed/accel/accel_to_decel to their configured values - #SET_VELOCITY_LIMIT VELOCITY={printer.configfile.settings.printer.max_velocity} ACCEL={printer.configfile.settings.printer.max_accel} ACCEL_TO_DECEL={printer.configfile.settings.printer.max_accel_to_decel} # DEPRECATED | Depercating of accel_to_decel param in klipper - SET_VELOCITY_LIMIT VELOCITY={printer.configfile.settings.printer.max_velocity} ACCEL={printer.configfile.settings.printer.max_accel} MIN_CRUISE_RATIO={printer.configfile.settings.printer.minimum_cruise_ratio} - - # Re-home and get position again for comparison: - M400 # Finish moves - https://github.com/AndrewEllis93/Print-Tuning-Guide/issues/66 - G28 XY # This is a full G28 to fix an issue with CoreXZ - https://github.com/AndrewEllis93/Print-Tuning-Guide/issues/12 - # Go to XY home positions (in case your homing override leaves it elsewhere) - RESTORE_DEFAULT_BOUNDARY - G90 - G0 X{printer.toolhead.axis_maximum.x-1} Y{printer.toolhead.axis_maximum.y-1} F{30*60} - G4 P1000 - GET_POSITION - - # Restore previous gcode state (absolute/relative, etc) - RESTORE_GCODE_STATE NAME=TEST_SPEED - - - -###################################################################################### \ No newline at end of file From 265504956581d7fc077e60f1dde87c7a3689fa0d Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Tue, 3 Mar 2026 16:20:06 +0000 Subject: [PATCH 57/70] Feat/eddy calibration panel (#188) * ADD: added eddy calibration logic * Rev: removed gcode movement --------- Co-authored-by: Roberto --- BlocksScreen/lib/panels/controlTab.py | 12 +++ BlocksScreen/lib/panels/mainWindow.py | 2 +- .../lib/panels/widgets/connectionPage.py | 15 ++- .../lib/panels/widgets/probeHelperPage.py | 99 +++++++++++++------ 4 files changed, 95 insertions(+), 33 deletions(-) diff --git a/BlocksScreen/lib/panels/controlTab.py b/BlocksScreen/lib/panels/controlTab.py index f2cc8412..28e48c2c 100644 --- a/BlocksScreen/lib/panels/controlTab.py +++ b/BlocksScreen/lib/panels/controlTab.py @@ -46,6 +46,7 @@ class ControlTab(QtWidgets.QStackedWidget): str, name="request-file-info" ) call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") + toggle_conn_page = QtCore.pyqtSignal(bool, name="call-load-panel") tune_display_buttons: dict = {} card_options: dict = {} @@ -76,6 +77,8 @@ def __init__( self.move_length: float = 1.0 self.move_speed: float = 25.0 self.probe_helper_page = ProbeHelper(self) + self.probe_helper_page.toggle_conn_page.connect(self.toggle_conn_page) + self.probe_helper_page.disable_popups.connect(self.disable_popups) self.addWidget(self.probe_helper_page) self.probe_helper_page.call_load_panel.connect(self.call_load_panel) self.printcores_page = SwapPrintcorePage(self) @@ -91,6 +94,15 @@ def __init__( self.probe_helper_page.query_printer_object.connect(self.ws.api.object_query) self.probe_helper_page.run_gcode_signal.connect(self.ws.api.run_gcode) self.probe_helper_page.request_back.connect(self.back_button) + self.printer.print_stats_update[str, str].connect( + self.probe_helper_page.on_print_stats_update + ) + self.printer.print_stats_update[str, dict].connect( + self.probe_helper_page.on_print_stats_update + ) + self.printer.print_stats_update[str, float].connect( + self.probe_helper_page.on_print_stats_update + ) self.printer.available_gcode_cmds.connect( self.probe_helper_page.on_available_gcode_cmds ) diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index ca3d5326..2a36b5cd 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -240,7 +240,7 @@ def __init__(self): self, LoadingOverlayWidget.AnimationGIF.DEFAULT ) self.loadscreen.add_widget(self.loadwidget) - + self.controlPanel.toggle_conn_page.connect(self.conn_window.set_toggle) self.cancelpage = CancelPage(self, ws=self.ws) self.cancelpage.request_file_info.connect(self.file_data.on_request_fileinfo) self.cancelpage.run_gcode.connect(self.ws.api.run_gcode) diff --git a/BlocksScreen/lib/panels/widgets/connectionPage.py b/BlocksScreen/lib/panels/widgets/connectionPage.py index a50ec750..a434ab01 100644 --- a/BlocksScreen/lib/panels/widgets/connectionPage.py +++ b/BlocksScreen/lib/panels/widgets/connectionPage.py @@ -35,6 +35,7 @@ def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket, /): self.state = "shutdown" self.dot_count = 0 self.message = None + self.conn_toggle: bool = True self.dot_timer = QtCore.QTimer(self) self.dot_timer.setInterval(1000) self.dot_timer.timeout.connect(self._add_dot) @@ -57,6 +58,11 @@ def __init__(self, parent: QtWidgets.QWidget, ws: MoonWebSocket, /): self.ws.klippy_connected_signal.connect(self.on_klippy_connected) self.ws.klippy_state_signal.connect(self.on_klippy_state) + @QtCore.pyqtSlot(bool, name="toggle_connection_page") + def set_toggle(self, toggle: bool): + """Toggle connection page showing or not""" + self.conn_toggle = toggle + def show_panel(self, reason: str | None = None): """Show widget""" self.show() @@ -68,10 +74,11 @@ def show_panel(self, reason: str | None = None): def showEvent(self, a0: QtCore.QEvent | None): """Handle show event""" - self.ws.api.refresh_update_status() - self.call_load_panel.emit(False, "") - self.call_cancel_panel.emit(False) - return super().showEvent(a0) + if self.conn_toggle: + self.ws.api.refresh_update_status() + self.call_load_panel.emit(False, "") + self.call_cancel_panel.emit(False) + return super().showEvent(a0) @QtCore.pyqtSlot(bool, name="on_klippy_connected") def on_klippy_connection(self, connected: bool): diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index 6ce968d3..34447db9 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -35,11 +35,17 @@ class ProbeHelper(QtWidgets.QWidget): ) call_load_panel = QtCore.pyqtSignal(bool, str, name="call-load-panel") + toggle_conn_page = QtCore.pyqtSignal(bool, name="toggles-conn-panel") + + disable_popups: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + bool, name="disable-popups" + ) + distances = ["0.01", ".025", "0.1", "0.5", "1"] _calibration_commands: list = [] helper_start: bool = False helper_initialize: bool = False - _zhop_height: float = float(distances[4]) + _zhop_height: float = float(distances[0]) card_options: dict = {} z_offset_method_type: str = "" z_offset_config_method: tuple = () @@ -85,6 +91,26 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: self.block_list = False self.target_temp = 0 self.current_temp = 0 + self._eddy_calibration_state = False + + @QtCore.pyqtSlot(str, dict, name="on_print_stats_update") + @QtCore.pyqtSlot(str, float, name="on_print_stats_update") + @QtCore.pyqtSlot(str, str, name="on_print_stats_update") + def on_print_stats_update(self, field: str, value: dict | float | str) -> None: + """Handle print stats object update""" + if isinstance(value, str): + if "state" in field: + if value in ("standby"): + if self._eddy_calibration_state: + self.call_load_panel.emit(True, "Almost done...\nPlease wait") + self.run_gcode_signal.emit(self._eddy_command) + + self.request_page_view.emit() + + self.disable_popups.emit(False) + self.toggle_conn_page.emit(True) + + self._eddy_calibration_state = False def on_klippy_status(self, state: str): """Handle Klippy status event change""" @@ -202,29 +228,28 @@ def on_object_config(self, config: dict | list) -> None: # BUG: If i don't add if not self.probe_config i'll just receive the configuration a bunch of times if isinstance(config, list): - ... - # if self.block_list: - # return - # else: - # self.block_list = True - - # _keys = [] - # if not isinstance(config, list): - # return - - # list(map(lambda item: _keys.extend(item.keys()), config)) - - # probe, *_ = config[0].items() - # self.z_offset_method_type = probe[0] # The one found first - # self.z_offset_method_config = ( - # probe[1], - # "PROBE_CALIBRATE", - # "Z_OFFSET_APPLY_PROBE", - # ) - # self.init_probe_config() - # if not _keys: - # return - # self._configure_option_cards(_keys) + if self.block_list: + return + else: + self.block_list = True + + _keys = [] + if not isinstance(config, list): + return + + list(map(lambda item: _keys.extend(item.keys()), config)) + + probe, *_ = config[0].items() + self.z_offset_method_type = probe[0] # The one found first + self.z_offset_method_config = ( + probe[1], + "PROBE_CALIBRATE", + "Z_OFFSET_APPLY_PROBE", + ) + self._init_probe_config() + if not _keys: + return + self._configure_option_cards(_keys) elif isinstance(config, dict): if config.get("stepper_z"): @@ -393,10 +418,6 @@ def handle_start_tool(self, sender: typing.Type[OptionCard]) -> None: for i in self.card_options.values(): i.setDisabled(True) - self.call_load_panel.emit(True, "Homing Axes...") - if self.z_offset_safe_xy: - self.run_gcode_signal.emit("G28\nM400") - self._move_to_pos(self.z_offset_safe_xy[0], self.z_offset_safe_xy[1], 100) self.helper_initialize = True _timer = QtCore.QTimer() _timer.setSingleShot(True) @@ -408,6 +429,26 @@ def handle_start_tool(self, sender: typing.Type[OptionCard]) -> None: _cmd = self._build_calibration_command(sender.name) # type:ignore if not _cmd: return + + self.disable_popups.emit(True) + self.run_gcode_signal.emit("G28\nM400") + if "eddy" in sender.name: # type:ignore + self.call_load_panel.emit(True, "Preparing Eddy Current Calibration...") + self.toggle_conn_page.emit(False) + self.run_gcode_signal.emit( + f"LDC_CALIBRATE_DRIVE_CURRENT CHIP={sender.name.split(' ')[1]}" # type:ignore + ) + self.run_gcode_signal.emit("M400\nSAVE_CONFIG") + + self._eddy_command = _cmd + self._eddy_calibration_state = True + return + else: + if self.z_offset_safe_xy: + self.call_load_panel.emit(True, "Homing Axes...") + self._move_to_pos( + self.z_offset_safe_xy[0], self.z_offset_safe_xy[1], 100 + ) self.run_gcode_signal.emit(_cmd) @QtCore.pyqtSlot(str, str, float, name="on_extruder_update") @@ -417,6 +458,8 @@ def on_extruder_update( """Handle extruder update""" if not self.helper_initialize: return + if self._eddy_calibration_state: + return if self.target_temp != 0: if self.current_temp == self.target_temp: if self.isVisible: From 1720ae0299cc7a0c84cdebf11efd3e859db0f6d6 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Tue, 3 Mar 2026 17:20:00 +0000 Subject: [PATCH 58/70] =?UTF-8?q?=20=20feat(logger):=20overhaul=20logging?= =?UTF-8?q?=20system=20with=20module-aware=20names=20and=20c=E2=80=A6=20(#?= =?UTF-8?q?186)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(logger): overhaul logging system with module-aware names and crash handling - Replace AsyncFileHandler with ThreadedFileHandler (queue + background thread wrapping TimedRotatingFileHandler; "async" was a misnomer) - Add LogManager singleton: configures root logger, optional StreamToLogger capture of stderr/stdout, handler deduplication - Add CrashHandler: installs sys.excepthook, threading.excepthook, and faulthandler for C-level crashes; supports exit_on_crash flag - Expose setup_logging() and get_logger() as the public API - Suppress noisy third-party loggers (urllib3, websocket, PIL) to WARNING - Fix all modules to use logging.getLogger(__name__) instead of the old named-file pattern (logs/BlocksScreen.log): BlocksScreen.py, configfile.py, lib/machine.py, lib/moonrest.py, lib/printer.py, lib/panels/mainWindow.py, lib/panels/widgets/connectionPage.py - Replace manual logger-iteration loop in MainWindow.closeEvent with LogManager.shutdown(); remove erroneous recursive self.close() call - Fix create_hotspot password param: str | None = None, raise ValueError instead of silently accepting empty/hardcoded default (bandit B107) - Replace all bare except/pass blocks with typed exception handling that writes to sys.__stderr__ when the logger itself may be unavailable (bandit B110) EOF )" * fix formatting * fix merge problems * add missing logger changes and remove double-formatting in QueueHandler → ThreadedFileHandler chain --- BlocksScreen/BlocksScreen.py | 21 +- BlocksScreen/configfile.py | 16 +- BlocksScreen/helper_methods.py | 6 +- BlocksScreen/lib/machine.py | 8 +- BlocksScreen/lib/moonrakerComm.py | 36 +- BlocksScreen/lib/moonrest.py | 4 +- BlocksScreen/lib/panels/mainWindow.py | 13 +- BlocksScreen/lib/panels/printTab.py | 2 +- .../lib/panels/widgets/connectionPage.py | 4 +- .../lib/panels/widgets/jobStatusPage.py | 2 +- BlocksScreen/lib/printer.py | 4 +- BlocksScreen/logger.py | 886 ++++++++++++++++-- 12 files changed, 891 insertions(+), 111 deletions(-) diff --git a/BlocksScreen/BlocksScreen.py b/BlocksScreen/BlocksScreen.py index a7a2098e..3ccceb74 100644 --- a/BlocksScreen/BlocksScreen.py +++ b/BlocksScreen/BlocksScreen.py @@ -2,11 +2,10 @@ import sys import typing -import logger from lib.panels.mainWindow import MainWindow +from logger import setup_logging from PyQt6 import QtCore, QtGui, QtWidgets -_logger = logging.getLogger(name="logs/BlocksScreen.log") QtGui.QGuiApplication.setAttribute( QtCore.Qt.ApplicationAttribute.AA_SynthesizeMouseForUnhandledTouchEvents, True, @@ -22,13 +21,6 @@ RESET = "\033[0m" -def setup_app_loggers(): - """Setup logger""" - _ = logger.create_logger(name="logs/BlocksScreen.log", level=logging.DEBUG) - _logger = logging.getLogger(name="logs/BlocksScreen.log") - _logger.info("============ BlocksScreen Initializing ============") - - def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): """Show splash screen on app initialization""" logo = QtGui.QPixmap("BlocksScreen/BlocksScreen/lib/ui/resources/logoblocks.png") @@ -39,7 +31,16 @@ def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): if __name__ == "__main__": - setup_app_loggers() + setup_logging( + filename="logs/BlocksScreen.log", + level=logging.DEBUG, # File gets DEBUG+ + console_output=True, # Print to terminal + console_level=logging.DEBUG, # Console gets DEBUG+ + capture_stderr=True, # Capture X11 errors + capture_stdout=False, # Don't capture print() + ) + _logger = logging.getLogger(__name__) + _logger.info("============ BlocksScreen Initializing ============") BlocksScreen = QtWidgets.QApplication([]) BlocksScreen.setApplicationName("BlocksScreen") BlocksScreen.setApplicationDisplayName("BlocksScreen") diff --git a/BlocksScreen/configfile.py b/BlocksScreen/configfile.py index 9536fffd..ef75d362 100644 --- a/BlocksScreen/configfile.py +++ b/BlocksScreen/configfile.py @@ -37,6 +37,8 @@ from helper_methods import check_file_on_path +logger = logging.getLogger(__name__) + HOME_DIR = os.path.expanduser("~/") WORKING_DIR = os.getcwd() DEFAULT_CONFIGFILE_PATH = pathlib.Path(HOME_DIR, "printer_data", "config") @@ -267,9 +269,9 @@ def add_section(self, section: str) -> None: self.config.add_section(section) self.update_pending = True except configparser.DuplicateSectionError as e: - logging.error(f'Section "{section}" already exists. {e}') + logger.error(f'Section "{section}" already exists. {e}') except configparser.Error as e: - logging.error(f'Unable to add "{section}" section to configuration: {e}') + logger.error(f'Unable to add "{section}" section to configuration: {e}') def add_option( self, @@ -297,9 +299,9 @@ def add_option( self.config.set(section, option, value) self.update_pending = True except configparser.DuplicateOptionError as e: - logging.error(f"Option {option} already present on {section}: {e}") + logger.error(f"Option {option} already present on {section}: {e}") except configparser.Error as e: - logging.error( + logger.error( f'Unable to add "{option}" option to section "{section}": {e} ' ) @@ -324,7 +326,7 @@ def update_option( self.config.set(section, option, str(value)) self.update_pending = True except Exception as e: - logging.error( + logger.error( f'Unable to update option "{option}" in section "{section}": {e}' ) @@ -349,7 +351,7 @@ def save_configuration(self) -> None: self.config.write(sio) sio.close() except Exception as e: - logging.error( + logger.error( f"ERROR: Unable to save new configuration, something went wrong while saving updated configuration. {e}" ) finally: @@ -443,6 +445,6 @@ def get_configparser() -> BlocksScreenConfig: config_object = BlocksScreenConfig(configfile=configfile, section="server") config_object.load_config() if not config_object.has_section("server"): - logging.error("Error loading configuration file for the application.") + logger.error("Error loading configuration file for the application.") raise ConfigError("Section [server] is missing from configuration") return config_object diff --git a/BlocksScreen/helper_methods.py b/BlocksScreen/helper_methods.py index b4dafc0f..25b76cac 100644 --- a/BlocksScreen/helper_methods.py +++ b/BlocksScreen/helper_methods.py @@ -14,6 +14,8 @@ import struct import typing +logger = logging.getLogger(__name__) + try: ctypes.cdll.LoadLibrary("libXext.so.6") libxext = ctypes.CDLL("libXext.so.6") @@ -220,9 +222,9 @@ def disable_dpms() -> None: set_dpms_mode(DPMSState.OFF) except OSError as e: - logging.exception(f"OSError couldn't load DPMS library: {e}") + logger.exception(f"OSError couldn't load DPMS library: {e}") except Exception as e: - logging.exception(f"Unexpected exception occurred {e}") + logger.exception(f"Unexpected exception occurred {e}") def convert_bytes_to_mb(self, bytes: int | float) -> float: diff --git a/BlocksScreen/lib/machine.py b/BlocksScreen/lib/machine.py index e1c4a0ca..69938e79 100644 --- a/BlocksScreen/lib/machine.py +++ b/BlocksScreen/lib/machine.py @@ -8,6 +8,8 @@ from PyQt6 import QtCore +logger = logging.getLogger(__name__) + class MachineControl(QtCore.QObject): service_restart = QtCore.pyqtSignal(str, name="service-restart") @@ -67,10 +69,10 @@ def _run_command(self, command: str): ) return p.stdout.strip() + "\n" + p.stderr.strip() except ValueError as e: - logging.error("Failed to parse command string '%s': '%s'", command, e) + logger.error("Failed to parse command string '%s': '%s'", command, e) raise RuntimeError(f"Invalid command format: {e}") from e except subprocess.CalledProcessError as e: - logging.error( + logger.error( "Caught exception (exit code %d) failed to run command: %s \nStderr: %s", e.returncode, command, @@ -82,4 +84,4 @@ def _run_command(self, command: str): subprocess.TimeoutExpired, FileNotFoundError, ): - logging.error("Caught exception failed to run command %s", command) + logger.error("Caught exception failed to run command %s", command) diff --git a/BlocksScreen/lib/moonrakerComm.py b/BlocksScreen/lib/moonrakerComm.py index ba298ba7..5f889d9f 100644 --- a/BlocksScreen/lib/moonrakerComm.py +++ b/BlocksScreen/lib/moonrakerComm.py @@ -14,7 +14,7 @@ from lib.utils.RepeatedTimer import RepeatedTimer from PyQt6 import QtCore, QtWidgets -_logger = logging.getLogger(name="logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class OneShotTokenError(Exception): @@ -67,7 +67,7 @@ def __init__(self, parent: QtCore.QObject) -> None: ) self.klippy_state_signal.connect(self.api.request_printer_info) - _logger.info("Websocket object initialized") + logger.info("Websocket object initialized") @QtCore.pyqtSlot(name="retry_wb_conn") def retry_wb_conn(self): @@ -102,10 +102,10 @@ def reconnect(self): else: raise TypeError("QApplication.instance expected ad non-None value") except Exception as e: - _logger.error( + logger.error( f"Error on sending Event {unable_to_connect_event.__class__.__name__} | Error message: {e}" ) - _logger.info( + logger.info( "Maximum number of connection retries reached, Unable to establish connection with Moonraker" ) return False @@ -114,11 +114,11 @@ def reconnect(self): def connect(self) -> bool: """Connect to websocket""" if self.connected: - _logger.info("Connection established") + logger.info("Connection established") return True self._reconnect_count += 1 self.connecting_signal[int].emit(int(self._reconnect_count)) - _logger.debug( + logger.debug( f"Establishing connection to Moonraker...\n Try number {self._reconnect_count}" ) # TODO Handle if i cannot connect to moonraker, request server.info and see if i get a result @@ -127,7 +127,7 @@ def connect(self) -> bool: if _oneshot_token is None: raise OneShotTokenError("Unable to retrieve oneshot token") except Exception as e: - _logger.info( + logger.info( f"Unexpected error occurred when trying to acquire oneshot token: {e}" ) return False @@ -148,11 +148,11 @@ def connect(self) -> bool: daemon=True, ) try: - _logger.info("Websocket Start...") - _logger.debug(self.ws.url) + logger.info("Websocket Start...") + logger.debug(self.ws.url) self._wst.start() except Exception as e: - _logger.info(f"Unexpected while starting websocket {self._wst.name}: {e}") + logger.info(f"Unexpected while starting websocket {self._wst.name}: {e}") return False return True @@ -162,14 +162,14 @@ def wb_disconnect(self) -> None: self.ws.close() if self._wst.is_alive(): self._wst.join() - _logger.info("Websocket closed") + logger.info("Websocket closed") def on_error(self, *args) -> None: """Websocket error callback""" # First argument is ws second is error message # TODO: Handle error messages _error = args[1] if len(args) == 2 else args[0] - _logger.info(f"Websocket error, disconnected: {_error}") + logger.info(f"Websocket error, disconnected: {_error}") self.connected = False self.disconnected = True @@ -199,11 +199,11 @@ def on_close(self, *args) -> None: else: raise TypeError("QApplication.instance expected non None value") except Exception as e: - _logger.info( + logger.info( f"Unexpected error when sending websocket close_event on disconnection: {e}" ) - _logger.info( + logger.info( f"Websocket closed, code: {_close_status_code}, message: {_close_message}" ) @@ -231,11 +231,11 @@ def on_open(self, *args) -> None: else: raise TypeError("QApplication.instance expected non None value") except Exception as e: - _logger.info(f"Unexpected error opening websocket: {e}") + logger.info(f"Unexpected error opening websocket: {e}") self.connected_signal.emit() self._retry_timer.stopTimer() - _logger.info(f"Connection to websocket achieved on {_ws}") + logger.info(f"Connection to websocket achieved on {_ws}") def on_message(self, *args) -> None: """Websocket on message callback @@ -300,9 +300,7 @@ def on_message(self, *args) -> None: else: raise TypeError("QApplication.instance expected non None value") except Exception as e: - _logger.info( - f"Unexpected error while creating websocket message event: {e}" - ) + logger.info(f"Unexpected error while creating websocket message event: {e}") def send_request(self, method: str, params: dict = {}) -> bool: """Send a request over the websocket diff --git a/BlocksScreen/lib/moonrest.py b/BlocksScreen/lib/moonrest.py index 1e43552a..2c663531 100644 --- a/BlocksScreen/lib/moonrest.py +++ b/BlocksScreen/lib/moonrest.py @@ -31,6 +31,8 @@ import requests from requests import Request, Response +logger = logging.getLogger(__name__) + class UncallableError(Exception): """Raised when a method is not callable""" @@ -145,4 +147,4 @@ def _request( return response.json() if json_response else response.content except Exception as e: - logging.info(f"Unexpected error while sending HTTP request: {e}") + logger.info(f"Unexpected error while sending HTTP request: {e}") diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 2a36b5cd..6a1b004d 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -28,10 +28,11 @@ from lib.ui.resources.main_menu_resources_rc import * from lib.ui.resources.system_resources_rc import * from lib.ui.resources.top_bar_resources_rc import * +from logger import LogManager from PyQt6 import QtCore, QtGui, QtWidgets from screensaver import ScreenSaver -_logger = logging.getLogger(name="logs/BlocksScreen.log") +_logger = logging.getLogger(__name__) def api_handler(func): @@ -782,16 +783,8 @@ def closeEvent(self, a0: typing.Optional[QtGui.QCloseEvent]) -> None: except Exception as e: _logger.warning("Network panel shutdown error: %s", e) - _loggers = [ - logging.getLogger(name) for name in logging.root.manager.loggerDict - ] # Get available logger handlers - for logger in _loggers: # noqa: F402 - if hasattr(logger, "cancel"): - _callback = getattr(logger, "cancel") - if callable(_callback): - _callback() self.ws.wb_disconnect() - self.close() + LogManager.shutdown() if a0 is None: return QtWidgets.QMainWindow.closeEvent(self, a0) diff --git a/BlocksScreen/lib/panels/printTab.py b/BlocksScreen/lib/panels/printTab.py index ddc9c91a..65927aae 100644 --- a/BlocksScreen/lib/panels/printTab.py +++ b/BlocksScreen/lib/panels/printTab.py @@ -20,7 +20,7 @@ from lib.utils.display_button import DisplayButton from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger(name="logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class PrintTab(QtWidgets.QStackedWidget): diff --git a/BlocksScreen/lib/panels/widgets/connectionPage.py b/BlocksScreen/lib/panels/widgets/connectionPage.py index a434ab01..3cd1fc21 100644 --- a/BlocksScreen/lib/panels/widgets/connectionPage.py +++ b/BlocksScreen/lib/panels/widgets/connectionPage.py @@ -5,6 +5,8 @@ from lib.ui.connectionWindow_ui import Ui_ConnectivityForm from PyQt6 import QtCore, QtWidgets +logger = logging.getLogger(__name__) + class ConnectionPage(QtWidgets.QFrame): text_updated = QtCore.pyqtSignal(int, name="connection_text_updated") @@ -141,7 +143,7 @@ def text_update(self, text: int | str | None = None): if self.state == "shutdown" and self.message is not None: return False self.dot_timer.stop() - logging.debug(f"[ConnectionWindowPanel] text_update: {text}") + logger.debug(f"[ConnectionWindowPanel] text_update: {text}") if text == "wb lost": self.panel.connectionTextBox.setText("Moonraker connection lost") if text is None: diff --git a/BlocksScreen/lib/panels/widgets/jobStatusPage.py b/BlocksScreen/lib/panels/widgets/jobStatusPage.py index f868c222..67add6b9 100644 --- a/BlocksScreen/lib/panels/widgets/jobStatusPage.py +++ b/BlocksScreen/lib/panels/widgets/jobStatusPage.py @@ -10,7 +10,7 @@ from lib.utils.display_button import DisplayButton from PyQt6 import QtCore, QtGui, QtWidgets -logger = logging.getLogger("logs/BlocksScreen.log") +logger = logging.getLogger(__name__) class JobStatusWidget(QtWidgets.QWidget): diff --git a/BlocksScreen/lib/printer.py b/BlocksScreen/lib/printer.py index 5889c19d..c6c76fbc 100644 --- a/BlocksScreen/lib/printer.py +++ b/BlocksScreen/lib/printer.py @@ -7,7 +7,7 @@ from lib.moonrakerComm import MoonWebSocket from PyQt6 import QtCore, QtWidgets -logger = logging.getLogger(name="logs/BlocksScreen.logs") +logger = logging.getLogger(__name__) class Printer(QtCore.QObject): @@ -511,7 +511,7 @@ def send_print_event(self, event: str): _print_state_upper = event[0].upper() _print_state_call = f"{_print_state_upper}{event[1:]}" if hasattr(events, f"Print{_print_state_call}"): - logging.debug( + logger.debug( "Print Event Caught, print is %s, calling event %s", _print_state_call, f"Print{_print_state_call}", diff --git a/BlocksScreen/logger.py b/BlocksScreen/logger.py index f63631e5..ce16db74 100644 --- a/BlocksScreen/logger.py +++ b/BlocksScreen/logger.py @@ -1,95 +1,873 @@ +from __future__ import annotations + +import atexit import copy +import faulthandler import logging -import logging.config import logging.handlers +import os import pathlib import queue +import sys import threading +import traceback +import types +from datetime import datetime +from typing import ClassVar, TextIO + +DEFAULT_FORMAT = ( + "[%(levelname)s] | %(asctime)s | %(name)s | " + "%(relativeCreated)6d | %(threadName)s : %(message)s" +) + +CRASH_LOG_PATH = "logs/blocksscreen_crash.log" +FAULT_LOG_PATH = "logs/blocksscreen_fault.log" + + +class StreamToLogger(TextIO): + """ + Redirects a stream (stdout/stderr) to a logger. + + Useful for capturing output from subprocesses, X11, or print statements. + """ + + def __init__( + self, + logger: logging.Logger, + level: int = logging.INFO, + original_stream: TextIO | None = None, + ) -> None: + self._logger = logger + self._level = level + self._original = original_stream + self._buffer = "" + + def write(self, message: str) -> int: + """Write message to logger.""" + if message: + if self._original: + try: + self._original.write(message) + self._original.flush() + except OSError: + # Original stream closed or broken pipe — continue logging + self._original = None + + self._buffer += message + + while "\n" in self._buffer: + line, self._buffer = self._buffer.split("\n", 1) + if line.strip(): + self._logger.log(self._level, line.rstrip()) + + return len(message) + + def flush(self) -> None: + """Flush remaining buffer.""" + if self._buffer.strip(): + self._logger.log(self._level, self._buffer.rstrip()) + self._buffer = "" + + if self._original: + try: + self._original.flush() + except OSError: + # Original stream closed or broken pipe + self._original = None + + def fileno(self) -> int: + """Return file descriptor for compatibility.""" + if self._original: + return self._original.fileno() + raise OSError("No file descriptor available") + + def isatty(self) -> bool: + """Check if stream is a TTY.""" + if self._original: + return self._original.isatty() + return False + + # Required for TextIO interface + def read(self, n: int = -1) -> str: + return "" + + def readline(self, limit: int = -1) -> str: + return "" + + def readlines(self, hint: int = -1) -> list[str]: + return [] + + def seek(self, offset: int, whence: int = 0) -> int: + return 0 + + def tell(self) -> int: + return 0 + + def truncate(self, size: int | None = None) -> int: + return 0 + + def writable(self) -> bool: + return True + + def readable(self) -> bool: + return False + + def seekable(self) -> bool: + return False + + def close(self) -> None: + self.flush() + + @property + def closed(self) -> bool: + return False + + def __enter__(self) -> "StreamToLogger": + return self + + def __exit__(self, *args) -> None: + self.close() class QueueHandler(logging.Handler): - """Handler that sends events to a queue""" + """ + Logging handler that sends records to a queue. + + Records are formatted before being placed on the queue, + then consumed by a ThreadedFileHandler worker in a background thread. + """ def __init__( self, - queue: queue.Queue, - format: str = "'[%(levelname)s] | %(asctime)s | %(name)s | %(relativeCreated)6d | %(threadName)s : %(message)s", - level=logging.DEBUG, - ): - super(QueueHandler, self).__init__() - self.log_queue = queue - self.setFormatter(logging.Formatter(format, validate=True)) - self.setLevel(level) - - def emit(self, record): - """Emit logging record""" + log_queue: queue.Queue, + level: int = logging.DEBUG, + ) -> None: + super().__init__(level) + self._queue = log_queue + + def emit(self, record: logging.LogRecord) -> None: + """Format and queue the log record.""" try: + # Format the message msg = self.format(record) + + # Copy record and update message record = copy.copy(record) - record.message = msg - record.name = record.name record.msg = msg - self.log_queue.put_nowait(record) + record.args = None # Already formatted + record.message = msg + + self._queue.put_nowait(record) except Exception: self.handleError(record) - def setFormatter(self, fmt: logging.Formatter | None) -> None: - """Set logging formatter""" - return super().setFormatter(fmt) +class ThreadedFileHandler(logging.handlers.TimedRotatingFileHandler): + """ + File handler that writes on a background thread. + + Wraps TimedRotatingFileHandler with a queue and worker thread + for non-blocking log writes. Automatically recreates log file + if deleted during runtime. + """ -class QueueListener(logging.handlers.TimedRotatingFileHandler): - """Threaded listener watching for log records on the queue handler queue, passes them for processing""" + def __init__( + self, + filename: str, + when: str = "midnight", + backup_count: int = 10, + encoding: str = "utf-8", + fmt: str = DEFAULT_FORMAT, + ) -> None: + self._log_path = pathlib.Path(filename) + + # Create log directory if needed + if self._log_path.parent != pathlib.Path("."): + self._log_path.parent.mkdir(parents=True, exist_ok=True) - def __init__(self, filename, encoding="utf-8"): - log_path = pathlib.Path(filename) - if log_path.parent != pathlib.Path("."): - log_path.parent.mkdir(parents=True, exist_ok=True) - super(QueueListener, self).__init__( + super().__init__( filename=filename, - when="MIDNIGHT", - backupCount=10, + when=when, + backupCount=backup_count, encoding=encoding, delay=True, ) - self.queue = queue.Queue() + self.setFormatter(logging.Formatter(fmt)) + + self._queue: queue.Queue[logging.LogRecord | None] = queue.Queue() + self._stop_event = threading.Event() self._thread = threading.Thread( - name=f"log.{filename}", target=self._run, daemon=True + name=f"logger-{self._log_path.stem}", + target=self._worker, + daemon=True, ) self._thread.start() - def _run(self): - while True: + def _ensure_file_exists(self) -> None: + """Ensure log file and directory exist, recreate if deleted.""" + try: + # Check if directory exists + if not self._log_path.parent.exists(): + self._log_path.parent.mkdir(parents=True, exist_ok=True) + + # Check if file was deleted (stream is open but file gone) + if self.stream is not None and not self._log_path.exists(): + # Close old stream + try: + self.stream.close() + except OSError: + pass # Stream already closed; safe to discard + self.stream = None + + # Reopen stream if needed + if self.stream is None: + self.stream = self._open() + + except OSError as exc: + sys.__stderr__.write( + f"[logger] Failed to recreate log file {self._log_path}: {exc}\n" + ) + + def emit(self, record: logging.LogRecord) -> None: + """Emit a record, recovering if the log file was deleted.""" + try: + super().emit(record) + except (OSError, ValueError): + self._ensure_file_exists() try: - record = self.queue.get(True) + super().emit(record) + except OSError as exc: + sys.__stderr__.write(f"[logger] Failed to write log record: {exc}\n") + + def _worker(self) -> None: + """Background worker that processes queued log records.""" + while not self._stop_event.is_set(): + try: + record = self._queue.get(timeout=0.5) if record is None: break - self.handle(record) + self.emit(record) except queue.Empty: - break + continue + except Exception as exc: + # Last resort: surface unexpected worker errors without crashing the thread + sys.__stderr__.write(f"[logger] Worker thread error: {exc}\n") + + @property + def queue(self) -> queue.Queue: + """Get the log queue for QueueHandler.""" + return self._queue - def close(self): - """Close logger listener""" - if self._thread is None: + def close(self) -> None: + """Stop worker thread and close file handler.""" + if self._thread is None or not self._thread.is_alive(): + super().close() return - self.queue.put_nowait(None) - self._thread.join() + + # Signal worker to stop + self._stop_event.set() + self._queue.put_nowait(None) + + # Wait for worker to finish + self._thread.join(timeout=2.0) self._thread = None + # Close the file handler + super().close() + + +class _ExcludeStreamLoggers(logging.Filter): + """Filter to exclude stdout/stderr loggers from console output.""" + + def filter(self, record: logging.LogRecord) -> bool: + # Exclude to avoid double printing (already goes to console via StreamToLogger) + return record.name not in ("stdout", "stderr") + + +class CrashHandler: + """ + Handles unhandled exceptions and C-level crashes. + + Writes detailed crash information to log files including: + - Full traceback with line numbers + - Local variables at each frame + - Thread information + - Timestamp + """ + + _instance: ClassVar[CrashHandler | None] = None + _installed: ClassVar[bool] = False + + def __init__( + self, + crash_log_path: str = CRASH_LOG_PATH, + fault_log_path: str = FAULT_LOG_PATH, + include_locals: bool = True, + exit_on_crash: bool = True, + ) -> None: + self._crash_log_path = pathlib.Path(crash_log_path) + self._fault_log_path = pathlib.Path(fault_log_path) + self._include_locals = include_locals + self._exit_on_crash = exit_on_crash + self._original_excepthook = sys.excepthook + self._original_threading_excepthook = getattr(threading, "excepthook", None) + self._fault_file: TextIO | None = None + + @classmethod + def install( + cls, + crash_log_path: str = CRASH_LOG_PATH, + fault_log_path: str = FAULT_LOG_PATH, + include_locals: bool = True, + exit_on_crash: bool = True, + ) -> CrashHandler: + """ + Install the crash handler. + + Should be called as early as possible in the application startup. + + Args: + crash_log_path: Path to write Python exception logs + fault_log_path: Path to write C-level fault logs (segfaults) + include_locals: Include local variables in traceback + exit_on_crash: Force exit after logging (for systemd restart) + + Returns: + The CrashHandler instance + """ + if cls._installed and cls._instance: + return cls._instance + + handler = cls(crash_log_path, fault_log_path, include_locals, exit_on_crash) + handler._install() + cls._instance = handler + cls._installed = True + + return handler + + def _install(self) -> None: + """Install exception hooks.""" + # Setup faulthandler for C-level crashes (segfaults, etc.) + try: + self._fault_file = open(self._fault_log_path, "w") + faulthandler.enable(file=self._fault_file, all_threads=True) + + # Also dump traceback on SIGUSR1 (useful for debugging hangs) + try: + import signal + + faulthandler.register( + signal.SIGUSR1, + file=self._fault_file, + all_threads=True, + ) + except (AttributeError, OSError): + pass # Not available on all platforms + + except Exception as e: + # Fall back to stderr + faulthandler.enable() + sys.stderr.write(f"Warning: Could not setup fault log file: {e}\n") + + # Install Python exception hook + sys.excepthook = self._exception_hook + + # Install threading exception hook (Python 3.8+) + if hasattr(threading, "excepthook"): + threading.excepthook = self._threading_exception_hook + + def _format_exception_detailed( + self, + exc_type: type[BaseException], + exc_value: BaseException, + exc_tb: types.TracebackType | None, + ) -> str: + """Format exception with detailed information.""" + lines: list[str] = [] + + # Header + lines.append("=" * 80) + lines.append("UNHANDLED EXCEPTION") + lines.append("=" * 80) + lines.append(f"Time: {datetime.now().isoformat()}") + lines.append(f"Thread: {threading.current_thread().name}") + lines.append(f"Exception Type: {exc_type.__module__}.{exc_type.__name__}") + lines.append(f"Exception Value: {exc_value}") + lines.append("") + + # Full traceback with context + lines.append("-" * 80) + lines.append("TRACEBACK (most recent call last):") + lines.append("-" * 80) + + # Extract frames for detailed info + tb_frames = traceback.extract_tb(exc_tb) + + for i, frame in enumerate(tb_frames): + lines.append("") + lines.append(f" Frame {i + 1}: {frame.filename}") + lines.append(f" Line {frame.lineno} in {frame.name}()") + lines.append(f" Code: {frame.line}") + + # Try to get local variables if enabled + if self._include_locals and exc_tb: + try: + # Navigate to the correct frame + current_tb = exc_tb + for _ in range(i): + if current_tb.tb_next: + current_tb = current_tb.tb_next + + frame_locals = current_tb.tb_frame.f_locals + if frame_locals: + lines.append(" Locals:") + for name, value in frame_locals.items(): + # Skip private/dunder and limit value length + if name.startswith("__"): + continue + try: + value_str = repr(value) + if len(value_str) > 200: + value_str = value_str[:200] + "..." + except ( + Exception + ): # repr() may raise arbitrary errors on broken objects + value_str = "" + lines.append(f" {name} = {value_str}") + except (AttributeError, TypeError): + lines.append(" Locals: ") + + # Standard traceback + lines.append("") + lines.append("-" * 80) + lines.append("STANDARD TRACEBACK:") + lines.append("-" * 80) + lines.append("".join(traceback.format_exception(exc_type, exc_value, exc_tb))) + + # Thread info + lines.append("-" * 80) + lines.append("ACTIVE THREADS:") + lines.append("-" * 80) + for thread in threading.enumerate(): + daemon_str = " (daemon)" if thread.daemon else "" + lines.append( + f" - {thread.name}{daemon_str}: {'alive' if thread.is_alive() else 'dead'}" + ) + + lines.append("") + lines.append("=" * 80) + + return "\n".join(lines) + + def _write_crash_log(self, content: str) -> None: + """Write crash information to log file.""" + try: + # Ensure directory exists + self._crash_log_path.parent.mkdir(parents=True, exist_ok=True) + + # Write to crash log + with open(self._crash_log_path, "w") as f: + f.write(content) + + # Also append to a history file + history_path = self._crash_log_path.with_suffix(".history.log") + with open(history_path, "a") as f: + f.write(content) + f.write("\n\n") + + except Exception as e: + # Last resort: write to stderr + sys.stderr.write(f"Failed to write crash log: {e}\n") + sys.stderr.write(content) + + def _exception_hook( + self, + exc_type: type[BaseException], + exc_value: BaseException, + exc_tb: types.TracebackType | None, + ) -> None: + """Handle uncaught exceptions.""" + # Don't handle keyboard interrupt + if issubclass(exc_type, KeyboardInterrupt): + self._original_excepthook(exc_type, exc_value, exc_tb) + return + + # Format detailed crash info + crash_info = self._format_exception_detailed(exc_type, exc_value, exc_tb) + + # Write to crash log + self._write_crash_log(crash_info) + + # Also log via logging if available (may fail if logging is not configured) + try: + logging.getLogger("crash").critical( + "Unhandled exception - see %s for details", self._crash_log_path + ) + except Exception as exc: + sys.__stderr__.write(f"[logger] Could not emit crash log record: {exc}\n") + + # Call original hook (prints traceback) + self._original_excepthook(exc_type, exc_value, exc_tb) + + # Force exit if configured (for systemd restart) + if self._exit_on_crash: + os._exit(1) + + def _threading_exception_hook(self, args: threading.ExceptHookArgs) -> None: + """Handle uncaught exceptions in threads.""" + # Format detailed crash info + crash_info = self._format_exception_detailed( + args.exc_type, args.exc_value, args.exc_traceback + ) + + # Add thread context + thread_info = ( + f"\nThread that crashed: {args.thread.name if args.thread else 'Unknown'}\n" + ) + crash_info = crash_info.replace( + "UNHANDLED EXCEPTION", f"UNHANDLED THREAD EXCEPTION{thread_info}" + ) + + # Write to crash log + self._write_crash_log(crash_info) + + # Log via logging (may fail if logging is not configured) + try: + logging.getLogger("crash").critical( + "Unhandled thread exception - see %s", self._crash_log_path + ) + except Exception as exc: + sys.__stderr__.write(f"[logger] Could not emit crash log record: {exc}\n") + + # Call original hook if available + if self._original_threading_excepthook: + self._original_threading_excepthook(args) + + # Force exit if configured (for systemd restart) + # Skip for daemon threads — transient errors in background workers + # (network, D-Bus) should not kill the whole process. + if self._exit_on_crash: + thread = args.thread + if thread is None or not thread.daemon: + os._exit(1) + + def uninstall(self) -> None: + """Restore original exception hooks.""" + sys.excepthook = self._original_excepthook + + if self._original_threading_excepthook and hasattr(threading, "excepthook"): + threading.excepthook = self._original_threading_excepthook + + if self._fault_file: + try: + self._fault_file.close() + except OSError: + pass # File already closed; nothing to recover + + CrashHandler._installed = False + CrashHandler._instance = None + + +class LogManager: + """ + Manages application logging. + + Creates async file loggers with queue-based handlers. + Ensures proper cleanup on application exit. + """ + + _handlers: ClassVar[dict[str, ThreadedFileHandler]] = {} + _initialized: ClassVar[bool] = False + _original_stdout: ClassVar[TextIO | None] = None + _original_stderr: ClassVar[TextIO | None] = None + _crash_handler: ClassVar[CrashHandler | None] = None + + @classmethod + def _ensure_initialized(cls) -> None: + """Register cleanup handler on first use.""" + if not cls._initialized: + atexit.register(cls.shutdown) + cls._initialized = True + + @classmethod + def setup( + cls, + filename: str = "logs/BlocksScreen.log", + level: int = logging.DEBUG, + fmt: str = DEFAULT_FORMAT, + capture_stdout: bool = False, + capture_stderr: bool = True, + console_output: bool = True, + console_level: int | None = None, + enable_crash_handler: bool = True, + crash_log_path: str = CRASH_LOG_PATH, + include_locals_in_crash: bool = True, + ) -> None: + """ + Setup root logger for entire application. + + Call once at startup. After this, all modules can use: + logger = logging.getLogger(__name__) + + Args: + filename: Log file path + level: Logging level for all loggers + fmt: Log format string + capture_stdout: Redirect stdout to logger + capture_stderr: Redirect stderr to logger + console_output: Also print logs to console + console_level: Console log level (defaults to same as level) + enable_crash_handler: Enable crash handler for unhandled exceptions + crash_log_path: Path to write crash logs + include_locals_in_crash: Include local variables in crash logs + """ + # Install crash handler FIRST (before anything else can fail) + if enable_crash_handler: + cls._crash_handler = CrashHandler.install( + crash_log_path=crash_log_path, + include_locals=include_locals_in_crash, + ) + + cls._ensure_initialized() + + # Store original streams before any redirection + if cls._original_stdout is None: + cls._original_stdout = sys.stdout + if cls._original_stderr is None: + cls._original_stderr = sys.stderr + + # Get root logger + root = logging.getLogger() + + # Don't add duplicate handlers + if root.handlers: + logging.getLogger(__name__).warning( + "Root logger already has handlers; skipping LogManager.setup()" + ) + return + + root.setLevel(level) + + # Create async file handler + file_handler = ThreadedFileHandler(filename, fmt=fmt) + cls._handlers["root"] = file_handler + + # Create queue handler that feeds the file handler + queue_handler = QueueHandler(file_handler.queue, level) + root.addHandler(queue_handler) + + # Add console handler + if console_output: + cls._add_console_handler(root, console_level or level, fmt) + + # Suppress verbose third-party library debug logs + for noisy in ("urllib3", "websocket", "PIL"): + logging.getLogger(noisy).setLevel(logging.WARNING) + + # Capture stdout/stderr (after console handler is set up) + if capture_stdout: + cls.redirect_stdout() + if capture_stderr: + cls.redirect_stderr() + + # Log startup + logging.getLogger(__name__).info( + "Logging initialized - crash logs: %s", crash_log_path + ) + + @classmethod + def _add_console_handler(cls, logger: logging.Logger, level: int, fmt: str) -> None: + """Add a console handler that prints to original stdout.""" + # Use original stdout to avoid recursion if stdout is redirected + stream = cls._original_stdout or sys.stdout + + console_handler = logging.StreamHandler(stream) + console_handler.setLevel(level) + console_handler.setFormatter(logging.Formatter(fmt)) + + # Filter out stderr logger to avoid double printing + console_handler.addFilter(_ExcludeStreamLoggers()) + + logger.addHandler(console_handler) + + @classmethod + def get_logger( + cls, + name: str, + filename: str | None = None, + level: int = logging.INFO, + fmt: str = DEFAULT_FORMAT, + ) -> logging.Logger: + """ + Get or create a named logger with its own file output. + + Args: + name: Logger name + filename: Log file path (defaults to "logs/{name}.log") + level: Logging level + fmt: Log format string + + Returns: + Configured Logger instance + """ + cls._ensure_initialized() + + logger = logging.getLogger(name) + + # Don't add duplicate handlers + if logger.handlers: + return logger + + logger.setLevel(level) + + # Create async file handler + if filename is None: + filename = f"logs/{name}.log" + + file_handler = ThreadedFileHandler(filename, fmt=fmt) + cls._handlers[name] = file_handler + + # Create queue handler that feeds the file handler + queue_handler = QueueHandler(file_handler.queue, level) + logger.addHandler(queue_handler) + + # Don't propagate to root (has its own file) + logger.propagate = False + + return logger + + @classmethod + def redirect_stdout(cls, logger_name: str = "stdout") -> None: + """ + Redirect stdout to logger. + + Captures print() statements and subprocess output. + """ + logger = logging.getLogger(logger_name) + sys.stdout = StreamToLogger(logger, logging.INFO, cls._original_stdout) + + @classmethod + def redirect_stderr(cls, logger_name: str = "stderr") -> None: + """ + Redirect stderr to logger. + + Captures X11 errors, warnings, and subprocess errors. + """ + logger = logging.getLogger(logger_name) + sys.stderr = StreamToLogger(logger, logging.WARNING, cls._original_stderr) + + @classmethod + def restore_streams(cls) -> None: + """Restore original stdout/stderr.""" + if cls._original_stdout: + sys.stdout = cls._original_stdout + if cls._original_stderr: + sys.stderr = cls._original_stderr + + @classmethod + def shutdown(cls) -> None: + """Close all handlers. Called automatically on exit.""" + # Restore original streams + cls.restore_streams() + + # Close handlers + for handler in cls._handlers.values(): + handler.close() + cls._handlers.clear() + + # Uninstall crash handler + if cls._crash_handler: + cls._crash_handler.uninstall() + cls._crash_handler = None + + +def setup_logging( + filename: str = "logs/app.log", + level: int = logging.DEBUG, + fmt: str = DEFAULT_FORMAT, + capture_stdout: bool = False, + capture_stderr: bool = True, + console_output: bool = True, + console_level: int | None = None, + enable_crash_handler: bool = True, + crash_log_path: str = CRASH_LOG_PATH, + include_locals_in_crash: bool = True, +) -> None: + """ + Setup logging for entire application. + + Call once at startup. After this, all modules can use: + import logging + logger = logging.getLogger(__name__) + + Args: + filename: Log file path + level: Logging level + fmt: Log format string + capture_stdout: Redirect stdout (print statements) to logger + capture_stderr: Redirect stderr (X11 errors, warnings) to logger + console_output: Also print logs to console/terminal + console_level: Console log level (defaults to same as level) + enable_crash_handler: Enable crash handler for unhandled exceptions + crash_log_path: Path to write crash logs + include_locals_in_crash: Include local variables in crash logs + """ + LogManager.setup( + filename=filename, + level=level, + fmt=fmt, + capture_stdout=capture_stdout, + capture_stderr=capture_stderr, + console_output=console_output, + console_level=console_level, + enable_crash_handler=enable_crash_handler, + crash_log_path=crash_log_path, + include_locals_in_crash=include_locals_in_crash, + ) + + +def get_logger( + name: str, + filename: str | None = None, + level: int = logging.INFO, + fmt: str = DEFAULT_FORMAT, +) -> logging.Logger: + """ + Get or create a logger with its own file output. + + Args: + name: Logger name + filename: Log file path (defaults to "logs/{name}.log") + level: Logging level + fmt: Log format string + + Returns: + Configured Logger instance + """ + return LogManager.get_logger(name, filename, level, fmt) + + +def install_crash_handler( + crash_log_path: str = CRASH_LOG_PATH, + fault_log_path: str = FAULT_LOG_PATH, + include_locals: bool = True, + exit_on_crash: bool = True, +) -> CrashHandler: + """ + Install crash handler without full logging setup. -global MainLoggingHandler + Use this if you want crash handling before logging is configured. + Call at the very beginning of your main.py. + Args: + crash_log_path: Path to write Python exception logs + fault_log_path: Path to write C-level fault logs + include_locals: Include local variables in traceback + exit_on_crash: Force process exit after logging crash (for systemd restart) -def create_logger( - name: str = "log", - level=logging.INFO, - format: str = "'[%(levelname)s] | %(asctime)s | %(name)s | %(relativeCreated)6d | %(threadName)s : %(message)s", -): - """Create amd return logger""" - global MainLoggingHandler - logger = logging.getLogger(name) - logger.setLevel(level) - ql = QueueListener(filename=name) - MainLoggingHandler = QueueHandler(ql.queue, format, level) - logger.addHandler(MainLoggingHandler) - return ql + Returns: + CrashHandler instance + """ + return CrashHandler.install( + crash_log_path, fault_log_path, include_locals, exit_on_crash + ) From 2d9d96e6def5fdc4b69120039a9137d96ecb9614 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Mon, 9 Mar 2026 19:01:39 +0000 Subject: [PATCH 59/70] fix(makefile): align clean, lint and format-check with CI (#187) --- Makefile | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index cefd99d8..b603a29c 100644 --- a/Makefile +++ b/Makefile @@ -78,11 +78,11 @@ rcc-all: ## Force recompile all .qrc files # ───────────────────────────────────────────────────────────────────────────── lint: ## Run pylint - $(PYTHON) -m pylint $(SRC) + $(PYTHON) -m pylint -j$(shell nproc) --recursive=y $(SRC)/ -format-check: ## Verify formatting without modifying files (ruff-based) - $(PYTHON) -m ruff format --check $(SRC) $(TESTS) - $(PYTHON) -m ruff check $(SRC) $(TESTS) +format-check: ## Verify formatting without modifying files (matches CI exactly) + $(PYTHON) -m ruff check --target-version=py311 --config=pyproject.toml + $(PYTHON) -m ruff format --diff --target-version=py311 --config=pyproject.toml security: ## Run bandit security scan $(PYTHON) -m bandit -c pyproject.toml -r $(SRC) @@ -145,11 +145,9 @@ docstrcov: ## Check docstring coverage (fail-under=80%, matches CI) # ───────────────────────────────────────────────────────────────────────────── clean: ## Remove build artefacts, caches, and coverage data - rm -rf dist/ build/ *.egg-info src/*.egg-info site/ htmlcov .coverage - find . -depth \ - \( -type f -name '*.py[co]' \ - -o -type d -name __pycache__ \ - -o -type d -name .pytest_cache \) -exec rm -rf {} + + rm -rf dist/ build/ *.egg-info src/*.egg-info site/ htmlcov/ .coverage .ruff_cache .mypy_cache + find . -type d \( -name __pycache__ -o -name .pytest_cache \) -exec rm -rf {} + 2>/dev/null || true + find . -type f -name '*.py[co]' -delete 2>/dev/null || true clean-venv: ## Remove the virtual environment (destructive!) @echo "Removing $(VENV)..." From eea8cc01d162d4d14c08ef2167a73579d94f34ce Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Mon, 9 Mar 2026 19:02:18 +0000 Subject: [PATCH 60/70] Bugfix/nozzle calli hide (#189) * Bugfix: hide screen when not active * Refactor : optimzied code --------- Co-authored-by: Roberto --- .../lib/panels/widgets/probeHelperPage.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index 34447db9..372499ef 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -524,14 +524,18 @@ def on_manual_probe_update(self, update: dict) -> None: # if update.get("z_position_lower"): # f"{update.get('z_position_lower'):.4f} mm" - if update.get("is_active"): - if not self.isVisible(): - self.request_page_view.emit() - - self.helper_initialize = False - self.helper_start = True + is_active = update.get("is_active", False) + if is_active and not self.isVisible(): + self.request_page_view.emit() + # Shared state updates + self.helper_initialize = False + self.helper_start = is_active + # UI updates + self._toggle_tool_buttons(is_active) + if is_active: self._hide_option_cards() - self._toggle_tool_buttons(True) + else: + self._show_option_cards() if update.get("z_position_upper"): self.old_offset_info.setText(f"{update.get('z_position_upper'):.4f} mm") From f15cea3417e6cfd07139725c5426208963b96bf7 Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Mon, 9 Mar 2026 19:34:04 +0000 Subject: [PATCH 61/70] feat(network): remove ethernet DHCP, require static IP for VLANs (#190) * feat(network): remove DHCP support for ethernet/VLAN connections * fix formatting --- BlocksScreen/lib/network/models.py | 3 +- BlocksScreen/lib/network/worker.py | 202 ++----------------- BlocksScreen/lib/panels/networkWindow.py | 90 +++------ tests/network/test_models_unit.py | 3 +- tests/network/test_network_ui.py | 97 +--------- tests/network/test_worker_unit.py | 236 +---------------------- 6 files changed, 58 insertions(+), 573 deletions(-) diff --git a/BlocksScreen/lib/network/models.py b/BlocksScreen/lib/network/models.py index b743d875..6b5f68d5 100644 --- a/BlocksScreen/lib/network/models.py +++ b/BlocksScreen/lib/network/models.py @@ -65,8 +65,7 @@ class PendingOperation(IntEnum): CONNECT = 5 ETHERNET_ON = 6 ETHERNET_OFF = 7 - WIFI_STATIC_IP = 8 # static IP or resetting to DHCP on a Wi-Fi profile - VLAN_DHCP = 9 # VLAN with DHCP (long-running, up to 45 s) + WIFI_STATIC_IP = 8 # VLAN with DHCP (long-running, up to 45 s) class NetworkStatus(IntEnum): diff --git a/BlocksScreen/lib/network/worker.py b/BlocksScreen/lib/network/worker.py index 227b7e9b..a1e971ae 100644 --- a/BlocksScreen/lib/network/worker.py +++ b/BlocksScreen/lib/network/worker.py @@ -91,7 +91,6 @@ def __init__(self) -> None: self._consecutive_dbus_errors: int = 0 self._background_tasks: set[asyncio.Task] = set() - self._deleted_vlan_ids: set[int] = set() self._signal_nm: dbus_nm.NetworkManager | None = None self._signal_wifi: dbus_nm.NetworkDeviceWireless | None = None @@ -164,7 +163,6 @@ async def _async_shutdown(self) -> None: self._primary_wired_iface = "" self._iface_to_device_path.clear() self._saved_cache.clear() - self._deleted_vlan_ids.clear() for task in list(self._background_tasks): if not task.done(): @@ -1758,11 +1756,9 @@ async def _async_create_vlan( dns1: str, dns2: str, ) -> None: - """Create and activate a VLAN connection on the primary wired interface. + """Create and activate a VLAN connection with a static IP on the primary wired interface. - If *ip_address* is empty the VLAN uses DHCP and waits up to 45 s for a - lease; otherwise a static configuration is applied. Emits - connection_result and state_changed when done. + Emits connection_result and state_changed when done. """ if not self._primary_wired_path: self.error_occurred.emit("create_vlan", "No wired device") @@ -1827,7 +1823,14 @@ async def _async_create_vlan( await self._delete_all_connections_by_id(vlan_conn_id) await asyncio.sleep(0.5) - use_dhcp = not ip_address + prefix = self._mask_to_prefix(subnet_mask) + ip_uint = self._ip_to_nm_uint32(ip_address) + gw_uint = self._ip_to_nm_uint32(gateway) if gateway else 0 + dns_list: list[int] = [] + if dns1: + dns_list.append(self._ip_to_nm_uint32(dns1)) + if dns2: + dns_list.append(self._ip_to_nm_uint32(dns2)) conn_props: dict[str, object] = { "connection": { @@ -1840,24 +1843,7 @@ async def _async_create_vlan( "id": ("u", vlan_id), "parent": ("s", iface), }, - "ipv6": {"method": ("s", "ignore")}, - } - - if use_dhcp: - conn_props["ipv4"] = { - "method": ("s", "auto"), - "route-metric": ("i", 500), - } - else: - prefix = self._mask_to_prefix(subnet_mask) - ip_uint = self._ip_to_nm_uint32(ip_address) - gw_uint = self._ip_to_nm_uint32(gateway) if gateway else 0 - dns_list: list[int] = [] - if dns1: - dns_list.append(self._ip_to_nm_uint32(dns1)) - if dns2: - dns_list.append(self._ip_to_nm_uint32(dns2)) - conn_props["ipv4"] = { + "ipv4": { "method": ("s", "manual"), "addresses": ( "aau", @@ -1866,50 +1852,14 @@ async def _async_create_vlan( "gateway": ("s", gateway or ""), "dns": ("au", dns_list), "route-metric": ("i", 500), - } + }, + "ipv6": {"method": ("s", "ignore")}, + } conn_path = await self._nm_settings().add_connection(conn_props) - - if use_dhcp: - vlan_iface = f"{iface}.{vlan_id}" - ok, _msg = await self._async_activate_vlan_with_timeout( - conn_path, vlan_id, vlan_iface, timeout=45.0 - ) - if not ok: - if vlan_id in self._deleted_vlan_ids: - self._deleted_vlan_ids.discard(vlan_id) - logger.info( - "VLAN %d was manually deleted during DHCP " - "activation — skipping cleanup", - vlan_id, - ) - self.connection_result.emit( - ConnectionResult( - False, - f"VLAN {vlan_id} was removed during DHCP activation.", - "vlan_dhcp_timeout", - ) - ) - return - await self._delete_all_connections_by_id(vlan_conn_id) - logger.info( - "Deleted VLAN %d profile after DHCP failure", - vlan_id, - ) - self.connection_result.emit( - ConnectionResult( - False, - "There isn't a DHCP VLAN server.\n" - "Use a static IP address for this VLAN.", - "vlan_dhcp_timeout", - ) - ) - self.state_changed.emit(await self._build_current_state()) - return - else: - await self._nm().activate_connection(conn_path, "/", "/") - self.state_changed.emit(await self._build_current_state()) - await asyncio.sleep(1.5) + await self._nm().activate_connection(conn_path, "/", "/") + self.state_changed.emit(await self._build_current_state()) + await asyncio.sleep(1.5) self.connection_result.emit( ConnectionResult(True, f"VLAN {vlan_id} connected") @@ -1923,7 +1873,6 @@ async def _async_create_vlan( async def _async_delete_vlan(self, vlan_id: int) -> None: """Delete all NM connection profiles for *vlan_id* and emit connection_result.""" try: - self._deleted_vlan_ids.add(vlan_id) deleted = await self._delete_all_connections_by_id(f"VLAN {vlan_id}") logger.info( "Deleted %d VLAN profile(s) for VLAN %d", @@ -1938,123 +1887,6 @@ async def _async_delete_vlan(self, vlan_id: int) -> None: logger.error("Failed to delete VLAN %d: %s", vlan_id, exc) self.error_occurred.emit("delete_vlan", str(exc)) - async def _async_activate_vlan_with_timeout( - self, - conn_path: str, - vlan_id: int, - iface: str, - timeout: float = 45.0, - ) -> tuple[bool, str]: - """Activate a VLAN and wait for DHCP via D-Bus ``state_changed`` signal. - - Subscribes to ``ActiveConnection.state_changed`` (signature ``'uu'``) - which fires ``(ConnectionState, ConnectionStateReason)`` on every NM - transition. This replaces the old poll-and-sleep loop, cutting - latency from ~2 s per poll to near-instant and eliminating - unnecessary D-Bus round-trips on resource-constrained Pi hardware. - - .. note:: The old code used ``_NM_ACTIVATED = 4`` which was - actually ``DEACTIVATED`` — DHCP success was never detected - via state polling. This version uses the correct enum values. - """ - try: - active_path = await self._nm().activate_connection(conn_path, "/", "/") - except Exception as exc: - return ( - False, - f"VLAN {vlan_id}: activation request failed — {exc}", - ) - - # Fresh proxy for signal subscription lifetime. - ac = dbus_nm.ActiveConnection(bus=self._system_bus, connection_path=active_path) - - try: - async with asyncio.timeout(timeout): - # state_changed signature 'uu' -> (state: int, reason: int) - async for ac_state, ac_reason in ac.state_changed: - if not self._running: - return False, "Worker shutting down" - - try: - state_name = dbus_nm.ConnectionState(ac_state).name - except ValueError: - state_name = str(ac_state) - try: - reason_name = dbus_nm.ConnectionStateReason(ac_reason).name - except ValueError: - reason_name = str(ac_reason) - - logger.debug( - "VLAN %d AC: state=%s reason=%s", - vlan_id, - state_name, - reason_name, - ) - - if ac_state == dbus_nm.ConnectionState.ACTIVATED: - logger.info( - "VLAN %d DHCP activated on %s", - vlan_id, - iface, - ) - return True, "" - - if ac_state in ( - dbus_nm.ConnectionState.DEACTIVATING, - dbus_nm.ConnectionState.DEACTIVATED, - ): - logger.warning( - "VLAN %d DHCP failed: %s/%s on %s", - vlan_id, - state_name, - reason_name, - iface, - ) - return ( - False, - f"VLAN {vlan_id}: no DHCP server " - f"responded on {iface}.\n" - "Use a static IP or connect a " - "DHCP server to this segment.", - ) - - except TimeoutError: - logger.warning( - "VLAN %d DHCP timed out after %.0f s — " - "deactivating to stop NM retry loop", - vlan_id, - timeout, - ) - try: - await self._nm().deactivate_connection(active_path) - except Exception as exc: - logger.debug("VLAN deactivation after DHCP timeout ignored: %s", exc) - return ( - False, - f"VLAN {vlan_id}: DHCP timed out after " - f"{int(timeout)} s.\nNo DHCP server responded " - "on this network segment.\n" - "Use a static IP address instead.", - ) - - except Exception as exc: - err_str = str(exc) - if "does not exist" in err_str or "No such" in err_str: - logger.debug( - "VLAN %d active connection gone (%s) — signal iterator ended", - vlan_id, - err_str, - ) - return ( - False, - f"VLAN {vlan_id}: connection was removed externally.", - ) - raise - - # Signal iterator ended without a terminal state (shouldn't - # happen, but defensive). - return False, f"VLAN {vlan_id}: unexpected end of state stream." - async def _get_active_vlans(self) -> tuple[VlanInfo, ...]: """Return a tuple of VlanInfo for all currently active VLAN connections.""" vlans: list[VlanInfo] = [] diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 0d0a6df5..f9b17a52 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -40,7 +40,6 @@ logger = logging.getLogger(__name__) LOAD_TIMEOUT_MS = 30_000 -VLAN_DHCP_TIMEOUT_MS = 50_000 # Generous: worker has 45 s, UI needs headroom STATUS_CHECK_INTERVAL_MS = 2_000 @@ -402,13 +401,6 @@ def _on_network_state_changed(self, state: NetworkState) -> None: return return - # VLAN DHCP: keep loading visible. - if self._pending_operation == PendingOperation.VLAN_DHCP: - # Update display behind the loading overlay so state is - # current when loading is eventually cleared. - self._sync_ethernet_panel(state) - return - # Wi-Fi static IP / DHCP reset: complete when we have the right IP. if self._pending_operation == PendingOperation.WIFI_STATIC_IP: ip = state.current_ip or "" @@ -546,22 +538,13 @@ def _on_operation_complete(self, result: ConnectionResult) -> None: # Loading cleared by state machine (IP appears) or reconnect_complete. # No popup — the updated IP in the header is the confirmation. pass - elif self._pending_operation == PendingOperation.VLAN_DHCP: - # Worker confirmed VLAN DHCP success — clear loading and - # refresh the display to show the new VLAN interface. - self._clear_loading() - state = self._nm.current_state - self._display_connected_state(state) - self._emit_status_icon(state) - self._show_info_popup(result.message) else: self._show_info_popup(result.message) else: msg_lower = result.message.lower() - # DHCP VLAN / Wi-Fi static-IP errors: clear loading and show the - # reason without the generic error prefix. - if result.error_code in ("vlan_dhcp_timeout", "duplicate_vlan"): + # Duplicate VLAN: clear loading and show the reason. + if result.error_code == "duplicate_vlan": self._clear_loading() self._show_error_popup(result.message) return @@ -967,18 +950,6 @@ def _handle_load_timeout(self) -> None: self._display_connected_state(state) return - # VLAN DHCP — the 50 s UI timer expired before the worker's 45 s - # D-Bus signal timeout. Clear loading and show a specific message. - if self._pending_operation == PendingOperation.VLAN_DHCP: - self._clear_loading() - self._display_connected_state(state) - self._show_error_popup( - "VLAN DHCP timed out.\n" - "No DHCP server responded.\n" - "Use a static IP for this VLAN." - ) - return - # Static IP / DHCP reset — if a state with an IP has arrived, accept it. if self._pending_operation == PendingOperation.WIFI_STATIC_IP: if state.current_ip: @@ -1249,40 +1220,35 @@ def _on_vlan_apply(self) -> None: dns1 = self.vlan_dns1_field.text().strip() dns2 = self.vlan_dns2_field.text().strip() - # When IP is empty -> DHCP mode (no validation needed) - use_dhcp = not ip_addr - - if not use_dhcp: - if not self.vlan_ip_field.is_valid(): - self._show_error_popup("Invalid IP address.") - return - if not self.vlan_mask_field.is_valid_mask(): - self._show_error_popup("Invalid subnet mask.") - return - if gateway and not self.vlan_gateway_field.is_valid(): - self._show_error_popup("Invalid gateway address.") - return - if dns1 and not self.vlan_dns1_field.is_valid(): - self._show_error_popup("Invalid primary DNS.") - return - if dns2 and not self.vlan_dns2_field.is_valid(): - self._show_error_popup("Invalid secondary DNS.") - return + if not ip_addr: + self._show_error_popup("IP address is required.") + return + if not self.vlan_ip_field.is_valid(): + self._show_error_popup("Invalid IP address.") + return + if not self.vlan_mask_field.is_valid_mask(): + self._show_error_popup("Invalid subnet mask.") + return + if gateway and not self.vlan_gateway_field.is_valid(): + self._show_error_popup("Invalid gateway address.") + return + if dns1 and not self.vlan_dns1_field.is_valid(): + self._show_error_popup("Invalid primary DNS.") + return + if dns2 and not self.vlan_dns2_field.is_valid(): + self._show_error_popup("Invalid secondary DNS.") + return self.setCurrentIndex(self.indexOf(self.main_network_page)) - if use_dhcp: - self._pending_operation = PendingOperation.VLAN_DHCP - self._set_loading_state(True, timeout_ms=VLAN_DHCP_TIMEOUT_MS) - else: - self._pending_operation = PendingOperation.ETHERNET_ON - self._set_loading_state(True) + self._pending_operation = PendingOperation.ETHERNET_ON + self._set_loading_state(True) self._nm.create_vlan_connection( vlan_id, - ip_addr, # empty -> DHCP - mask if not use_dhcp else "", - gateway if not use_dhcp else "", - dns1 if not use_dhcp else "", - dns2 if not use_dhcp else "", + ip_addr, + mask, + gateway, + dns1, + dns2, ) self._nm.request_state_soon(delay_ms=3000) @@ -3386,7 +3352,7 @@ def _make_row(label_text, field): content_layout.addWidget(_make_row("VLAN ID", self.vlan_id_spinbox)) self.vlan_ip_field = IPAddressLineEdit( - parent=self.vlan_page, placeholder="192.168.1.100 (empty = DHCP)" + parent=self.vlan_page, placeholder="192.168.1.100" ) content_layout.addWidget(_make_row("IP Address", self.vlan_ip_field)) diff --git a/tests/network/test_models_unit.py b/tests/network/test_models_unit.py index 8a8da409..5eb4b451 100644 --- a/tests/network/test_models_unit.py +++ b/tests/network/test_models_unit.py @@ -116,7 +116,7 @@ def test_medium_is_default_for_saved_network(self): class TestPendingOperation: def test_all_members_exist(self): - assert len(PendingOperation) == 10 + assert len(PendingOperation) == 9 def test_is_int_enum(self): """PendingOperation values are plain ints (memory-efficient).""" @@ -160,7 +160,6 @@ def test_all_expected_names(self): "ETHERNET_ON", "ETHERNET_OFF", "WIFI_STATIC_IP", - "VLAN_DHCP", } def test_ethernet_on_value(self): diff --git a/tests/network/test_network_ui.py b/tests/network/test_network_ui.py index 2e7aa431..466ec14a 100644 --- a/tests/network/test_network_ui.py +++ b/tests/network/test_network_ui.py @@ -601,26 +601,6 @@ def test_static_ip_keeps_loading_for_old_ip(self, win): w._on_network_state_changed(state) assert w._is_connecting - def test_vlan_dhcp_keeps_loading_even_with_ethernet(self, win): - """VLAN DHCP must NOT clear loading when ethernet is already connected. - - This is the core fix — the old code used ETHERNET_ON which - cleared immediately because ethernet was already up. - """ - w, _ = win - self._start_loading(w, PendingOperation.VLAN_DHCP) - w._on_network_state_changed(_eth_state()) - # Loading MUST stay visible — VLAN DHCP is still in progress - assert w._is_connecting - assert w.loadingwidget.isVisible() - - def test_vlan_dhcp_syncs_ethernet_panel(self, win): - """While VLAN DHCP loading is visible, the ethernet panel must update.""" - w, _ = win - self._start_loading(w, PendingOperation.VLAN_DHCP) - with patch.object(w, "_sync_ethernet_panel") as mock_sync: - w._on_network_state_changed(_eth_state()) - mock_sync.assert_called() # ───────────────────────────────────────────────────────────────────────────── @@ -701,51 +681,6 @@ def test_failure_clears_loading(self, win): w._on_operation_complete(result) assert not w.loadingwidget.isVisible() - def test_vlan_dhcp_timeout_shows_error(self, win): - w, _ = win - w._is_first_run = False - result = ConnectionResult( - success=False, - message="No DHCP server", - error_code="vlan_dhcp_timeout", - ) - with patch.object(w, "_show_error_popup") as mock_err: - w._on_operation_complete(result) - mock_err.assert_called_once() - - def test_vlan_dhcp_timeout_clears_loading(self, win): - """VLAN DHCP timeout must clear loading state.""" - w, _ = win - w._is_first_run = False - w._is_connecting = True - w._pending_operation = PendingOperation.VLAN_DHCP - w.loadingwidget.setVisible(True) - result = ConnectionResult( - success=False, - message="No DHCP server", - error_code="vlan_dhcp_timeout", - ) - w._on_operation_complete(result) - assert not w.loadingwidget.isVisible() - assert not w._is_connecting - - def test_vlan_dhcp_success_clears_loading(self, win): - """Successful VLAN DHCP must clear loading and update display.""" - w, nm = win - w._is_first_run = False - w._is_connecting = True - w._pending_operation = PendingOperation.VLAN_DHCP - w.loadingwidget.setVisible(True) - nm.current_state = _eth_state() - result = ConnectionResult(success=True, message="VLAN 100 connected") - with patch.object(w, "_display_connected_state") as mock_disp: - with patch.object(w, "_show_info_popup") as mock_info: - w._on_operation_complete(result) - mock_disp.assert_called_once() - mock_info.assert_called_once_with("VLAN 100 connected") - assert not w.loadingwidget.isVisible() - assert not w._is_connecting - def test_transient_mismatch_retries(self, win, qapp): """NM device-mismatch error during Wi-Fi connect -> retry scheduled.""" w, nm = win @@ -950,15 +885,6 @@ def test_generic_timeout_shows_error_popup(self, win): mock_err.assert_called_once() assert not w.loadingwidget.isVisible() - def test_vlan_dhcp_timeout_shows_specific_error(self, win): - """50 s UI timer fires before worker's 45 s signal timeout.""" - w, nm = win - self._setup(w, nm, PendingOperation.VLAN_DHCP, _eth_state()) - with patch.object(w, "_show_error_popup") as mock_err: - w._handle_load_timeout() - mock_err.assert_called_once() - assert "VLAN DHCP" in mock_err.call_args[0][0] - assert not w.loadingwidget.isVisible() # ───────────────────────────────────────────────────────────────────────────── @@ -967,37 +893,28 @@ def test_vlan_dhcp_timeout_shows_specific_error(self, win): class TestOnVlanApply: - """Verify _on_vlan_apply routes DHCP -> VLAN_DHCP and static -> ETHERNET_ON.""" + """Verify _on_vlan_apply requires static IP and sets ETHERNET_ON.""" - def test_dhcp_mode_sets_vlan_dhcp_pending(self, win): + def test_empty_ip_shows_error(self, win): w, nm = win w._is_first_run = False w.vlan_id_spinbox.setValue(100) - w.vlan_ip_field.setText("") # empty IP -> DHCP - w._on_vlan_apply() - assert w._pending_operation == PendingOperation.VLAN_DHCP - assert w._is_connecting - nm.create_vlan_connection.assert_called_once_with(100, "", "", "", "", "") + w.vlan_ip_field.setText("") + with patch.object(w, "_show_error_popup") as mock_err: + w._on_vlan_apply() + mock_err.assert_called_once_with("IP address is required.") + nm.create_vlan_connection.assert_not_called() def test_static_ip_mode_sets_ethernet_on_pending(self, win): w, nm = win w._is_first_run = False w.vlan_ip_field.setText("10.0.0.1") w.vlan_mask_field.setText("255.255.255.0") - # Mock validation to pass w.vlan_ip_field.is_valid = MagicMock(return_value=True) w.vlan_mask_field.is_valid_mask = MagicMock(return_value=True) w._on_vlan_apply() assert w._pending_operation == PendingOperation.ETHERNET_ON - def test_dhcp_mode_uses_longer_timeout(self, win): - """VLAN DHCP loading must use VLAN_DHCP_TIMEOUT_MS (50 s).""" - w, nm = win - w._is_first_run = False - w.vlan_ip_field.setText("") # DHCP - w._on_vlan_apply() - assert w._load_timer.interval() == 50000 - class TestOnNetworkError: def test_shows_error_popup(self, win): diff --git a/tests/network/test_worker_unit.py b/tests/network/test_worker_unit.py index bcc2426f..43fce526 100644 --- a/tests/network/test_worker_unit.py +++ b/tests/network/test_worker_unit.py @@ -60,7 +60,7 @@ def _make_worker(qapp, *, running=True, with_wifi=True, with_wired=False): w._is_hotspot_active = False w._consecutive_dbus_errors = 0 w._background_tasks = set() - w._deleted_vlan_ids = set() + # Signal-reactive architecture: persistent signal proxies w._signal_nm = None @@ -97,7 +97,7 @@ def _bare_worker(qapp): w._is_hotspot_active = False w._consecutive_dbus_errors = 0 w._background_tasks = set() - w._deleted_vlan_ids = set() + w._signal_nm = None w._signal_wifi = None w._signal_wired = None @@ -128,7 +128,7 @@ def _make(qapp, *, running=True, wifi=True, wired=True): w._is_hotspot_active = False w._consecutive_dbus_errors = 0 w._background_tasks = set() - w._deleted_vlan_ids = set() + w._signal_nm = None w._signal_wifi = None w._signal_wired = None @@ -2569,182 +2569,6 @@ def test_happy_path(self, qapp): -class _FakeSignal: - """Async iterable that yields pre-programmed (state, reason) tuples.""" - - def __init__(self, events: list[tuple[int, int]]): - self._events = events - - def __aiter__(self): - return self - - async def __anext__(self): - if not self._events: - raise StopAsyncIteration - return self._events.pop(0) - - -class TestActivateVlanWithTimeout: - """Tests for the signal-reactive _async_activate_vlan_with_timeout.""" - - def _patch_ac(self, w, events): - """Patch dbus_nm.ActiveConnection to return a mock with a fake signal.""" - mock_ac = MagicMock() - mock_ac.state_changed = _FakeSignal(events) - return patch.object( - _worker_mod.dbus_nm, "ActiveConnection", return_value=mock_ac - ) - - def test_activated_returns_success(self, qapp): - w = _make(qapp) - nm = AsyncProxyMock() - nm.activate_connection = AsyncMock(return_value="/active/1") - _wire(w, nm=nm) - - # ConnectionState.ACTIVATING=1, then ACTIVATED=2 - events = [(1, 1), (2, 1)] - with self._patch_ac(w, events): - ok, msg = _run( - w._async_activate_vlan_with_timeout( - "/path/vlan", 100, "eth0.100", timeout=5.0 - ) - ) - assert ok is True - assert msg == "" - - def test_deactivated_returns_failure(self, qapp): - w = _make(qapp) - nm = AsyncProxyMock() - nm.activate_connection = AsyncMock(return_value="/active/2") - _wire(w, nm=nm) - - # ACTIVATING then DEACTIVATED (DHCP failed) - events = [(1, 1), (4, 6)] # reason 6 = CONNECT_TIMEOUT - with self._patch_ac(w, events): - ok, msg = _run( - w._async_activate_vlan_with_timeout( - "/path/vlan", 200, "eth0.200", timeout=5.0 - ) - ) - assert ok is False - assert "no DHCP server" in msg - - def test_deactivating_returns_failure(self, qapp): - w = _make(qapp) - nm = AsyncProxyMock() - nm.activate_connection = AsyncMock(return_value="/active/3") - _wire(w, nm=nm) - - events = [(1, 1), (3, 5)] # DEACTIVATING, reason=IP_CONFIG_INVALID - with self._patch_ac(w, events): - ok, msg = _run( - w._async_activate_vlan_with_timeout( - "/path/vlan", 300, "eth0.300", timeout=5.0 - ) - ) - assert ok is False - - def test_activation_request_failure(self, qapp): - w = _make(qapp) - nm = AsyncProxyMock() - nm.activate_connection = AsyncMock(side_effect=RuntimeError("boom")) - _wire(w, nm=nm) - - ok, msg = _run( - w._async_activate_vlan_with_timeout( - "/path/vlan", 400, "eth0.400", timeout=5.0 - ) - ) - assert ok is False - assert "activation request failed" in msg - - def test_object_destroyed_returns_failure(self, qapp): - w = _make(qapp) - nm = AsyncProxyMock() - nm.activate_connection = AsyncMock(return_value="/active/4") - _wire(w, nm=nm) - - # Simulate object destruction — signal iterator raises - class _DestroyedSignal: - def __aiter__(self): - return self - - async def __anext__(self): - raise Exception("Object does not exist at path") - - mock_ac = MagicMock() - mock_ac.state_changed = _DestroyedSignal() - with patch.object( - _worker_mod.dbus_nm, "ActiveConnection", return_value=mock_ac - ): - ok, msg = _run( - w._async_activate_vlan_with_timeout( - "/path/vlan", 500, "eth0.500", timeout=5.0 - ) - ) - assert ok is False - assert "removed externally" in msg - - def test_timeout_deactivates_and_returns_failure(self, qapp): - w = _make(qapp) - nm = AsyncProxyMock() - nm.activate_connection = AsyncMock(return_value="/active/5") - nm.deactivate_connection = AsyncMock() - _wire(w, nm=nm) - - # Signal that never reaches terminal state — just stays ACTIVATING - class _SlowSignal: - async def __aiter__(self): - while True: - await asyncio.sleep(0.5) - yield (1, 1) # ACTIVATING, NONE - - mock_ac = MagicMock() - mock_ac.state_changed = _SlowSignal() - with patch.object( - _worker_mod.dbus_nm, "ActiveConnection", return_value=mock_ac - ): - ok, msg = _run( - w._async_activate_vlan_with_timeout( - "/path/vlan", 600, "eth0.600", timeout=1.5 - ) - ) - assert ok is False - assert "timed out" in msg - nm.deactivate_connection.assert_awaited_once() - - def test_worker_shutdown_during_wait(self, qapp): - w = _make(qapp) - nm = AsyncProxyMock() - nm.activate_connection = AsyncMock(return_value="/active/6") - _wire(w, nm=nm) - - class _ShutdownSignal: - def __init__(self, worker): - self._w = worker - - def __aiter__(self): - return self - - async def __anext__(self): - self._w._running = False - return (1, 1) - - mock_ac = MagicMock() - mock_ac.state_changed = _ShutdownSignal(w) - with patch.object( - _worker_mod.dbus_nm, "ActiveConnection", return_value=mock_ac - ): - ok, msg = _run( - w._async_activate_vlan_with_timeout( - "/path/vlan", 700, "eth0.700", timeout=5.0 - ) - ) - assert ok is False - assert "shutting down" in msg - - - class TestCreateVlan: def test_no_wired_device_emits_error(self, qapp): w = _make(qapp, wired=False) @@ -2753,58 +2577,7 @@ def test_no_wired_device_emits_error(self, qapp): _run(w._async_create_vlan(100, "10.0.0.1", "255.255.255.0", "10.0.0.1", "", "")) assert errors == ["create_vlan"] - def test_happy_path_dhcp(self, qapp): - w = _make(qapp) - nm = AsyncProxyMock(wireless_enabled=False) - nm.activate_connection = AsyncMock(return_value="/active/path") - _wire(w, nm=nm) - wifi = AsyncProxyMock() - wifi.disconnect = AsyncMock() - _wire(w, wifi_proxy=wifi) - settings = AsyncProxyMock() - settings.add_connection = AsyncMock(return_value="/path/vlan") - settings.list_connections = AsyncMock(return_value=[]) - _wire(w, settings=settings) - w._is_ethernet_connected = AsyncMock(return_value=True) - w._wait_for_wifi_radio = AsyncMock(return_value=True) - w._deactivate_connection_by_id = AsyncMock(return_value=False) - w._delete_all_connections_by_id = AsyncMock() - w._build_current_state = AsyncMock(return_value=NetworkState()) - w._async_activate_vlan_with_timeout = AsyncMock(return_value=(True, "")) - - results = [] - w.connection_result.connect(results.append) - # DHCP mode — empty ip_address - _run(w._async_create_vlan(100, "", "255.255.255.0", "", "", "")) - settings.add_connection.assert_awaited_once() - w._async_activate_vlan_with_timeout.assert_awaited_once() - assert any(r.success for r in results) - - def test_dhcp_failure_emits_error_and_cleans_up(self, qapp): - w = _make(qapp) - nm = AsyncProxyMock(wireless_enabled=False) - nm.activate_connection = AsyncMock(return_value="/active/path") - _wire(w, nm=nm) - settings = AsyncProxyMock() - settings.add_connection = AsyncMock(return_value="/path/vlan") - settings.list_connections = AsyncMock(return_value=[]) - _wire(w, settings=settings) - w._is_ethernet_connected = AsyncMock(return_value=True) - w._deactivate_connection_by_id = AsyncMock(return_value=False) - w._delete_all_connections_by_id = AsyncMock() - w._build_current_state = AsyncMock(return_value=NetworkState()) - w._async_activate_vlan_with_timeout = AsyncMock( - return_value=(False, "No DHCP server") - ) - - results = [] - w.connection_result.connect(results.append) - _run(w._async_create_vlan(100, "", "", "", "", "")) - # Profile should be cleaned up on failure - assert w._delete_all_connections_by_id.await_count >= 1 - assert any(not r.success for r in results) - - def test_static_ip_skips_signal_wait(self, qapp): + def test_static_ip_happy_path(self, qapp): w = _make(qapp) nm = AsyncProxyMock(wireless_enabled=False) nm.activate_connection = AsyncMock() @@ -2837,7 +2610,6 @@ def test_happy_path(self, qapp): _run(w._async_delete_vlan(100)) w._delete_all_connections_by_id.assert_awaited_once() assert results[0].success is True - assert 100 in w._deleted_vlan_ids def test_exception_emits_error(self, qapp): w = _make(qapp) From fc38dc429e9428daa704c9ae81426f2161243711 Mon Sep 17 00:00:00 2001 From: Roberto Martins Date: Mon, 9 Mar 2026 19:35:04 +0000 Subject: [PATCH 62/70] Bugfix/eddy current calibration home (#191) * bugfix: home before eddy current paper-test * refactor : ran ruff formatter --- BlocksScreen/lib/panels/widgets/probeHelperPage.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index 372499ef..fe2fa180 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -102,6 +102,10 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: if "state" in field: if value in ("standby"): if self._eddy_calibration_state: + self.run_gcode_signal.emit("G28\nM400") + self._move_to_pos( + self.z_offset_safe_xy[0], self.z_offset_safe_xy[1], 100 + ) self.call_load_panel.emit(True, "Almost done...\nPlease wait") self.run_gcode_signal.emit(self._eddy_command) From 11cbfcfa306094ab6cab11f66ac340083397ca1c Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Mon, 9 Mar 2026 19:36:19 +0000 Subject: [PATCH 63/70] refactor(keyboard): delete unused keys, always show . in all screens, simplify the code and added unit tests (#192) --- .../lib/panels/widgets/keyboardPage.py | 1319 ++++------------- tests/util/test_keyboard_page_unit.py | 223 +++ 2 files changed, 472 insertions(+), 1070 deletions(-) create mode 100644 tests/util/test_keyboard_page_unit.py diff --git a/BlocksScreen/lib/panels/widgets/keyboardPage.py b/BlocksScreen/lib/panels/widgets/keyboardPage.py index 4f1d13d8..88ba9f2c 100644 --- a/BlocksScreen/lib/panels/widgets/keyboardPage.py +++ b/BlocksScreen/lib/panels/widgets/keyboardPage.py @@ -1,1166 +1,345 @@ -from lib.utils.icon_button import IconButton +import typing +from lib.utils.icon_button import IconButton from PyQt6 import QtCore, QtGui, QtWidgets +_LOWERCASE = list("qwertyuiopasdfghjklzxcvbnm") +_UPPERCASE = list("QWERTYUIOPASDFGHJKLZXCVBNM") +_NUMBERS = [ + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "0", + "@", + "#", + "$", + '"', + "&&", + "*", + "-", + "+", + "=", + "(", + ")", + "!", + ":", + ";", + "'", + "?", +] +_SYMBOLS = [ + "~", + "`", + "|", + "%", + "^", + "°", + "_", + "{", + "}", + "[", + "]", + "<", + ">", + "/", + "\\", + ",", + ".", + "-", + "+", + "=", + "!", + "?", + ":", + ";", + "'", + "#", +] + +_NUM_KEYS = 26 + + +def _make_key_font(size: int = 29) -> QtGui.QFont: + font = QtGui.QFont() + font.setFamily("Modern") + font.setPointSize(size) + return font + class CustomQwertyKeyboard(QtWidgets.QWidget): - """A custom keyboard for inserting integer values""" + """Custom on-screen QWERTY keyboard for touch input.""" - value_selected = QtCore.pyqtSignal(str, name="value_selected") - request_back = QtCore.pyqtSignal(name="request_back") + value_selected: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="value_selected" + ) + request_back: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + name="request_back" + ) - def __init__( - self, - parent, - ) -> None: + def __init__(self, parent: QtWidgets.QWidget) -> None: super().__init__(parent) - self._setupUi() self.current_value: str = "" - self.symbolsrun = False - self.setCursor( - QtCore.Qt.CursorShape.BlankCursor - ) # Disable cursor on touch keyboard + self.symbolsrun: bool = False + self._key_buttons: list[QtWidgets.QPushButton] = [] - self.K_q.clicked.connect(lambda: self.value_inserted(str(self.K_q.text()))) - self.K_w.clicked.connect(lambda: self.value_inserted(str(self.K_w.text()))) - self.K_e.clicked.connect(lambda: self.value_inserted(str(self.K_e.text()))) - self.K_r.clicked.connect(lambda: self.value_inserted(str(self.K_r.text()))) - self.K_t.clicked.connect(lambda: self.value_inserted(str(self.K_t.text()))) - self.K_y.clicked.connect(lambda: self.value_inserted(str(self.K_y.text()))) - self.K_u.clicked.connect(lambda: self.value_inserted(str(self.K_u.text()))) - self.K_i.clicked.connect(lambda: self.value_inserted(str(self.K_i.text()))) - self.K_o.clicked.connect(lambda: self.value_inserted(str(self.K_o.text()))) - self.K_p.clicked.connect(lambda: self.value_inserted(str(self.K_p.text()))) - self.K_a.clicked.connect(lambda: self.value_inserted(str(self.K_a.text()))) - self.K_s.clicked.connect(lambda: self.value_inserted(str(self.K_s.text()))) - self.K_d.clicked.connect(lambda: self.value_inserted(str(self.K_d.text()))) - self.K_f.clicked.connect(lambda: self.value_inserted(str(self.K_f.text()))) - self.K_g.clicked.connect(lambda: self.value_inserted(str(self.K_g.text()))) - self.K_h.clicked.connect(lambda: self.value_inserted(str(self.K_h.text()))) - self.K_j.clicked.connect(lambda: self.value_inserted(str(self.K_j.text()))) - self.K_k.clicked.connect(lambda: self.value_inserted(str(self.K_k.text()))) - self.K_l.clicked.connect(lambda: self.value_inserted(str(self.K_l.text()))) - self.K_z.clicked.connect(lambda: self.value_inserted(str(self.K_z.text()))) - self.K_x.clicked.connect(lambda: self.value_inserted(str(self.K_x.text()))) - self.K_c.clicked.connect(lambda: self.value_inserted(str(self.K_c.text()))) - self.K_v.clicked.connect(lambda: self.value_inserted(str(self.K_v.text()))) - self.K_b.clicked.connect(lambda: self.value_inserted(str(self.K_b.text()))) - self.K_n.clicked.connect(lambda: self.value_inserted(str(self.K_n.text()))) - self.K_m.clicked.connect(lambda: self.value_inserted(str(self.K_m.text()))) + self._setup_ui() + self.setCursor(QtCore.Qt.CursorShape.BlankCursor) + for btn in self._key_buttons: + btn.clicked.connect(lambda _, b=btn: self.value_inserted(b.text())) + + self.K_dot.clicked.connect(lambda: self.value_inserted(".")) self.K_space.clicked.connect(lambda: self.value_inserted(" ")) self.k_Enter.clicked.connect(lambda: self.value_inserted("enter")) self.k_delete.clicked.connect(lambda: self.value_inserted("clear")) - - self.inserted_value.setText("") - self.K_keychange.clicked.connect(self.handle_keyboard_layout) self.K_shift.clicked.connect(self.handle_keyboard_layout) + self.numpad_back_btn.clicked.connect(self.request_back.emit) - self.numpad_back_btn.clicked.connect(lambda: self.request_back.emit()) + self.inserted_value.setText("") - self.setStyleSheet(""" - QPushButton { - background-color: #dfdfdf; - border-radius: 10px; - padding: 6px; - font-size: 25px; - } - QPushButton:pressed { - background-color: lightgrey; - color: black; - } - QPushButton:checked { - background-color: #212120; - color: white; - } - """) + self.setStyleSheet( + "QPushButton {" + " background-color: #dfdfdf;" + " border-radius: 10px;" + " padding: 6px;" + " font-size: 25px;" + "}" + "QPushButton:pressed {" + " background-color: lightgrey;" + " color: black;" + "}" + "QPushButton:checked {" + " background-color: #212120;" + " color: white;" + "}" + ) self.handle_keyboard_layout() - def handle_keyboard_layout(self): - """Verifies if shift is toggled, changes layout accordingly""" + def handle_keyboard_layout(self) -> None: + """Update key labels based on current shift/keychange state.""" shift = self.K_shift.isChecked() keychange = self.K_keychange.isChecked() - # references to all key buttons - keys = [ - self.K_q, - self.K_w, - self.K_e, - self.K_r, - self.K_t, - self.K_y, - self.K_u, - self.K_i, - self.K_o, - self.K_p, - self.K_a, - self.K_s, - self.K_d, - self.K_f, - self.K_g, - self.K_h, - self.K_j, - self.K_k, - self.K_l, - self.K_z, - self.K_x, - self.K_c, - self.K_v, - self.K_b, - self.K_n, - self.K_m, - ] - - # ------------------------------- - # Keyboard Layouts - # ------------------------------- - lowercase = list("qwertyuiopasdfghjklzxcvbnm") - uppercase = list("QWERTYUIOPASDFGHJKLZXCVBNM") - numbers = [ - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "0", - "@", - "#", - "$", - "%", - "&&", - "*", - "-", - "+", - "=", - "(", - ")", - "!", - ":", - ";", - "'", - "?", - ] - symbols = [ - "~", - "`", - "|", - "•", - "√", - "π", - "÷", - "×", - "¶", - "∆", - "€", - "£", - "¥", - "₩", - "^", - "°", - "_", - "{", - "}", - "[", - "]", - "<", - ">", - "/", - "\\", - ",", - ".", - ] - - # ------------------------------- - # Logic - # ------------------------------- - if not keychange and not shift: - layout = lowercase - + layout = _LOWERCASE elif not keychange and shift: if self.symbolsrun: - layout = lowercase + layout = _LOWERCASE self.K_shift.setChecked(False) self.symbolsrun = False else: - layout = uppercase + layout = _UPPERCASE elif keychange and not shift: - layout = numbers + layout = _NUMBERS elif keychange and shift: - layout = symbols + layout = _SYMBOLS self.symbolsrun = True else: - layout = lowercase + layout = _LOWERCASE - # update all button texts - for btn, txt in zip(keys, layout): + for btn, txt in zip(self._key_buttons, layout): btn.setText(txt) - # update shift button text for clarity - if keychange: - self.K_shift.setText("#+=") - else: - self.K_shift.setText("Shift") + self.K_shift.setText("#+=") if keychange else self.K_shift.setText("⇧") def value_inserted(self, value: str) -> None: - """Handle value insertion on the keyboard - - Args: - value (int | str): value - """ - + """Handle key press: append char, delete, or submit on enter.""" if value == "&&": value = "&" - if "enter" in value: + if value == "enter": self.value_selected.emit(self.current_value) self.current_value = "" self.inserted_value.setText("") return - elif "clear" in value: + if value == "clear": if len(self.current_value) > 1: self.current_value = self.current_value[:-1] - else: self.current_value = "" - else: - self.current_value += str(value) + self.current_value += value - self.inserted_value.setText(str(self.current_value)) + self.inserted_value.setText(self.current_value) def set_value(self, value: str) -> None: - """Set keyboard value""" + """Pre-fill keyboard input with an existing value.""" self.current_value = value self.inserted_value.setText(value) - def _setupUi(self): + def _create_key_button( + self, + parent: QtWidgets.QWidget, + name: str, + *, + min_w: int = 75, + min_h: int = 50, + max_h: int = 50, + h_policy: QtWidgets.QSizePolicy.Policy = ( + QtWidgets.QSizePolicy.Policy.Expanding + ), + v_policy: QtWidgets.QSizePolicy.Policy = (QtWidgets.QSizePolicy.Policy.Fixed), + ) -> QtWidgets.QPushButton: + """Create a styled key button with consistent appearance.""" + btn = QtWidgets.QPushButton(parent=parent) + policy = QtWidgets.QSizePolicy(h_policy, v_policy) + btn.setSizePolicy(policy) + btn.setMinimumSize(QtCore.QSize(min_w, min_h)) + btn.setMaximumSize(QtCore.QSize(16777215, max_h)) + btn.setFont(_make_key_font()) + btn.setTabletTracking(False) + btn.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) + btn.setFlat(True) + btn.setObjectName(name) + return btn + + def _setup_ui(self) -> None: self.setObjectName("self") - self.setEnabled(True) self.resize(800, 480) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Preferred, - QtWidgets.QSizePolicy.Policy.Preferred, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) - self.setSizePolicy(sizePolicy) self.setMaximumSize(QtCore.QSize(800, 480)) self.setCursor(QtGui.QCursor(QtCore.Qt.CursorShape.ArrowCursor)) self.setLayoutDirection(QtCore.Qt.LayoutDirection.LeftToRight) - self.layoutWidget_2 = QtWidgets.QWidget(parent=self) - self.layoutWidget_2.setGeometry(QtCore.QRect(10, 150, 781, 52)) - self.layoutWidget_2.setObjectName("layoutWidget_2") - self.gridLayout_2 = QtWidgets.QGridLayout(self.layoutWidget_2) - self.gridLayout_2.setSizeConstraint( - QtWidgets.QLayout.SizeConstraint.SetMinimumSize - ) - self.gridLayout_2.setContentsMargins(0, 0, 0, 0) - self.gridLayout_2.setHorizontalSpacing(2) - self.gridLayout_2.setVerticalSpacing(5) - self.gridLayout_2.setObjectName("gridLayout_2") - self.K_p = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_p.sizePolicy().hasHeightForWidth()) - self.K_p.setSizePolicy(sizePolicy) - self.K_p.setMinimumSize(QtCore.QSize(75, 50)) - self.K_p.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_p.setFont(font) - self.K_p.setTabletTracking(False) - self.K_p.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_p.setFlat(True) - self.K_p.setObjectName("K_p") - self.gridLayout_2.addWidget( - self.K_p, 0, 9, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_i = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_i.sizePolicy().hasHeightForWidth()) - self.K_i.setSizePolicy(sizePolicy) - self.K_i.setMinimumSize(QtCore.QSize(75, 50)) - self.K_i.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_i.setFont(font) - self.K_i.setTabletTracking(False) - self.K_i.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_i.setFlat(True) - self.K_i.setObjectName("K_i") - self.gridLayout_2.addWidget( - self.K_i, 0, 7, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_y = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_y.sizePolicy().hasHeightForWidth()) - self.K_y.setSizePolicy(sizePolicy) - self.K_y.setMinimumSize(QtCore.QSize(75, 50)) - self.K_y.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_y.setFont(font) - self.K_y.setTabletTracking(False) - self.K_y.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_y.setFlat(True) - self.K_y.setObjectName("K_y") - self.gridLayout_2.addWidget( - self.K_y, 0, 5, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_t = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_t.sizePolicy().hasHeightForWidth()) - self.K_t.setSizePolicy(sizePolicy) - self.K_t.setMinimumSize(QtCore.QSize(75, 50)) - self.K_t.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_t.setFont(font) - self.K_t.setTabletTracking(False) - self.K_t.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_t.setFlat(True) - self.K_t.setObjectName("K_t") - self.gridLayout_2.addWidget( - self.K_t, 0, 4, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_r = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_r.sizePolicy().hasHeightForWidth()) - self.K_r.setSizePolicy(sizePolicy) - self.K_r.setMinimumSize(QtCore.QSize(75, 50)) - self.K_r.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_r.setFont(font) - self.K_r.setTabletTracking(False) - self.K_r.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_r.setFlat(True) - self.K_r.setObjectName("K_r") - self.gridLayout_2.addWidget( - self.K_r, 0, 3, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_o = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_o.sizePolicy().hasHeightForWidth()) - self.K_o.setSizePolicy(sizePolicy) - self.K_o.setMinimumSize(QtCore.QSize(75, 50)) - self.K_o.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_o.setFont(font) - self.K_o.setTabletTracking(False) - self.K_o.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_o.setFlat(True) - self.K_o.setObjectName("K_o") - self.gridLayout_2.addWidget( - self.K_o, 0, 8, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_u = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_u.sizePolicy().hasHeightForWidth()) - self.K_u.setSizePolicy(sizePolicy) - self.K_u.setMinimumSize(QtCore.QSize(75, 50)) - self.K_u.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_u.setFont(font) - self.K_u.setTabletTracking(False) - self.K_u.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_u.setFlat(True) - self.K_u.setObjectName("K_u") - self.gridLayout_2.addWidget( - self.K_u, 0, 6, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_w = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_w.sizePolicy().hasHeightForWidth()) - self.K_w.setSizePolicy(sizePolicy) - self.K_w.setMinimumSize(QtCore.QSize(75, 50)) - self.K_w.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_w.setFont(font) - self.K_w.setTabletTracking(False) - self.K_w.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_w.setFlat(True) - self.K_w.setObjectName("K_w") - self.gridLayout_2.addWidget( - self.K_w, - 0, - 1, - 1, - 1, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - self.K_e = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_e.sizePolicy().hasHeightForWidth()) - self.K_e.setSizePolicy(sizePolicy) - self.K_e.setMinimumSize(QtCore.QSize(75, 50)) - self.K_e.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_e.setFont(font) - self.K_e.setTabletTracking(False) - self.K_e.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_e.setFlat(True) - self.K_e.setObjectName("K_e") - self.gridLayout_2.addWidget( - self.K_e, 0, 2, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter - ) - self.K_q = QtWidgets.QPushButton(parent=self.layoutWidget_2) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, - QtWidgets.QSizePolicy.Policy.Expanding, - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_q.sizePolicy().hasHeightForWidth()) - self.K_q.setSizePolicy(sizePolicy) - self.K_q.setMinimumSize(QtCore.QSize(75, 50)) - self.K_q.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_q.setFont(font) - self.K_q.setTabletTracking(False) - self.K_q.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_q.setFlat(True) - self.K_q.setObjectName("K_q") - self.gridLayout_2.addWidget( - self.K_q, - 0, - 0, - 1, - 1, - QtCore.Qt.AlignmentFlag.AlignHCenter | QtCore.Qt.AlignmentFlag.AlignVCenter, - ) - self.layoutWidget = QtWidgets.QWidget(parent=self) - self.layoutWidget.setGeometry(QtCore.QRect(50, 220, 701, 52)) - self.layoutWidget.setMinimumSize(QtCore.QSize(64, 0)) - self.layoutWidget.setObjectName("layoutWidget") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.layoutWidget) - self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout_2.setSpacing(2) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.K_a = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_a.sizePolicy().hasHeightForWidth()) - self.K_a.setSizePolicy(sizePolicy) - self.K_a.setMinimumSize(QtCore.QSize(75, 50)) - self.K_a.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_a.setFont(font) - self.K_a.setTabletTracking(False) - self.K_a.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_a.setFlat(True) - self.K_a.setObjectName("K_a") - self.horizontalLayout_2.addWidget(self.K_a) - self.K_s = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_s.sizePolicy().hasHeightForWidth()) - self.K_s.setSizePolicy(sizePolicy) - self.K_s.setMinimumSize(QtCore.QSize(75, 50)) - self.K_s.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_s.setFont(font) - self.K_s.setTabletTracking(False) - self.K_s.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_s.setFlat(True) - self.K_s.setObjectName("K_s") - self.horizontalLayout_2.addWidget(self.K_s) - self.K_d = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_d.sizePolicy().hasHeightForWidth()) - self.K_d.setSizePolicy(sizePolicy) - self.K_d.setMinimumSize(QtCore.QSize(75, 50)) - self.K_d.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_d.setFont(font) - self.K_d.setTabletTracking(False) - self.K_d.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_d.setFlat(True) - self.K_d.setObjectName("K_d") - self.horizontalLayout_2.addWidget(self.K_d) - self.K_f = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_f.sizePolicy().hasHeightForWidth()) - self.K_f.setSizePolicy(sizePolicy) - self.K_f.setMinimumSize(QtCore.QSize(75, 50)) - self.K_f.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_f.setFont(font) - self.K_f.setTabletTracking(False) - self.K_f.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_f.setFlat(True) - self.K_f.setObjectName("K_f") - self.horizontalLayout_2.addWidget(self.K_f) - self.K_g = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_g.sizePolicy().hasHeightForWidth()) - self.K_g.setSizePolicy(sizePolicy) - self.K_g.setMinimumSize(QtCore.QSize(75, 50)) - self.K_g.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_g.setFont(font) - self.K_g.setTabletTracking(False) - self.K_g.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_g.setFlat(True) - self.K_g.setObjectName("K_g") - self.horizontalLayout_2.addWidget(self.K_g) - self.K_h = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_h.sizePolicy().hasHeightForWidth()) - self.K_h.setSizePolicy(sizePolicy) - self.K_h.setMinimumSize(QtCore.QSize(75, 50)) - self.K_h.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_h.setFont(font) - self.K_h.setTabletTracking(False) - self.K_h.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_h.setFlat(True) - self.K_h.setObjectName("K_h") - self.horizontalLayout_2.addWidget(self.K_h) - self.K_j = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_j.sizePolicy().hasHeightForWidth()) - self.K_j.setSizePolicy(sizePolicy) - self.K_j.setMinimumSize(QtCore.QSize(75, 50)) - self.K_j.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_j.setFont(font) - self.K_j.setTabletTracking(False) - self.K_j.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_j.setFlat(True) - self.K_j.setObjectName("K_j") - self.horizontalLayout_2.addWidget(self.K_j) - self.K_k = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_k.sizePolicy().hasHeightForWidth()) - self.K_k.setSizePolicy(sizePolicy) - self.K_k.setMinimumSize(QtCore.QSize(75, 50)) - self.K_k.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_k.setFont(font) - self.K_k.setTabletTracking(False) - self.K_k.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_k.setFlat(True) - self.K_k.setObjectName("K_k") - self.horizontalLayout_2.addWidget(self.K_k) - self.K_l = QtWidgets.QPushButton(parent=self.layoutWidget) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_l.sizePolicy().hasHeightForWidth()) - self.K_l.setSizePolicy(sizePolicy) - self.K_l.setMinimumSize(QtCore.QSize(75, 50)) - self.K_l.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_l.setFont(font) - self.K_l.setTabletTracking(False) - self.K_l.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_l.setFlat(True) - self.K_l.setObjectName("K_l") - self.horizontalLayout_2.addWidget(self.K_l) - self.layoutWidget_3 = QtWidgets.QWidget(parent=self) - self.layoutWidget_3.setGeometry(QtCore.QRect(100, 290, 591, 52)) - self.layoutWidget_3.setObjectName("layoutWidget_3") - self.horizontalLayout = QtWidgets.QHBoxLayout(self.layoutWidget_3) - self.horizontalLayout.setSizeConstraint( - QtWidgets.QLayout.SizeConstraint.SetMinimumSize - ) - self.horizontalLayout.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout.setSpacing(2) - self.horizontalLayout.setObjectName("horizontalLayout") - self.K_z = QtWidgets.QPushButton(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_z.sizePolicy().hasHeightForWidth()) - self.K_z.setSizePolicy(sizePolicy) - self.K_z.setMinimumSize(QtCore.QSize(60, 50)) - self.K_z.setMaximumSize(QtCore.QSize(16777215, 60)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_z.setFont(font) - self.K_z.setTabletTracking(False) - self.K_z.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_z.setFlat(True) - self.K_z.setObjectName("K_z") - self.horizontalLayout.addWidget(self.K_z) - self.K_x = QtWidgets.QPushButton(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_x.sizePolicy().hasHeightForWidth()) - self.K_x.setSizePolicy(sizePolicy) - self.K_x.setMinimumSize(QtCore.QSize(60, 50)) - self.K_x.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_x.setFont(font) - self.K_x.setTabletTracking(False) - self.K_x.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_x.setFlat(True) - self.K_x.setObjectName("K_x") - self.horizontalLayout.addWidget(self.K_x) - self.K_c = QtWidgets.QPushButton(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_c.sizePolicy().hasHeightForWidth()) - self.K_c.setSizePolicy(sizePolicy) - self.K_c.setMinimumSize(QtCore.QSize(60, 50)) - self.K_c.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_c.setFont(font) - self.K_c.setTabletTracking(False) - self.K_c.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_c.setFlat(True) - self.K_c.setObjectName("K_c") - self.horizontalLayout.addWidget(self.K_c) - self.K_v = QtWidgets.QPushButton(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_v.sizePolicy().hasHeightForWidth()) - self.K_v.setSizePolicy(sizePolicy) - self.K_v.setMinimumSize(QtCore.QSize(60, 50)) - self.K_v.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_v.setFont(font) - self.K_v.setTabletTracking(False) - self.K_v.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_v.setFlat(True) - self.K_v.setObjectName("K_v") - self.horizontalLayout.addWidget(self.K_v) - self.K_b = QtWidgets.QPushButton(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_b.sizePolicy().hasHeightForWidth()) - self.K_b.setSizePolicy(sizePolicy) - self.K_b.setMinimumSize(QtCore.QSize(60, 50)) - self.K_b.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_b.setFont(font) - self.K_b.setTabletTracking(False) - self.K_b.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_b.setFlat(True) - self.K_b.setObjectName("K_b") - self.horizontalLayout.addWidget(self.K_b) - self.K_n = QtWidgets.QPushButton(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_n.sizePolicy().hasHeightForWidth()) - self.K_n.setSizePolicy(sizePolicy) - self.K_n.setMinimumSize(QtCore.QSize(60, 50)) - self.K_n.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_n.setFont(font) - self.K_n.setTabletTracking(False) - self.K_n.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_n.setFlat(True) - self.K_n.setObjectName("K_n") - self.horizontalLayout.addWidget(self.K_n) - self.K_m = QtWidgets.QPushButton(parent=self.layoutWidget_3) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Fixed - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_m.sizePolicy().hasHeightForWidth()) - self.K_m.setSizePolicy(sizePolicy) - self.K_m.setMinimumSize(QtCore.QSize(60, 50)) - self.K_m.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(29) - font.setBold(False) - font.setItalic(False) - font.setUnderline(False) - font.setWeight(50) - font.setStrikeOut(False) - font.setStyleStrategy(QtGui.QFont.StyleStrategy.PreferDefault) - self.K_m.setFont(font) - self.K_m.setTabletTracking(False) - self.K_m.setLayoutDirection(QtCore.Qt.LayoutDirection.RightToLeft) - self.K_m.setFlat(True) - self.K_m.setObjectName("K_m") - self.horizontalLayout.addWidget(self.K_m) + + # Row 1: qwertyuiop (grid layout) + row1_widget = QtWidgets.QWidget(parent=self) + row1_widget.setGeometry(QtCore.QRect(10, 150, 781, 52)) + row1_layout = QtWidgets.QGridLayout(row1_widget) + row1_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize) + row1_layout.setContentsMargins(0, 0, 0, 0) + row1_layout.setHorizontalSpacing(2) + row1_layout.setVerticalSpacing(5) + + row1_keys = "qwertyuiop" + for col, letter in enumerate(row1_keys): + btn = self._create_key_button( + row1_widget, + f"K_{letter}", + v_policy=QtWidgets.QSizePolicy.Policy.Expanding, + ) + row1_layout.addWidget( + btn, 0, col, 1, 1, QtCore.Qt.AlignmentFlag.AlignHCenter + ) + self._key_buttons.append(btn) + + # Row 2: asdfghjkl (hbox layout) + row2_widget = QtWidgets.QWidget(parent=self) + row2_widget.setGeometry(QtCore.QRect(50, 220, 701, 52)) + row2_widget.setMinimumSize(QtCore.QSize(64, 0)) + row2_layout = QtWidgets.QHBoxLayout(row2_widget) + row2_layout.setContentsMargins(0, 0, 0, 0) + row2_layout.setSpacing(2) + + for letter in "asdfghjkl": + btn = self._create_key_button(row2_widget, f"K_{letter}") + row2_layout.addWidget(btn) + self._key_buttons.append(btn) + + # Row 3: zxcvbnm (hbox layout) + row3_widget = QtWidgets.QWidget(parent=self) + row3_widget.setGeometry(QtCore.QRect(100, 290, 591, 52)) + row3_layout = QtWidgets.QHBoxLayout(row3_widget) + row3_layout.setSizeConstraint(QtWidgets.QLayout.SizeConstraint.SetMinimumSize) + row3_layout.setContentsMargins(0, 0, 0, 0) + row3_layout.setSpacing(2) + + for letter in "zxcvbnm": + btn = self._create_key_button(row3_widget, f"K_{letter}", min_w=60) + row3_layout.addWidget(btn) + self._key_buttons.append(btn) + + # Shift button (left of row 3) + self.K_shift = QtWidgets.QPushButton(parent=self) + self.K_shift.setGeometry(QtCore.QRect(10, 280, 81, 51)) + self.K_shift.setCheckable(True) + self.K_shift.setText("Shift") + self.K_shift.setObjectName("K_shift") + + # Delete button (right of row 3) + self.k_delete = QtWidgets.QPushButton(parent=self) + self.k_delete.setGeometry(QtCore.QRect(700, 280, 81, 51)) + self.k_delete.setText("\u232b") + self.k_delete.setObjectName("k_delete") + + # Bottom row: [123] [.] [ space ] [enter] + self.K_keychange = QtWidgets.QPushButton(parent=self) + self.K_keychange.setGeometry(QtCore.QRect(20, 350, 93, 60)) + self.K_keychange.setCheckable(True) + self.K_keychange.setText("123") + self.K_keychange.setObjectName("K_keychange") + + self.K_dot = QtWidgets.QPushButton(parent=self) + self.K_dot.setGeometry(QtCore.QRect(120, 362, 55, 60)) + self.K_dot.setFont(_make_key_font()) + self.K_dot.setText(".") + self.K_dot.setFlat(True) + self.K_dot.setObjectName("K_dot") + self.K_space = QtWidgets.QPushButton(parent=self) - self.K_space.setGeometry(QtCore.QRect(120, 362, 551, 60)) - sizePolicy = QtWidgets.QSizePolicy( + self.K_space.setGeometry(QtCore.QRect(177, 362, 494, 60)) + policy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Expanding, ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_space.sizePolicy().hasHeightForWidth()) - self.K_space.setSizePolicy(sizePolicy) + self.K_space.setSizePolicy(policy) self.K_space.setMinimumSize(QtCore.QSize(0, 60)) self.K_space.setMaximumSize(QtCore.QSize(16777215, 60)) self.K_space.setObjectName("K_space") + self.k_Enter = QtWidgets.QPushButton(parent=self) self.k_Enter.setGeometry(QtCore.QRect(680, 350, 93, 60)) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.k_Enter.sizePolicy().hasHeightForWidth()) - self.k_Enter.setSizePolicy(sizePolicy) - self.k_Enter.setMinimumSize(QtCore.QSize(0, 0)) - self.k_Enter.setMaximumSize(QtCore.QSize(16777215, 16777215)) + self.k_Enter.setText("\u23ce") self.k_Enter.setAutoRepeat(False) self.k_Enter.setObjectName("k_Enter") - self.k_delete = QtWidgets.QPushButton(parent=self) - self.k_delete.setGeometry(QtCore.QRect(700, 280, 81, 51)) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.k_delete.sizePolicy().hasHeightForWidth()) - self.k_delete.setSizePolicy(sizePolicy) - self.k_delete.setMinimumSize(QtCore.QSize(0, 0)) - self.k_delete.setMaximumSize(QtCore.QSize(16777215, 16777215)) - self.k_delete.setObjectName("k_delete") - self.K_keychange = QtWidgets.QPushButton(parent=self) - self.K_keychange.setGeometry(QtCore.QRect(20, 350, 93, 60)) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_keychange.sizePolicy().hasHeightForWidth()) - self.K_keychange.setSizePolicy(sizePolicy) - self.K_keychange.setMinimumSize(QtCore.QSize(0, 0)) - self.K_keychange.setMaximumSize(QtCore.QSize(16777215, 16777215)) - self.K_keychange.setCheckable(True) - self.K_keychange.setObjectName("K_keychange") - self.K_shift = QtWidgets.QPushButton(parent=self) - self.K_shift.setGeometry(QtCore.QRect(10, 280, 81, 51)) - sizePolicy = QtWidgets.QSizePolicy( - QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Minimum - ) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.K_shift.sizePolicy().hasHeightForWidth()) - self.K_shift.setSizePolicy(sizePolicy) - self.K_shift.setMinimumSize(QtCore.QSize(0, 0)) - self.K_shift.setMaximumSize(QtCore.QSize(16777215, 16777215)) - self.K_shift.setCheckable(True) - self.K_shift.setObjectName("K_shift") + + # Back button (top-right) self.numpad_back_btn = IconButton(parent=self) - self.numpad_back_btn.setEnabled(True) self.numpad_back_btn.setGeometry(QtCore.QRect(720, 20, 60, 60)) - sizePolicy = QtWidgets.QSizePolicy( + policy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.Policy.MinimumExpanding, QtWidgets.QSizePolicy.Policy.MinimumExpanding, ) - sizePolicy.setHorizontalStretch(1) - sizePolicy.setVerticalStretch(1) - sizePolicy.setHeightForWidth( - self.numpad_back_btn.sizePolicy().hasHeightForWidth() - ) - self.numpad_back_btn.setSizePolicy(sizePolicy) + policy.setHorizontalStretch(1) + policy.setVerticalStretch(1) + self.numpad_back_btn.setSizePolicy(policy) self.numpad_back_btn.setMinimumSize(QtCore.QSize(60, 60)) self.numpad_back_btn.setMaximumSize(QtCore.QSize(60, 60)) - self.numpad_back_btn.setStyleSheet("") - self.numpad_back_btn.setText("") self.numpad_back_btn.setIconSize(QtCore.QSize(16, 16)) - self.numpad_back_btn.setCheckable(False) - self.numpad_back_btn.setChecked(False) self.numpad_back_btn.setFlat(True) self.numpad_back_btn.setProperty( "icon_pixmap", QtGui.QPixmap(":/ui/media/btn_icons/back.svg") ) + self.numpad_back_btn.setProperty("button_type", "icon") self.numpad_back_btn.setObjectName("numpad_back_btn") - self.layoutWidget1 = QtWidgets.QWidget(parent=self) - self.layoutWidget1.setGeometry(QtCore.QRect(10, 90, 781, 48)) - self.layoutWidget1.setObjectName("layoutWidget1") - self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.layoutWidget1) - self.verticalLayout_2.setContentsMargins(0, 0, 0, 0) - self.verticalLayout_2.setObjectName("verticalLayout_2") - self.inserted_value = QtWidgets.QLabel(parent=self.layoutWidget1) + + # Input display area + input_widget = QtWidgets.QWidget(parent=self) + input_widget.setGeometry(QtCore.QRect(10, 90, 781, 48)) + input_layout = QtWidgets.QVBoxLayout(input_widget) + input_layout.setContentsMargins(0, 0, 0, 0) + + self.inserted_value = QtWidgets.QLabel(parent=input_widget) self.inserted_value.setMinimumSize(QtCore.QSize(500, 0)) self.inserted_value.setMaximumSize(QtCore.QSize(16777215, 50)) - font = QtGui.QFont() - font.setFamily("Modern") - font.setPointSize(18) - self.inserted_value.setFont(font) - self.inserted_value.setAutoFillBackground(False) - self.inserted_value.setStyleSheet( - "color: white;\n " - ) + self.inserted_value.setFont(_make_key_font(18)) + self.inserted_value.setStyleSheet("color: white;") self.inserted_value.setFrameShape(QtWidgets.QFrame.Shape.NoFrame) self.inserted_value.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) self.inserted_value.setLineWidth(0) - self.inserted_value.setText("") - self.inserted_value.setScaledContents(False) self.inserted_value.setAlignment( QtCore.Qt.AlignmentFlag.AlignBottom | QtCore.Qt.AlignmentFlag.AlignHCenter ) - self.inserted_value.setIndent(0) - self.inserted_value.setTextInteractionFlags( - QtCore.Qt.TextInteractionFlag.LinksAccessibleByMouse - ) self.inserted_value.setObjectName("inserted_value") - self.verticalLayout_2.addWidget(self.inserted_value) - self.line = QtWidgets.QFrame(parent=self.layoutWidget1) - self.line.setFrameShape(QtWidgets.QFrame.Shape.HLine) - self.line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) - self.line.setObjectName("line") - self.verticalLayout_2.addWidget(self.line) - - self.retranslateUi() - QtCore.QMetaObject.connectSlotsByName(self) + input_layout.addWidget(self.inserted_value) - def retranslateUi(self): - _translate = QtCore.QCoreApplication.translate - self.setWindowTitle(_translate("self", "Form")) - self.K_p.setText(_translate("self", "P")) - self.K_p.setProperty("position", _translate("self", "left")) - self.K_i.setText(_translate("self", "I")) - self.K_i.setProperty("position", _translate("self", "left")) - self.K_y.setText(_translate("self", "Y")) - self.K_y.setProperty("position", _translate("self", "left")) - self.K_t.setText(_translate("self", "T")) - self.K_t.setProperty("position", _translate("self", "left")) - self.K_r.setText(_translate("self", "R")) - self.K_r.setProperty("position", _translate("self", "left")) - self.K_o.setText(_translate("self", "O")) - self.K_o.setProperty("position", _translate("self", "left")) - self.K_u.setText(_translate("self", "U")) - self.K_u.setProperty("position", _translate("self", "left")) - self.K_w.setText(_translate("self", "W")) - self.K_w.setProperty("position", _translate("self", "left")) - self.K_e.setText(_translate("self", "E")) - self.K_e.setProperty("position", _translate("self", "left")) - self.K_q.setText(_translate("self", "Q")) - self.K_q.setProperty("position", _translate("self", "left")) - self.K_a.setText(_translate("self", "A")) - self.K_a.setProperty("position", _translate("self", "left")) - self.K_s.setText(_translate("self", "S")) - self.K_s.setProperty("position", _translate("self", "left")) - self.K_d.setText(_translate("self", "D")) - self.K_d.setProperty("position", _translate("self", "left")) - self.K_f.setText(_translate("self", "F")) - self.K_f.setProperty("position", _translate("self", "left")) - self.K_g.setText(_translate("self", "G")) - self.K_g.setProperty("position", _translate("self", "left")) - self.K_h.setText(_translate("self", "H")) - self.K_h.setProperty("position", _translate("self", "left")) - self.K_j.setText(_translate("self", "J")) - self.K_j.setProperty("position", _translate("self", "left")) - self.K_k.setText(_translate("self", "K")) - self.K_k.setProperty("position", _translate("self", "left")) - self.K_l.setText(_translate("self", "L")) - self.K_l.setProperty("position", _translate("self", "left")) - self.K_z.setText(_translate("self", "Z")) - self.K_z.setProperty("position", _translate("self", "left")) - self.K_x.setText(_translate("self", "X")) - self.K_x.setProperty("position", _translate("self", "left")) - self.K_c.setText(_translate("self", "C")) - self.K_c.setProperty("position", _translate("self", "left")) - self.K_v.setText(_translate("self", "V")) - self.K_v.setProperty("position", _translate("self", "left")) - self.K_b.setText(_translate("self", "B")) - self.K_b.setProperty("position", _translate("self", "left")) - self.K_n.setText(_translate("self", "N")) - self.K_n.setProperty("position", _translate("self", "left")) - self.K_m.setText(_translate("self", "M")) - self.K_m.setProperty("position", _translate("self", "left")) - self.K_space.setText(_translate("self", "Spacing")) - self.k_Enter.setText(_translate("self", "⏎")) - self.k_delete.setText(_translate("self", "⌫")) - self.K_keychange.setText(_translate("self", "123")) - self.K_shift.setText(_translate("self", "⇧")) - self.numpad_back_btn.setProperty("button_type", _translate("self", "icon")) + line = QtWidgets.QFrame(parent=input_widget) + line.setFrameShape(QtWidgets.QFrame.Shape.HLine) + line.setFrameShadow(QtWidgets.QFrame.Shadow.Sunken) + input_layout.addWidget(line) diff --git a/tests/util/test_keyboard_page_unit.py b/tests/util/test_keyboard_page_unit.py new file mode 100644 index 00000000..00490070 --- /dev/null +++ b/tests/util/test_keyboard_page_unit.py @@ -0,0 +1,223 @@ +"""Unit tests for CustomQwertyKeyboard (keyboardPage.py). + +Tests cover layout switching, value insertion, signal emission, +the dedicated dot button, and layout array length invariants. +""" + +import sys +import types +from pathlib import Path + +import pytest +from PyQt6 import QtCore, QtWidgets + +# BlocksScreen/ must be on sys.path so ``from lib.utils...`` resolves +# (matching the runtime import style used inside the package). +_bs_dir = str(Path(__file__).resolve().parent.parent / "BlocksScreen") +if _bs_dir not in sys.path: + sys.path.insert(0, _bs_dir) + +# Stub ``lib.utils.icon_button`` to avoid loading Qt resource files. +_icon_stub = types.ModuleType("lib.utils.icon_button") +_icon_stub.IconButton = QtWidgets.QPushButton # type: ignore[attr-defined] +sys.modules.setdefault("lib.utils.icon_button", _icon_stub) + +# Force-reload the real module — the network conftest registers a stub +# that lacks the layout constants we need. +for _key in [ + "lib.panels.widgets.keyboardPage", + "BlocksScreen.lib.panels.widgets.keyboardPage", +]: + sys.modules.pop(_key, None) + +from BlocksScreen.lib.panels.widgets.keyboardPage import ( # noqa: E402 + _LOWERCASE, _NUM_KEYS, _NUMBERS, _SYMBOLS, _UPPERCASE, + CustomQwertyKeyboard) + + +@pytest.fixture() +def keyboard(qtbot): + """Create a keyboard widget with IconButton mocked out.""" + kb = CustomQwertyKeyboard(parent=None) + qtbot.addWidget(kb) + return kb + + +class TestLayoutArrayLengths: + """Guard against the off-by-one bug that hid the dot key.""" + + def test_lowercase_has_26_keys(self): + assert len(_LOWERCASE) == _NUM_KEYS + + def test_uppercase_has_26_keys(self): + assert len(_UPPERCASE) == _NUM_KEYS + + def test_numbers_has_26_keys(self): + assert len(_NUMBERS) == _NUM_KEYS + + def test_symbols_has_26_keys(self): + assert len(_SYMBOLS) == _NUM_KEYS + + +class TestDefaultLayout: + """Keyboard starts in lowercase.""" + + def test_initial_layout_is_lowercase(self, keyboard): + texts = [btn.text() for btn in keyboard._key_buttons] + assert texts == _LOWERCASE + + def test_shift_button_says_shift(self, keyboard): + assert keyboard.K_shift.text() == "⇧" + + +class TestLayoutSwitching: + """Layout changes based on shift/keychange toggle state.""" + + def test_shift_gives_uppercase(self, keyboard): + keyboard.K_shift.setChecked(True) + keyboard.handle_keyboard_layout() + texts = [btn.text() for btn in keyboard._key_buttons] + assert texts == _UPPERCASE + + def test_keychange_gives_numbers(self, keyboard): + keyboard.K_keychange.setChecked(True) + keyboard.handle_keyboard_layout() + texts = [btn.text() for btn in keyboard._key_buttons] + assert texts == _NUMBERS + + def test_keychange_shift_gives_symbols(self, keyboard): + keyboard.K_keychange.setChecked(True) + keyboard.K_shift.setChecked(True) + keyboard.handle_keyboard_layout() + texts = [btn.text() for btn in keyboard._key_buttons] + assert texts == _SYMBOLS + + def test_shift_label_changes_to_hash_in_number_mode(self, keyboard): + keyboard.K_keychange.setChecked(True) + keyboard.handle_keyboard_layout() + assert keyboard.K_shift.text() == "#+=" + + def test_symbolsrun_returns_to_lowercase(self, keyboard): + """After symbols, pressing shift should reset to lowercase.""" + keyboard.K_keychange.setChecked(True) + keyboard.K_shift.setChecked(True) + keyboard.handle_keyboard_layout() + assert keyboard.symbolsrun is True + + keyboard.K_keychange.setChecked(False) + keyboard.K_shift.setChecked(True) + keyboard.handle_keyboard_layout() + + texts = [btn.text() for btn in keyboard._key_buttons] + assert texts == _LOWERCASE + assert keyboard.symbolsrun is False + assert keyboard.K_shift.isChecked() is False + + +class TestValueInsertion: + """Character insertion, deletion, and submission.""" + + def test_typing_characters(self, keyboard): + keyboard.value_inserted("h") + keyboard.value_inserted("i") + assert keyboard.current_value == "hi" + assert keyboard.inserted_value.text() == "hi" + + def test_space_inserts_space(self, keyboard): + keyboard.value_inserted("a") + keyboard.value_inserted(" ") + assert keyboard.current_value == "a " + + def test_clear_deletes_last_char(self, keyboard): + keyboard.value_inserted("a") + keyboard.value_inserted("b") + keyboard.value_inserted("clear") + assert keyboard.current_value == "a" + + def test_clear_on_single_char_empties(self, keyboard): + keyboard.value_inserted("x") + keyboard.value_inserted("clear") + assert keyboard.current_value == "" + + def test_clear_on_empty_stays_empty(self, keyboard): + keyboard.value_inserted("clear") + assert keyboard.current_value == "" + + def test_ampersand_conversion(self, keyboard): + keyboard.value_inserted("&&") + assert keyboard.current_value == "&" + + def test_enter_emits_and_resets(self, keyboard, qtbot): + keyboard.value_inserted("p") + keyboard.value_inserted("w") + + with qtbot.waitSignal(keyboard.value_selected, timeout=1000) as sig: + keyboard.value_inserted("enter") + + assert sig.args == ["pw"] + assert keyboard.current_value == "" + assert keyboard.inserted_value.text() == "" + + +class TestSetValue: + """Pre-filling the keyboard input field.""" + + def test_set_value_prefills(self, keyboard): + keyboard.set_value("pre-filled") + assert keyboard.current_value == "pre-filled" + assert keyboard.inserted_value.text() == "pre-filled" + + def test_set_value_then_type(self, keyboard): + keyboard.set_value("abc") + keyboard.value_inserted("d") + assert keyboard.current_value == "abcd" + + +class TestDotButton: + """Dedicated dot button is always accessible regardless of layout.""" + + def test_dot_button_exists(self, keyboard): + assert hasattr(keyboard, "K_dot") + assert keyboard.K_dot.text() == "." + + def test_dot_click_inserts_dot(self, keyboard, qtbot): + qtbot.mouseClick(keyboard.K_dot, QtCore.Qt.MouseButton.LeftButton) + assert keyboard.current_value == "." + + def test_dot_always_visible_across_layouts(self, keyboard): + """The dot button is independent of layout switching.""" + for checked_kc, checked_sh in [ + (False, False), + (False, True), + (True, False), + (True, True), + ]: + keyboard.K_keychange.setChecked(checked_kc) + keyboard.K_shift.setChecked(checked_sh) + keyboard.handle_keyboard_layout() + assert keyboard.K_dot.text() == "." + + +class TestButtonClicks: + """Clicking buttons triggers the correct value insertion.""" + + def test_key_button_click_inserts_text(self, keyboard, qtbot): + first_btn = keyboard._key_buttons[0] + assert first_btn.text() == "q" + qtbot.mouseClick(first_btn, QtCore.Qt.MouseButton.LeftButton) + assert keyboard.current_value == "q" + + def test_space_button_click(self, keyboard, qtbot): + qtbot.mouseClick(keyboard.K_space, QtCore.Qt.MouseButton.LeftButton) + assert keyboard.current_value == " " + + def test_delete_button_click(self, keyboard, qtbot): + keyboard.value_inserted("x") + qtbot.mouseClick(keyboard.k_delete, QtCore.Qt.MouseButton.LeftButton) + assert keyboard.current_value == "" + + def test_back_button_emits_signal(self, keyboard, qtbot): + with qtbot.waitSignal(keyboard.request_back, timeout=1000): + qtbot.mouseClick( + keyboard.numpad_back_btn, QtCore.Qt.MouseButton.LeftButton + ) From 1f8a308087b6f0a4231251dcf784509db8562429 Mon Sep 17 00:00:00 2001 From: Hugo Costa Date: Wed, 11 Mar 2026 17:36:07 +0000 Subject: [PATCH 64/70] USB tools (#193) * Udisks2 dbus interfaces for File system and dev Block * Add Udisks2 Drive interface * Finalize UDisks2 Interfaces * Finished blocking udisks2 dbus interfaces and manager * Add initial async UDisks2 Dbus interfaces and manager * Added parallel monitoring for added/removed interfaces for Udisks2 signals, new properties on interfaces * WIP (switching laptops) * WIP Udev monitoring * WIP: Add UDisks2 Partition Interface, properties changed signal handling The properties of a device although uncommon may change, so a handler for the signal UDisks2 manager `properties_changed` was added. The logic is still missing here. The initial representation of a Device is added but not finished * Created a module for this usb storage devices Since code can be added in the future to increase the functionality of the usb storage devices communication, the different parts of the feature were seperated into different files in order to create a module that can later be called to handle all usb storage devices. This will make code additions and refactors cleaner in the future. The commit seperates the previous `usb_manager.py` into six different files: seperate wrappers for the sdbus UDisks2 service (blocking and async), usb storage asynchronous manager, and a WIP udev monitor that may be deleted in the future. This is still a work in progress so don't pay to much attention to what is done * Moved to module * WIP * Seperated Device definition * Rplaced string literals with Enum members, added new property to interface * WIP All phases are now in place, mounting and symlink creation next * WIP version and type checking * WIP print to udisks2 signals * Add symlink traking * WIP, mounting, symlink creation and removal, input validation, exception handling * WIP: changing laptops * Finish static typing for arguments * Finish static typing for arguments for blocking * Wip: remove comments and prints * add: added banner popup for usb * Del: debug prints * Remove useless comments * Finish available module signals * Make banner popup adhere to the same file naming * Format, remove deprecated typing.Deque, change var name * Changed var name for something more explicit * Fix typo from the prior commit * Del prints from debugging * Change method names, auto restart when usb monitor stops * Add active control flag, incomplete * Add new signals, finish restart logic * Finish restart logic, handle restart type * Finish restart type logic * gcodes_dir can also be none now * Add as a requirement * Instantiate USBManager * Add slot for .aboutToQuit the app, start transition to this cleanup * Fix bug TYPE_CHECKING = True, this broke everything * return fallback on no section * Call usb_manager, fetch gcode dir from configfile if available * Cleanup * Add default usb-manager config section * Call banner on hardware signals * Remove unused experiment * Add docstrings * Add docstrings to FileSystem async interface * Midway adding docstrings to Block device async interface * Finish Block method docstrings * Finish Drive interface methods docstrings * Finish adding documentation * Correctly import the storage package * Pass device name or id_label as the symlink name * Fix device initialization * Del dead code * Typo fix * Del unused var type * Add return types * Typo fixes * Rem unused imports * Add doctring to fire_n_forget * Fix banner parent, pass signals correctly * Only pass the parent if it exists * bugfix: black background * Add a 'USB' prefix to the symlink name so that the screen can assign a usb icon to the symlink * Del accidental import --------- Co-authored-by: Roberto --- BlocksScreen.cfg | 5 +- BlocksScreen/BlocksScreen.py | 18 +- BlocksScreen/configfile.py | 2 +- BlocksScreen/devices/storage/__init__.py | 24 + BlocksScreen/devices/storage/device.py | 104 ++++ BlocksScreen/devices/storage/udisks2.py | 565 +++++++++++++++++ .../devices/storage/udisks2_dbus_async.py | 568 ++++++++++++++++++ .../devices/storage/usb_controller.py | 144 +++++ BlocksScreen/lib/panels/mainWindow.py | 76 ++- .../lib/panels/widgets/bannerPopup.py | 222 +++++++ pyproject.toml | 1 + 11 files changed, 1688 insertions(+), 41 deletions(-) create mode 100644 BlocksScreen/devices/storage/__init__.py create mode 100644 BlocksScreen/devices/storage/device.py create mode 100644 BlocksScreen/devices/storage/udisks2.py create mode 100644 BlocksScreen/devices/storage/udisks2_dbus_async.py create mode 100644 BlocksScreen/devices/storage/usb_controller.py create mode 100644 BlocksScreen/lib/panels/widgets/bannerPopup.py diff --git a/BlocksScreen.cfg b/BlocksScreen.cfg index b5807fa0..42ab150b 100644 --- a/BlocksScreen.cfg +++ b/BlocksScreen.cfg @@ -3,4 +3,7 @@ host: localhost port: 7125 [screensaver] -timeout: 5000 \ No newline at end of file +timeout: 5000 + +[usb_manager] +gcodes_dir: ~/printer_data/gcodes/ diff --git a/BlocksScreen/BlocksScreen.py b/BlocksScreen/BlocksScreen.py index 3ccceb74..22468a8b 100644 --- a/BlocksScreen/BlocksScreen.py +++ b/BlocksScreen/BlocksScreen.py @@ -3,7 +3,7 @@ import typing from lib.panels.mainWindow import MainWindow -from logger import setup_logging +from logger import setup_logging, LogManager from PyQt6 import QtCore, QtGui, QtWidgets QtGui.QGuiApplication.setAttribute( @@ -30,14 +30,19 @@ def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): splash.finish(window) +def on_quit() -> None: + logging.info("Final exit cleanup") + LogManager.shutdown() + + if __name__ == "__main__": setup_logging( filename="logs/BlocksScreen.log", - level=logging.DEBUG, # File gets DEBUG+ - console_output=True, # Print to terminal - console_level=logging.DEBUG, # Console gets DEBUG+ - capture_stderr=True, # Capture X11 errors - capture_stdout=False, # Don't capture print() + level=logging.DEBUG, + console_output=True, + console_level=logging.DEBUG, + capture_stderr=True, + capture_stdout=False, ) _logger = logging.getLogger(__name__) _logger.info("============ BlocksScreen Initializing ============") @@ -47,5 +52,6 @@ def show_splash(window: typing.Optional[QtWidgets.QWidget] = None): BlocksScreen.setDesktopFileName("BlocksScreen") main_window = MainWindow() BlocksScreen.processEvents() + BlocksScreen.aboutToQuit.connect(on_quit) main_window.show() sys.exit(BlocksScreen.exec()) diff --git a/BlocksScreen/configfile.py b/BlocksScreen/configfile.py index ef75d362..ed5828a0 100644 --- a/BlocksScreen/configfile.py +++ b/BlocksScreen/configfile.py @@ -104,7 +104,7 @@ def get_section( ) -> BlocksScreenConfig: """Get configfile section""" if not self.config.has_section(section): - raise configparser.NoSectionError(f"No section with name: {section}") + return fallback return BlocksScreenConfig(self.configfile, section) def get_options(self) -> list: diff --git a/BlocksScreen/devices/storage/__init__.py b/BlocksScreen/devices/storage/__init__.py new file mode 100644 index 00000000..beeb1563 --- /dev/null +++ b/BlocksScreen/devices/storage/__init__.py @@ -0,0 +1,24 @@ +from .usb_controller import USBManager + +__doc__ = """ + +The storage package contains a tool that monitors +pluggable usb devices via python-sdbus library. +While offering an automounting option. +The package is also capable of creating a symlink that +points directly to the mounted usb drive on the gcodes +directory. + + +There is still a lot of functionality missing, that may +be added in the future, but for now it just automounts, +creates symlinks, cleans up broken symlinks on the +gcodes directory. + + +All tools related to storage devices should be contained +in this package directory. +""" +__version__ = "0.0.1" +__all__ = ["USBManager"] +__name__ = "storage" diff --git a/BlocksScreen/devices/storage/device.py b/BlocksScreen/devices/storage/device.py new file mode 100644 index 00000000..c4057913 --- /dev/null +++ b/BlocksScreen/devices/storage/device.py @@ -0,0 +1,104 @@ +from .udisks2_dbus_async import ( + UDisks2BlockAsyncInterface, + UDisks2DriveAsyncInterface, + UDisks2PartitionAsyncInterface, + UDisks2FileSystemAsyncInterface, + UDisks2PartitionTableAsyncInterface, +) + + +class Device: + def __init__( + self, + path: str, + DriveInterface: UDisks2DriveAsyncInterface, + symlink_path: str, + ) -> None: + self.path: str = path + self.symlink_path: str = symlink_path + self.driver_interface: UDisks2DriveAsyncInterface = DriveInterface + self.partitions: dict[str, UDisks2PartitionAsyncInterface] = {} + self.raw_block: dict[str, UDisks2BlockAsyncInterface] = {} + self.logical_blocks: dict[str, UDisks2BlockAsyncInterface] = {} + self.file_systems: dict[str, UDisks2FileSystemAsyncInterface] = {} + self.partition_tables: dict[str, UDisks2PartitionTableAsyncInterface] = {} + self.symlinks: list[str] = [] + + def get_logical_blocks(self) -> dict[str, UDisks2BlockAsyncInterface]: + """The available logical blocks for the device""" + return self.logical_blocks + + def get_driver(self) -> UDisks2DriveAsyncInterface | None: + """Get current device driver""" + if not self.driver_interface: + return None + return self.driver_interface + + def update_file_system( + self, path: str, data: UDisks2FileSystemAsyncInterface + ) -> None: + """Add or update a filesystem for this device + + Args: + path (str): filesystem path + data (UDisks2FileSystemAsyncInterface): The interface + """ + self.file_systems.update({path: data}) + + def update_raw_block(self, path: str, block: UDisks2BlockAsyncInterface) -> None: + """Add or update a raw block for this device + + Args: + path (str): block path + data (UDisks2BlockAsyncInterface): The blocks interface + """ + self.raw_block.update({path: block}) + + def update_logical_blocks( + self, path: str, block: UDisks2BlockAsyncInterface + ) -> None: + """Add or update a logical block for this device + + Args: + path (str): block path + data (UDisks2BlockAsyncInterface): The block interface + """ + self.logical_blocks.update({path: block}) + + def update_part_table( + self, path: str, part: UDisks2PartitionTableAsyncInterface + ) -> None: + """Add or update partition table for this device + + Args: + path (str): Partition table path + part (UDisks2PartitionTableAsyncInterface): The interface + """ + self.partition_tables.update({path: part}) + + def update_partitions( + self, path: str, block: UDisks2PartitionAsyncInterface + ) -> None: + """Add or update partitions for the current device + + Args: + path (str): the partition path + data (UDisks2PartitionAsyncInterface): The partition interface + """ + self.partitions.update({path: block}) + + def kill(self) -> None: + """Delete the device and removes any track of it + + Especially used when devices were removed unsafely + """ + self.delete() + + def delete(self) -> None: + """Cleanup and delete this device""" + del self.driver_interface + self.partitions.clear() + self.raw_block.clear() + self.file_systems.clear() + self.partition_tables.clear() + self.symlinks.clear() diff --git a/BlocksScreen/devices/storage/udisks2.py b/BlocksScreen/devices/storage/udisks2.py new file mode 100644 index 00000000..4f44c499 --- /dev/null +++ b/BlocksScreen/devices/storage/udisks2.py @@ -0,0 +1,565 @@ +import asyncio +import logging +import os +import pathlib +import typing +from collections.abc import Coroutine +import unicodedata + +import sdbus +from PyQt6 import QtCore + +from .device import Device +from .udisks2_dbus_async import ( + Interfaces, + UDisks2AsyncManager, + UDisks2BlockAsyncInterface, + UDisks2DriveAsyncInterface, + UDisks2FileSystemAsyncInterface, + UDisks2PartitionAsyncInterface, + UDisks2PartitionTableAsyncInterface, +) + +UDisks2_service: str = "org.freedesktop.UDisks2" +UDisks2_obj_path: str = "org/freedesktop/UDisks2" +AlreadyMountedException = "org.freedesktop.UDisks2.Error.AlreadyMounted" + +_T = typing.TypeVar(name="_T") + + +def validate_label(label: str, strict: bool = True, max_length: int = 100) -> str: + """ + Comprehensive validation for filesystem labels with security protection. + + Args: + label: Raw input label to validate + strict: If True, returns empty string for any invalid input + max_length: Maximum allowed length in bytes + + Returns: + Sanitized and validated label safe for filesystem use + """ + if not label: + return "" + if not label.strip(): + return "" + if len(label.encode("utf-8")) > max_length: + return "" if strict else label[:max_length] + normalized_label = unicodedata.normalize("NFC", label) + if any(ord(char) < 32 for char in normalized_label): + if strict: + return "" + normalized_label = "".join(char for char in normalized_label if ord(char) >= 32) + + dangerous_chars = { + "\0", + "\x00", + "/", + "\\", + ";", + "|", + "&", + "$", + "`", + "(", + ")", + "{", + "}", + "[", + "]", + "<", + ">", + '"', + "'", + "*", + "?", + "!", + } + clean_label = "".join(c for c in normalized_label if c not in dangerous_chars) + if ( + ".." in clean_label + or clean_label.startswith("/") + or clean_label.startswith("\\") + ): + return ( + "" + if strict + else clean_label.replace("..", "").replace("/", "_").replace("\\", "_") + ) + + final_label = clean_label.strip(" .")[:max_length] + return final_label if final_label else "" + + +def fire_n_forget( + coro: Coroutine[typing.Any, typing.Any, typing.Any], + name: str, + task_stack: set[asyncio.Task[typing.Any]], +) -> asyncio.Task[typing.Any]: + task: asyncio.Task[typing.Any] = asyncio.create_task(coro, name=name) + task_stack.add(task) + """Create a task for a specified coroutine and run it + + Args: + coro (Coroutine): coroutine to create task + name (str): Name for the task + task_stack (set): Task stack that keeps track of currently running tasks + """ + + def cleanup(task: asyncio.Task[_T]) -> None: + """Cleanup task""" + task_stack.discard(task) + try: + task.result() + except asyncio.CancelledError: + task.cancel() + logging.error("Task %s was cancelled", task.get_name()) + except Exception as e: + logging.error( + "Caught exception in %s : %s", task.get_name(), e, exc_info=True + ) + + task.add_done_callback(cleanup) + return task + + +class UDisksDBusAsync(QtCore.QThread): + hardware_detected: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="hardware-detected" + ) + device_added: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, dict, name="device-added" + ) + device_removed: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, str, name="device-removed" + ) # device path + hardware_removed: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="hardware-removed" + ) # device path + device_mounted: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, str, name="device-mounted" + ) # device path, new symlink path + device_unmounted: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="device-unmounted" + ) + + def __init__(self, parent: QtCore.QObject, gcodes_dir: str) -> None: + super().__init__(parent) + self.task_stack: set[asyncio.Task[typing.Any]] = set() + self.gcodes_path: pathlib.Path = pathlib.Path(gcodes_dir) + self.system_bus: sdbus.SdBus = sdbus.sd_bus_open_system() + self._active: bool = False + if not self.system_bus: + self.close() + return + sdbus.set_default_bus(self.system_bus) + self.obj_manager: UDisks2AsyncManager = UDisks2AsyncManager.new_proxy( + service_name="org.freedesktop.UDisks2", + object_path="/org/freedesktop/UDisks2", + bus=self.system_bus, + ) + self.loop: asyncio.AbstractEventLoop | None = None + self.stop_event: asyncio.Event = asyncio.Event() + self.listener_running: bool = False + self.controlled_devs: dict[str, Device] = {} + self._cleanup_broken_symlinks() + + @property + def active(self) -> bool: + return self._active + + def run(self) -> None: + """Start UDisks2 USB monitoring""" + self.stop_event.clear() + try: + self.loop = asyncio.new_event_loop() + self._active = True + asyncio.set_event_loop(self.loop) + self.loop.run_until_complete(self.monitor_dbus()) + except asyncio.CancelledError as err: + logging.error("Caught exception on udisks2 monitor, %s", err) + self.close() + return + + def close(self) -> None: + """Close usb devices monitoring thread and run loop""" + try: + if not self.loop: + return + if self.loop.is_running(): + self.stop_event.set() + self.loop.call_soon_threadsafe(self.loop.stop) + _ = self.wait() + self._active = False + for path in self.controlled_devs.keys(): + dev: Device = self.controlled_devs.pop(path) + dev.delete() + self.terminate() + self.deleteLater() + except asyncio.CancelledError as e: + logging.error( + "Caught exception while trying to close Udisks2 monitor: %s", e + ) + + async def monitor_dbus(self) -> None: + """Schedule coroutines for UDisks2 signals `interfaces_added`, `interfaces_removed` + and `properties_changed`. Creates symlink upon device insertion and cleans up symlink on removal. + + """ + tasks: dict[str, Coroutine[typing.Any, typing.Any, typing.Any]] = { + "add": self._add_interface_listener(), + "rem": self._rem_interface_listener(), + "prop": self._properties_changed_listener(), + } + _ = fire_n_forget( + coro=self.restore_tracked(), + name="Main-Restore-Discovery", + task_stack=self.task_stack, + ) + managed_tasks: list[asyncio.Task[typing.Any]] = [] + for name, coro in tasks.items(): + t = asyncio.create_task(coro, name=name) + self.task_stack.add(t) + t.add_done_callback(lambda _: self.task_stack.discard(t)) + managed_tasks.append(t) + try: + await asyncio.gather(*managed_tasks) + except asyncio.CancelledError as e: + for task in self.task_stack: + _ = task.cancel() + logging.info("UDisks2 Monitor stopped: %s", e) + self._active = False + except sdbus.SdBusBaseError as e: + logging.error("Caught generic fatal Sdbus exception: %s", e, exc_info=True) + self.close() + except Exception as e: + logging.error("Caught exception UDisks2 listeners failed: %s", e) + self._active = False + + async def restore_tracked(self) -> None: + """Get and restore controlled mass storage devices""" + info = await self.obj_manager.get_managed_objects() + for path, interfaces in info.items(): + fire_n_forget( + coro=self._handle_new_device(path, interfaces), + name=f"Restore-Discovery-{path}", + task_stack=self.task_stack, + ) + + async def _add_interface_listener(self) -> None: + """Handle add interface signal from UDisks2 DBus connection + + Adds the new device to internal tracking, can be retrieved with device path + Creates symlink onto specified directory configured on the class + """ + async for path, interfaces in self.obj_manager.interfaces_added: + fire_n_forget( + self._handle_new_device(path, interfaces), + name=f"UDisks-Discovery-{path}", + task_stack=self.task_stack, + ) + + async def _handle_new_device(self, path: str, interfaces) -> None: + """Handle new devices, can be used on `interfaces_added` signal and + when recovering states from `get_managed_objects` + + """ + try: + if Interfaces.Drive.value in interfaces: + ddev: UDisks2DriveAsyncInterface = UDisks2DriveAsyncInterface.new_proxy( + service_name=UDisks2_service, + object_path=path, + bus=self.system_bus, + ) + hwbus: str = await ddev.connection_bus + logging.debug( + "New Hardware device recognized type: %s \n path: %s", + hwbus, + path, + ) + media_removable, ejectable, con_bus = await asyncio.gather( + ddev.media_removable, ddev.ejectable, ddev.connection_bus + ) + if not (media_removable and ejectable and con_bus == "usb"): + # Only handle usb devices and removable storage media + return + device: Device = Device( + path, DriveInterface=ddev, symlink_path=self.gcodes_path.as_posix() + ) + self.controlled_devs.update({path: device}) + self.hardware_detected[str].emit(path) + if Interfaces.Block.value in interfaces: + bdev: UDisks2BlockAsyncInterface = UDisks2BlockAsyncInterface.new_proxy( + service_name=UDisks2_service, + object_path=path, + bus=self.system_bus, + ) + drv_path: str = await bdev.drive + hint_sys, hint_ignore = await asyncio.gather( + bdev.hint_system, bdev.hint_ignore + ) + dev_name = await bdev.hint_name or await bdev.id_label + if hint_sys or hint_ignore: + # Always ignore device if these flags are set + return + if drv_path in self.controlled_devs: + dev: Device = self.controlled_devs[drv_path] + if all( + phase in interfaces + for phase in ( + Interfaces.PartitionTable.value, + Interfaces.Block.value, + ) + ): + devpt: UDisks2PartitionTableAsyncInterface = ( + UDisks2PartitionTableAsyncInterface.new_proxy( + service_name=UDisks2_service, + object_path=path, + bus=self.system_bus, + ) + ) + dev.update_raw_block(path, bdev) + dev.update_part_table(path, devpt) + if Interfaces.Filesystem.value in interfaces: + devfs: UDisks2FileSystemAsyncInterface = ( + UDisks2FileSystemAsyncInterface.new_proxy( + service_name=UDisks2_service, + object_path=path, + bus=self.system_bus, + ) + ) + dev.update_file_system(path, devfs) + self.device_added.emit(path, interfaces) + _label = dev_name or "" + self.mount(dev, _label) + if Interfaces.Partition.value in interfaces: + devpart: UDisks2PartitionAsyncInterface = ( + UDisks2PartitionAsyncInterface.new_proxy( + service_name=UDisks2_service, + object_path=path, + bus=self.system_bus, + ) + ) + dev.update_partitions(path, devpart) + dev.update_logical_blocks(path, bdev) + except sdbus.dbus_exceptions.DbusUnknownMethodError as e: + logging.error( + "Caught exception on device inserted unknown method: %s", + e, + exc_info=True, + ) + except sdbus.dbus_exceptions.DbusUnknownInterfaceError as e: + logging.error( + "Caught exception on device inserted unknown interface: %s", + e, + exc_info=True, + ) + except Exception as e: + logging.error( + "Caught fatal exception during discovery process %s: %s", + path, + e, + exc_info=True, + ) + + async def _properties_changed_listener(self) -> None: + """Handle properties_changed signal from UDisks2 Dbus connection + + Updates tracked objects + """ + async for ( + path, + changed_properties, + invalid_properties, + ) in self.obj_manager.properties_changed: + pass + + async def _rem_interface_listener(self) -> None: + """Handle device removal signals from UDisks2 Dbus connection + + Removes tracked interface and cleans up any left behind data + """ + async for path, interfaces in self.obj_manager.interfaces_removed: + try: + if Interfaces.Drive.value in interfaces: + if path in self.controlled_devs: + device: Device = self.controlled_devs.pop(path) + device.kill() + del device + self.hardware_removed[str].emit(path) + self._cleanup_broken_symlinks() + except sdbus.dbus_exceptions.DbusUnknownMethodError as e: + logging.error( + "Caught exception on device removed unknown method: %s", + e, + exc_info=True, + ) + except sdbus.dbus_exceptions.DbusUnknownInterfaceError as e: + logging.error( + "Caught exception on device removed unknown interface %s", + e, + exc_info=True, + ) + except Exception as e: + logging.error( + "Caught fatal exception on removed device: %s, %s", + path, + e, + exc_info=True, + ) + + def mount(self, device: Device, label: str = ""): + """Mounts the devices mountpoints""" + for path, filesystem in device.file_systems.items(): + _ = fire_n_forget( + coro=self._mount_filesystem(filesystem, label), + name=f"Mount-filesystem-{path}", + task_stack=self.task_stack, + ) + + async def _mount_filesystem( + self, filesystem: UDisks2FileSystemAsyncInterface, label: str = "" + ) -> str: + val_label: str = validate_label(label) + try: + opts: dict[str, tuple[str, typing.Any]] = { + "auto.no_user_interactions": ("b", True), + "fstype": ("s", "auto"), + "as-user": ("s", os.environ.get("USER")), + "options": ("s", "rw,relatime,sync"), + } + mnt_path: str = await filesystem.mount(opts) + return self.add_symlink( + path=mnt_path, label=val_label, dst_path=self.gcodes_path.as_posix() + ) + except sdbus.SdBusUnmappedMessageError as e: + if AlreadyMountedException in e.args[0]: + logging.debug( + "Device filesystem already mounted on %s, verifying gcodes symlink", + str(e.args[1]), + ) + mount_points: list[bytes] = await filesystem.mount_points + if not mount_points: + return "" + mpoint: str = mount_points[0].decode("utf-8").strip("\x00") + if os.path.exists(mpoint): + return "" + return self.add_symlink( + path=mpoint, + dst_path=self.gcodes_path.as_posix(), + label=val_label, + ) + except Exception as e: + logging.error( + "Caught exception while mounting file system %s : %s", + filesystem, + e, + exc_info=True, + ) + return "" + + def add_symlink( + self, + path: str, + dst_path: str, + label: str = "", + _index: int = 0, + _validated: bool = False, + ) -> str: + """Create symlink on `dst_path` + + If there is a symlink created on `dst_path` with the same label, + which points to the same `path` then it will return the `dst_path` + as validation. If `dst_path` does not resolve to the same `path` + then it will cleanup that symlink and create a replacement. + + In case there is no `label` then the created `symlink` on `dst_path` + will default to **USB DRIVE**. If *USB DRIVE* symlink already exists + then it will create a variant of that fallback **USB DRIVE [1-254]** + + Be careful with the provided directories and labels. They must come + clean or else this method won't work. + """ + if not _validated and label: + label = validate_label(label, strict=True) + label = "USB " + label + fallback: str = "USB DRIVE" if _index == 0 else str(f"USB DRIVE {_index}") + dstb = pathlib.Path(dst_path).joinpath(label if label else fallback) + try: + if not os.path.islink(dstb): + os.symlink(src=path, dst=dstb) + return dstb.as_posix() if os.path.exists(dstb) else "" + if os.path.islink(dstb): + if dstb.resolve().as_posix() == pathlib.Path(path).as_posix(): + return dstb.as_posix() + if not label: + _index += 1 + if _index == 255: + return "" + return self.add_symlink(path, dst_path, label, _index, _validated=True) + if self.rem_symlink(path=dstb.as_posix()): + return self.add_symlink(path, dst_path, label, _validated=True) + except PermissionError: + logging.error( + "Caught fatal exception no permissions, unable to create symlink on specified path" + ) + except OSError as e: + logging.error("Caught fatal exception OSERROR %s", e) + return "" + + def rem_symlink(self, path: str | pathlib.Path) -> bool: + """Remove `ONLY` symlinks located in `path` if it is allowed""" + resolved_path = pathlib.Path(path) + resolved_gcodes_path = pathlib.Path(self.gcodes_path).as_posix() + try: + _ = resolved_path.relative_to(resolved_gcodes_path) + except ValueError: + logging.error("Path transversal attempt in rem_symlink: %s", path) + return False + + if not os.path.islink(resolved_path): + logging.error("Provided path %s is NOT a symlink, refusing to delete", path) + return False + try: + os.remove(resolved_path) + return True + except (PermissionError, OSError): + logging.error("Caught fatal exception failed to remove symlink %s", path) + return False + + def _cleanup_symlinks(self) -> None: + """Cleanup all symlinks on gcodes directory + + This method is private, if used outside of it's intended purpose + devices will lose track of what symlinks are associated with them + + USE WITH CARE + """ + for dir in self.gcodes_path.rglob("*"): + if os.path.islink(dir): + _ = self.rem_symlink(dir.as_posix()) + + def _cleanup_broken_symlinks(self) -> None: + for dir in self.gcodes_path.rglob("*"): + if os.path.islink(dir) and not os.path.exists(dir): + _ = self.rem_symlink(dir) + + def _resolve_symlinks( + self, path: str | pathlib.Path, mount_path: str | pathlib.Path + ) -> bool: + """Checks if `gcodes directory` already has a symlink that + resolves to the same mount directory. + + + This method returns on the first encounter. It does not + evaluate any other symlinks after that. + """ + for dir in self.gcodes_path.rglob("*"): + if os.path.islink(dir): + if ( + pathlib.Path(path).resolve().as_posix() + == pathlib.Path(mount_path).as_posix() + ): + # This `path` resolves to the `mount_path` + return True + return False diff --git a/BlocksScreen/devices/storage/udisks2_dbus_async.py b/BlocksScreen/devices/storage/udisks2_dbus_async.py new file mode 100644 index 00000000..5f884c02 --- /dev/null +++ b/BlocksScreen/devices/storage/udisks2_dbus_async.py @@ -0,0 +1,568 @@ +# Sdbus Udisks2 Interface classes and manager +# +# Contains interface classes for async and blocking api of sdbus +# +# +# Hugo Costa hugo.santos.costa@gmail.com +import enum +import typing + +import sdbus + + +class Interfaces(enum.Enum): + Filesystem = "org.freedesktop.UDisks2.Filesystem" + Drive = "org.freedesktop.UDisks2.Drive" + Partition = "org.freedesktop.UDisks2.Partition" + Block = "org.freedesktop.UDisks2.Block" + PartitionTable = "org.freedesktop.UDisks2.PartitionTable" + + @classmethod + def has_value(cls, value) -> bool: + return value in (item.value for item in cls) + + +class UDisks2AsyncManager(sdbus.DbusObjectManagerInterfaceAsync): + """Subclassed async dbus object manager""" + + def __init__(self) -> None: + super().__init__() + + +class UDisks2PartitionTableAsyncInterface( + sdbus.DbusInterfaceCommonAsync, interface_name=Interfaces.PartitionTable.value +): + def __init__(self) -> None: + super().__init__() + + @sdbus.dbus_property_async(property_signature="ao") + def partitions(self) -> list[str]: + """Get list of object paths of the `org.freedesktop.Udisks2.Partitions` + + Returns: + list[str]: list of object paths + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def type(self) -> str: + """Get the type of partition table detected + + If blank the partition table was detected but it's unknown + Returns: + str: Known values ['dos', 'gpt', ''] + + """ + raise NotImplementedError + + +class UDisks2PartitionAsyncInterface( + sdbus.DbusInterfaceCommonAsync, interface_name=Interfaces.Partition.value +): + def __init__(self) -> None: + super().__init__() + + @sdbus.dbus_method_async(input_signature="s") + async def set_type(self, type: str) -> None: + """Set new partition type + + Args: + type (str): New partition type + """ + raise NotImplementedError + + @sdbus.dbus_method_async(input_signature="s") + async def set_name(self, name: str) -> None: + """Set partition name + + Args: + name (str): new partition name + """ + raise NotImplementedError + + @sdbus.dbus_method_async(input_signature="a{sv}") + async def delete(self, opts: dict[str, typing.Any]) -> None: + """Deletes the partition + + Args: + options (dict[str, any]) + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="u") + def number(self) -> int: + """Number of the partition on the partition table + + Returns: + number (int): partition number + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def type(self) -> str: + """Partition type + + Returns: + type (str): The partition type. For `dos` partition + tables this string is a hexadecimal code (0x83, 0xfd). + For `gpt` partition tables this is the UUID""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="t") + def flags(self) -> int: + """Flags describing the partition. + --------------- + For `dos` partitions: + - Bit 7 - The partition is marked as bootable + --------------- + For `gpt` partitions : + - Bit 0 - System Partition + - Bit 2 - Legacy BIOS bootable + - Bit 60 - Read-only + - Bit 62 - Hidden + - Bit 63 - Do not automount + + + Returns: + flags (int): current flags + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="t") + def offset(self) -> int: + """Offset of the partition in bytes + + Returns: + Offset (int): partition offset + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="t") + def size(self) -> int: + """Partition size, in bytes + + Returns: + Size (int): partition size + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def name(self) -> str: + """Partition name + Returns: + name (str): partition name + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def uuid(self) -> str: + """Partition UUID + Returns: + uuid (str): partition uuid + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="o") + def table(self) -> str: + """Object path of the `org.freedesktop.Udisks2.PartitionTable` object that + the partition belongs to. + + Returns: + table (str): path + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def is_container(self) -> bool: + """Set to True if the partition itself is a container for other partitions + + For example, for dos partition tables, this applies to so-called extended + partition (partitions of type 0x05, 0x0f or 0x85) containing so-called logical partitions. + + + Returns: + is_container (bool): if it is a container + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def is_contained(self) -> bool: + """Set to True if the partition is contained in another partition + Returns: + is_contained (bool): if it's contained + """ + raise NotImplementedError + + +class UDisks2FileSystemAsyncInterface( + sdbus.DbusInterfaceCommonAsync, interface_name=Interfaces.Filesystem.value +): + def __init__(self) -> None: + super().__init__() + + @sdbus.dbus_method_async(input_signature="a{sv}", result_signature="s") + async def mount(self, opts) -> str: + """Mounts the filesystem + + Args: + options dict[str, tuple[str, any]]: Options to mount the filesystem + + Returns: + path (str): mount path + """ + raise NotImplementedError + + @sdbus.dbus_method_async(input_signature="a{sv}") + async def unmount(self, opts) -> None: + """Unmount a mounted device + + Args: + options dict[str, any]: Known options (in addition to the standart options) include `force` (of type `b`) + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="t") + def size(self) -> int: + """Size of the filesystem. This is the amount + of bytes used on the block device representing an outer + filesystem boundary + + Returns: + size (int): Size of the filesystem + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="ayy") + def mount_points(self) -> list[bytes]: + """An array of filesystem paths for where the + file system on the device is mounted. If the + device is not mounted, this array will be empty + + Returns + mount_points (list[bytes]): Array of filesystem paths + """ + raise NotImplementedError + + +class UDisks2BlockAsyncInterface( + sdbus.DbusInterfaceCommonAsync, interface_name=Interfaces.Block.value +): + def __init__(self) -> None: + super().__init__() + + @sdbus.dbus_property_async(property_signature="s") + def hint_name(self) -> str: + """Hint name, if not blank, the name to + that presents the device + + Returns: + name (str): name of the device""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def hint_system(self) -> bool: + """If the device is considered a system device + True if it is. System devices are devices that + require additional permissions to access + + Returns + hint system (bool): If device is `system device`""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def hint_ignore(self) -> bool: + """If the device should be hidden from users + Returns + ignore (bool): True if the system should be ignored""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="ay") + def device(self) -> list[int] | bytes: + """Special device file for the block device + + Returns: + file path (list[int] | bytes): The file path""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="t") + def device_number(self) -> int: + """Device `dev_t` of the block device""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def id(self) -> str: + """Unique persistent identifier for the device + blank if no such identifier is available + + Returns: + id (str): unique identifier""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def id_label(self) -> str: + """Label for the filesystem or other structured + data on the block device. + If the property is blank there is no label or + it is unknown + + Returns: + label (str): filesystem label for the block""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def id_UUID(self) -> str: + """UUID of the filesystem or other structured + data on the block device. Do not make any + assumptions about the UUID as its format + depends on what kind of data is on the device + + Returns: + uuid (str): uuid""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def id_usage(self) -> str: + """Result of probing for signatures on the block device + Known values include + - filesystem -> Used for mountable filesystems + - crypto -> Used for e.g. LUKS devices + - raid -> Used for e.g. RAID members + - other -> Something else was detected + + ----- + If blank no known signature was detected. It doesn't + necessarily mean the device contains no structured data; + it only means that probing failed + + Returns: + usage (str): usage signature""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def id_type(self) -> str: + """Property that contains more information about + the probing result of the blocks device. It depends + on the IdUsage property + - filesystem -> The mountable file system that was detected (e.g. vfat). + - crypto -> Encrypted data. Known values include crypto_LUKS. + - raid -> RAID or similar. Known values include LVM2_member (for LVM2 components), linux_raid_member (for MD-RAID components.) + - other -> Something else. Known values include swap (for swap space), suspend (data used when resuming from suspend-to-disk). + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="ayy") + def symlinks(self) -> list[bytes]: + """Known symlinks in `/dev` that point to the device + in the file **Device** property. + + Returns: + symlinks (list[bytes]): available symlinks + """ + raise NotImplementedError + + @sdbus.dbus_method_async(input_signature="a{sv}") + async def rescan(self, opts: dict[str, typing.Any]) -> None: + """Request that the kernel and core OS rescans the + contents of the device and update their state to reflect + this + + Args: + options: unused + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="o") + def drive(self) -> str: + """The org.freedesktop.UDisks2.Drive object that the + block device belongs to, or '/' if no such object + exits + + Returns: + drive (str): path + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="a(sa{sv})") + def configuration(self) -> list[typing.Any]: + """The configuration for the device + This is an array of pairs (type, details), where `type` is + a string identifying the configuration source and the + `details` has the actual configuration data. + For entries of type `fstab` known configurations are: + - fsname (type 'ay') - The special device + - dir (type 'ay') - The mount point + - type (type 'ay') - The filesystem type + - opts (type 'ay') - Options + - freq (type 'i') - Dump frequency in days + - passno (type 'i') - Pass number of parallel fsck + For entries of type `crypttab` known configurations are: + - name (type 'ay') - The name to set the device up as + - device (type 'ay') - The special device + - passphrase-path (type 'ay') - Either empty to specify + that no password is set, otherwise a path to a file + containing the encryption password. This may also point + to a special device file in /dev such as /dev/random + - options (type 'ay') - Options + """ + raise NotImplementedError + + +class UDisks2DriveAsyncInterface( + sdbus.DbusInterfaceCommonAsync, interface_name=Interfaces.Drive.value +): + def __init__(self) -> None: + super().__init__() + + @sdbus.dbus_property_async(property_signature="s") + def revision(self) -> str: + """Firmware revision or blank if unknown + Returns: + revision (str): revision or blank + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def WWN(self) -> str: + """The World Wide Name of the drive or blank if unknown + Returns: + wwn (str) : wwn or none + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="a{sv}") + def configuration(self) -> dict[str, typing.Any]: + """Configuration directives applied to the drive when + its connected. + + Returns: + configurations (dict): applied configurations + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def can_power_off(self) -> bool: + """Whether the drive can be safely removed/powered off + + Returns: + can_power_off (bool): whether it can be removed or powered off""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def model(self) -> str: + """Name for the model of the drive + + Returns: + model (str): name of the model, blank if unknown""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def connection_bus(self) -> str: + """Physical connection bus for the drive, as seen + by the user + + Returns: + connection bus (str): physical connection bus ['usb', 'sdio', 'ieee1394'] + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def serial(self) -> str: + """Serial number + + Returns: + serial (str): serial number blank if unknown + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def ejectable(self) -> bool: + """Whether the media can be ejected from the drive of the + drive accepts the `eject` command to switch its state + so that the it displays 'Safe To Remove' + + *This is only a guess* + Returns: + ejectable (bool): can be ejected + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def removable(self) -> bool: + """Hint whether the drive and/or its media is considered + removable by the user. + + *This is only a guess* + + Returns: + removable (bool): whether the drive is considered removable + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="t") + def time_detected(self) -> int: + """The time the drive was first detected + + Returns: + time (int): time it was first detected + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def media_available(self) -> bool: + """This is always True if `MediaChangeDetected` is False + Returns: + media available (bool): True if media change detected is false + """ + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def media_changed_detected(self) -> bool: + """Set to true only if media changes are detected + + Returns: + media change detected (bool): True if media changes are detected""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def media(self) -> str: + """The kind of media + + Returns: + media (str): The kind of media, blank if unknown""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="b") + def media_removable(self) -> bool: + """Whether the media can be removed from the drive + + Returns: + media_removable (bool): Whether it can be removed""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def id(self) -> str: + """Unique persistent identifier for the device or + blank if not available + + Returns: + id (str): Identifier e.g “ST32000542AS-6XW00W51”""" + raise NotImplementedError + + @sdbus.dbus_property_async(property_signature="s") + def vendor(self) -> str: + """Name for the vendor of the drive or blank if + unknown + + Returns: + vendor (str): Name of the vendor or blank""" + raise NotImplementedError + + @sdbus.dbus_method_async(input_signature="a{sv}") + async def eject(self, opts: dict[str, typing.Any]) -> None: + """Ejects the media from the drive + + Args: + options (dict): currently unused + """ + + raise NotImplementedError diff --git a/BlocksScreen/devices/storage/usb_controller.py b/BlocksScreen/devices/storage/usb_controller.py new file mode 100644 index 00000000..8094e8df --- /dev/null +++ b/BlocksScreen/devices/storage/usb_controller.py @@ -0,0 +1,144 @@ +import logging +import os +import typing +from PyQt6 import QtCore + +from .udisks2 import UDisksDBusAsync +from lib.panels.widgets.bannerPopup import BannerPopup + +ResType: typing.TypeAlias = typing.Literal["always", "none"] + + +class USBManager(QtCore.QObject): + usb_add: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, dict, name="usb-add" + ) + usb_rem: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, str, name="usb-rem" + ) + usb_hardware_detected: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="hardware-detected" + ) + usb_hardware_removed: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="hardware-removed" + ) + usb_monitor_started: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + name="usb-monitor-started" + ) + usb_monitor_finished: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + name="usb-monitor-finished" + ) + usb_mounted: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, str, name="device-mounted" + ) + + usb_unmounted: typing.ClassVar[QtCore.pyqtSignal] = QtCore.pyqtSignal( + str, name="device-unmounted" + ) + + def __init__(self, parent: QtCore.QObject, gcodes_dir: str | None) -> None: + super().__init__(parent) + self.gcodes_dir: str = gcodes_dir or os.path.expanduser("~/printer_data/gcodes") + if not (os.path.isdir(self.gcodes_dir) and os.path.exists(self.gcodes_dir)): + logging.info("Provided gcodes directory does not exist.") + self.udisks: UDisksDBusAsync = UDisksDBusAsync( + parent=self, gcodes_dir=self.gcodes_dir + ) + # self.banner = BannerPopup(self) + self.banner = BannerPopup() + self._restart_type: ResType = "always" + self.udisks.start(self.udisks.Priority.InheritPriority) + self.udisks.hardware_detected.connect(self.handle_new_hardware) + self.udisks.hardware_detected.connect(self.usb_hardware_detected) + self.udisks.hardware_removed.connect(self.handle_rem_hardware) + self.udisks.hardware_removed.connect(self.usb_hardware_removed) + self.udisks.device_added.connect(self.handle_new_device) + self.udisks.device_added.connect(self.usb_add) + self.udisks.device_removed.connect(self.handle_rem_device) + self.udisks.device_removed.connect(self.usb_rem) + self.udisks.device_mounted.connect(self.handle_mounted_device) + self.udisks.device_mounted.connect(self.usb_mounted) + self.udisks.device_unmounted.connect(self.handle_unmounted_device) + self.udisks.device_unmounted.connect(self.usb_unmounted) + self.udisks.started.connect(self.usb_monitor_started) + self.udisks.finished.connect(self.usb_monitor_finished) + self.need_restart: bool = False + self.udisks.finished.connect(self._handle_full_restart) + if self.restart_type == "always": + self.udisks.finished.connect(self._handle_monitor_finished) + + def restart(self) -> None: + """Restart usb monitoring tool""" + if not self.udisks.active: + self.udisks.start(self.udisks.Priority.InheritPriority) + return + self.udisks.close() + self.need_restart = True + + def close(self) -> None: + """Close usb monitoring tool""" + self.udisks.close() + self.deleteLater() + + def _handle_full_restart(self) -> None: + if self.need_restart: + self.udisks.start(self.udisks.Priority.InheritPriority) + self.need_restart = False + + @property + def restart_type(self) -> ResType: + return self._restart_type + + @restart_type.setter + def restart_type(self, type: ResType) -> None: + """Tool restart type, currently there are only two + options available. + + - `always` - restarts the tool every time it stops + - `none` - doesn't restart the tool at all + """ + if type not in ("always", "none"): + logging.info("Unknown restart type %s", (type,)) + if type == "always": + if not self._restart_type == "always": + self.udisks.finished.connect(self._handle_monitor_finished) + else: + try: + self.udisks.finished.disconnect(self._handle_monitor_finished) + except TypeError: + pass + self._restart_type = type + + @QtCore.pyqtSlot(name="monitor-finished") + def _handle_monitor_finished(self) -> None: + # Just restart the monitor for now + self.restart() + + @QtCore.pyqtSlot(str, str, name="device-mounted") + def handle_mounted_device(self, path, symlink) -> None: + """Handle new mounted device""" + pass + + @QtCore.pyqtSlot(str, name="device-unmounted") + def handle_unmounted_device(self, path) -> None: + pass + + @QtCore.pyqtSlot(str, dict, name="device-added") + def handle_new_device(self, path, interface) -> None: + """Handle new device""" + pass + + @QtCore.pyqtSlot(str, name="device-removed") + def handle_rem_device(self, path) -> None: + """Handle device removed""" + pass + + @QtCore.pyqtSlot(str, name="hardware_detected") + def handle_new_hardware(self, path: str) -> None: + """Handle new usb device hardware""" + self.banner.new_message(self.banner.MessageType.CONNECT) + + @QtCore.pyqtSlot(str, name="hardware_removed") + def handle_rem_hardware(self, path: str) -> None: + """Handle usb device hardware removed""" + self.banner.new_message(self.banner.MessageType.DISCONNECT) diff --git a/BlocksScreen/lib/panels/mainWindow.py b/BlocksScreen/lib/panels/mainWindow.py index 6a1b004d..77d17e86 100644 --- a/BlocksScreen/lib/panels/mainWindow.py +++ b/BlocksScreen/lib/panels/mainWindow.py @@ -4,13 +4,13 @@ import events from configfile import BlocksScreenConfig, get_configparser +from devices.storage import USBManager from lib.files import Files from lib.machine import MachineControl from lib.moonrakerComm import MoonWebSocket from lib.network import WifiIconKey from lib.panels.controlTab import ControlTab from lib.panels.filamentTab import FilamentTab -from lib.panels.widgets.notificationPage import NotificationPage from lib.panels.networkWindow import NetworkControlWindow, PixmapCache from lib.panels.printTab import PrintTab from lib.panels.utilitiesTab import UtilitiesTab @@ -18,6 +18,7 @@ from lib.panels.widgets.cancelPage import CancelPage from lib.panels.widgets.connectionPage import ConnectionPage from lib.panels.widgets.loadWidget import LoadingOverlayWidget +from lib.panels.widgets.notificationPage import NotificationPage from lib.panels.widgets.updatePage import UpdatePage from lib.printer import Printer from lib.ui.mainWindow_ui import Ui_MainWindow # With header @@ -28,7 +29,6 @@ from lib.ui.resources.main_menu_resources_rc import * from lib.ui.resources.system_resources_rc import * from lib.ui.resources.top_bar_resources_rc import * -from logger import LogManager from PyQt6 import QtCore, QtGui, QtWidgets from screensaver import ScreenSaver @@ -113,6 +113,13 @@ def __init__(self): self.screensaver = ScreenSaver(self) self._popup_toggle: bool = False self.ui.main_content_widget.setCurrentIndex(0) + + usb_config = self.config.get_section("usb_manager", fallback=None) + gdir = None + if usb_config: + gdir = usb_config.get("gcodes_dir", default=None) + + self.usb_manager: USBManager = USBManager(parent=self, gcodes_dir=gdir) self.ws = MoonWebSocket(self) self.notiPage = NotificationPage(self) self.mc = MachineControl(self) @@ -120,16 +127,14 @@ def __init__(self): self.index_stack = deque(maxlen=4) self.printer = Printer(self, self.ws) self.conn_window = ConnectionPage(self, self.ws) - self.up = UpdatePage(self) - self.up.hide() - + self.update_page = UpdatePage(self) + self.update_page.hide() self.conn_window.call_cancel_panel.connect(self.handle_cancel_print) self.installEventFilter(self.conn_window) self.printPanel = PrintTab( self.ui.printTab, self.file_data, self.ws, self.printer ) QtWidgets.QApplication.setOverrideCursor(QtCore.Qt.CursorShape.BlankCursor) - self.filamentPanel = FilamentTab(self.ui.filamentTab, self.printer, self.ws) self.controlPanel = ControlTab(self.ui.controlTab, self.ws, self.printer) self.utilitiesPanel = UtilitiesTab(self.ui.utilitiesTab, self.ws, self.printer) @@ -210,21 +215,27 @@ def __init__(self): self.controlPanel.probe_helper_page.handle_error_response ) self.controlPanel.disable_popups.connect(self.popup_toggle) - self.on_update_message.connect(self.up.handle_update_message) - self.up.request_full_update.connect(self.ws.api.full_update) - self.up.request_recover_repo[str].connect(self.ws.api.recover_corrupt_repo) - self.up.request_recover_repo[str, bool].connect( + self.on_update_message.connect(self.update_page.handle_update_message) + self.update_page.request_full_update.connect(self.ws.api.full_update) + self.update_page.request_recover_repo[str].connect( self.ws.api.recover_corrupt_repo ) - self.up.request_refresh_update.connect(self.ws.api.refresh_update_status) - self.up.request_refresh_update[str].connect(self.ws.api.refresh_update_status) - self.up.request_rollback_update.connect(self.ws.api.rollback_update) - self.up.request_update_client.connect(self.ws.api.update_client) - self.up.request_update_klipper.connect(self.ws.api.update_klipper) - self.up.request_update_moonraker.connect(self.ws.api.update_moonraker) - self.up.request_update_status.connect(self.ws.api.update_status) - self.up.request_update_system.connect(self.ws.api.update_system) - self.up.update_back_btn.clicked.connect(self.up.hide) + self.update_page.request_recover_repo[str, bool].connect( + self.ws.api.recover_corrupt_repo + ) + self.update_page.request_refresh_update.connect( + self.ws.api.refresh_update_status + ) + self.update_page.request_refresh_update[str].connect( + self.ws.api.refresh_update_status + ) + self.update_page.request_rollback_update.connect(self.ws.api.rollback_update) + self.update_page.request_update_client.connect(self.ws.api.update_client) + self.update_page.request_update_klipper.connect(self.ws.api.update_klipper) + self.update_page.request_update_moonraker.connect(self.ws.api.update_moonraker) + self.update_page.request_update_status.connect(self.ws.api.update_status) + self.update_page.request_update_system.connect(self.ws.api.update_system) + self.update_page.update_back_btn.clicked.connect(self.update_page.hide) self.utilitiesPanel.show_update_page.connect(self.show_update_page) self.conn_window.update_button_clicked.connect(self.show_update_page) self.ui.extruder_temp_display.display_format = "upper_downer" @@ -302,22 +313,22 @@ def show_LoadScreen(self, show: bool = True, msg: str = ""): def show_update_page(self, fullscreen: bool): """Slot for displaying update Panel""" if not fullscreen: - self.up.setParent(self.ui.main_content_widget) + self.update_page.setParent(self.ui.main_content_widget) current_index = self.ui.main_content_widget.currentIndex() tab_rect = self.ui.main_content_widget.tabBar().tabRect(current_index) width = tab_rect.width() - _parent_size = self.up.parent().size() - self.up.setGeometry( + _parent_size = self.update_page.parent().size() + self.update_page.setGeometry( width, 0, _parent_size.width() - width, _parent_size.height() ) else: - self.up.setParent(self) - self.up.setGeometry(0, 0, self.width(), self.height()) + self.update_page.setParent(self) + self.update_page.setGeometry(0, 0, self.width(), self.height()) - self.up.raise_() - self.up.updateGeometry() - self.up.repaint() - self.up.show() + self.update_page.raise_() + self.update_page.updateGeometry() + self.update_page.repaint() + self.update_page.show() @QtCore.pyqtSlot(name="on-cancel-print") def on_cancel_print(self): @@ -426,7 +437,7 @@ def reset_tab_indexes(self): Used to grantee all tabs reset to their first page once the user leaves the tab """ - self.up.hide() + self.update_page.hide() self.printPanel.setCurrentIndex(0) self.filamentPanel.setCurrentIndex(0) self.controlPanel.setCurrentIndex(0) @@ -776,15 +787,14 @@ def set_header_nozzle_diameter(self, diam: str): self.ui.nozzle_size_icon.setText(f"{diam}mm") self.ui.nozzle_size_icon.update() - def closeEvent(self, a0: typing.Optional[QtGui.QCloseEvent]) -> None: + def closeEvent(self, a0: QtGui.QCloseEvent | None) -> None: """Handles GUI closing""" try: self.networkPanel.close() + self.usb_manager.close() except Exception as e: - _logger.warning("Network panel shutdown error: %s", e) - + _logger.warning("Error shutting down: %s", e) self.ws.wb_disconnect() - LogManager.shutdown() if a0 is None: return QtWidgets.QMainWindow.closeEvent(self, a0) diff --git a/BlocksScreen/lib/panels/widgets/bannerPopup.py b/BlocksScreen/lib/panels/widgets/bannerPopup.py new file mode 100644 index 00000000..7db54547 --- /dev/null +++ b/BlocksScreen/lib/panels/widgets/bannerPopup.py @@ -0,0 +1,222 @@ +import enum +from collections import deque + +from lib.utils.icon_button import IconButton +from PyQt6 import QtCore, QtGui, QtWidgets + + +class BannerPopup(QtWidgets.QWidget): + class MessageType(enum.Enum): + """Popup Message type (level)""" + + CONNECT = enum.auto() + DISCONNECT = enum.auto() + CORRUPTED = enum.auto() + UNKNOWN = enum.auto() + + def __init__(self, parent=None) -> None: + if parent: + super().__init__(parent) + else: + super().__init__() + self.timeout_timer = QtCore.QTimer(self) + self.timeout_timer.setSingleShot(True) + self.messages: deque = deque() + self.isShown = False + self.default_background_color = QtGui.QColor(164, 164, 164) + self.setAttribute(QtCore.Qt.WidgetAttribute.WA_TranslucentBackground, True) + self.setMouseTracking(True) + self.setWindowFlags( + QtCore.Qt.WindowType.FramelessWindowHint + | QtCore.Qt.WindowType.Tool + | QtCore.Qt.WindowType.X11BypassWindowManagerHint + ) + self._setupUI() + self.slide_in_animation = QtCore.QPropertyAnimation(self, b"geometry") + self.slide_in_animation.setDuration(1000) + self.slide_in_animation.setEasingCurve(QtCore.QEasingCurve.Type.OutCubic) + self.slide_out_animation = QtCore.QPropertyAnimation(self, b"geometry") + self.slide_out_animation.setDuration(200) + self.slide_out_animation.setEasingCurve(QtCore.QEasingCurve.Type.InCubic) + self.oneshot = QtCore.QTimer(self) + self.oneshot.setInterval(5000) + self.oneshot.setSingleShot(True) + self.oneshot.timeout.connect(self._add_popup) + self.timeout_timer.setInterval(4000) + self.slide_out_animation.finished.connect(self.on_slide_out_finished) + self.slide_in_animation.finished.connect(self.on_slide_in_finished) + self.timeout_timer.timeout.connect(lambda: self.slide_out_animation.start()) + self.actionbtn.clicked.connect(self.slide_out_animation.start) + + def event(self, a0): + if a0.type() in (QtCore.QEvent.Type.MouseButtonPress,): + if self.rect().contains(a0.position().toPoint()): + self.timeout_timer.stop() + self.slide_out_animation.setStartValue( + self.slide_in_animation.currentValue() + ) + self.slide_in_animation.stop() + self.slide_out_animation.start() + + return super().event(a0) + + def on_slide_in_finished(self): + """Handle slide in animation finished""" + self.timeout_timer.start() + + def on_slide_out_finished(self): + """Handle slide out animation finished""" + self.hide() + self.isShown = False + self.timeout_timer.stop() + self._add_popup() + + def _calculate_target_geometry(self) -> QtCore.QRect: + """Calculate on end posisition rect for popup""" + app_instance = QtWidgets.QApplication.instance() + main_window = app_instance.activeWindow() if app_instance else None + if main_window is None and app_instance: + for widget in app_instance.allWidgets(): + if isinstance(widget, QtWidgets.QMainWindow): + main_window = widget + break + parent_rect = main_window.geometry() + width = int(parent_rect.width() * 0.35) + height = 80 + x = parent_rect.x() + parent_rect.width() - width + 50 + y = parent_rect.y() + 30 + return QtCore.QRect(x, y, width, height) + + def updateMask(self) -> None: + """Update widget mask properties""" + path = QtGui.QPainterPath() + path.addRoundedRect(self.rect().toRectF(), 50, 70) + region = QtGui.QRegion(path.toFillPolygon(QtGui.QTransform()).toPolygon()) + self.setMask(region) + + def mousePressEvent(self, a0: QtGui.QMouseEvent | None) -> None: + """Re-implemented method, handle mouse press events""" + return + + def new_message( + self, + message_type: MessageType = MessageType.CONNECT, + ): + """Create new popup message + + Args: + message_type (MessageType, optional): Message Level, See `MessageType` Types. Defaults to MessageType.CONNECT . + Returns: + _type_: _description_ + """ + if len(self.messages) == 4: + return + + self.messages.append( + { + "type": message_type, + } + ) + return self._add_popup() + + def _add_popup(self) -> None: + """Add popup to queue""" + if self.isShown: + if self.oneshot.isActive(): + return + self.oneshot.start() + return + if ( + self.messages + and self.slide_in_animation.state() + == QtCore.QPropertyAnimation.State.Stopped + and self.slide_out_animation.state() + == QtCore.QPropertyAnimation.State.Stopped + ): + message_entry = self.messages.popleft() + message_type = message_entry.get("type") + + message = "Unknown Event" + icon = ":ui/media/btn_icons/info.svg" + + # TODO: missing usb icons + match message_type: + case BannerPopup.MessageType.CONNECT: + message = "Usb Connected" + # icon = "" + case BannerPopup.MessageType.DISCONNECT: + message = "Usb Disconnected" + # icon = "" + case BannerPopup.MessageType.CORRUPTED: + message = "Usb Corrupted" + icon = ":/ui/media/btn_icons/troubleshoot.svg" + end_rect = self._calculate_target_geometry() + start_rect = end_rect.translated(end_rect.width() * 2, 0) + + self.icon_label.setPixmap(QtGui.QPixmap(icon)) + + self.slide_in_animation.setStartValue(start_rect) + self.slide_in_animation.setEndValue(end_rect) + self.slide_out_animation.setStartValue(end_rect) + self.slide_out_animation.setEndValue(start_rect) + self.setGeometry(end_rect) + self.text_label.setText(message) + self.show() + + def showEvent(self, a0: QtGui.QShowEvent | None) -> None: + """Re-implementation, widget show""" + self.slide_in_animation.start() + self.isShown = True + super().showEvent(a0) + + def resizeEvent(self, a0: QtGui.QResizeEvent) -> None: + """Re-implementation, handle resize event""" + self.updateMask() + super().resizeEvent(a0) + + def paintEvent(self, a0: QtGui.QPaintEvent) -> None: + """Re-implemented method, paint widget""" + painter = QtGui.QPainter(self) + painter.setRenderHint(QtGui.QPainter.RenderHint.Antialiasing, True) + + _base_color = self.default_background_color + + center_point = QtCore.QPointF(self.rect().center()) + gradient = QtGui.QRadialGradient(center_point, self.rect().width() / 2.0) + + gradient.setColorAt(0, _base_color.darker(160)) + gradient.setColorAt(1.0, _base_color.darker(200)) + + painter.setBrush(gradient) + painter.setPen(QtCore.Qt.PenStyle.NoPen) + painter.drawRoundedRect(self.rect(), 50, 70) + + def _setupUI(self) -> None: + self.horizontal_layout = QtWidgets.QHBoxLayout(self) + self.horizontal_layout.setContentsMargins(5, 5, 5, 5) + + self.icon_label = QtWidgets.QLabel(self) + self.icon_label.setFixedSize(QtCore.QSize(60, 60)) + self.icon_label.setMaximumSize(QtCore.QSize(60, 60)) + self.icon_label.setScaledContents(True) + + self.text_label = QtWidgets.QLabel(self) + self.text_label.setStyleSheet("background: transparent; color:white") + self.text_label.setAlignment(QtCore.Qt.AlignmentFlag.AlignCenter) + self.text_label.setWordWrap(True) + font = self.text_label.font() + font.setPixelSize(18) + font.setFamily("sans-serif") + palette = self.text_label.palette() + palette.setColor( + QtGui.QPalette.ColorRole.WindowText, QtCore.Qt.GlobalColor.white + ) + self.text_label.setPalette(palette) + self.text_label.setFont(font) + + self.actionbtn = IconButton(self) + self.actionbtn.setMaximumSize(QtCore.QSize(60, 60)) + + self.horizontal_layout.addWidget(self.icon_label) + self.horizontal_layout.addWidget(self.text_label) + self.horizontal_layout.addWidget(self.actionbtn) diff --git a/pyproject.toml b/pyproject.toml index 9f7e1cfb..bd07f501 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ 'typing==3.7.4.3', 'websocket-client==1.9.0', 'qrcode==8.2', + 'pillow==12.1.1', ] requires-python = "==3.11.2" readme = "README.md" From c05e39a60b58587e85a187c8bc3dda76116a4183 Mon Sep 17 00:00:00 2001 From: HugoCLSC Date: Thu, 12 Mar 2026 10:31:41 +0000 Subject: [PATCH 65/70] Fix merge issues --- .../lib/panels/widgets/probeHelperPage.py | 6 - .../lib/ui/resources/icon_resources.qrc | 16 - .../lib/ui/resources/icon_resources_rc.py | 1840 +++-------------- 3 files changed, 283 insertions(+), 1579 deletions(-) diff --git a/BlocksScreen/lib/panels/widgets/probeHelperPage.py b/BlocksScreen/lib/panels/widgets/probeHelperPage.py index bc389941..fe2fa180 100644 --- a/BlocksScreen/lib/panels/widgets/probeHelperPage.py +++ b/BlocksScreen/lib/panels/widgets/probeHelperPage.py @@ -91,7 +91,6 @@ def __init__(self, parent: QtWidgets.QWidget) -> None: self.block_list = False self.target_temp = 0 self.current_temp = 0 -<<<<<<< HEAD self._eddy_calibration_state = False @QtCore.pyqtSlot(str, dict, name="on_print_stats_update") @@ -116,8 +115,6 @@ def on_print_stats_update(self, field: str, value: dict | float | str) -> None: self.toggle_conn_page.emit(True) self._eddy_calibration_state = False -======= ->>>>>>> origin/main def on_klippy_status(self, state: str): """Handle Klippy status event change""" @@ -465,11 +462,8 @@ def on_extruder_update( """Handle extruder update""" if not self.helper_initialize: return -<<<<<<< HEAD if self._eddy_calibration_state: return -======= ->>>>>>> origin/main if self.target_temp != 0: if self.current_temp == self.target_temp: if self.isVisible: diff --git a/BlocksScreen/lib/ui/resources/icon_resources.qrc b/BlocksScreen/lib/ui/resources/icon_resources.qrc index 3aa2f152..4acd3718 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources.qrc +++ b/BlocksScreen/lib/ui/resources/icon_resources.qrc @@ -1,6 +1,5 @@ -<<<<<<< HEAD media/btn_icons/network/static_ip.svg media/btn_icons/wifi_config.svg media/btn_icons/network/0bar_wifi.svg @@ -14,21 +13,6 @@ media/btn_icons/network/4bar_wifi.svg media/btn_icons/network/4bar_wifi_protected.svg media/btn_icons/network/ethernet_connected.svg -======= - media/btn_icons/0bar_wifi.svg - media/btn_icons/0bar_wifi_protected.svg - media/btn_icons/1bar_wifi.svg - media/btn_icons/1bar_wifi_protected.svg - media/btn_icons/2bar_wifi.svg - media/btn_icons/2bar_wifi_protected.svg - media/btn_icons/3bar_wifi.svg - media/btn_icons/3bar_wifi_protected.svg - media/btn_icons/4bar_wifi.svg - media/btn_icons/4bar_wifi_protected.svg - media/btn_icons/wifi_config.svg - media/btn_icons/wifi_locked.svg - media/btn_icons/wifi_unlocked.svg ->>>>>>> origin/main media/btn_icons/hotspot.svg media/btn_icons/retry_wifi.svg diff --git a/BlocksScreen/lib/ui/resources/icon_resources_rc.py b/BlocksScreen/lib/ui/resources/icon_resources_rc.py index 23bd66f5..073887dd 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/icon_resources_rc.py @@ -6,7 +6,7 @@ # # WARNING! All changes made in this file will be lost! -from PyQt6 import QtCore +from PyQt5 import QtCore qt_resource_data = b"\ \x00\x00\x08\x62\ @@ -19418,202 +19418,6 @@ \x22\x32\x34\x36\x2e\x32\x38\x22\x20\x77\x69\x64\x74\x68\x3d\x22\ \x35\x32\x35\x2e\x39\x31\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\ \x31\x30\x37\x2e\x34\x35\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -<<<<<<< HEAD -======= -\x00\x00\x05\x95\ -\x3c\ -\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ -\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ -\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\ -\x35\x34\x30\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ -\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ -\x31\x38\x2c\x37\x37\x43\x34\x30\x32\x2c\x37\x38\x2e\x37\x37\x2c\ -\x34\x39\x30\x2e\x31\x2c\x31\x31\x37\x2e\x30\x37\x2c\x35\x36\x33\ -\x2e\x39\x2c\x31\x39\x33\x2e\x30\x39\x63\x31\x34\x2e\x38\x2c\x31\ -\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\x2e\x39\x33\ -\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\x31\x2e\x31\x32\x2c\x31\ -\x33\x2e\x31\x37\x2d\x32\x38\x2e\x36\x32\x2c\x31\x33\x2d\x34\x31\ -\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\x30\x38\x2d\x31\x34\x2e\ -\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\x2e\x37\x31\x2d\x34\x36\ -\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\ -\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\x37\x35\x2d\x31\x32\x37\ -\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\x35\x34\x2d\x39\x2e\x31\ -\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\x2e\x33\x31\x2d\x31\x35\ -\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x2d\x34\x35\x2c\x31\x37\x2e\ -\x30\x35\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\x33\x2d\x31\ -\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x33\x2d\x38\x2e\x35\x37\ -\x2c\x39\x2e\x31\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\x32\x2e\x33\ -\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\x30\x39\x2d\ -\x37\x2e\x35\x37\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\x30\x39\x2d\ -\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\x33\x38\x30\x2c\x33\ -\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x37\x2e\x36\x32\x2d\x35\ -\x30\x2e\x33\x38\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\x38\x39\ -\x2e\x39\x33\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\x37\x2d\ -\x36\x30\x2e\x34\x36\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x39\x2e\ -\x38\x37\x2c\x32\x37\x37\x2e\x34\x33\x2c\x37\x38\x2e\x34\x38\x2c\ -\x32\x39\x30\x2e\x31\x38\x2c\x37\x37\x5a\x22\x2f\x3e\x3c\x70\x61\ -\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\ -\x20\x64\x3d\x22\x4d\x34\x36\x39\x2e\x38\x37\x2c\x33\x33\x32\x2e\ -\x32\x31\x63\x2d\x31\x31\x2c\x2e\x31\x38\x2d\x31\x37\x2e\x38\x33\ -\x2d\x33\x2e\x30\x37\x2d\x32\x33\x2e\x35\x32\x2d\x39\x2e\x31\x32\ -\x43\x34\x31\x34\x2c\x32\x38\x38\x2e\x36\x35\x2c\x33\x37\x35\x2e\ -\x32\x32\x2c\x32\x36\x37\x2e\x31\x36\x2c\x33\x33\x30\x2e\x31\x2c\ -\x32\x36\x30\x2e\x36\x38\x63\x2d\x36\x37\x2e\x33\x37\x2d\x39\x2e\ -\x36\x37\x2d\x31\x32\x36\x2e\x31\x37\x2c\x31\x30\x2e\x38\x33\x2d\ -\x31\x37\x35\x2e\x33\x39\x2c\x36\x31\x2e\x34\x31\x2d\x31\x36\x2e\ -\x33\x35\x2c\x31\x36\x2e\x38\x2d\x34\x30\x2e\x36\x37\x2c\x31\x32\ -\x2d\x34\x37\x2e\x39\x31\x2d\x31\x30\x2d\x33\x2e\x39\x2d\x31\x31\ -\x2e\x39\x2d\x31\x2e\x33\x38\x2d\x32\x32\x2e\x38\x2c\x36\x2e\x38\ -\x39\x2d\x33\x31\x2e\x35\x32\x2c\x34\x31\x2d\x34\x33\x2e\x32\x34\ -\x2c\x38\x39\x2e\x37\x35\x2d\x37\x30\x2e\x34\x38\x2c\x31\x34\x36\ -\x2e\x38\x34\x2d\x37\x39\x2e\x32\x38\x2c\x37\x35\x2e\x36\x2d\x31\ -\x31\x2e\x36\x36\x2c\x31\x34\x33\x2e\x36\x39\x2c\x38\x2e\x30\x35\ -\x2c\x32\x30\x33\x2e\x39\x31\x2c\x35\x38\x2e\x33\x36\x61\x32\x30\ -\x35\x2e\x37\x34\x2c\x32\x30\x35\x2e\x37\x34\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x32\x33\x2e\x32\x35\x2c\x32\x32\x2e\x36\x39\x63\x38\x2e\ -\x30\x38\x2c\x39\x2e\x33\x2c\x39\x2e\x35\x2c\x32\x30\x2e\x36\x32\ -\x2c\x34\x2e\x35\x39\x2c\x33\x32\x2e\x33\x34\x53\x34\x37\x38\x2e\ -\x36\x32\x2c\x33\x33\x31\x2e\x36\x36\x2c\x34\x36\x39\x2e\x38\x37\ -\x2c\x33\x33\x32\x2e\x32\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\ -\x3d\x22\x4d\x31\x38\x34\x2e\x35\x32\x2c\x33\x38\x37\x2e\x34\x63\ -\x30\x2d\x39\x2e\x32\x32\x2c\x33\x2e\x35\x37\x2d\x31\x36\x2e\x36\ -\x35\x2c\x39\x2e\x36\x31\x2d\x32\x32\x2e\x38\x31\x43\x32\x31\x38\ -\x2c\x33\x34\x30\x2e\x32\x34\x2c\x32\x34\x36\x2c\x33\x32\x34\x2e\ -\x37\x38\x2c\x32\x37\x38\x2e\x37\x37\x2c\x33\x32\x30\x2e\x31\x35\ -\x63\x34\x38\x2e\x36\x36\x2d\x36\x2e\x38\x36\x2c\x39\x30\x2e\x38\ -\x33\x2c\x38\x2e\x32\x35\x2c\x31\x32\x36\x2e\x36\x33\x2c\x34\x34\ -\x2c\x31\x30\x2e\x31\x38\x2c\x31\x30\x2e\x31\x35\x2c\x31\x32\x2e\ -\x38\x31\x2c\x32\x34\x2c\x37\x2e\x34\x35\x2c\x33\x36\x2e\x30\x35\ -\x2d\x38\x2e\x34\x34\x2c\x31\x39\x2d\x33\x31\x2c\x32\x33\x2e\x34\ -\x35\x2d\x34\x35\x2e\x33\x32\x2c\x38\x2e\x36\x36\x2d\x31\x33\x2e\ -\x34\x2d\x31\x33\x2e\x38\x33\x2d\x32\x38\x2e\x38\x2d\x32\x33\x2e\ -\x36\x33\x2d\x34\x37\x2e\x31\x31\x2d\x32\x37\x2e\x35\x34\x2d\x33\ -\x33\x2e\x32\x32\x2d\x37\x2e\x31\x2d\x36\x32\x2e\x33\x36\x2c\x31\ -\x2e\x36\x35\x2d\x38\x37\x2c\x32\x36\x2e\x37\x37\x2d\x31\x36\x2e\ -\x36\x36\x2c\x31\x37\x2d\x34\x32\x2e\x33\x2c\x31\x30\x2d\x34\x37\ -\x2e\x39\x33\x2d\x31\x33\x2e\x32\x37\x41\x36\x39\x2e\x32\x38\x2c\ -\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x38\x34\x2e\ -\x35\x32\x2c\x33\x38\x37\x2e\x34\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ -\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\ -\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x32\x33\x63\x2d\x32\x32\x2c\ -\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x36\x2d\x33\x39\ -\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\x2e\x33\ -\x38\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\ -\x2e\x38\x36\x2d\x34\x31\x2e\x38\x2c\x32\x32\x2e\x32\x35\x2c\x30\ -\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x38\x2c\x33\x39\x2e\ -\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ -\x32\x33\x2c\x33\x30\x30\x2c\x35\x32\x33\x5a\x22\x2f\x3e\x3c\x2f\ -\x73\x76\x67\x3e\ -\x00\x00\x06\x23\ -\x3c\ -\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ -\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ -\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x35\x65\x36\ -\x30\x36\x31\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ -\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ -\x32\x36\x2c\x39\x34\x2e\x37\x63\x31\x31\x30\x2e\x39\x34\x2c\x31\ -\x2e\x37\x37\x2c\x31\x39\x38\x2e\x33\x39\x2c\x33\x39\x2e\x37\x37\ -\x2c\x32\x37\x31\x2e\x36\x32\x2c\x31\x31\x35\x2e\x32\x31\x2c\x31\ -\x34\x2e\x36\x39\x2c\x31\x35\x2e\x31\x33\x2c\x31\x31\x2e\x37\x32\ -\x2c\x33\x33\x2e\x36\x37\x2c\x33\x2e\x32\x34\x2c\x34\x33\x2e\x37\ -\x2d\x31\x31\x2c\x31\x33\x2e\x30\x37\x2d\x32\x38\x2e\x34\x2c\x31\ -\x32\x2e\x38\x39\x2d\x34\x31\x2e\x31\x35\x2e\x38\x33\x2d\x31\x35\ -\x2d\x31\x34\x2e\x31\x36\x2d\x33\x30\x2d\x32\x38\x2e\x35\x2d\x34\ -\x36\x2e\x32\x35\x2d\x34\x30\x2e\x37\x39\x2d\x33\x38\x2e\x31\x31\ -\x2d\x32\x38\x2e\x37\x38\x2d\x38\x30\x2e\x37\x31\x2d\x34\x36\x2e\ -\x33\x39\x2d\x31\x32\x36\x2e\x37\x38\x2d\x35\x34\x2e\x32\x33\x2d\ -\x35\x33\x2e\x36\x2d\x39\x2e\x31\x32\x2d\x31\x30\x36\x2e\x30\x39\ -\x2d\x34\x2e\x32\x38\x2d\x31\x35\x37\x2e\x32\x38\x2c\x31\x35\x2e\ -\x31\x31\x43\x31\x34\x39\x2c\x31\x39\x31\x2e\x34\x35\x2c\x31\x31\ -\x30\x2c\x32\x31\x38\x2e\x31\x32\x2c\x37\x36\x2e\x34\x2c\x32\x35\ -\x33\x2e\x38\x35\x63\x2d\x38\x2e\x35\x2c\x39\x2d\x31\x38\x2e\x35\ -\x2c\x31\x32\x2e\x32\x33\x2d\x33\x30\x2c\x37\x2e\x39\x31\x2d\x31\ -\x39\x2e\x39\x33\x2d\x37\x2e\x35\x2d\x32\x35\x2d\x33\x33\x2e\x38\ -\x32\x2d\x39\x2e\x36\x36\x2d\x35\x30\x2e\x32\x31\x61\x33\x37\x37\ -\x2e\x33\x2c\x33\x37\x37\x2e\x33\x2c\x30\x2c\x30\x2c\x31\x2c\x35\ -\x37\x2e\x31\x38\x2d\x35\x30\x63\x34\x32\x2e\x37\x2d\x33\x30\x2e\ -\x33\x35\x2c\x38\x39\x2e\x32\x34\x2d\x35\x30\x2e\x37\x31\x2c\x31\ -\x33\x39\x2e\x36\x39\x2d\x36\x30\x43\x32\x35\x35\x2e\x34\x31\x2c\ -\x39\x37\x2e\x35\x35\x2c\x32\x37\x37\x2e\x36\x2c\x39\x36\x2e\x31\ -\x38\x2c\x32\x39\x30\x2e\x32\x36\x2c\x39\x34\x2e\x37\x5a\x22\x2f\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x36\x38\x2e\x35\x31\x2c\ -\x33\x34\x38\x63\x2d\x31\x30\x2e\x39\x31\x2e\x31\x38\x2d\x31\x37\ -\x2e\x36\x39\x2d\x33\x2e\x30\x35\x2d\x32\x33\x2e\x33\x34\x2d\x39\ -\x2e\x30\x36\x2d\x33\x32\x2e\x31\x32\x2d\x33\x34\x2e\x31\x38\x2d\ -\x37\x30\x2e\x35\x39\x2d\x35\x35\x2e\x35\x2d\x31\x31\x35\x2e\x33\ -\x36\x2d\x36\x31\x2e\x39\x33\x2d\x36\x36\x2e\x38\x35\x2d\x39\x2e\ -\x35\x39\x2d\x31\x32\x35\x2e\x32\x2c\x31\x30\x2e\x37\x35\x2d\x31\ -\x37\x34\x2c\x36\x30\x2e\x39\x34\x2d\x31\x36\x2e\x32\x32\x2c\x31\ -\x36\x2e\x36\x37\x2d\x34\x30\x2e\x33\x36\x2c\x31\x31\x2e\x39\x31\ -\x2d\x34\x37\x2e\x35\x34\x2d\x31\x30\x2d\x33\x2e\x38\x38\x2d\x31\ -\x31\x2e\x38\x2d\x31\x2e\x33\x37\x2d\x32\x32\x2e\x36\x32\x2c\x36\ -\x2e\x38\x34\x2d\x33\x31\x2e\x32\x37\x2c\x34\x30\x2e\x36\x38\x2d\ -\x34\x32\x2e\x39\x31\x2c\x38\x39\x2e\x30\x36\x2d\x36\x39\x2e\x39\ -\x34\x2c\x31\x34\x35\x2e\x37\x31\x2d\x37\x38\x2e\x36\x38\x2c\x37\ -\x35\x2d\x31\x31\x2e\x35\x37\x2c\x31\x34\x32\x2e\x35\x39\x2c\x38\ -\x2c\x32\x30\x32\x2e\x33\x35\x2c\x35\x37\x2e\x39\x32\x61\x32\x30\ -\x33\x2e\x34\x37\x2c\x32\x30\x33\x2e\x34\x37\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x32\x33\x2e\x30\x37\x2c\x32\x32\x2e\x35\x32\x63\x38\x2c\ -\x39\x2e\x32\x32\x2c\x39\x2e\x34\x33\x2c\x32\x30\x2e\x34\x36\x2c\ -\x34\x2e\x35\x36\x2c\x33\x32\x2e\x30\x38\x53\x34\x37\x37\x2e\x31\ -\x39\x2c\x33\x34\x37\x2e\x34\x31\x2c\x34\x36\x38\x2e\x35\x31\x2c\ -\x33\x34\x38\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x31\ -\x38\x35\x2e\x33\x36\x2c\x34\x30\x32\x2e\x37\x33\x63\x30\x2d\x39\ -\x2e\x31\x35\x2c\x33\x2e\x35\x35\x2d\x31\x36\x2e\x35\x32\x2c\x39\ -\x2e\x35\x34\x2d\x32\x32\x2e\x36\x34\x2c\x32\x33\x2e\x36\x36\x2d\ -\x32\x34\x2e\x31\x35\x2c\x35\x31\x2e\x34\x34\x2d\x33\x39\x2e\x35\ -\x2c\x38\x34\x2d\x34\x34\x2e\x30\x39\x2c\x34\x38\x2e\x32\x38\x2d\ -\x36\x2e\x38\x31\x2c\x39\x30\x2e\x31\x34\x2c\x38\x2e\x31\x39\x2c\ -\x31\x32\x35\x2e\x36\x37\x2c\x34\x33\x2e\x36\x32\x2c\x31\x30\x2e\ -\x30\x39\x2c\x31\x30\x2e\x30\x37\x2c\x31\x32\x2e\x37\x2c\x32\x33\ -\x2e\x38\x32\x2c\x37\x2e\x33\x39\x2c\x33\x35\x2e\x37\x37\x2d\x38\ -\x2e\x33\x39\x2c\x31\x38\x2e\x38\x35\x2d\x33\x30\x2e\x37\x37\x2c\ -\x32\x33\x2e\x32\x37\x2d\x34\x35\x2c\x38\x2e\x36\x2d\x31\x33\x2e\ -\x33\x2d\x31\x33\x2e\x37\x33\x2d\x32\x38\x2e\x35\x38\x2d\x32\x33\ -\x2e\x34\x35\x2d\x34\x36\x2e\x37\x35\x2d\x32\x37\x2e\x33\x33\x2d\ -\x33\x33\x2d\x37\x2e\x30\x35\x2d\x36\x31\x2e\x38\x38\x2c\x31\x2e\ -\x36\x34\x2d\x38\x36\x2e\x33\x2c\x32\x36\x2e\x35\x36\x2d\x31\x36\ -\x2e\x35\x34\x2c\x31\x36\x2e\x38\x38\x2d\x34\x32\x2c\x39\x2e\x39\ -\x33\x2d\x34\x37\x2e\x35\x37\x2d\x31\x33\x2e\x31\x37\x41\x37\x30\ -\x2e\x34\x31\x2c\x37\x30\x2e\x34\x31\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x31\x38\x35\x2e\x33\x36\x2c\x34\x30\x32\x2e\x37\x33\x5a\x22\x2f\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x33\x37\ -\x2e\x33\x63\x2d\x32\x31\x2e\x38\x33\x2c\x30\x2d\x33\x38\x2e\x38\ -\x34\x2d\x31\x38\x2e\x33\x31\x2d\x33\x38\x2e\x38\x31\x2d\x34\x31\ -\x2e\x37\x39\x2c\x30\x2d\x32\x33\x2e\x31\x39\x2c\x31\x37\x2e\x30\ -\x36\x2d\x34\x31\x2e\x35\x31\x2c\x33\x38\x2e\x35\x36\x2d\x34\x31\ -\x2e\x34\x37\x2c\x32\x32\x2e\x30\x39\x2c\x30\x2c\x33\x39\x2c\x31\ -\x38\x2c\x33\x39\x2c\x34\x31\x2e\x34\x39\x53\x33\x32\x31\x2e\x39\ -\x31\x2c\x35\x33\x37\x2e\x32\x39\x2c\x33\x30\x30\x2c\x35\x33\x37\ -\x2e\x33\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\ -\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x31\x35\ -\x36\x2e\x37\x36\x2c\x36\x31\x2c\x33\x30\x30\x2c\x32\x35\x39\x2e\ -\x38\x2c\x34\x34\x33\x2e\x32\x35\x2c\x36\x31\x68\x35\x37\x2e\x39\ -\x33\x4c\x33\x32\x39\x2c\x33\x30\x30\x2c\x35\x30\x31\x2e\x31\x39\ -\x2c\x35\x33\x39\x48\x34\x34\x33\x2e\x32\x35\x4c\x33\x30\x30\x2c\ -\x33\x34\x30\x2e\x31\x39\x2c\x31\x35\x36\x2e\x37\x36\x2c\x35\x33\ -\x39\x48\x39\x38\x2e\x38\x31\x4c\x32\x37\x31\x2c\x33\x30\x30\x2c\ -\x39\x38\x2e\x38\x33\x2c\x36\x31\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\ -\x67\x3e\ ->>>>>>> origin/main \x00\x00\x0b\x4d\ \x00\ \x00\x38\xfa\x78\x9c\xed\x9b\x49\x6f\x1d\xc7\x15\x85\xff\x0a\xc1\ @@ -20328,7 +20132,6 @@ \x39\x37\x2e\x35\x35\x2c\x32\x37\x37\x2e\x36\x2c\x39\x36\x2e\x31\ \x38\x2c\x32\x39\x30\x2e\x32\x36\x2c\x39\x34\x2e\x37\x5a\x22\x2f\ \x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -<<<<<<< HEAD \x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x36\x38\x2e\x35\x31\x2c\ \x33\x34\x38\x63\x2d\x31\x30\x2e\x39\x31\x2e\x31\x38\x2d\x31\x37\ \x2e\x36\x39\x2d\x33\x2e\x30\x35\x2d\x32\x33\x2e\x33\x34\x2d\x39\ @@ -20462,82 +20265,6 @@ \x2d\x34\x32\x2c\x38\x2e\x30\x32\x68\x2d\x33\x31\x2e\x30\x36\x76\ \x34\x36\x2e\x35\x39\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ \ -======= -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x35\x35\x35\x2e\x33\x32\x2c\ -\x33\x39\x39\x2e\x37\x63\x30\x2d\x38\x2e\x37\x32\x2d\x34\x2e\x33\ -\x34\x2d\x31\x33\x2e\x32\x31\x2d\x31\x32\x2e\x38\x31\x2d\x31\x33\ -\x2e\x34\x32\x2d\x34\x2e\x35\x39\x2d\x2e\x31\x31\x2d\x39\x2e\x31\ -\x39\x2c\x30\x2d\x31\x34\x2e\x31\x31\x2c\x30\x2c\x30\x2d\x35\x2e\ -\x31\x35\x2e\x31\x39\x2d\x39\x2e\x33\x39\x2c\x30\x2d\x31\x33\x2e\ -\x36\x31\x2d\x2e\x37\x35\x2d\x31\x34\x2e\x31\x37\x2e\x32\x34\x2d\ -\x32\x38\x2e\x37\x39\x2d\x32\x2e\x38\x33\x2d\x34\x32\x2e\x34\x2d\ -\x38\x2e\x32\x31\x2d\x33\x36\x2e\x32\x39\x2d\x34\x33\x2d\x36\x30\ -\x2e\x31\x39\x2d\x37\x38\x2e\x31\x34\x2d\x35\x35\x2e\x34\x38\x2d\ -\x33\x36\x2e\x37\x32\x2c\x34\x2e\x39\x32\x2d\x36\x33\x2e\x36\x33\ -\x2c\x33\x36\x2e\x37\x34\x2d\x36\x33\x2e\x35\x31\x2c\x37\x35\x2e\ -\x30\x36\x2c\x30\x2c\x31\x31\x2e\x38\x38\x2c\x30\x2c\x32\x33\x2e\ -\x37\x35\x2c\x30\x2c\x33\x36\x2e\x34\x31\x68\x2d\x37\x2e\x34\x33\ -\x63\x2d\x32\x2e\x34\x35\x2c\x30\x2d\x34\x2e\x39\x2d\x2e\x30\x35\ -\x2d\x37\x2e\x33\x34\x2c\x30\x2d\x38\x2e\x35\x33\x2e\x32\x38\x2d\ -\x31\x32\x2e\x31\x32\x2c\x34\x2e\x31\x36\x2d\x31\x32\x2e\x31\x32\ -\x2c\x31\x33\x2e\x31\x31\x71\x30\x2c\x35\x34\x2e\x38\x37\x2c\x30\ -\x2c\x31\x30\x39\x2e\x37\x35\x63\x30\x2c\x31\x30\x2e\x34\x34\x2c\ -\x33\x2e\x35\x39\x2c\x31\x34\x2e\x33\x31\x2c\x31\x33\x2e\x35\x32\ -\x2c\x31\x34\x2e\x33\x32\x71\x38\x35\x2e\x36\x33\x2c\x30\x2c\x31\ -\x37\x31\x2e\x32\x37\x2c\x30\x63\x39\x2e\x32\x34\x2c\x30\x2c\x31\ -\x33\x2e\x35\x33\x2d\x34\x2e\x34\x33\x2c\x31\x33\x2e\x35\x33\x2d\ -\x31\x34\x51\x35\x35\x35\x2e\x34\x2c\x34\x35\x34\x2e\x35\x38\x2c\ -\x35\x35\x35\x2e\x33\x32\x2c\x33\x39\x39\x2e\x37\x5a\x6d\x2d\x35\ -\x35\x2e\x39\x34\x2d\x31\x33\x2e\x38\x31\x48\x34\x31\x33\x2e\x32\ -\x63\x30\x2d\x31\x35\x2d\x31\x2e\x33\x33\x2d\x32\x39\x2e\x37\x39\ -\x2e\x33\x31\x2d\x34\x34\x2e\x31\x39\x2c\x32\x2e\x35\x32\x2d\x32\ -\x32\x2e\x30\x38\x2c\x32\x32\x2e\x36\x31\x2d\x33\x38\x2e\x33\x38\ -\x2c\x34\x33\x2e\x35\x37\x2d\x33\x37\x2e\x34\x39\x61\x34\x33\x2e\ -\x39\x34\x2c\x34\x33\x2e\x39\x34\x2c\x30\x2c\x30\x2c\x31\x2c\x34\ -\x32\x2e\x31\x36\x2c\x34\x31\x2e\x35\x33\x43\x35\x30\x30\x2c\x33\ -\x35\x39\x2c\x34\x39\x39\x2e\x33\x38\x2c\x33\x37\x32\x2e\x34\x31\ -\x2c\x34\x39\x39\x2e\x33\x38\x2c\x33\x38\x35\x2e\x38\x39\x5a\x22\ -\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x36\x38\x2e\x38\x34\ -\x2c\x33\x37\x37\x2e\x37\x38\x63\x31\x2e\x38\x38\x2d\x2e\x30\x36\ -\x2c\x33\x2e\x37\x31\x2c\x30\x2c\x35\x2e\x34\x37\x2c\x30\x68\x31\ -\x2e\x30\x39\x76\x2d\x33\x2e\x34\x39\x63\x30\x2d\x38\x2e\x32\x39\ -\x2c\x30\x2d\x31\x36\x2e\x33\x34\x2c\x30\x2d\x32\x34\x2e\x33\x39\ -\x61\x38\x39\x2e\x37\x37\x2c\x38\x39\x2e\x37\x37\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x2e\x35\x35\x2d\x31\x30\x63\x2d\x32\x38\x2e\x38\x39\ -\x2d\x31\x38\x2e\x33\x31\x2d\x36\x31\x2e\x32\x36\x2d\x32\x35\x2e\ -\x32\x33\x2d\x39\x37\x2e\x31\x37\x2d\x32\x30\x2e\x31\x37\x2d\x33\ -\x32\x2e\x38\x2c\x34\x2e\x36\x33\x2d\x36\x30\x2e\x38\x2c\x32\x30\ -\x2e\x30\x39\x2d\x38\x34\x2e\x36\x34\x2c\x34\x34\x2e\x34\x34\x2d\ -\x36\x2c\x36\x2e\x31\x36\x2d\x39\x2e\x36\x33\x2c\x31\x33\x2e\x35\ -\x39\x2d\x39\x2e\x36\x31\x2c\x32\x32\x2e\x38\x31\x61\x36\x39\x2e\ -\x32\x38\x2c\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x37\x2e\x33\x38\x63\x35\x2e\x36\x33\x2c\x32\x33\x2e\x32\x37\ -\x2c\x33\x31\x2e\x32\x37\x2c\x33\x30\x2e\x32\x38\x2c\x34\x37\x2e\ -\x39\x33\x2c\x31\x33\x2e\x32\x37\x2c\x32\x34\x2e\x36\x31\x2d\x32\ -\x35\x2e\x31\x32\x2c\x35\x33\x2e\x37\x35\x2d\x33\x33\x2e\x38\x37\ -\x2c\x38\x37\x2d\x32\x36\x2e\x37\x37\x41\x38\x33\x2e\x37\x35\x2c\ -\x38\x33\x2e\x37\x35\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x34\x39\x2e\ -\x31\x35\x2c\x33\x39\x33\x43\x33\x35\x31\x2e\x31\x37\x2c\x33\x38\ -\x33\x2e\x34\x37\x2c\x33\x35\x38\x2c\x33\x37\x38\x2e\x31\x33\x2c\ -\x33\x36\x38\x2e\x38\x34\x2c\x33\x37\x37\x2e\x37\x38\x5a\x22\x2f\ -\x3e\x3c\x72\x65\x63\x74\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x32\x22\x20\x78\x3d\x22\x34\x34\x37\x2e\x36\x39\x22\x20\ -\x79\x3d\x22\x33\x38\x36\x2e\x31\x33\x22\x20\x77\x69\x64\x74\x68\ -\x3d\x22\x31\x36\x2e\x39\x34\x22\x20\x68\x65\x69\x67\x68\x74\x3d\ -\x22\x31\x32\x37\x2e\x36\x31\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\ -\x72\x6d\x3d\x22\x74\x72\x61\x6e\x73\x6c\x61\x74\x65\x28\x34\x35\ -\x31\x2e\x37\x35\x20\x2d\x31\x39\x30\x2e\x37\x37\x29\x20\x72\x6f\ -\x74\x61\x74\x65\x28\x34\x35\x29\x22\x2f\x3e\x3c\x72\x65\x63\x74\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x78\ -\x3d\x22\x34\x34\x37\x2e\x36\x39\x22\x20\x79\x3d\x22\x33\x38\x36\ -\x2e\x31\x33\x22\x20\x77\x69\x64\x74\x68\x3d\x22\x31\x36\x2e\x39\ -\x34\x22\x20\x68\x65\x69\x67\x68\x74\x3d\x22\x31\x32\x37\x2e\x36\ -\x31\x22\x20\x74\x72\x61\x6e\x73\x66\x6f\x72\x6d\x3d\x22\x74\x72\ -\x61\x6e\x73\x6c\x61\x74\x65\x28\x31\x30\x39\x36\x2e\x38\x36\x20\ -\x34\x34\x35\x2e\x35\x33\x29\x20\x72\x6f\x74\x61\x74\x65\x28\x31\ -\x33\x35\x29\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ ->>>>>>> origin/main \x00\x00\x05\x80\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -20629,8 +20356,6 @@ \x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\x32\x33\x2c\x33\x30\ \x30\x2c\x35\x32\x33\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x05\x95\ -<<<<<<< HEAD -======= \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ \x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ @@ -20722,101 +20447,7 @@ \x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ \x32\x33\x2c\x33\x30\x30\x2c\x35\x32\x33\x5a\x22\x2f\x3e\x3c\x2f\ \x73\x76\x67\x3e\ -\x00\x00\x09\xc5\ ->>>>>>> origin/main -\x3c\ -\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ -\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ -\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\ -\x35\x34\x30\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ -\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ -\x31\x38\x2c\x37\x37\x43\x34\x30\x32\x2c\x37\x38\x2e\x37\x37\x2c\ -\x34\x39\x30\x2e\x31\x2c\x31\x31\x37\x2e\x30\x37\x2c\x35\x36\x33\ -\x2e\x39\x2c\x31\x39\x33\x2e\x30\x39\x63\x31\x34\x2e\x38\x2c\x31\ -\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\x2e\x39\x33\ -\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\x31\x2e\x31\x32\x2c\x31\ -\x33\x2e\x31\x37\x2d\x32\x38\x2e\x36\x32\x2c\x31\x33\x2d\x34\x31\ -\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\x30\x38\x2d\x31\x34\x2e\ -\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\x2e\x37\x31\x2d\x34\x36\ -\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\ -\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\x37\x35\x2d\x31\x32\x37\ -\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\x35\x34\x2d\x39\x2e\x31\ -\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\x2e\x33\x31\x2d\x31\x35\ -\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x2d\x34\x35\x2c\x31\x37\x2e\ -\x30\x35\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\x33\x2d\x31\ -\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x33\x2d\x38\x2e\x35\x37\ -\x2c\x39\x2e\x31\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\x32\x2e\x33\ -\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\x30\x39\x2d\ -\x37\x2e\x35\x37\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\x30\x39\x2d\ -\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\x33\x38\x30\x2c\x33\ -\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x35\x37\x2e\x36\x32\x2d\x35\ -\x30\x2e\x33\x38\x63\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\x38\x39\ -\x2e\x39\x33\x2d\x35\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\x37\x2d\ -\x36\x30\x2e\x34\x36\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x39\x2e\ -\x38\x37\x2c\x32\x37\x37\x2e\x34\x33\x2c\x37\x38\x2e\x34\x38\x2c\ -\x32\x39\x30\x2e\x31\x38\x2c\x37\x37\x5a\x22\x2f\x3e\x3c\x70\x61\ -\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\ -\x20\x64\x3d\x22\x4d\x34\x36\x39\x2e\x38\x37\x2c\x33\x33\x32\x2e\ -\x32\x31\x63\x2d\x31\x31\x2c\x2e\x31\x38\x2d\x31\x37\x2e\x38\x33\ -\x2d\x33\x2e\x30\x37\x2d\x32\x33\x2e\x35\x32\x2d\x39\x2e\x31\x32\ -\x43\x34\x31\x34\x2c\x32\x38\x38\x2e\x36\x35\x2c\x33\x37\x35\x2e\ -\x32\x32\x2c\x32\x36\x37\x2e\x31\x36\x2c\x33\x33\x30\x2e\x31\x2c\ -\x32\x36\x30\x2e\x36\x38\x63\x2d\x36\x37\x2e\x33\x37\x2d\x39\x2e\ -\x36\x37\x2d\x31\x32\x36\x2e\x31\x37\x2c\x31\x30\x2e\x38\x33\x2d\ -\x31\x37\x35\x2e\x33\x39\x2c\x36\x31\x2e\x34\x31\x2d\x31\x36\x2e\ -\x33\x35\x2c\x31\x36\x2e\x38\x2d\x34\x30\x2e\x36\x37\x2c\x31\x32\ -\x2d\x34\x37\x2e\x39\x31\x2d\x31\x30\x2d\x33\x2e\x39\x2d\x31\x31\ -\x2e\x39\x2d\x31\x2e\x33\x38\x2d\x32\x32\x2e\x38\x2c\x36\x2e\x38\ -\x39\x2d\x33\x31\x2e\x35\x32\x2c\x34\x31\x2d\x34\x33\x2e\x32\x34\ -\x2c\x38\x39\x2e\x37\x35\x2d\x37\x30\x2e\x34\x38\x2c\x31\x34\x36\ -\x2e\x38\x34\x2d\x37\x39\x2e\x32\x38\x2c\x37\x35\x2e\x36\x2d\x31\ -\x31\x2e\x36\x36\x2c\x31\x34\x33\x2e\x36\x39\x2c\x38\x2e\x30\x35\ -\x2c\x32\x30\x33\x2e\x39\x31\x2c\x35\x38\x2e\x33\x36\x61\x32\x30\ -\x35\x2e\x37\x34\x2c\x32\x30\x35\x2e\x37\x34\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x32\x33\x2e\x32\x35\x2c\x32\x32\x2e\x36\x39\x63\x38\x2e\ -\x30\x38\x2c\x39\x2e\x33\x2c\x39\x2e\x35\x2c\x32\x30\x2e\x36\x32\ -\x2c\x34\x2e\x35\x39\x2c\x33\x32\x2e\x33\x34\x53\x34\x37\x38\x2e\ -\x36\x32\x2c\x33\x33\x31\x2e\x36\x36\x2c\x34\x36\x39\x2e\x38\x37\ -\x2c\x33\x33\x32\x2e\x32\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\ -\x3d\x22\x4d\x31\x38\x34\x2e\x35\x32\x2c\x33\x38\x37\x2e\x34\x63\ -\x30\x2d\x39\x2e\x32\x32\x2c\x33\x2e\x35\x37\x2d\x31\x36\x2e\x36\ -\x35\x2c\x39\x2e\x36\x31\x2d\x32\x32\x2e\x38\x31\x43\x32\x31\x38\ -\x2c\x33\x34\x30\x2e\x32\x34\x2c\x32\x34\x36\x2c\x33\x32\x34\x2e\ -\x37\x38\x2c\x32\x37\x38\x2e\x37\x37\x2c\x33\x32\x30\x2e\x31\x35\ -\x63\x34\x38\x2e\x36\x36\x2d\x36\x2e\x38\x36\x2c\x39\x30\x2e\x38\ -\x33\x2c\x38\x2e\x32\x35\x2c\x31\x32\x36\x2e\x36\x33\x2c\x34\x34\ -\x2c\x31\x30\x2e\x31\x38\x2c\x31\x30\x2e\x31\x35\x2c\x31\x32\x2e\ -\x38\x31\x2c\x32\x34\x2c\x37\x2e\x34\x35\x2c\x33\x36\x2e\x30\x35\ -\x2d\x38\x2e\x34\x34\x2c\x31\x39\x2d\x33\x31\x2c\x32\x33\x2e\x34\ -\x35\x2d\x34\x35\x2e\x33\x32\x2c\x38\x2e\x36\x36\x2d\x31\x33\x2e\ -\x34\x2d\x31\x33\x2e\x38\x33\x2d\x32\x38\x2e\x38\x2d\x32\x33\x2e\ -\x36\x33\x2d\x34\x37\x2e\x31\x31\x2d\x32\x37\x2e\x35\x34\x2d\x33\ -\x33\x2e\x32\x32\x2d\x37\x2e\x31\x2d\x36\x32\x2e\x33\x36\x2c\x31\ -\x2e\x36\x35\x2d\x38\x37\x2c\x32\x36\x2e\x37\x37\x2d\x31\x36\x2e\ -\x36\x36\x2c\x31\x37\x2d\x34\x32\x2e\x33\x2c\x31\x30\x2d\x34\x37\ -\x2e\x39\x33\x2d\x31\x33\x2e\x32\x37\x41\x36\x39\x2e\x32\x38\x2c\ -\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x31\x38\x34\x2e\ -\x35\x32\x2c\x33\x38\x37\x2e\x34\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ -\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\ -\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x32\x33\x63\x2d\x32\x32\x2c\ -\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x36\x2d\x33\x39\ -\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\x2e\x33\ -\x38\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\ -\x2e\x38\x36\x2d\x34\x31\x2e\x38\x2c\x32\x32\x2e\x32\x35\x2c\x30\ -\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x38\x2c\x33\x39\x2e\ -\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ -\x32\x33\x2c\x33\x30\x30\x2c\x35\x32\x33\x5a\x22\x2f\x3e\x3c\x2f\ -\x73\x76\x67\x3e\ -<<<<<<< HEAD -\x00\x00\x04\xfe\ +\x00\x00\x04\xfe\ \x3c\ \x3f\x78\x6d\x6c\x20\x76\x65\x72\x73\x69\x6f\x6e\x3d\x22\x31\x2e\ \x30\x22\x20\x65\x6e\x63\x6f\x64\x69\x6e\x67\x3d\x22\x55\x54\x46\ @@ -20898,8 +20529,6 @@ \x39\x2c\x31\x31\x34\x2e\x37\x31\x68\x2d\x31\x36\x2e\x31\x39\x76\ \x2d\x34\x30\x2e\x35\x32\x68\x31\x36\x2e\x31\x39\x76\x34\x30\x2e\ \x35\x32\x5a\x22\x2f\x3e\x0a\x3c\x2f\x73\x76\x67\x3e\ -======= ->>>>>>> origin/main \x00\x00\x05\x95\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -21036,658 +20665,131 @@ \x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\ \x20\x64\x3d\x22\x4d\x33\x30\x30\x2c\x35\x31\x37\x2e\x33\x37\x63\ \x2d\x32\x32\x2c\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\ -\x35\x2d\x33\x39\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\ -\x32\x33\x2e\x33\x37\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\ -\x33\x2c\x33\x38\x2e\x38\x36\x2d\x34\x31\x2e\x37\x39\x2c\x32\x32\ -\x2e\x32\x35\x2c\x30\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\ -\x37\x2c\x33\x39\x2e\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\ -\x32\x2e\x31\x2c\x35\x31\x37\x2e\x33\x37\x2c\x33\x30\x30\x2c\x35\ -\x31\x37\x2e\x33\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\ -\x4d\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x68\x31\ -\x34\x2e\x33\x35\x61\x31\x30\x2e\x36\x34\x2c\x31\x30\x2e\x36\x34\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x32\x2c\x31\x63\x32\x37\ -\x2e\x34\x37\x2c\x34\x2c\x34\x39\x2e\x36\x2c\x32\x36\x2e\x36\x37\ -\x2c\x35\x31\x2e\x36\x38\x2c\x35\x34\x2e\x32\x36\x2c\x31\x2e\x31\ -\x37\x2c\x31\x35\x2e\x35\x33\x2e\x35\x38\x2c\x33\x31\x2e\x32\x2e\ -\x37\x38\x2c\x34\x36\x2e\x38\x2c\x30\x2c\x32\x2e\x31\x32\x2c\x30\ -\x2c\x34\x2e\x32\x35\x2c\x30\x2c\x36\x2e\x38\x36\x68\x31\x31\x2e\ -\x37\x35\x63\x31\x33\x2e\x39\x34\x2c\x30\x2c\x31\x39\x2e\x33\x36\ -\x2c\x35\x2e\x33\x39\x2c\x31\x39\x2e\x33\x36\x2c\x31\x39\x2e\x32\ -\x32\x2c\x30\x2c\x33\x33\x2e\x32\x35\x2d\x2e\x31\x38\x2c\x36\x36\ -\x2e\x35\x2e\x31\x32\x2c\x39\x39\x2e\x37\x35\x2e\x30\x39\x2c\x39\ -\x2e\x36\x38\x2d\x32\x2e\x39\x33\x2c\x31\x36\x2e\x36\x37\x2d\x31\ -\x32\x2e\x31\x36\x2c\x32\x30\x2e\x33\x39\x48\x33\x37\x35\x2e\x31\ -\x32\x63\x2d\x37\x2e\x32\x34\x2d\x33\x2e\x31\x37\x2d\x31\x32\x2d\ -\x38\x2e\x31\x31\x2d\x31\x32\x2d\x31\x36\x2e\x35\x33\x2c\x30\x2d\ -\x33\x35\x2e\x34\x36\x2d\x2e\x30\x38\x2d\x37\x30\x2e\x39\x31\x2c\ -\x30\x2d\x31\x30\x36\x2e\x33\x37\x2c\x30\x2d\x31\x30\x2e\x32\x39\ -\x2c\x36\x2e\x32\x34\x2d\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\x35\ -\x36\x2d\x31\x36\x2e\x34\x34\x2c\x34\x2e\x36\x39\x2d\x2e\x30\x39\ -\x2c\x39\x2e\x33\x39\x2c\x30\x2c\x31\x34\x2e\x35\x31\x2c\x30\x2c\ -\x30\x2d\x31\x35\x2e\x37\x33\x2d\x2e\x30\x38\x2d\x33\x30\x2e\x35\ -\x39\x2c\x30\x2d\x34\x35\x2e\x34\x35\x2e\x31\x39\x2d\x32\x38\x2e\ -\x37\x39\x2c\x31\x37\x2e\x32\x38\x2d\x35\x32\x2e\x33\x36\x2c\x34\ -\x33\x2e\x38\x39\x2d\x36\x30\x2e\x35\x38\x43\x34\x34\x31\x2e\x37\ -\x31\x2c\x32\x38\x32\x2e\x31\x34\x2c\x34\x34\x35\x2e\x34\x31\x2c\ -\x32\x38\x31\x2e\x33\x33\x2c\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\ -\x30\x2e\x33\x37\x5a\x6d\x33\x38\x2e\x33\x31\x2c\x31\x30\x38\x2e\ -\x34\x39\x63\x30\x2d\x31\x36\x2e\x35\x34\x2e\x39\x34\x2d\x33\x32\ -\x2e\x37\x33\x2d\x2e\x32\x35\x2d\x34\x38\x2e\x37\x37\x2d\x31\x2e\ -\x33\x31\x2d\x31\x37\x2e\x38\x32\x2d\x31\x35\x2e\x39\x33\x2d\x32\ -\x39\x2e\x37\x35\x2d\x33\x32\x2e\x37\x37\x2d\x32\x38\x2e\x37\x37\ -\x2d\x31\x36\x2e\x36\x35\x2c\x31\x2d\x32\x39\x2e\x32\x31\x2c\x31\ -\x34\x2e\x37\x33\x2d\x32\x39\x2e\x34\x32\x2c\x33\x32\x2e\x35\x32\ -\x2d\x2e\x31\x36\x2c\x31\x33\x2e\x37\x37\x2c\x30\x2c\x32\x37\x2e\ -\x35\x34\x2c\x30\x2c\x34\x31\x2e\x33\x31\x61\x33\x33\x2e\x31\x39\ -\x2c\x33\x33\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x32\ -\x2c\x33\x2e\x37\x31\x5a\x6d\x2d\x33\x31\x2e\x33\x33\x2c\x34\x36\ -\x2e\x37\x31\x61\x31\x34\x2e\x39\x31\x2c\x31\x34\x2e\x39\x31\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x31\x33\x2e\x36\x38\x2c\x38\x2e\x36\x36\ -\x63\x2d\x32\x2e\x38\x32\x2c\x35\x2e\x35\x37\x2d\x32\x2e\x38\x2c\ -\x31\x31\x2e\x35\x39\x2c\x31\x2e\x36\x2c\x31\x35\x2e\x38\x35\x2c\ -\x34\x2e\x31\x36\x2c\x34\x2c\x34\x2e\x34\x34\x2c\x38\x2e\x33\x35\ -\x2c\x34\x2e\x32\x32\x2c\x31\x33\x2e\x33\x32\x61\x36\x38\x2e\x33\ -\x38\x2c\x36\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\ -\x37\x2e\x37\x31\x63\x2e\x33\x37\x2c\x35\x2e\x33\x33\x2c\x33\x2e\ -\x36\x2c\x38\x2e\x38\x33\x2c\x38\x2c\x38\x2e\x38\x37\x73\x37\x2e\ -\x37\x35\x2d\x33\x2e\x34\x36\x2c\x38\x2e\x30\x37\x2d\x38\x2e\x37\ -\x36\x61\x31\x31\x31\x2e\x34\x34\x2c\x31\x31\x31\x2e\x34\x34\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x31\x2e\x35\x36\x2c\x39\x2e\ -\x36\x38\x2c\x39\x2e\x36\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x2e\ -\x32\x2d\x38\x2e\x31\x33\x63\x34\x2e\x37\x31\x2d\x34\x2e\x36\x34\ -\x2c\x35\x2e\x36\x31\x2d\x31\x30\x2e\x35\x33\x2c\x33\x2d\x31\x36\ -\x2e\x37\x32\x41\x31\x35\x2e\x34\x38\x2c\x31\x35\x2e\x34\x38\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x34\x35\x36\x2e\x30\x35\x2c\x34\x33\x35\ -\x2e\x35\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x33\ -\x37\x39\x2e\x35\x38\x2c\x33\x38\x34\x2e\x33\x31\x63\x33\x2e\x31\ -\x38\x2d\x2e\x30\x36\x2c\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\ -\x2e\x36\x31\x2c\x30\x2c\x30\x2d\x33\x2e\x32\x34\x2c\x30\x2d\x36\ -\x2e\x34\x33\x2c\x30\x2d\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\ -\x35\x33\x2c\x30\x2d\x32\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\ -\x35\x38\x2d\x33\x32\x2e\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\ -\x38\x2e\x39\x33\x2d\x33\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\ -\x34\x2d\x32\x39\x2e\x35\x37\x43\x32\x34\x36\x2c\x33\x31\x39\x2e\ -\x31\x34\x2c\x32\x31\x38\x2c\x33\x33\x34\x2e\x36\x31\x2c\x31\x39\ -\x34\x2e\x31\x33\x2c\x33\x35\x39\x63\x2d\x36\x2c\x36\x2e\x31\x37\ -\x2d\x39\x2e\x36\x33\x2c\x31\x33\x2e\x35\x39\x2d\x39\x2e\x36\x31\ -\x2c\x32\x32\x2e\x38\x31\x61\x36\x38\x2e\x39\x33\x2c\x36\x38\x2e\ -\x39\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x33\x38\x63\ -\x35\x2e\x36\x33\x2c\x32\x33\x2e\x32\x38\x2c\x33\x31\x2e\x32\x37\ -\x2c\x33\x30\x2e\x32\x38\x2c\x34\x37\x2e\x39\x33\x2c\x31\x33\x2e\ -\x32\x37\x2c\x32\x34\x2e\x36\x31\x2d\x32\x35\x2e\x31\x31\x2c\x35\ -\x33\x2e\x37\x35\x2d\x33\x33\x2e\x38\x36\x2c\x38\x37\x2d\x32\x36\ -\x2e\x37\x37\x2c\x31\x35\x2c\x33\x2e\x32\x2c\x32\x38\x2c\x31\x30\ -\x2e\x33\x35\x2c\x33\x39\x2e\x36\x2c\x32\x30\x2e\x34\x32\x43\x33\ -\x36\x33\x2e\x31\x37\x2c\x33\x38\x38\x2e\x38\x32\x2c\x33\x37\x30\ -\x2e\x30\x37\x2c\x33\x38\x34\x2e\x34\x39\x2c\x33\x37\x39\x2e\x35\ -\x38\x2c\x33\x38\x34\x2e\x33\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ -\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\ -\x64\x3d\x22\x4d\x34\x35\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\ -\x31\x61\x32\x36\x2e\x31\x32\x2c\x32\x36\x2e\x31\x32\x2c\x30\x2c\ -\x30\x2c\x30\x2d\x37\x2e\x37\x39\x2c\x31\x2e\x36\x36\x63\x35\x2e\ -\x35\x39\x2c\x35\x2e\x37\x32\x2c\x31\x32\x2e\x33\x34\x2c\x38\x2e\ -\x37\x38\x2c\x32\x33\x2c\x38\x2e\x36\x31\x61\x32\x34\x2c\x32\x34\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x36\x2e\x32\x35\x2d\x31\x2e\x32\x37\ -\x41\x32\x35\x2e\x36\x38\x2c\x32\x35\x2e\x36\x38\x2c\x30\x2c\x30\ -\x2c\x30\x2c\x34\x35\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\x31\ -\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x33\x36\x2e\ -\x36\x32\x2c\x32\x37\x38\x2e\x34\x38\x63\x32\x2e\x36\x31\x2d\x2e\ -\x38\x2c\x35\x2e\x32\x33\x2d\x31\x2e\x34\x36\x2c\x37\x2e\x37\x37\ -\x2d\x32\x2e\x30\x39\x2c\x31\x2e\x31\x34\x2d\x2e\x32\x38\x2c\x32\ -\x2e\x32\x38\x2d\x2e\x35\x36\x2c\x33\x2e\x34\x32\x2d\x2e\x38\x36\ -\x6c\x2e\x36\x32\x2d\x2e\x31\x36\x68\x31\x36\x6c\x2e\x39\x33\x2e\ -\x33\x39\x63\x2e\x33\x36\x2e\x31\x35\x2e\x37\x32\x2e\x33\x33\x2c\ -\x31\x2e\x30\x37\x2e\x35\x6c\x2e\x33\x37\x2e\x31\x39\x61\x36\x36\ -\x2e\x31\x38\x2c\x36\x36\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x32\x36\x2e\x38\x33\x2c\x31\x30\x2e\x33\x38\x2c\x33\x30\x2e\x33\ -\x35\x2c\x33\x30\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\ -\x39\x34\x2d\x31\x30\x2e\x31\x35\x41\x32\x30\x35\x2c\x32\x30\x35\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x36\x34\x2e\x34\x34\x2c\x32\x35\ -\x34\x63\x2d\x36\x30\x2e\x32\x32\x2d\x35\x30\x2e\x33\x31\x2d\x31\ -\x32\x38\x2e\x33\x31\x2d\x37\x30\x2d\x32\x30\x33\x2e\x39\x31\x2d\ -\x35\x38\x2e\x33\x36\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\x2d\ -\x31\x30\x35\x2e\x38\x34\x2c\x33\x36\x2d\x31\x34\x36\x2e\x38\x34\ -\x2c\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\x37\x2c\x38\x2e\x37\x32\ -\x2d\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\x36\x32\x2d\x36\x2e\x38\ -\x39\x2c\x33\x31\x2e\x35\x32\x2c\x37\x2e\x32\x34\x2c\x32\x32\x2c\ -\x33\x31\x2e\x35\x36\x2c\x32\x36\x2e\x38\x33\x2c\x34\x37\x2e\x39\ -\x31\x2c\x31\x30\x2c\x34\x39\x2e\x32\x32\x2d\x35\x30\x2e\x35\x37\ -\x2c\x31\x30\x38\x2d\x37\x31\x2e\x30\x37\x2c\x31\x37\x35\x2e\x33\ -\x39\x2d\x36\x31\x2e\x34\x41\x31\x38\x37\x2e\x39\x32\x2c\x31\x38\ -\x37\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x31\x35\x2c\x32\ -\x38\x39\x2e\x36\x38\x2c\x36\x37\x2e\x34\x39\x2c\x36\x37\x2e\x34\ -\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x33\x36\x2e\x36\x32\x2c\x32\ -\x37\x38\x2e\x34\x38\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -<<<<<<< HEAD -\x00\x00\x0a\x76\ -\x3c\ -\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ -\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ -\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\x35\x34\x30\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\ -\x32\x64\x33\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x66\x69\x6c\x6c\ -\x3a\x23\x39\x32\x39\x34\x39\x37\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\ -\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\ -\x4d\x34\x34\x37\x2e\x31\x37\x2c\x33\x33\x34\x2e\x38\x35\x61\x32\ -\x36\x2e\x31\x32\x2c\x32\x36\x2e\x31\x32\x2c\x30\x2c\x30\x2c\x30\ -\x2d\x37\x2e\x37\x39\x2c\x31\x2e\x36\x36\x63\x35\x2e\x35\x39\x2c\ -\x35\x2e\x37\x32\x2c\x31\x32\x2e\x33\x34\x2c\x38\x2e\x37\x38\x2c\ -\x32\x33\x2c\x38\x2e\x36\x31\x61\x32\x33\x2e\x35\x35\x2c\x32\x33\ -\x2e\x35\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x36\x2e\x32\x35\x2d\x31\ -\x2e\x32\x37\x41\x32\x35\x2e\x37\x31\x2c\x32\x35\x2e\x37\x31\x2c\ -\x30\x2c\x30\x2c\x30\x2c\x34\x34\x37\x2e\x31\x37\x2c\x33\x33\x34\ -\x2e\x38\x35\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x32\ -\x39\x30\x2e\x31\x38\x2c\x37\x31\x2e\x33\x35\x43\x34\x30\x32\x2c\ -\x37\x33\x2e\x31\x34\x2c\x34\x39\x30\x2e\x31\x2c\x31\x31\x31\x2e\ -\x34\x34\x2c\x35\x36\x33\x2e\x39\x2c\x31\x38\x37\x2e\x34\x35\x63\ -\x31\x34\x2e\x38\x2c\x31\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\ -\x2c\x33\x33\x2e\x39\x33\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2e\x30\ -\x35\x2d\x31\x31\x2e\x31\x32\x2c\x31\x33\x2e\x31\x36\x2d\x32\x38\ -\x2e\x36\x32\x2c\x31\x33\x2d\x34\x31\x2e\x34\x36\x2e\x38\x33\x2d\ -\x31\x35\x2e\x30\x38\x2d\x31\x34\x2e\x32\x37\x2d\x33\x30\x2e\x32\ -\x2d\x32\x38\x2e\x37\x32\x2d\x34\x36\x2e\x36\x31\x2d\x34\x31\x2e\ -\x31\x31\x2d\x33\x38\x2e\x34\x2d\x32\x39\x2d\x38\x31\x2e\x33\x33\ -\x2d\x34\x36\x2e\x37\x34\x2d\x31\x32\x37\x2e\x37\x35\x2d\x35\x34\ -\x2e\x36\x34\x2d\x35\x34\x2d\x39\x2e\x32\x2d\x31\x30\x36\x2e\x39\ -\x32\x2d\x34\x2e\x33\x32\x2d\x31\x35\x38\x2e\x35\x2c\x31\x35\x2e\ -\x32\x32\x2d\x34\x35\x2c\x31\x37\x2d\x38\x34\x2e\x32\x39\x2c\x34\ -\x33\x2e\x39\x33\x2d\x31\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\ -\x34\x2d\x38\x2e\x35\x37\x2c\x39\x2e\x31\x2d\x31\x38\x2e\x36\x35\ -\x2c\x31\x32\x2e\x33\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\ -\x30\x2e\x30\x39\x2d\x37\x2e\x35\x36\x2d\x32\x35\x2e\x32\x2d\x33\ -\x34\x2e\x30\x38\x2d\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\ -\x33\x38\x30\x2e\x35\x31\x2c\x33\x38\x30\x2e\x35\x31\x2c\x30\x2c\ -\x30\x2c\x31\x2c\x35\x37\x2e\x36\x32\x2d\x35\x30\x2e\x33\x38\x63\ -\x34\x33\x2d\x33\x30\x2e\x35\x38\x2c\x38\x39\x2e\x39\x33\x2d\x35\ -\x31\x2e\x31\x2c\x31\x34\x30\x2e\x37\x37\x2d\x36\x30\x2e\x34\x35\ -\x43\x32\x35\x35\x2e\x30\x37\x2c\x37\x34\x2e\x32\x33\x2c\x32\x37\ -\x37\x2e\x34\x33\x2c\x37\x32\x2e\x38\x35\x2c\x32\x39\x30\x2e\x31\ -\x38\x2c\x37\x31\x2e\x33\x35\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\ -\x3d\x22\x4d\x33\x30\x30\x2c\x35\x31\x37\x2e\x33\x37\x63\x2d\x32\ -\x32\x2c\x30\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x35\x2d\ -\x33\x39\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\ -\x2e\x33\x37\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\ -\x33\x38\x2e\x38\x36\x2d\x34\x31\x2e\x37\x39\x2c\x32\x32\x2e\x32\ -\x35\x2c\x30\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x37\x2c\ -\x33\x39\x2e\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\ -\x31\x2c\x35\x31\x37\x2e\x33\x37\x2c\x33\x30\x30\x2c\x35\x31\x37\ -\x2e\x33\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\x4d\x34\ -\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x68\x31\x34\x2e\ -\x33\x35\x61\x31\x30\x2e\x36\x34\x2c\x31\x30\x2e\x36\x34\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x32\x2c\x31\x63\x32\x37\x2e\x34\ -\x37\x2c\x34\x2c\x34\x39\x2e\x36\x2c\x32\x36\x2e\x36\x37\x2c\x35\ -\x31\x2e\x36\x38\x2c\x35\x34\x2e\x32\x36\x2c\x31\x2e\x31\x37\x2c\ -\x31\x35\x2e\x35\x33\x2e\x35\x38\x2c\x33\x31\x2e\x32\x2e\x37\x38\ -\x2c\x34\x36\x2e\x38\x2c\x30\x2c\x32\x2e\x31\x32\x2c\x30\x2c\x34\ -\x2e\x32\x35\x2c\x30\x2c\x36\x2e\x38\x36\x68\x31\x31\x2e\x37\x35\ -\x63\x31\x33\x2e\x39\x34\x2c\x30\x2c\x31\x39\x2e\x33\x36\x2c\x35\ -\x2e\x33\x39\x2c\x31\x39\x2e\x33\x36\x2c\x31\x39\x2e\x32\x32\x2c\ -\x30\x2c\x33\x33\x2e\x32\x35\x2d\x2e\x31\x38\x2c\x36\x36\x2e\x35\ -\x2e\x31\x32\x2c\x39\x39\x2e\x37\x35\x2e\x30\x39\x2c\x39\x2e\x36\ -\x38\x2d\x32\x2e\x39\x33\x2c\x31\x36\x2e\x36\x37\x2d\x31\x32\x2e\ -\x31\x36\x2c\x32\x30\x2e\x33\x39\x48\x33\x37\x35\x2e\x31\x32\x63\ -\x2d\x37\x2e\x32\x34\x2d\x33\x2e\x31\x37\x2d\x31\x32\x2d\x38\x2e\ -\x31\x31\x2d\x31\x32\x2d\x31\x36\x2e\x35\x33\x2c\x30\x2d\x33\x35\ -\x2e\x34\x36\x2d\x2e\x30\x38\x2d\x37\x30\x2e\x39\x31\x2c\x30\x2d\ -\x31\x30\x36\x2e\x33\x37\x2c\x30\x2d\x31\x30\x2e\x32\x39\x2c\x36\ -\x2e\x32\x34\x2d\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\x35\x36\x2d\ -\x31\x36\x2e\x34\x34\x2c\x34\x2e\x36\x39\x2d\x2e\x30\x39\x2c\x39\ -\x2e\x33\x39\x2c\x30\x2c\x31\x34\x2e\x35\x31\x2c\x30\x2c\x30\x2d\ -\x31\x35\x2e\x37\x33\x2d\x2e\x30\x38\x2d\x33\x30\x2e\x35\x39\x2c\ -\x30\x2d\x34\x35\x2e\x34\x35\x2e\x31\x39\x2d\x32\x38\x2e\x37\x39\ -\x2c\x31\x37\x2e\x32\x38\x2d\x35\x32\x2e\x33\x36\x2c\x34\x33\x2e\ -\x38\x39\x2d\x36\x30\x2e\x35\x38\x43\x34\x34\x31\x2e\x37\x31\x2c\ -\x32\x38\x32\x2e\x31\x34\x2c\x34\x34\x35\x2e\x34\x31\x2c\x32\x38\ -\x31\x2e\x33\x33\x2c\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\ -\x33\x37\x5a\x6d\x33\x38\x2e\x33\x31\x2c\x31\x30\x38\x2e\x34\x39\ -\x63\x30\x2d\x31\x36\x2e\x35\x34\x2e\x39\x34\x2d\x33\x32\x2e\x37\ -\x33\x2d\x2e\x32\x35\x2d\x34\x38\x2e\x37\x37\x2d\x31\x2e\x33\x31\ -\x2d\x31\x37\x2e\x38\x32\x2d\x31\x35\x2e\x39\x33\x2d\x32\x39\x2e\ -\x37\x35\x2d\x33\x32\x2e\x37\x37\x2d\x32\x38\x2e\x37\x37\x2d\x31\ -\x36\x2e\x36\x35\x2c\x31\x2d\x32\x39\x2e\x32\x31\x2c\x31\x34\x2e\ -\x37\x33\x2d\x32\x39\x2e\x34\x32\x2c\x33\x32\x2e\x35\x32\x2d\x2e\ -\x31\x36\x2c\x31\x33\x2e\x37\x37\x2c\x30\x2c\x32\x37\x2e\x35\x34\ -\x2c\x30\x2c\x34\x31\x2e\x33\x31\x61\x33\x33\x2e\x31\x39\x2c\x33\ -\x33\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x32\x2c\x33\ -\x2e\x37\x31\x5a\x6d\x2d\x33\x31\x2e\x33\x33\x2c\x34\x36\x2e\x37\ -\x31\x61\x31\x34\x2e\x39\x31\x2c\x31\x34\x2e\x39\x31\x2c\x30\x2c\ -\x30\x2c\x30\x2d\x31\x33\x2e\x36\x38\x2c\x38\x2e\x36\x36\x63\x2d\ -\x32\x2e\x38\x32\x2c\x35\x2e\x35\x37\x2d\x32\x2e\x38\x2c\x31\x31\ -\x2e\x35\x39\x2c\x31\x2e\x36\x2c\x31\x35\x2e\x38\x35\x2c\x34\x2e\ -\x31\x36\x2c\x34\x2c\x34\x2e\x34\x34\x2c\x38\x2e\x33\x35\x2c\x34\ -\x2e\x32\x32\x2c\x31\x33\x2e\x33\x32\x61\x36\x38\x2e\x33\x38\x2c\ -\x36\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\x37\x2e\ -\x37\x31\x63\x2e\x33\x37\x2c\x35\x2e\x33\x33\x2c\x33\x2e\x36\x2c\ -\x38\x2e\x38\x33\x2c\x38\x2c\x38\x2e\x38\x37\x73\x37\x2e\x37\x35\ -\x2d\x33\x2e\x34\x36\x2c\x38\x2e\x30\x37\x2d\x38\x2e\x37\x36\x61\ -\x31\x31\x31\x2e\x34\x34\x2c\x31\x31\x31\x2e\x34\x34\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x30\x2d\x31\x31\x2e\x35\x36\x2c\x39\x2e\x36\x38\ -\x2c\x39\x2e\x36\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x2e\x32\x2d\ -\x38\x2e\x31\x33\x63\x34\x2e\x37\x31\x2d\x34\x2e\x36\x34\x2c\x35\ -\x2e\x36\x31\x2d\x31\x30\x2e\x35\x33\x2c\x33\x2d\x31\x36\x2e\x37\ -\x32\x41\x31\x35\x2e\x34\x38\x2c\x31\x35\x2e\x34\x38\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x34\x35\x36\x2e\x30\x35\x2c\x34\x33\x35\x2e\x35\ -\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x37\x39\ -\x2e\x35\x38\x2c\x33\x38\x34\x2e\x33\x31\x63\x33\x2e\x31\x38\x2d\ -\x2e\x30\x36\x2c\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\x2e\x36\ -\x31\x2c\x30\x2c\x30\x2d\x33\x2e\x32\x34\x2c\x30\x2d\x36\x2e\x34\ -\x33\x2c\x30\x2d\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\x35\x33\ -\x2c\x30\x2d\x32\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\x35\x38\ -\x2d\x33\x32\x2e\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\x38\x2e\ -\x39\x33\x2d\x33\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\x34\x2d\ -\x32\x39\x2e\x35\x37\x43\x32\x34\x36\x2c\x33\x31\x39\x2e\x31\x34\ -\x2c\x32\x31\x38\x2c\x33\x33\x34\x2e\x36\x31\x2c\x31\x39\x34\x2e\ -\x31\x33\x2c\x33\x35\x39\x63\x2d\x36\x2c\x36\x2e\x31\x37\x2d\x39\ -\x2e\x36\x33\x2c\x31\x33\x2e\x35\x39\x2d\x39\x2e\x36\x31\x2c\x32\ -\x32\x2e\x38\x31\x61\x36\x38\x2e\x39\x33\x2c\x36\x38\x2e\x39\x33\ -\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x33\x38\x63\x35\x2e\ -\x36\x33\x2c\x32\x33\x2e\x32\x38\x2c\x33\x31\x2e\x32\x37\x2c\x33\ -\x30\x2e\x32\x38\x2c\x34\x37\x2e\x39\x33\x2c\x31\x33\x2e\x32\x37\ -\x2c\x32\x34\x2e\x36\x31\x2d\x32\x35\x2e\x31\x31\x2c\x35\x33\x2e\ -\x37\x35\x2d\x33\x33\x2e\x38\x36\x2c\x38\x37\x2d\x32\x36\x2e\x37\ -\x37\x2c\x31\x35\x2c\x33\x2e\x32\x2c\x32\x38\x2c\x31\x30\x2e\x33\ -\x35\x2c\x33\x39\x2e\x36\x2c\x32\x30\x2e\x34\x32\x43\x33\x36\x33\ -\x2e\x31\x37\x2c\x33\x38\x38\x2e\x38\x32\x2c\x33\x37\x30\x2e\x30\ -\x37\x2c\x33\x38\x34\x2e\x34\x39\x2c\x33\x37\x39\x2e\x35\x38\x2c\ -\x33\x38\x34\x2e\x33\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\ -\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\ -\x22\x4d\x34\x33\x36\x2e\x36\x32\x2c\x32\x37\x38\x2e\x34\x38\x63\ -\x32\x2e\x36\x31\x2d\x2e\x38\x2c\x35\x2e\x32\x33\x2d\x31\x2e\x34\ -\x36\x2c\x37\x2e\x37\x37\x2d\x32\x2e\x30\x39\x2c\x31\x2e\x31\x34\ -\x2d\x2e\x32\x38\x2c\x32\x2e\x32\x38\x2d\x2e\x35\x36\x2c\x33\x2e\ -\x34\x32\x2d\x2e\x38\x36\x6c\x2e\x36\x32\x2d\x2e\x31\x36\x68\x31\ -\x36\x6c\x2e\x39\x33\x2e\x33\x39\x63\x2e\x33\x36\x2e\x31\x35\x2e\ -\x37\x32\x2e\x33\x33\x2c\x31\x2e\x30\x37\x2e\x35\x6c\x2e\x33\x37\ -\x2e\x31\x39\x61\x36\x36\x2e\x31\x38\x2c\x36\x36\x2e\x31\x38\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x32\x36\x2e\x38\x33\x2c\x31\x30\x2e\x33\ -\x38\x2c\x33\x30\x2e\x33\x35\x2c\x33\x30\x2e\x33\x35\x2c\x30\x2c\ -\x30\x2c\x30\x2d\x35\x2e\x39\x34\x2d\x31\x30\x2e\x31\x35\x41\x32\ -\x30\x35\x2c\x32\x30\x35\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x36\x34\ -\x2e\x34\x34\x2c\x32\x35\x34\x63\x2d\x36\x30\x2e\x32\x32\x2d\x35\ -\x30\x2e\x33\x31\x2d\x31\x32\x38\x2e\x33\x31\x2d\x37\x30\x2d\x32\ -\x30\x33\x2e\x39\x31\x2d\x35\x38\x2e\x33\x36\x2d\x35\x37\x2e\x30\ -\x39\x2c\x38\x2e\x38\x2d\x31\x30\x35\x2e\x38\x34\x2c\x33\x36\x2d\ -\x31\x34\x36\x2e\x38\x34\x2c\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\ -\x37\x2c\x38\x2e\x37\x32\x2d\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\ -\x36\x32\x2d\x36\x2e\x38\x39\x2c\x33\x31\x2e\x35\x32\x2c\x37\x2e\ -\x32\x34\x2c\x32\x32\x2c\x33\x31\x2e\x35\x36\x2c\x32\x36\x2e\x38\ -\x33\x2c\x34\x37\x2e\x39\x31\x2c\x31\x30\x2c\x34\x39\x2e\x32\x32\ -\x2d\x35\x30\x2e\x35\x37\x2c\x31\x30\x38\x2d\x37\x31\x2e\x30\x37\ -\x2c\x31\x37\x35\x2e\x33\x39\x2d\x36\x31\x2e\x34\x41\x31\x38\x37\ -\x2e\x39\x32\x2c\x31\x38\x37\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x34\x31\x35\x2c\x32\x38\x39\x2e\x36\x38\x2c\x36\x37\x2e\x34\ -\x39\x2c\x36\x37\x2e\x34\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x33\ -\x36\x2e\x36\x32\x2c\x32\x37\x38\x2e\x34\x38\x5a\x22\x2f\x3e\x3c\ -\x2f\x73\x76\x67\x3e\ -\x00\x00\x0a\x5b\ -\x3c\ -\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ -\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ -\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x38\x63\x63\x35\x34\x30\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x39\x32\x39\ -\x34\x39\x37\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\x65\x3e\x3c\x2f\x64\ -\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x32\x39\x30\x2e\ -\x31\x38\x2c\x37\x31\x2e\x33\x35\x43\x34\x30\x32\x2c\x37\x33\x2e\ -\x31\x34\x2c\x34\x39\x30\x2e\x31\x2c\x31\x31\x31\x2e\x34\x34\x2c\ -\x35\x36\x33\x2e\x39\x2c\x31\x38\x37\x2e\x34\x35\x63\x31\x34\x2e\ -\x38\x2c\x31\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\x2c\x33\x33\ -\x2e\x39\x33\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2e\x30\x35\x2d\x31\ -\x31\x2e\x31\x32\x2c\x31\x33\x2e\x31\x36\x2d\x32\x38\x2e\x36\x32\ -\x2c\x31\x33\x2d\x34\x31\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\ -\x30\x38\x2d\x31\x34\x2e\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\ -\x2e\x37\x32\x2d\x34\x36\x2e\x36\x31\x2d\x34\x31\x2e\x31\x31\x2d\ -\x33\x38\x2e\x34\x2d\x32\x39\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\ -\x2e\x37\x34\x2d\x31\x32\x37\x2e\x37\x35\x2d\x35\x34\x2e\x36\x34\ -\x2d\x35\x34\x2d\x39\x2e\x32\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\ -\x2e\x33\x32\x2d\x31\x35\x38\x2e\x35\x2c\x31\x35\x2e\x32\x32\x2d\ -\x34\x35\x2c\x31\x37\x2d\x38\x34\x2e\x32\x39\x2c\x34\x33\x2e\x39\ -\x33\x2d\x31\x31\x38\x2e\x31\x36\x2c\x37\x39\x2e\x39\x34\x2d\x38\ -\x2e\x35\x37\x2c\x39\x2e\x31\x2d\x31\x38\x2e\x36\x35\x2c\x31\x32\ -\x2e\x33\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\x2d\x32\x30\x2e\x30\ -\x39\x2d\x37\x2e\x35\x36\x2d\x32\x35\x2e\x32\x2d\x33\x34\x2e\x30\ -\x38\x2d\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\x31\x61\x33\x38\x30\ -\x2e\x35\x31\x2c\x33\x38\x30\x2e\x35\x31\x2c\x30\x2c\x30\x2c\x31\ -\x2c\x35\x37\x2e\x36\x32\x2d\x35\x30\x2e\x33\x38\x63\x34\x33\x2d\ -\x33\x30\x2e\x35\x38\x2c\x38\x39\x2e\x39\x33\x2d\x35\x31\x2e\x31\ -\x2c\x31\x34\x30\x2e\x37\x37\x2d\x36\x30\x2e\x34\x35\x43\x32\x35\ -\x35\x2e\x30\x37\x2c\x37\x34\x2e\x32\x33\x2c\x32\x37\x37\x2e\x34\ -\x33\x2c\x37\x32\x2e\x38\x35\x2c\x32\x39\x30\x2e\x31\x38\x2c\x37\ -\x31\x2e\x33\x35\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\ -\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\ -\x33\x30\x30\x2c\x35\x31\x37\x2e\x33\x37\x63\x2d\x32\x32\x2c\x30\ -\x2d\x33\x39\x2e\x31\x35\x2d\x31\x38\x2e\x34\x35\x2d\x33\x39\x2e\ -\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\x32\x33\x2e\x33\x37\ -\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\x2e\ -\x38\x36\x2d\x34\x31\x2e\x37\x39\x2c\x32\x32\x2e\x32\x35\x2c\x30\ -\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\x37\x2c\x33\x39\x2e\ -\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\ -\x31\x37\x2e\x33\x37\x2c\x33\x30\x30\x2c\x35\x31\x37\x2e\x33\x37\ -\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x34\x34\x39\x2e\ -\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x68\x31\x34\x2e\x33\x35\x61\ -\x31\x30\x2e\x36\x34\x2c\x31\x30\x2e\x36\x34\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x32\x2e\x34\x32\x2c\x31\x63\x32\x37\x2e\x34\x37\x2c\x34\ -\x2c\x34\x39\x2e\x36\x2c\x32\x36\x2e\x36\x37\x2c\x35\x31\x2e\x36\ -\x38\x2c\x35\x34\x2e\x32\x36\x2c\x31\x2e\x31\x37\x2c\x31\x35\x2e\ -\x35\x33\x2e\x35\x38\x2c\x33\x31\x2e\x32\x2e\x37\x38\x2c\x34\x36\ -\x2e\x38\x2c\x30\x2c\x32\x2e\x31\x32\x2c\x30\x2c\x34\x2e\x32\x35\ -\x2c\x30\x2c\x36\x2e\x38\x36\x68\x31\x31\x2e\x37\x35\x63\x31\x33\ -\x2e\x39\x34\x2c\x30\x2c\x31\x39\x2e\x33\x36\x2c\x35\x2e\x33\x39\ -\x2c\x31\x39\x2e\x33\x36\x2c\x31\x39\x2e\x32\x32\x2c\x30\x2c\x33\ -\x33\x2e\x32\x35\x2d\x2e\x31\x38\x2c\x36\x36\x2e\x35\x2e\x31\x32\ -\x2c\x39\x39\x2e\x37\x35\x2e\x30\x39\x2c\x39\x2e\x36\x38\x2d\x32\ -\x2e\x39\x33\x2c\x31\x36\x2e\x36\x37\x2d\x31\x32\x2e\x31\x36\x2c\ -\x32\x30\x2e\x33\x39\x48\x33\x37\x35\x2e\x31\x32\x63\x2d\x37\x2e\ -\x32\x34\x2d\x33\x2e\x31\x37\x2d\x31\x32\x2d\x38\x2e\x31\x31\x2d\ -\x31\x32\x2d\x31\x36\x2e\x35\x33\x2c\x30\x2d\x33\x35\x2e\x34\x36\ -\x2d\x2e\x30\x38\x2d\x37\x30\x2e\x39\x31\x2c\x30\x2d\x31\x30\x36\ -\x2e\x33\x37\x2c\x30\x2d\x31\x30\x2e\x32\x39\x2c\x36\x2e\x32\x34\ -\x2d\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\x35\x36\x2d\x31\x36\x2e\ -\x34\x34\x2c\x34\x2e\x36\x39\x2d\x2e\x30\x39\x2c\x39\x2e\x33\x39\ -\x2c\x30\x2c\x31\x34\x2e\x35\x31\x2c\x30\x2c\x30\x2d\x31\x35\x2e\ -\x37\x33\x2d\x2e\x30\x38\x2d\x33\x30\x2e\x35\x39\x2c\x30\x2d\x34\ -\x35\x2e\x34\x35\x2e\x31\x39\x2d\x32\x38\x2e\x37\x39\x2c\x31\x37\ -\x2e\x32\x38\x2d\x35\x32\x2e\x33\x36\x2c\x34\x33\x2e\x38\x39\x2d\ -\x36\x30\x2e\x35\x38\x43\x34\x34\x31\x2e\x37\x31\x2c\x32\x38\x32\ -\x2e\x31\x34\x2c\x34\x34\x35\x2e\x34\x31\x2c\x32\x38\x31\x2e\x33\ -\x33\x2c\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x5a\ -\x6d\x33\x38\x2e\x33\x31\x2c\x31\x30\x38\x2e\x34\x39\x63\x30\x2d\ -\x31\x36\x2e\x35\x34\x2e\x39\x34\x2d\x33\x32\x2e\x37\x33\x2d\x2e\ -\x32\x35\x2d\x34\x38\x2e\x37\x37\x2d\x31\x2e\x33\x31\x2d\x31\x37\ -\x2e\x38\x32\x2d\x31\x35\x2e\x39\x33\x2d\x32\x39\x2e\x37\x35\x2d\ -\x33\x32\x2e\x37\x37\x2d\x32\x38\x2e\x37\x37\x2d\x31\x36\x2e\x36\ -\x35\x2c\x31\x2d\x32\x39\x2e\x32\x31\x2c\x31\x34\x2e\x37\x33\x2d\ -\x32\x39\x2e\x34\x32\x2c\x33\x32\x2e\x35\x32\x2d\x2e\x31\x36\x2c\ -\x31\x33\x2e\x37\x37\x2c\x30\x2c\x32\x37\x2e\x35\x34\x2c\x30\x2c\ -\x34\x31\x2e\x33\x31\x61\x33\x33\x2e\x31\x39\x2c\x33\x33\x2e\x31\ -\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x32\x2c\x33\x2e\x37\x31\ -\x5a\x6d\x2d\x33\x31\x2e\x33\x33\x2c\x34\x36\x2e\x37\x31\x61\x31\ -\x34\x2e\x39\x31\x2c\x31\x34\x2e\x39\x31\x2c\x30\x2c\x30\x2c\x30\ -\x2d\x31\x33\x2e\x36\x38\x2c\x38\x2e\x36\x36\x63\x2d\x32\x2e\x38\ -\x32\x2c\x35\x2e\x35\x37\x2d\x32\x2e\x38\x2c\x31\x31\x2e\x35\x39\ -\x2c\x31\x2e\x36\x2c\x31\x35\x2e\x38\x35\x2c\x34\x2e\x31\x36\x2c\ -\x34\x2c\x34\x2e\x34\x34\x2c\x38\x2e\x33\x35\x2c\x34\x2e\x32\x32\ -\x2c\x31\x33\x2e\x33\x32\x61\x36\x38\x2e\x33\x38\x2c\x36\x38\x2e\ -\x33\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\x37\x2e\x37\x31\x63\ -\x2e\x33\x37\x2c\x35\x2e\x33\x33\x2c\x33\x2e\x36\x2c\x38\x2e\x38\ -\x33\x2c\x38\x2c\x38\x2e\x38\x37\x73\x37\x2e\x37\x35\x2d\x33\x2e\ -\x34\x36\x2c\x38\x2e\x30\x37\x2d\x38\x2e\x37\x36\x61\x31\x31\x31\ -\x2e\x34\x34\x2c\x31\x31\x31\x2e\x34\x34\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x30\x2d\x31\x31\x2e\x35\x36\x2c\x39\x2e\x36\x38\x2c\x39\x2e\ -\x36\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x2e\x32\x2d\x38\x2e\x31\ -\x33\x63\x34\x2e\x37\x31\x2d\x34\x2e\x36\x34\x2c\x35\x2e\x36\x31\ -\x2d\x31\x30\x2e\x35\x33\x2c\x33\x2d\x31\x36\x2e\x37\x32\x41\x31\ -\x35\x2e\x34\x38\x2c\x31\x35\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x34\x35\x36\x2e\x30\x35\x2c\x34\x33\x35\x2e\x35\x37\x5a\x22\ -\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x37\x39\x2e\x35\x38\ -\x2c\x33\x38\x34\x2e\x33\x31\x63\x33\x2e\x31\x38\x2d\x2e\x30\x36\ -\x2c\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\x2e\x36\x31\x2c\x30\ -\x2c\x30\x2d\x33\x2e\x32\x34\x2c\x30\x2d\x36\x2e\x34\x33\x2c\x30\ -\x2d\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\x35\x33\x2c\x30\x2d\ -\x32\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\x35\x38\x2d\x33\x32\ -\x2e\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\x38\x2e\x39\x33\x2d\ -\x33\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\x34\x2d\x32\x39\x2e\ -\x35\x37\x43\x32\x34\x36\x2c\x33\x31\x39\x2e\x31\x34\x2c\x32\x31\ -\x38\x2c\x33\x33\x34\x2e\x36\x31\x2c\x31\x39\x34\x2e\x31\x33\x2c\ -\x33\x35\x39\x63\x2d\x36\x2c\x36\x2e\x31\x37\x2d\x39\x2e\x36\x33\ -\x2c\x31\x33\x2e\x35\x39\x2d\x39\x2e\x36\x31\x2c\x32\x32\x2e\x38\ -\x31\x61\x36\x38\x2e\x39\x33\x2c\x36\x38\x2e\x39\x33\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x33\x38\x63\x35\x2e\x36\x33\x2c\ -\x32\x33\x2e\x32\x38\x2c\x33\x31\x2e\x32\x37\x2c\x33\x30\x2e\x32\ -\x38\x2c\x34\x37\x2e\x39\x33\x2c\x31\x33\x2e\x32\x37\x2c\x32\x34\ -\x2e\x36\x31\x2d\x32\x35\x2e\x31\x31\x2c\x35\x33\x2e\x37\x35\x2d\ -\x33\x33\x2e\x38\x36\x2c\x38\x37\x2d\x32\x36\x2e\x37\x37\x2c\x31\ -\x35\x2c\x33\x2e\x32\x2c\x32\x38\x2c\x31\x30\x2e\x33\x35\x2c\x33\ -\x39\x2e\x36\x2c\x32\x30\x2e\x34\x32\x43\x33\x36\x33\x2e\x31\x37\ -\x2c\x33\x38\x38\x2e\x38\x32\x2c\x33\x37\x30\x2e\x30\x37\x2c\x33\ -\x38\x34\x2e\x34\x39\x2c\x33\x37\x39\x2e\x35\x38\x2c\x33\x38\x34\ -\x2e\x33\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ -\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\ -\x35\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\x31\x61\x32\x36\x2e\ -\x31\x32\x2c\x32\x36\x2e\x31\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x37\ -\x2e\x37\x39\x2c\x31\x2e\x36\x36\x63\x35\x2e\x35\x39\x2c\x35\x2e\ -\x37\x32\x2c\x31\x32\x2e\x33\x34\x2c\x38\x2e\x37\x38\x2c\x32\x33\ -\x2c\x38\x2e\x36\x31\x61\x32\x34\x2c\x32\x34\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x36\x2e\x32\x35\x2d\x31\x2e\x32\x37\x41\x32\x35\x2e\x36\ -\x38\x2c\x32\x35\x2e\x36\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x35\ -\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\x31\x5a\x22\x2f\x3e\x3c\ -\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\ -\x31\x22\x20\x64\x3d\x22\x4d\x34\x33\x36\x2e\x36\x32\x2c\x32\x37\ -\x38\x2e\x34\x38\x63\x32\x2e\x36\x31\x2d\x2e\x38\x2c\x35\x2e\x32\ -\x33\x2d\x31\x2e\x34\x36\x2c\x37\x2e\x37\x37\x2d\x32\x2e\x30\x39\ -\x2c\x31\x2e\x31\x34\x2d\x2e\x32\x38\x2c\x32\x2e\x32\x38\x2d\x2e\ -\x35\x36\x2c\x33\x2e\x34\x32\x2d\x2e\x38\x36\x6c\x2e\x36\x32\x2d\ -\x2e\x31\x36\x68\x31\x36\x6c\x2e\x39\x33\x2e\x33\x39\x63\x2e\x33\ -\x36\x2e\x31\x35\x2e\x37\x32\x2e\x33\x33\x2c\x31\x2e\x30\x37\x2e\ -\x35\x6c\x2e\x33\x37\x2e\x31\x39\x61\x36\x36\x2e\x31\x38\x2c\x36\ -\x36\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x36\x2e\x38\x33\ -\x2c\x31\x30\x2e\x33\x38\x2c\x33\x30\x2e\x33\x35\x2c\x33\x30\x2e\ -\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\x39\x34\x2d\x31\x30\ -\x2e\x31\x35\x41\x32\x30\x35\x2c\x32\x30\x35\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x34\x36\x34\x2e\x34\x34\x2c\x32\x35\x34\x63\x2d\x36\x30\ -\x2e\x32\x32\x2d\x35\x30\x2e\x33\x31\x2d\x31\x32\x38\x2e\x33\x31\ -\x2d\x37\x30\x2d\x32\x30\x33\x2e\x39\x31\x2d\x35\x38\x2e\x33\x36\ -\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\x2d\x31\x30\x35\x2e\x38\ -\x34\x2c\x33\x36\x2d\x31\x34\x36\x2e\x38\x34\x2c\x37\x39\x2e\x32\ -\x38\x2d\x38\x2e\x32\x37\x2c\x38\x2e\x37\x32\x2d\x31\x30\x2e\x37\ -\x39\x2c\x31\x39\x2e\x36\x32\x2d\x36\x2e\x38\x39\x2c\x33\x31\x2e\ -\x35\x32\x2c\x37\x2e\x32\x34\x2c\x32\x32\x2c\x33\x31\x2e\x35\x36\ -\x2c\x32\x36\x2e\x38\x33\x2c\x34\x37\x2e\x39\x31\x2c\x31\x30\x2c\ -\x34\x39\x2e\x32\x32\x2d\x35\x30\x2e\x35\x37\x2c\x31\x30\x38\x2d\ -\x37\x31\x2e\x30\x37\x2c\x31\x37\x35\x2e\x33\x39\x2d\x36\x31\x2e\ -\x34\x41\x31\x38\x37\x2e\x39\x32\x2c\x31\x38\x37\x2e\x39\x32\x2c\ -\x30\x2c\x30\x2c\x31\x2c\x34\x31\x35\x2c\x32\x38\x39\x2e\x36\x38\ -\x2c\x36\x37\x2e\x34\x39\x2c\x36\x37\x2e\x34\x39\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x34\x33\x36\x2e\x36\x32\x2c\x32\x37\x38\x2e\x34\x38\ -\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ -\x00\x00\x0b\x47\ -======= -\x00\x00\x06\x37\ ->>>>>>> origin/main -\x3c\ -\x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ -\x20\x64\x61\x74\x61\x2d\x6e\x61\x6d\x65\x3d\x22\x4c\x61\x79\x65\ -\x72\x20\x31\x22\x20\x78\x6d\x6c\x6e\x73\x3d\x22\x68\x74\x74\x70\ -\x3a\x2f\x2f\x77\x77\x77\x2e\x77\x33\x2e\x6f\x72\x67\x2f\x32\x30\ -\x30\x30\x2f\x73\x76\x67\x22\x20\x76\x69\x65\x77\x42\x6f\x78\x3d\ -\x22\x30\x20\x30\x20\x36\x30\x30\x20\x36\x30\x30\x22\x3e\x3c\x64\ -\x65\x66\x73\x3e\x3c\x73\x74\x79\x6c\x65\x3e\x2e\x63\x6c\x73\x2d\ -\x31\x7b\x66\x69\x6c\x6c\x3a\x23\x64\x30\x64\x32\x64\x33\x3b\x7d\ -\x2e\x63\x6c\x73\x2d\x32\x7b\x66\x69\x6c\x6c\x3a\x23\x39\x32\x39\ -\x34\x39\x37\x3b\x7d\x2e\x63\x6c\x73\x2d\x33\x7b\x66\x69\x6c\x6c\ -\x3a\x23\x35\x65\x36\x30\x36\x31\x3b\x7d\x3c\x2f\x73\x74\x79\x6c\ -\x65\x3e\x3c\x2f\x64\x65\x66\x73\x3e\x3c\x70\x61\x74\x68\x20\x63\ -\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\ -\x4d\x32\x39\x30\x2e\x31\x38\x2c\x38\x32\x2e\x36\x32\x43\x34\x30\ -\x32\x2c\x38\x34\x2e\x34\x2c\x34\x39\x30\x2e\x31\x2c\x31\x32\x32\ -\x2e\x37\x2c\x35\x36\x33\x2e\x39\x2c\x31\x39\x38\x2e\x37\x31\x63\ -\x31\x34\x2e\x38\x2c\x31\x35\x2e\x32\x35\x2c\x31\x31\x2e\x38\x31\ -\x2c\x33\x33\x2e\x39\x33\x2c\x33\x2e\x32\x36\x2c\x34\x34\x2d\x31\ -\x31\x2e\x31\x32\x2c\x31\x33\x2e\x31\x36\x2d\x32\x38\x2e\x36\x32\ -\x2c\x31\x33\x2d\x34\x31\x2e\x34\x36\x2e\x38\x33\x2d\x31\x35\x2e\ -\x30\x38\x2d\x31\x34\x2e\x32\x37\x2d\x33\x30\x2e\x32\x2d\x32\x38\ -\x2e\x37\x31\x2d\x34\x36\x2e\x36\x31\x2d\x34\x31\x2e\x31\x2d\x33\ -\x38\x2e\x34\x2d\x32\x39\x2d\x38\x31\x2e\x33\x33\x2d\x34\x36\x2e\ -\x37\x35\x2d\x31\x32\x37\x2e\x37\x35\x2d\x35\x34\x2e\x36\x35\x2d\ -\x35\x34\x2d\x39\x2e\x31\x39\x2d\x31\x30\x36\x2e\x39\x32\x2d\x34\ -\x2e\x33\x32\x2d\x31\x35\x38\x2e\x35\x2c\x31\x35\x2e\x32\x33\x43\ -\x31\x34\x37\x2e\x38\x34\x2c\x31\x38\x30\x2e\x31\x32\x2c\x31\x30\ -\x38\x2e\x35\x35\x2c\x32\x30\x37\x2c\x37\x34\x2e\x36\x38\x2c\x32\ -\x34\x33\x63\x2d\x38\x2e\x35\x37\x2c\x39\x2e\x31\x2d\x31\x38\x2e\ -\x36\x35\x2c\x31\x32\x2e\x33\x32\x2d\x33\x30\x2e\x31\x39\x2c\x38\ -\x2d\x32\x30\x2e\x30\x39\x2d\x37\x2e\x35\x36\x2d\x32\x35\x2e\x32\ -\x2d\x33\x34\x2e\x30\x38\x2d\x39\x2e\x37\x34\x2d\x35\x30\x2e\x36\ -\x41\x33\x38\x30\x2c\x33\x38\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x39\ -\x32\x2e\x33\x37\x2c\x31\x35\x30\x63\x34\x33\x2d\x33\x30\x2e\x35\ -\x37\x2c\x38\x39\x2e\x39\x33\x2d\x35\x31\x2e\x30\x39\x2c\x31\x34\ -\x30\x2e\x37\x37\x2d\x36\x30\x2e\x34\x35\x43\x32\x35\x35\x2e\x30\ -\x37\x2c\x38\x35\x2e\x34\x39\x2c\x32\x37\x37\x2e\x34\x33\x2c\x38\ -\x34\x2e\x31\x31\x2c\x32\x39\x30\x2e\x31\x38\x2c\x38\x32\x2e\x36\ -\x32\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\ -\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x30\x30\ -\x2c\x35\x32\x38\x2e\x36\x34\x63\x2d\x32\x32\x2c\x30\x2d\x33\x39\ -\x2e\x31\x35\x2d\x31\x38\x2e\x34\x36\x2d\x33\x39\x2e\x31\x31\x2d\ -\x34\x32\x2e\x31\x32\x2c\x30\x2d\x32\x33\x2e\x33\x37\x2c\x31\x37\ -\x2e\x31\x39\x2d\x34\x31\x2e\x38\x33\x2c\x33\x38\x2e\x38\x36\x2d\ -\x34\x31\x2e\x37\x39\x2c\x32\x32\x2e\x32\x35\x2c\x30\x2c\x33\x39\ -\x2e\x33\x32\x2c\x31\x38\x2e\x31\x37\x2c\x33\x39\x2e\x33\x34\x2c\ -\x34\x31\x2e\x38\x31\x53\x33\x32\x32\x2e\x31\x2c\x35\x32\x38\x2e\ -\x36\x33\x2c\x33\x30\x30\x2c\x35\x32\x38\x2e\x36\x34\x5a\x22\x2f\ -\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\ -\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x33\x37\x39\x2e\x35\x38\x2c\ -\x33\x39\x35\x2e\x35\x37\x63\x33\x2e\x31\x38\x2d\x2e\x30\x36\x2c\ -\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\x2e\x36\x31\x2c\x30\x2c\ -\x30\x2d\x33\x2e\x32\x33\x2c\x30\x2d\x36\x2e\x34\x33\x2c\x30\x2d\ -\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\x35\x32\x2c\x30\x2d\x32\ -\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\x35\x38\x2d\x33\x32\x2e\ -\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\x38\x2e\x39\x33\x2d\x33\ -\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\x34\x2d\x32\x39\x2e\x35\ -\x37\x2d\x33\x32\x2e\x38\x2c\x34\x2e\x36\x32\x2d\x36\x30\x2e\x38\ -\x2c\x32\x30\x2e\x30\x39\x2d\x38\x34\x2e\x36\x34\x2c\x34\x34\x2e\ -\x34\x33\x2d\x36\x2c\x36\x2e\x31\x37\x2d\x39\x2e\x36\x33\x2c\x31\ -\x33\x2e\x36\x2d\x39\x2e\x36\x31\x2c\x32\x32\x2e\x38\x32\x61\x36\ -\x39\x2e\x32\x38\x2c\x36\x39\x2e\x32\x38\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x31\x2c\x37\x2e\x33\x38\x63\x35\x2e\x36\x33\x2c\x32\x33\x2e\ -\x32\x37\x2c\x33\x31\x2e\x32\x37\x2c\x33\x30\x2e\x32\x37\x2c\x34\ -\x37\x2e\x39\x33\x2c\x31\x33\x2e\x32\x36\x2c\x32\x34\x2e\x36\x31\ -\x2d\x32\x35\x2e\x31\x31\x2c\x35\x33\x2e\x37\x35\x2d\x33\x33\x2e\ -\x38\x36\x2c\x38\x37\x2d\x32\x36\x2e\x37\x36\x2c\x31\x35\x2c\x33\ -\x2e\x32\x2c\x32\x38\x2c\x31\x30\x2e\x33\x35\x2c\x33\x39\x2e\x36\ -\x2c\x32\x30\x2e\x34\x32\x43\x33\x36\x33\x2e\x31\x37\x2c\x34\x30\ -\x30\x2e\x30\x38\x2c\x33\x37\x30\x2e\x30\x37\x2c\x33\x39\x35\x2e\ -\x37\x35\x2c\x33\x37\x39\x2e\x35\x38\x2c\x33\x39\x35\x2e\x35\x37\ +\x35\x2d\x33\x39\x2e\x31\x31\x2d\x34\x32\x2e\x31\x31\x2c\x30\x2d\ +\x32\x33\x2e\x33\x37\x2c\x31\x37\x2e\x31\x39\x2d\x34\x31\x2e\x38\ +\x33\x2c\x33\x38\x2e\x38\x36\x2d\x34\x31\x2e\x37\x39\x2c\x32\x32\ +\x2e\x32\x35\x2c\x30\x2c\x33\x39\x2e\x33\x32\x2c\x31\x38\x2e\x31\ +\x37\x2c\x33\x39\x2e\x33\x34\x2c\x34\x31\x2e\x38\x31\x53\x33\x32\ +\x32\x2e\x31\x2c\x35\x31\x37\x2e\x33\x37\x2c\x33\x30\x30\x2c\x35\ +\x31\x37\x2e\x33\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\ +\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\x3d\x22\ +\x4d\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\x30\x2e\x33\x37\x68\x31\ +\x34\x2e\x33\x35\x61\x31\x30\x2e\x36\x34\x2c\x31\x30\x2e\x36\x34\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x32\x2c\x31\x63\x32\x37\ +\x2e\x34\x37\x2c\x34\x2c\x34\x39\x2e\x36\x2c\x32\x36\x2e\x36\x37\ +\x2c\x35\x31\x2e\x36\x38\x2c\x35\x34\x2e\x32\x36\x2c\x31\x2e\x31\ +\x37\x2c\x31\x35\x2e\x35\x33\x2e\x35\x38\x2c\x33\x31\x2e\x32\x2e\ +\x37\x38\x2c\x34\x36\x2e\x38\x2c\x30\x2c\x32\x2e\x31\x32\x2c\x30\ +\x2c\x34\x2e\x32\x35\x2c\x30\x2c\x36\x2e\x38\x36\x68\x31\x31\x2e\ +\x37\x35\x63\x31\x33\x2e\x39\x34\x2c\x30\x2c\x31\x39\x2e\x33\x36\ +\x2c\x35\x2e\x33\x39\x2c\x31\x39\x2e\x33\x36\x2c\x31\x39\x2e\x32\ +\x32\x2c\x30\x2c\x33\x33\x2e\x32\x35\x2d\x2e\x31\x38\x2c\x36\x36\ +\x2e\x35\x2e\x31\x32\x2c\x39\x39\x2e\x37\x35\x2e\x30\x39\x2c\x39\ +\x2e\x36\x38\x2d\x32\x2e\x39\x33\x2c\x31\x36\x2e\x36\x37\x2d\x31\ +\x32\x2e\x31\x36\x2c\x32\x30\x2e\x33\x39\x48\x33\x37\x35\x2e\x31\ +\x32\x63\x2d\x37\x2e\x32\x34\x2d\x33\x2e\x31\x37\x2d\x31\x32\x2d\ +\x38\x2e\x31\x31\x2d\x31\x32\x2d\x31\x36\x2e\x35\x33\x2c\x30\x2d\ +\x33\x35\x2e\x34\x36\x2d\x2e\x30\x38\x2d\x37\x30\x2e\x39\x31\x2c\ +\x30\x2d\x31\x30\x36\x2e\x33\x37\x2c\x30\x2d\x31\x30\x2e\x32\x39\ +\x2c\x36\x2e\x32\x34\x2d\x31\x36\x2e\x32\x35\x2c\x31\x36\x2e\x35\ +\x36\x2d\x31\x36\x2e\x34\x34\x2c\x34\x2e\x36\x39\x2d\x2e\x30\x39\ +\x2c\x39\x2e\x33\x39\x2c\x30\x2c\x31\x34\x2e\x35\x31\x2c\x30\x2c\ +\x30\x2d\x31\x35\x2e\x37\x33\x2d\x2e\x30\x38\x2d\x33\x30\x2e\x35\ +\x39\x2c\x30\x2d\x34\x35\x2e\x34\x35\x2e\x31\x39\x2d\x32\x38\x2e\ +\x37\x39\x2c\x31\x37\x2e\x32\x38\x2d\x35\x32\x2e\x33\x36\x2c\x34\ +\x33\x2e\x38\x39\x2d\x36\x30\x2e\x35\x38\x43\x34\x34\x31\x2e\x37\ +\x31\x2c\x32\x38\x32\x2e\x31\x34\x2c\x34\x34\x35\x2e\x34\x31\x2c\ +\x32\x38\x31\x2e\x33\x33\x2c\x34\x34\x39\x2e\x30\x37\x2c\x32\x38\ +\x30\x2e\x33\x37\x5a\x6d\x33\x38\x2e\x33\x31\x2c\x31\x30\x38\x2e\ +\x34\x39\x63\x30\x2d\x31\x36\x2e\x35\x34\x2e\x39\x34\x2d\x33\x32\ +\x2e\x37\x33\x2d\x2e\x32\x35\x2d\x34\x38\x2e\x37\x37\x2d\x31\x2e\ +\x33\x31\x2d\x31\x37\x2e\x38\x32\x2d\x31\x35\x2e\x39\x33\x2d\x32\ +\x39\x2e\x37\x35\x2d\x33\x32\x2e\x37\x37\x2d\x32\x38\x2e\x37\x37\ +\x2d\x31\x36\x2e\x36\x35\x2c\x31\x2d\x32\x39\x2e\x32\x31\x2c\x31\ +\x34\x2e\x37\x33\x2d\x32\x39\x2e\x34\x32\x2c\x33\x32\x2e\x35\x32\ +\x2d\x2e\x31\x36\x2c\x31\x33\x2e\x37\x37\x2c\x30\x2c\x32\x37\x2e\ +\x35\x34\x2c\x30\x2c\x34\x31\x2e\x33\x31\x61\x33\x33\x2e\x31\x39\ +\x2c\x33\x33\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x30\x2c\x2e\x34\x32\ +\x2c\x33\x2e\x37\x31\x5a\x6d\x2d\x33\x31\x2e\x33\x33\x2c\x34\x36\ +\x2e\x37\x31\x61\x31\x34\x2e\x39\x31\x2c\x31\x34\x2e\x39\x31\x2c\ +\x30\x2c\x30\x2c\x30\x2d\x31\x33\x2e\x36\x38\x2c\x38\x2e\x36\x36\ +\x63\x2d\x32\x2e\x38\x32\x2c\x35\x2e\x35\x37\x2d\x32\x2e\x38\x2c\ +\x31\x31\x2e\x35\x39\x2c\x31\x2e\x36\x2c\x31\x35\x2e\x38\x35\x2c\ +\x34\x2e\x31\x36\x2c\x34\x2c\x34\x2e\x34\x34\x2c\x38\x2e\x33\x35\ +\x2c\x34\x2e\x32\x32\x2c\x31\x33\x2e\x33\x32\x61\x36\x38\x2e\x33\ +\x38\x2c\x36\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2c\ +\x37\x2e\x37\x31\x63\x2e\x33\x37\x2c\x35\x2e\x33\x33\x2c\x33\x2e\ +\x36\x2c\x38\x2e\x38\x33\x2c\x38\x2c\x38\x2e\x38\x37\x73\x37\x2e\ +\x37\x35\x2d\x33\x2e\x34\x36\x2c\x38\x2e\x30\x37\x2d\x38\x2e\x37\ +\x36\x61\x31\x31\x31\x2e\x34\x34\x2c\x31\x31\x31\x2e\x34\x34\x2c\ +\x30\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x31\x2e\x35\x36\x2c\x39\x2e\ +\x36\x38\x2c\x39\x2e\x36\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x33\x2e\ +\x32\x2d\x38\x2e\x31\x33\x63\x34\x2e\x37\x31\x2d\x34\x2e\x36\x34\ +\x2c\x35\x2e\x36\x31\x2d\x31\x30\x2e\x35\x33\x2c\x33\x2d\x31\x36\ +\x2e\x37\x32\x41\x31\x35\x2e\x34\x38\x2c\x31\x35\x2e\x34\x38\x2c\ +\x30\x2c\x30\x2c\x30\x2c\x34\x35\x36\x2e\x30\x35\x2c\x34\x33\x35\ +\x2e\x35\x37\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\ +\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\x22\x20\x64\x3d\x22\x4d\x33\ +\x37\x39\x2e\x35\x38\x2c\x33\x38\x34\x2e\x33\x31\x63\x33\x2e\x31\ +\x38\x2d\x2e\x30\x36\x2c\x36\x2e\x33\x31\x2d\x2e\x30\x35\x2c\x39\ +\x2e\x36\x31\x2c\x30\x2c\x30\x2d\x33\x2e\x32\x34\x2c\x30\x2d\x36\ +\x2e\x34\x33\x2c\x30\x2d\x39\x2e\x36\x31\x2c\x30\x2d\x31\x30\x2e\ +\x35\x33\x2c\x30\x2d\x32\x30\x2e\x34\x38\x2c\x30\x2d\x33\x30\x2e\ +\x35\x38\x2d\x33\x32\x2e\x31\x34\x2d\x32\x35\x2e\x32\x37\x2d\x36\ +\x38\x2e\x39\x33\x2d\x33\x35\x2e\x34\x32\x2d\x31\x31\x30\x2e\x34\ +\x34\x2d\x32\x39\x2e\x35\x37\x43\x32\x34\x36\x2c\x33\x31\x39\x2e\ +\x31\x34\x2c\x32\x31\x38\x2c\x33\x33\x34\x2e\x36\x31\x2c\x31\x39\ +\x34\x2e\x31\x33\x2c\x33\x35\x39\x63\x2d\x36\x2c\x36\x2e\x31\x37\ +\x2d\x39\x2e\x36\x33\x2c\x31\x33\x2e\x35\x39\x2d\x39\x2e\x36\x31\ +\x2c\x32\x32\x2e\x38\x31\x61\x36\x38\x2e\x39\x33\x2c\x36\x38\x2e\ +\x39\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x31\x2c\x37\x2e\x33\x38\x63\ +\x35\x2e\x36\x33\x2c\x32\x33\x2e\x32\x38\x2c\x33\x31\x2e\x32\x37\ +\x2c\x33\x30\x2e\x32\x38\x2c\x34\x37\x2e\x39\x33\x2c\x31\x33\x2e\ +\x32\x37\x2c\x32\x34\x2e\x36\x31\x2d\x32\x35\x2e\x31\x31\x2c\x35\ +\x33\x2e\x37\x35\x2d\x33\x33\x2e\x38\x36\x2c\x38\x37\x2d\x32\x36\ +\x2e\x37\x37\x2c\x31\x35\x2c\x33\x2e\x32\x2c\x32\x38\x2c\x31\x30\ +\x2e\x33\x35\x2c\x33\x39\x2e\x36\x2c\x32\x30\x2e\x34\x32\x43\x33\ +\x36\x33\x2e\x31\x37\x2c\x33\x38\x38\x2e\x38\x32\x2c\x33\x37\x30\ +\x2e\x30\x37\x2c\x33\x38\x34\x2e\x34\x39\x2c\x33\x37\x39\x2e\x35\ +\x38\x2c\x33\x38\x34\x2e\x33\x31\x5a\x22\x2f\x3e\x3c\x70\x61\x74\ +\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x31\x22\x20\ +\x64\x3d\x22\x4d\x34\x35\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\ +\x31\x61\x32\x36\x2e\x31\x32\x2c\x32\x36\x2e\x31\x32\x2c\x30\x2c\ +\x30\x2c\x30\x2d\x37\x2e\x37\x39\x2c\x31\x2e\x36\x36\x63\x35\x2e\ +\x35\x39\x2c\x35\x2e\x37\x32\x2c\x31\x32\x2e\x33\x34\x2c\x38\x2e\ +\x37\x38\x2c\x32\x33\x2c\x38\x2e\x36\x31\x61\x32\x34\x2c\x32\x34\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x36\x2e\x32\x35\x2d\x31\x2e\x32\x37\ +\x41\x32\x35\x2e\x36\x38\x2c\x32\x35\x2e\x36\x38\x2c\x30\x2c\x30\ +\x2c\x30\x2c\x34\x35\x34\x2e\x36\x35\x2c\x33\x31\x36\x2e\x33\x31\ \x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\ -\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x35\x34\x2e\ -\x36\x35\x2c\x33\x32\x37\x2e\x35\x37\x61\x32\x36\x2e\x31\x32\x2c\ -\x32\x36\x2e\x31\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x37\x2e\x37\x39\ -\x2c\x31\x2e\x36\x36\x63\x35\x2e\x35\x39\x2c\x35\x2e\x37\x32\x2c\ -\x31\x32\x2e\x33\x34\x2c\x38\x2e\x37\x39\x2c\x32\x33\x2c\x38\x2e\ -\x36\x31\x61\x32\x33\x2e\x35\x35\x2c\x32\x33\x2e\x35\x35\x2c\x30\ -\x2c\x30\x2c\x30\x2c\x36\x2e\x32\x35\x2d\x31\x2e\x32\x37\x41\x32\ -\x35\x2e\x37\x31\x2c\x32\x35\x2e\x37\x31\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x34\x35\x34\x2e\x36\x35\x2c\x33\x32\x37\x2e\x35\x37\x5a\x22\ -\x2f\x3e\x3c\x70\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\ -\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x33\x36\x2e\x36\x32\ -\x2c\x32\x38\x39\x2e\x37\x34\x63\x32\x2e\x36\x31\x2d\x2e\x38\x2c\ -\x35\x2e\x32\x33\x2d\x31\x2e\x34\x35\x2c\x37\x2e\x37\x37\x2d\x32\ -\x2e\x30\x38\x6c\x33\x2e\x34\x32\x2d\x2e\x38\x37\x2e\x36\x32\x2d\ -\x2e\x31\x36\x68\x31\x36\x6c\x2e\x39\x33\x2e\x33\x39\x63\x2e\x33\ -\x36\x2e\x31\x36\x2e\x37\x32\x2e\x33\x33\x2c\x31\x2e\x30\x37\x2e\ -\x35\x31\x6c\x2e\x33\x37\x2e\x31\x38\x61\x36\x36\x2e\x31\x38\x2c\ -\x36\x36\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x31\x2c\x32\x36\x2e\x38\ -\x33\x2c\x31\x30\x2e\x33\x38\x2c\x33\x30\x2e\x34\x35\x2c\x33\x30\ -\x2e\x34\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\x39\x34\x2d\x31\ -\x30\x2e\x31\x35\x2c\x32\x30\x35\x2e\x38\x33\x2c\x32\x30\x35\x2e\ -\x38\x33\x2c\x30\x2c\x30\x2c\x30\x2d\x32\x33\x2e\x32\x35\x2d\x32\ -\x32\x2e\x37\x63\x2d\x36\x30\x2e\x32\x32\x2d\x35\x30\x2e\x33\x2d\ -\x31\x32\x38\x2e\x33\x31\x2d\x37\x30\x2d\x32\x30\x33\x2e\x39\x31\ -\x2d\x35\x38\x2e\x33\x36\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\ -\x31\x2d\x31\x30\x35\x2e\x38\x34\x2c\x33\x36\x2e\x30\x35\x2d\x31\ -\x34\x36\x2e\x38\x34\x2c\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\x37\ -\x2c\x38\x2e\x37\x33\x2d\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\x36\ -\x33\x2d\x36\x2e\x38\x39\x2c\x33\x31\x2e\x35\x32\x2c\x37\x2e\x32\ -\x34\x2c\x32\x32\x2c\x33\x31\x2e\x35\x36\x2c\x32\x36\x2e\x38\x33\ -\x2c\x34\x37\x2e\x39\x31\x2c\x31\x30\x2c\x34\x39\x2e\x32\x32\x2d\ -\x35\x30\x2e\x35\x37\x2c\x31\x30\x38\x2d\x37\x31\x2e\x30\x37\x2c\ -\x31\x37\x35\x2e\x33\x39\x2d\x36\x31\x2e\x34\x41\x31\x38\x38\x2e\ -\x31\x34\x2c\x31\x38\x38\x2e\x31\x34\x2c\x30\x2c\x30\x2c\x31\x2c\ -\x34\x31\x35\x2c\x33\x30\x30\x2e\x39\x34\x2c\x36\x37\x2e\x34\x39\ -\x2c\x36\x37\x2e\x34\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x33\x36\ -\x2e\x36\x32\x2c\x32\x38\x39\x2e\x37\x34\x5a\x22\x2f\x3e\x3c\x70\ -\x61\x74\x68\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x32\ -\x22\x20\x64\x3d\x22\x4d\x34\x34\x39\x2e\x30\x37\x2c\x32\x39\x31\ -\x2e\x36\x33\x68\x31\x34\x2e\x33\x35\x61\x31\x30\x2e\x36\x34\x2c\ -\x31\x30\x2e\x36\x34\x2c\x30\x2c\x30\x2c\x30\x2c\x32\x2e\x34\x32\ -\x2c\x31\x63\x32\x37\x2e\x34\x37\x2c\x34\x2c\x34\x39\x2e\x36\x2c\ -\x32\x36\x2e\x36\x37\x2c\x35\x31\x2e\x36\x38\x2c\x35\x34\x2e\x32\ -\x36\x2c\x31\x2e\x31\x37\x2c\x31\x35\x2e\x35\x33\x2e\x35\x38\x2c\ -\x33\x31\x2e\x32\x2e\x37\x38\x2c\x34\x36\x2e\x38\x2c\x30\x2c\x32\ -\x2e\x31\x33\x2c\x30\x2c\x34\x2e\x32\x35\x2c\x30\x2c\x36\x2e\x38\ -\x37\x68\x31\x31\x2e\x37\x35\x63\x31\x33\x2e\x39\x34\x2c\x30\x2c\ -\x31\x39\x2e\x33\x36\x2c\x35\x2e\x33\x39\x2c\x31\x39\x2e\x33\x36\ -\x2c\x31\x39\x2e\x32\x32\x2c\x30\x2c\x33\x33\x2e\x32\x35\x2d\x2e\ -\x31\x38\x2c\x36\x36\x2e\x35\x2e\x31\x32\x2c\x39\x39\x2e\x37\x35\ -\x2e\x30\x39\x2c\x39\x2e\x36\x38\x2d\x32\x2e\x39\x33\x2c\x31\x36\ -\x2e\x36\x37\x2d\x31\x32\x2e\x31\x36\x2c\x32\x30\x2e\x33\x39\x48\ -\x33\x37\x35\x2e\x31\x32\x63\x2d\x37\x2e\x32\x34\x2d\x33\x2e\x31\ -\x37\x2d\x31\x32\x2d\x38\x2e\x31\x31\x2d\x31\x32\x2d\x31\x36\x2e\ -\x35\x33\x2c\x30\x2d\x33\x35\x2e\x34\x36\x2d\x2e\x30\x38\x2d\x37\ -\x30\x2e\x39\x31\x2c\x30\x2d\x31\x30\x36\x2e\x33\x37\x2c\x30\x2d\ -\x31\x30\x2e\x32\x39\x2c\x36\x2e\x32\x34\x2d\x31\x36\x2e\x32\x34\ -\x2c\x31\x36\x2e\x35\x36\x2d\x31\x36\x2e\x34\x34\x2c\x34\x2e\x36\ -\x39\x2d\x2e\x30\x39\x2c\x39\x2e\x33\x39\x2c\x30\x2c\x31\x34\x2e\ -\x35\x31\x2c\x30\x2c\x30\x2d\x31\x35\x2e\x37\x33\x2d\x2e\x30\x38\ -\x2d\x33\x30\x2e\x35\x39\x2c\x30\x2d\x34\x35\x2e\x34\x34\x2e\x31\ -\x39\x2d\x32\x38\x2e\x38\x2c\x31\x37\x2e\x32\x38\x2d\x35\x32\x2e\ -\x33\x37\x2c\x34\x33\x2e\x38\x39\x2d\x36\x30\x2e\x35\x39\x43\x34\ -\x34\x31\x2e\x37\x31\x2c\x32\x39\x33\x2e\x34\x31\x2c\x34\x34\x35\ -\x2e\x34\x31\x2c\x32\x39\x32\x2e\x35\x39\x2c\x34\x34\x39\x2e\x30\ -\x37\x2c\x32\x39\x31\x2e\x36\x33\x5a\x6d\x33\x38\x2e\x33\x31\x2c\ -\x31\x30\x38\x2e\x34\x39\x63\x30\x2d\x31\x36\x2e\x35\x34\x2e\x39\ -\x34\x2d\x33\x32\x2e\x37\x33\x2d\x2e\x32\x35\x2d\x34\x38\x2e\x37\ -\x37\x2d\x31\x2e\x33\x31\x2d\x31\x37\x2e\x38\x32\x2d\x31\x35\x2e\ -\x39\x33\x2d\x32\x39\x2e\x37\x35\x2d\x33\x32\x2e\x37\x37\x2d\x32\ -\x38\x2e\x37\x37\x2d\x31\x36\x2e\x36\x35\x2c\x31\x2d\x32\x39\x2e\ -\x32\x31\x2c\x31\x34\x2e\x37\x33\x2d\x32\x39\x2e\x34\x32\x2c\x33\ -\x32\x2e\x35\x32\x2d\x2e\x31\x36\x2c\x31\x33\x2e\x37\x37\x2c\x30\ -\x2c\x32\x37\x2e\x35\x34\x2c\x30\x2c\x34\x31\x2e\x33\x31\x61\x33\ -\x33\x2e\x31\x39\x2c\x33\x33\x2e\x31\x39\x2c\x30\x2c\x30\x2c\x30\ -\x2c\x2e\x34\x32\x2c\x33\x2e\x37\x31\x5a\x6d\x2d\x33\x31\x2e\x33\ -\x33\x2c\x34\x36\x2e\x37\x31\x61\x31\x34\x2e\x39\x32\x2c\x31\x34\ -\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x33\x2e\x36\x38\x2c\ -\x38\x2e\x36\x36\x63\x2d\x32\x2e\x38\x32\x2c\x35\x2e\x35\x37\x2d\ -\x32\x2e\x38\x2c\x31\x31\x2e\x35\x39\x2c\x31\x2e\x36\x2c\x31\x35\ -\x2e\x38\x36\x2c\x34\x2e\x31\x36\x2c\x34\x2c\x34\x2e\x34\x34\x2c\ -\x38\x2e\x33\x34\x2c\x34\x2e\x32\x32\x2c\x31\x33\x2e\x33\x31\x61\ -\x36\x38\x2e\x33\x38\x2c\x36\x38\x2e\x33\x38\x2c\x30\x2c\x30\x2c\ -\x30\x2c\x30\x2c\x37\x2e\x37\x31\x63\x2e\x33\x37\x2c\x35\x2e\x33\ -\x34\x2c\x33\x2e\x36\x2c\x38\x2e\x38\x33\x2c\x38\x2c\x38\x2e\x38\ -\x37\x73\x37\x2e\x37\x35\x2d\x33\x2e\x34\x36\x2c\x38\x2e\x30\x37\ -\x2d\x38\x2e\x37\x36\x61\x31\x31\x31\x2e\x34\x33\x2c\x31\x31\x31\ -\x2e\x34\x33\x2c\x30\x2c\x30\x2c\x30\x2c\x30\x2d\x31\x31\x2e\x35\ -\x36\x2c\x39\x2e\x36\x39\x2c\x39\x2e\x36\x39\x2c\x30\x2c\x30\x2c\ -\x31\x2c\x33\x2e\x32\x2d\x38\x2e\x31\x33\x63\x34\x2e\x37\x31\x2d\ -\x34\x2e\x36\x34\x2c\x35\x2e\x36\x31\x2d\x31\x30\x2e\x35\x33\x2c\ -\x33\x2d\x31\x36\x2e\x37\x32\x41\x31\x35\x2e\x34\x38\x2c\x31\x35\ -\x2e\x34\x38\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x35\x36\x2e\x30\x35\ -\x2c\x34\x34\x36\x2e\x38\x33\x5a\x22\x2f\x3e\x3c\x70\x61\x74\x68\ -\x20\x63\x6c\x61\x73\x73\x3d\x22\x63\x6c\x73\x2d\x33\x22\x20\x64\ -\x3d\x22\x4d\x33\x35\x38\x2e\x32\x34\x2c\x34\x31\x35\x2e\x38\x38\ -\x63\x30\x2d\x31\x32\x2e\x30\x35\x2c\x37\x2e\x37\x2d\x31\x39\x2e\ -\x36\x39\x2c\x32\x30\x2d\x31\x39\x2e\x39\x33\x2c\x33\x2e\x32\x35\ -\x2d\x2e\x30\x36\x2c\x36\x2e\x34\x35\x2c\x30\x2c\x39\x2e\x38\x34\ -\x2c\x30\x68\x31\x2e\x32\x34\x63\x30\x2d\x32\x2e\x35\x32\x2c\x30\ -\x2d\x35\x2c\x30\x2d\x37\x2e\x34\x38\x6c\x2d\x36\x31\x2e\x37\x31\ -\x2d\x38\x35\x2e\x36\x32\x2c\x31\x37\x32\x2e\x32\x31\x2d\x32\x33\ -\x39\x48\x34\x34\x31\x2e\x38\x37\x4c\x32\x39\x38\x2e\x36\x32\x2c\ -\x32\x36\x32\x2e\x36\x33\x2c\x31\x35\x35\x2e\x33\x38\x2c\x36\x33\ -\x2e\x38\x36\x48\x39\x37\x2e\x34\x35\x6c\x31\x37\x32\x2e\x32\x31\ -\x2c\x32\x33\x39\x2d\x31\x37\x32\x2e\x32\x33\x2c\x32\x33\x39\x68\ -\x35\x37\x2e\x39\x35\x4c\x32\x39\x38\x2e\x36\x32\x2c\x33\x34\x33\ -\x6c\x35\x39\x2e\x35\x39\x2c\x38\x32\x2e\x36\x39\x5a\x22\x2f\x3e\ -\x3c\x2f\x73\x76\x67\x3e\ -<<<<<<< HEAD -======= +\x22\x63\x6c\x73\x2d\x31\x22\x20\x64\x3d\x22\x4d\x34\x33\x36\x2e\ +\x36\x32\x2c\x32\x37\x38\x2e\x34\x38\x63\x32\x2e\x36\x31\x2d\x2e\ +\x38\x2c\x35\x2e\x32\x33\x2d\x31\x2e\x34\x36\x2c\x37\x2e\x37\x37\ +\x2d\x32\x2e\x30\x39\x2c\x31\x2e\x31\x34\x2d\x2e\x32\x38\x2c\x32\ +\x2e\x32\x38\x2d\x2e\x35\x36\x2c\x33\x2e\x34\x32\x2d\x2e\x38\x36\ +\x6c\x2e\x36\x32\x2d\x2e\x31\x36\x68\x31\x36\x6c\x2e\x39\x33\x2e\ +\x33\x39\x63\x2e\x33\x36\x2e\x31\x35\x2e\x37\x32\x2e\x33\x33\x2c\ +\x31\x2e\x30\x37\x2e\x35\x6c\x2e\x33\x37\x2e\x31\x39\x61\x36\x36\ +\x2e\x31\x38\x2c\x36\x36\x2e\x31\x38\x2c\x30\x2c\x30\x2c\x31\x2c\ +\x32\x36\x2e\x38\x33\x2c\x31\x30\x2e\x33\x38\x2c\x33\x30\x2e\x33\ +\x35\x2c\x33\x30\x2e\x33\x35\x2c\x30\x2c\x30\x2c\x30\x2d\x35\x2e\ +\x39\x34\x2d\x31\x30\x2e\x31\x35\x41\x32\x30\x35\x2c\x32\x30\x35\ +\x2c\x30\x2c\x30\x2c\x30\x2c\x34\x36\x34\x2e\x34\x34\x2c\x32\x35\ +\x34\x63\x2d\x36\x30\x2e\x32\x32\x2d\x35\x30\x2e\x33\x31\x2d\x31\ +\x32\x38\x2e\x33\x31\x2d\x37\x30\x2d\x32\x30\x33\x2e\x39\x31\x2d\ +\x35\x38\x2e\x33\x36\x2d\x35\x37\x2e\x30\x39\x2c\x38\x2e\x38\x2d\ +\x31\x30\x35\x2e\x38\x34\x2c\x33\x36\x2d\x31\x34\x36\x2e\x38\x34\ +\x2c\x37\x39\x2e\x32\x38\x2d\x38\x2e\x32\x37\x2c\x38\x2e\x37\x32\ +\x2d\x31\x30\x2e\x37\x39\x2c\x31\x39\x2e\x36\x32\x2d\x36\x2e\x38\ +\x39\x2c\x33\x31\x2e\x35\x32\x2c\x37\x2e\x32\x34\x2c\x32\x32\x2c\ +\x33\x31\x2e\x35\x36\x2c\x32\x36\x2e\x38\x33\x2c\x34\x37\x2e\x39\ +\x31\x2c\x31\x30\x2c\x34\x39\x2e\x32\x32\x2d\x35\x30\x2e\x35\x37\ +\x2c\x31\x30\x38\x2d\x37\x31\x2e\x30\x37\x2c\x31\x37\x35\x2e\x33\ +\x39\x2d\x36\x31\x2e\x34\x41\x31\x38\x37\x2e\x39\x32\x2c\x31\x38\ +\x37\x2e\x39\x32\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x31\x35\x2c\x32\ +\x38\x39\x2e\x36\x38\x2c\x36\x37\x2e\x34\x39\x2c\x36\x37\x2e\x34\ +\x39\x2c\x30\x2c\x30\x2c\x31\x2c\x34\x33\x36\x2e\x36\x32\x2c\x32\ +\x37\x38\x2e\x34\x38\x5a\x22\x2f\x3e\x3c\x2f\x73\x76\x67\x3e\ \x00\x00\x0a\x76\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -22209,7 +21311,6 @@ \x35\x37\x2e\x39\x35\x4c\x32\x39\x38\x2e\x36\x32\x2c\x33\x34\x33\ \x6c\x35\x39\x2e\x35\x39\x2c\x38\x32\x2e\x36\x39\x5a\x22\x2f\x3e\ \x3c\x2f\x73\x76\x67\x3e\ ->>>>>>> origin/main \x00\x00\x0a\x70\ \x3c\ \x73\x76\x67\x20\x69\x64\x3d\x22\x4c\x61\x79\x65\x72\x5f\x31\x22\ @@ -27949,17 +27050,6 @@ \x0c\xa5\x75\xc7\ \x00\x6c\ \x00\x6f\x00\x67\x00\x6f\x00\x5f\x00\x42\x00\x4c\x00\x4f\x00\x43\x00\x4b\x00\x53\x00\x2e\x00\x73\x00\x76\x00\x67\ -<<<<<<< HEAD -======= -\x00\x0d\ -\x00\xdd\x57\xa7\ -\x00\x31\ -\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0d\ -\x02\xdd\x57\xa7\ -\x00\x30\ -\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ ->>>>>>> origin/main \x00\x0f\ \x05\x15\x19\xa7\ \x00\x77\ @@ -27989,10 +27079,6 @@ \x00\x34\ \x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ \x00\x0d\ -\x0a\xdd\x57\x87\ -\x00\x34\ -\x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ -\x00\x0d\ \x0c\xdd\x57\x87\ \x00\x33\ \x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ @@ -28010,13 +27096,6 @@ \x00\x32\ \x00\x62\x00\x61\x00\x72\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x5f\x00\x70\x00\x72\x00\x6f\x00\x74\x00\x65\x00\x63\x00\x74\ \x00\x65\x00\x64\x00\x2e\x00\x73\x00\x76\x00\x67\ -<<<<<<< HEAD -======= -\x00\x0b\ -\x0f\x22\xf7\x67\ -\x00\x6e\ -\x00\x6f\x00\x5f\x00\x77\x00\x69\x00\x66\x00\x69\x00\x2e\x00\x73\x00\x76\x00\x67\ ->>>>>>> origin/main \x00\x17\ \x0f\x29\x74\x27\ \x00\x33\ @@ -28264,7 +27343,6 @@ qt_resource_struct_v1 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x10\x00\x00\x00\x01\ -<<<<<<< HEAD \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa3\ \x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ \x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ @@ -28279,22 +27357,6 @@ \x00\x00\x01\x02\x00\x02\x00\x00\x00\x01\x00\x00\x00\x38\ \x00\x00\x01\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x34\ \x00\x00\x01\x42\x00\x02\x00\x00\x00\x01\x00\x00\x00\x28\ -======= -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa1\ -\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9e\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ -\x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x80\ -\x00\x00\x00\x5a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7c\ -\x00\x00\x00\x6c\x00\x02\x00\x00\x00\x01\x00\x00\x00\x76\ -\x00\x00\x00\x7e\x00\x02\x00\x00\x00\x01\x00\x00\x00\x68\ -\x00\x00\x00\x90\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5e\ -\x00\x00\x00\xa2\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4f\ -\x00\x00\x00\xc0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4a\ -\x00\x00\x00\xdc\x00\x02\x00\x00\x00\x01\x00\x00\x00\x3a\ -\x00\x00\x01\x02\x00\x02\x00\x00\x00\x01\x00\x00\x00\x36\ -\x00\x00\x01\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x32\ -\x00\x00\x01\x42\x00\x02\x00\x00\x00\x01\x00\x00\x00\x26\ ->>>>>>> origin/main \x00\x00\x01\x68\x00\x02\x00\x00\x00\x01\x00\x00\x00\x20\ \x00\x00\x01\x84\x00\x02\x00\x00\x00\x01\x00\x00\x00\x11\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x12\ @@ -28317,7 +27379,6 @@ \x00\x00\x03\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x3a\x55\ \x00\x00\x03\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x3c\xcf\ \x00\x00\x03\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x3f\x43\ -<<<<<<< HEAD \x00\x00\x04\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x40\xdd\ \x00\x00\x04\x38\x00\x00\x00\x00\x00\x01\x00\x00\x43\x59\ \x00\x00\x04\x5a\x00\x00\x00\x00\x00\x01\x00\x00\x45\xd5\ @@ -28489,192 +27550,16 @@ \x00\x00\x18\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x51\x4c\ \x00\x00\x18\x82\x00\x00\x00\x00\x00\x01\x00\x06\x56\x67\ \x00\x00\x18\xaa\x00\x00\x00\x00\x00\x01\x00\x06\x57\xba\ -======= -\x00\x00\x04\x16\x00\x00\x00\x00\x00\x01\x00\x00\x41\xbf\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x27\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x28\ -\x00\x00\x04\x38\x00\x00\x00\x00\x00\x01\x00\x00\x44\x3b\ -\x00\x00\x04\x52\x00\x00\x00\x00\x00\x01\x00\x00\x45\x3c\ -\x00\x00\x04\x82\x00\x00\x00\x00\x00\x01\x00\x00\x4a\x54\ -\x00\x00\x04\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x56\x16\ -\x00\x00\x04\xdc\x00\x00\x00\x00\x00\x01\x00\x00\x58\x86\ -\x00\x00\x05\x02\x00\x00\x00\x00\x00\x01\x00\x00\x5e\x1e\ -\x00\x00\x05\x26\x00\x00\x00\x00\x00\x01\x00\x00\x62\x7a\ -\x00\x00\x05\x60\x00\x00\x00\x00\x00\x01\x00\x00\x69\x07\ -\x00\x00\x05\x7c\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x03\ -\x00\x00\x05\xa4\x00\x00\x00\x00\x00\x01\x00\x00\x6d\x01\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x33\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x34\ -\x00\x00\x05\xd6\x00\x01\x00\x00\x00\x01\x00\x00\x77\x6b\ -\x00\x00\x05\xe8\x00\x00\x00\x00\x00\x01\x00\x02\x3a\xc0\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x37\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x38\ -\x00\x00\x05\xfc\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ -\x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x41\x71\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3b\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x44\ -\x00\x00\x06\x5a\x00\x02\x00\x00\x00\x07\x00\x00\x00\x3d\ -\x00\x00\x06\x6c\x00\x00\x00\x00\x00\x01\x00\x02\x43\x77\ -\x00\x00\x06\xa0\x00\x00\x00\x00\x00\x01\x00\x02\x4d\xdb\ -\x00\x00\x06\xd4\x00\x00\x00\x00\x00\x01\x00\x02\x58\x04\ -\x00\x00\x07\x0e\x00\x00\x00\x00\x00\x01\x00\x02\x62\x5c\ -\x00\x00\x07\x42\x00\x00\x00\x00\x00\x01\x00\x02\x6c\x74\ -\x00\x00\x07\x78\x00\x00\x00\x00\x00\x01\x00\x02\x76\x9d\ -\x00\x00\x07\xb0\x00\x00\x00\x00\x00\x01\x00\x02\x80\xb3\ -\x00\x00\x07\xe6\x00\x00\x00\x00\x00\x01\x00\x02\x8b\x19\ -\x00\x00\x08\x0e\x00\x00\x00\x00\x00\x01\x00\x02\x95\x48\ -\x00\x00\x08\x3a\x00\x00\x00\x00\x00\x01\x00\x02\x9f\x8f\ -\x00\x00\x08\x76\x00\x00\x00\x00\x00\x01\x00\x02\xa1\x90\ -\x00\x00\x08\xa2\x00\x00\x00\x00\x00\x01\x00\x02\xbc\xd5\ -\x00\x00\x08\xce\x00\x00\x00\x00\x00\x01\x00\x02\xc2\x1e\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4b\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x03\x00\x00\x00\x4c\ -\x00\x00\x09\x02\x00\x00\x00\x00\x00\x01\x00\x02\xc5\x96\ -\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x02\xce\x78\ -\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x02\xd3\xad\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x50\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x51\ -\x00\x00\x09\x4e\x00\x00\x00\x00\x00\x01\x00\x02\xdd\x82\ -\x00\x00\x09\x76\x00\x00\x00\x00\x00\x01\x00\x02\xe2\x7d\ -\x00\x00\x09\xae\x00\x00\x00\x00\x00\x01\x00\x02\xeb\x51\ -\x00\x00\x09\xe6\x00\x00\x00\x00\x00\x01\x00\x02\xf3\xf5\ -\x00\x00\x0a\x16\x00\x00\x00\x00\x00\x01\x00\x02\xfb\x94\ -\x00\x00\x0a\x3a\x00\x00\x00\x00\x00\x01\x00\x03\x03\x70\ -\x00\x00\x0a\x5e\x00\x00\x00\x00\x00\x01\x00\x03\x0b\x26\ -\x00\x00\x0a\x92\x00\x00\x00\x00\x00\x01\x00\x03\x13\x34\ -\x00\x00\x0a\xc6\x00\x00\x00\x00\x00\x01\x00\x03\x1b\x16\ -\x00\x00\x0a\xfa\x00\x00\x00\x00\x00\x01\x00\x03\x22\xe0\ -\x00\x00\x0b\x34\x00\x00\x00\x00\x00\x01\x00\x03\x2a\x47\ -\x00\x00\x0b\x58\x00\x00\x00\x00\x00\x01\x00\x03\x2e\x04\ -\x00\x00\x0b\x86\x00\x00\x00\x00\x00\x01\x00\x03\x31\xdd\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x5f\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x08\x00\x00\x00\x60\ -\x00\x00\x0b\xac\x00\x00\x00\x00\x00\x01\x00\x03\x37\xe6\ -\x00\x00\x0b\xd8\x00\x00\x00\x00\x00\x01\x00\x03\x5a\x6a\ -\x00\x00\x0c\x06\x00\x00\x00\x00\x00\x01\x00\x03\x60\x57\ -\x00\x00\x0c\x30\x00\x00\x00\x00\x00\x01\x00\x03\x62\x77\ -\x00\x00\x0c\x58\x00\x00\x00\x00\x00\x01\x00\x03\x6b\x0f\ -\x00\x00\x0c\x72\x00\x00\x00\x00\x00\x01\x00\x03\x7a\x58\ -\x00\x00\x0c\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x80\xd6\ -\x00\x00\x0c\xc6\x00\x00\x00\x00\x00\x01\x00\x03\x8b\x50\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x69\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x6a\ -\x00\x00\x0c\xfc\x00\x00\x00\x00\x00\x01\x00\x03\x94\x97\ -\x00\x00\x0d\x2a\x00\x00\x00\x00\x00\x01\x00\x03\x97\x3e\ -\x00\x00\x0d\x58\x00\x01\x00\x00\x00\x01\x00\x03\xa3\xbb\ -\x00\x00\x0d\x84\x00\x00\x00\x00\x00\x01\x00\x03\xd1\x3a\ -\x00\x00\x0d\xa4\x00\x00\x00\x00\x00\x01\x00\x03\xd5\xf1\ -\x00\x00\x0d\xd6\x00\x01\x00\x00\x00\x01\x00\x04\x2e\xea\ -\x00\x00\x0e\x08\x00\x00\x00\x00\x00\x01\x00\x04\x63\x84\ -\x00\x00\x0e\x22\x00\x00\x00\x00\x00\x01\x00\x04\x68\xce\ -\x00\x00\x0e\x3c\x00\x00\x00\x00\x00\x01\x00\x04\x6e\x5d\ -\x00\x00\x0e\x56\x00\x00\x00\x00\x00\x01\x00\x04\x73\xc6\ -\x00\x00\x0e\x6e\x00\x00\x00\x00\x00\x01\x00\x04\x7f\xa4\ -\x00\x00\x0e\x8c\x00\x00\x00\x00\x00\x01\x00\x04\x85\xa8\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x77\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x78\ -\x00\x00\x0e\xb4\x00\x00\x00\x00\x00\x01\x00\x04\x8a\xdd\ -\x00\x00\x0e\xc8\x00\x00\x00\x00\x00\x01\x00\x04\x90\xda\ -\x00\x00\x0e\xda\x00\x00\x00\x00\x00\x01\x00\x04\x92\x60\ -\x00\x00\x0e\xec\x00\x00\x00\x00\x00\x01\x00\x04\x98\x5a\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7d\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x7e\ -\x00\x00\x0f\x00\x00\x00\x00\x00\x00\x01\x00\x04\x9a\xb0\ -\x00\x00\x0f\x2c\x00\x00\x00\x00\x00\x01\x00\x04\xa1\x97\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x81\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x10\x00\x00\x00\x82\ -\x00\x00\x0f\x50\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xfb\ -\x00\x00\x0f\x70\x00\x00\x00\x00\x00\x01\x00\x04\xb0\x94\ -\x00\x00\x0f\x90\x00\x01\x00\x00\x00\x01\x00\x04\xb6\xbb\ -\x00\x00\x0f\xb4\x00\x00\x00\x00\x00\x01\x00\x04\xc2\x0c\ -\x00\x00\x0f\xd6\x00\x00\x00\x00\x00\x01\x00\x04\xc9\xb1\ -\x00\x00\x0f\xf2\x00\x00\x00\x00\x00\x01\x00\x04\xda\xb1\ -\x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x32\ -\x00\x00\x10\x36\x00\x00\x00\x00\x00\x01\x00\x04\xe9\xb6\ -\x00\x00\x10\x56\x00\x00\x00\x00\x00\x01\x00\x04\xef\x4f\ -\x00\x00\x10\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xf9\x18\ -\x00\x00\x10\x9e\x00\x00\x00\x00\x00\x01\x00\x04\xfe\xb1\ -\x00\x00\x10\xd2\x00\x00\x00\x00\x00\x01\x00\x05\x09\x25\ -\x00\x00\x10\xee\x00\x00\x00\x00\x00\x01\x00\x05\x0f\x60\ -\x00\x00\x11\x22\x00\x00\x00\x00\x00\x01\x00\x05\x19\xda\ -\x00\x00\x11\x56\x00\x00\x00\x00\x00\x01\x00\x05\x24\x39\ -\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x84\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x94\ -\x00\x00\x11\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x39\xf8\ -\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x43\x8e\ -\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6a\ -\x00\x00\x12\x42\x00\x00\x00\x00\x00\x01\x00\x05\x55\xae\ -\x00\x00\x12\x6c\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x37\ -\x00\x00\x12\x98\x00\x00\x00\x00\x00\x01\x00\x05\x63\x95\ -\x00\x00\x12\xce\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x84\ -\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x79\x27\ -\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x7e\x5c\ -\x00\x00\x12\xec\x00\x00\x00\x00\x00\x01\x00\x05\x88\x31\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9f\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ -\x00\x00\x13\x14\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xfb\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\xa3\ -\x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x94\xc1\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x75\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\x9a\ -\x00\x00\x13\x9a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\x1a\ -\x00\x00\x13\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xc7\ -\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xac\x9c\ -\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xad\x8c\ -\x00\x00\x14\x02\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xb5\ -\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb7\xec\ -\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xcc\xf9\ -\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xd1\xe9\ -\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xd4\xe8\ -\x00\x00\x14\x90\x00\x00\x00\x00\x00\x01\x00\x05\xda\xf2\ -\x00\x00\x14\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xde\x41\ -\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x62\ -\x00\x00\x14\xf0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x59\ -\x00\x00\x15\x04\x00\x00\x00\x00\x00\x01\x00\x05\xea\x6a\ -\x00\x00\x15\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xee\x2d\ -\x00\x00\x15\x42\x00\x00\x00\x00\x00\x01\x00\x05\xf7\xe1\ -\x00\x00\x15\x6c\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x2f\ -\x00\x00\x15\x8e\x00\x00\x00\x00\x00\x01\x00\x05\xfe\xbd\ -\x00\x00\x15\xb4\x00\x00\x00\x00\x00\x01\x00\x06\x03\x5e\ -\x00\x00\x15\xc8\x00\x00\x00\x00\x00\x01\x00\x06\x0d\x30\ -\x00\x00\x15\xf4\x00\x00\x00\x00\x00\x01\x00\x06\x12\x7a\ -\x00\x00\x16\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x18\x81\ -\x00\x00\x16\x32\x00\x00\x00\x00\x00\x01\x00\x06\x19\x65\ -\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x1b\xae\ -\x00\x00\x16\x74\x00\x00\x00\x00\x00\x01\x00\x06\x22\x5e\ -\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x25\xa2\ -\x00\x00\x16\xa8\x00\x00\x00\x00\x00\x01\x00\x06\x26\xce\ -\x00\x00\x16\xc2\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x8f\ -\x00\x00\x16\xe4\x00\x00\x00\x00\x00\x01\x00\x06\x2d\xb1\ -\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\xa4\ -\x00\x00\x17\x22\x00\x00\x00\x00\x00\x01\x00\x06\x36\xa8\ -\x00\x00\x17\x44\x00\x00\x00\x00\x00\x01\x00\x06\x37\xc9\ -\x00\x00\x17\x64\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x9d\ -\x00\x00\x17\x92\x00\x00\x00\x00\x00\x01\x00\x06\x43\x09\ -\x00\x00\x17\xb6\x00\x00\x00\x00\x00\x01\x00\x06\x4a\xc9\ -\x00\x00\x17\xda\x00\x00\x00\x00\x00\x01\x00\x06\x4f\xe4\ -\x00\x00\x18\x02\x00\x00\x00\x00\x00\x01\x00\x06\x51\x37\ ->>>>>>> origin/main " qt_resource_struct_v2 = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x10\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ -<<<<<<< HEAD \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa3\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x94\ -======= -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa1\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x0a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9e\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x1a\x00\x02\x00\x00\x00\x01\x00\x00\x00\x92\ ->>>>>>> origin/main \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x46\x00\x02\x00\x00\x00\x01\x00\x00\x00\x82\ \x00\x00\x00\x00\x00\x00\x00\x00\ @@ -28707,542 +27592,383 @@ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x13\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ -\x00\x00\x01\x9b\x93\x1e\x07\xf6\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x08\x66\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x02\x10\x00\x00\x00\x00\x00\x01\x00\x00\x09\x52\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x02\x38\x00\x00\x00\x00\x00\x01\x00\x00\x0a\x35\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x02\x60\x00\x00\x00\x00\x00\x01\x00\x00\x0b\x18\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x02\x88\x00\x00\x00\x00\x00\x01\x00\x00\x0c\x06\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x02\xbc\x00\x00\x00\x00\x00\x01\x00\x00\x14\x42\ -\x00\x00\x01\x9b\x93\x1e\x07\xf6\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x02\xe8\x00\x00\x00\x00\x00\x01\x00\x00\x1d\xf7\ -\x00\x00\x01\x9b\x93\x1e\x07\xea\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x03\x12\x00\x00\x00\x00\x00\x01\x00\x00\x22\x41\ -\x00\x00\x01\x9b\x93\x1e\x07\xea\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x03\x32\x00\x00\x00\x00\x00\x01\x00\x00\x26\x90\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x03\x4e\x00\x00\x00\x00\x00\x01\x00\x00\x2c\x6e\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x03\x6a\x00\x00\x00\x00\x00\x01\x00\x00\x30\xca\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x03\x92\x00\x00\x00\x00\x00\x01\x00\x00\x32\xb1\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x21\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x22\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x03\xb2\x00\x00\x00\x00\x00\x01\x00\x00\x3a\x55\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x03\xd6\x00\x00\x00\x00\x00\x01\x00\x00\x3c\xcf\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x03\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x3f\x43\ -\x00\x00\x01\x9c\xb3\x36\xe2\x17\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ \x00\x00\x04\x1a\x00\x00\x00\x00\x00\x01\x00\x00\x40\xdd\ -\x00\x00\x01\x9b\x93\x1e\x08\x06\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x04\x38\x00\x00\x00\x00\x00\x01\x00\x00\x43\x59\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x04\x5a\x00\x00\x00\x00\x00\x01\x00\x00\x45\xd5\ -\x00\x00\x01\x9c\xb3\x36\xe2\x17\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x29\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x2a\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x04\x7e\x00\x00\x00\x00\x00\x01\x00\x00\x47\x6d\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x04\x98\x00\x00\x00\x00\x00\x01\x00\x00\x48\x6e\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x04\xc8\x00\x00\x00\x00\x00\x01\x00\x00\x4d\x86\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x04\xf8\x00\x00\x00\x00\x00\x01\x00\x00\x59\x48\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x05\x22\x00\x00\x00\x00\x00\x01\x00\x00\x5b\xb8\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x05\x48\x00\x00\x00\x00\x00\x01\x00\x00\x61\x50\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x05\x6c\x00\x00\x00\x00\x00\x01\x00\x00\x65\xac\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x05\xa6\x00\x00\x00\x00\x00\x01\x00\x00\x6c\x39\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x05\xc2\x00\x00\x00\x00\x00\x01\x00\x00\x6f\x35\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x05\xea\x00\x00\x00\x00\x00\x01\x00\x00\x70\x33\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x35\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x36\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x06\x1c\x00\x01\x00\x00\x00\x01\x00\x00\x7a\x9d\ -\x00\x00\x01\x9b\x93\x1e\x07\xfe\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x06\x2e\x00\x00\x00\x00\x00\x01\x00\x02\x3d\xf2\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x39\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3a\ \x00\x00\x00\x00\x00\x00\x00\x00\ -<<<<<<< HEAD \x00\x00\x06\x42\x00\x00\x00\x00\x00\x01\x00\x02\x42\x78\ -\x00\x00\x01\x9b\xfa\xbd\x22\x8a\ +\x00\x00\x01\x9b\xc6\xe6\xb1\x6c\ \x00\x00\x06\x70\x00\x00\x00\x00\x00\x01\x00\x02\x44\xa3\ -\x00\x00\x01\x9b\xfa\xbd\x22\x8a\ +\x00\x00\x01\x9b\xc6\xe6\xb1\x6c\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3d\ -======= -\x00\x00\x05\xfc\x00\x00\x00\x00\x00\x01\x00\x02\x3f\x46\ -\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ -\x00\x00\x06\x2a\x00\x00\x00\x00\x00\x01\x00\x02\x41\x71\ -\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x3b\ ->>>>>>> origin/main \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x06\x00\x00\x00\x46\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x06\xa0\x00\x02\x00\x00\x00\x07\x00\x00\x00\x3f\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x06\xb2\x00\x00\x00\x00\x00\x01\x00\x02\x46\xa9\ -\x00\x00\x01\x9b\x93\x1e\x0b\x4a\ +\x00\x00\x01\x98\x55\x96\x0d\x7b\ \x00\x00\x06\xe6\x00\x00\x00\x00\x00\x01\x00\x02\x51\x0d\ -\x00\x00\x01\x9b\x93\x1e\x0d\x7a\ +\x00\x00\x01\x98\x55\x96\x0d\x7b\ \x00\x00\x07\x1a\x00\x00\x00\x00\x00\x01\x00\x02\x5b\x36\ -\x00\x00\x01\x9b\x93\x1e\x0b\x4a\ +\x00\x00\x01\x98\x55\x96\x0d\x7b\ \x00\x00\x07\x54\x00\x00\x00\x00\x00\x01\x00\x02\x65\x8e\ -\x00\x00\x01\x9b\x93\x1e\x0d\x7a\ +\x00\x00\x01\x98\x55\x96\x0d\x7b\ \x00\x00\x07\x88\x00\x00\x00\x00\x00\x01\x00\x02\x6f\xa6\ -\x00\x00\x01\x9b\x93\x1e\x0b\x4a\ +\x00\x00\x01\x98\x55\x96\x0d\x7b\ \x00\x00\x07\xbe\x00\x00\x00\x00\x00\x01\x00\x02\x79\xcf\ -\x00\x00\x01\x9b\x93\x1e\x0b\x4a\ +\x00\x00\x01\x98\x55\x96\x0d\x7b\ \x00\x00\x07\xf6\x00\x00\x00\x00\x00\x01\x00\x02\x83\xe5\ -\x00\x00\x01\x9b\x93\x1e\x0d\x7a\ +\x00\x00\x01\x98\x55\x96\x0d\x7b\ \x00\x00\x08\x2c\x00\x00\x00\x00\x00\x01\x00\x02\x8e\x4b\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x08\x54\x00\x00\x00\x00\x00\x01\x00\x02\x98\x7a\ -\x00\x00\x01\x9b\x93\x1e\x08\x06\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x08\x80\x00\x00\x00\x00\x00\x01\x00\x02\xa2\xc1\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x08\xbc\x00\x00\x00\x00\x00\x01\x00\x02\xa4\xc2\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x08\xe8\x00\x00\x00\x00\x00\x01\x00\x02\xc0\x07\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x09\x14\x00\x00\x00\x00\x00\x01\x00\x02\xc5\x50\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x4d\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x03\x00\x00\x00\x4e\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x09\x48\x00\x00\x00\x00\x00\x01\x00\x02\xc8\xc8\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x09\x66\x00\x00\x00\x00\x00\x01\x00\x02\xd1\xaa\ -\x00\x00\x01\x9c\xb3\x36\xe2\x17\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ \x00\x00\x09\x7a\x00\x00\x00\x00\x00\x01\x00\x02\xd7\x8b\ -\x00\x00\x01\x9c\xb3\x36\xe2\x17\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x52\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0d\x00\x00\x00\x53\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x09\x94\x00\x00\x00\x00\x00\x01\x00\x02\xe3\x1e\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x09\xbc\x00\x00\x00\x00\x00\x01\x00\x02\xe8\x19\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x09\xf4\x00\x00\x00\x00\x00\x01\x00\x02\xf0\xed\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x0a\x2c\x00\x00\x00\x00\x00\x01\x00\x02\xf9\x91\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x0a\x5c\x00\x00\x00\x00\x00\x01\x00\x03\x01\x30\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x0a\x80\x00\x00\x00\x00\x00\x01\x00\x03\x09\x0c\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x0a\xa4\x00\x00\x00\x00\x00\x01\x00\x03\x10\xc2\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x0a\xd8\x00\x00\x00\x00\x00\x01\x00\x03\x18\xd0\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x0b\x0c\x00\x00\x00\x00\x00\x01\x00\x03\x20\xb2\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x0b\x40\x00\x00\x00\x00\x00\x01\x00\x03\x28\x7c\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x0b\x7a\x00\x00\x00\x00\x00\x01\x00\x03\x2f\xe3\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x0b\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x33\xa0\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x0b\xcc\x00\x00\x00\x00\x00\x01\x00\x03\x37\x79\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x61\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x08\x00\x00\x00\x62\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0b\xf2\x00\x00\x00\x00\x00\x01\x00\x03\x3d\x82\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x0c\x1e\x00\x00\x00\x00\x00\x01\x00\x03\x60\x06\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x0c\x4c\x00\x00\x00\x00\x00\x01\x00\x03\x65\xf3\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x0c\x76\x00\x00\x00\x00\x00\x01\x00\x03\x68\x13\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x0c\x9e\x00\x00\x00\x00\x00\x01\x00\x03\x70\xab\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x0c\xb8\x00\x00\x00\x00\x00\x01\x00\x03\x7f\xf4\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x0c\xe4\x00\x00\x00\x00\x00\x01\x00\x03\x86\x72\ -\x00\x00\x01\x9b\x93\x1e\x08\x06\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x0d\x0c\x00\x00\x00\x00\x00\x01\x00\x03\x90\xec\ -\x00\x00\x01\x9b\x93\x1e\x08\x06\ +\x00\x00\x01\x9a\x27\x73\xa6\xfc\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x6b\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x6c\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0d\x42\x00\x00\x00\x00\x00\x01\x00\x03\x9a\x33\ -\x00\x00\x01\x9b\x93\x1e\x07\xea\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x0d\x70\x00\x00\x00\x00\x00\x01\x00\x03\x9c\xda\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x0d\x9e\x00\x01\x00\x00\x00\x01\x00\x03\xa9\x57\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x0d\xca\x00\x00\x00\x00\x00\x01\x00\x03\xd6\xd6\ -\x00\x00\x01\x9b\x93\x1e\x07\xea\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x0d\xea\x00\x00\x00\x00\x00\x01\x00\x03\xdb\x8d\ -\x00\x00\x01\x9b\x93\x1e\x07\xea\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x0e\x1c\x00\x01\x00\x00\x00\x01\x00\x04\x34\x86\ -\x00\x00\x01\x9b\x93\x1e\x07\xea\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x0e\x4e\x00\x00\x00\x00\x00\x01\x00\x04\x69\x20\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x0e\x68\x00\x00\x00\x00\x00\x01\x00\x04\x6e\x6a\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x0e\x82\x00\x00\x00\x00\x00\x01\x00\x04\x73\xf9\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x0e\x9c\x00\x00\x00\x00\x00\x01\x00\x04\x79\x62\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x0e\xb4\x00\x00\x00\x00\x00\x01\x00\x04\x85\x40\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x0e\xd2\x00\x00\x00\x00\x00\x01\x00\x04\x8b\x44\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x79\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x7a\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0e\xfa\x00\x00\x00\x00\x00\x01\x00\x04\x90\x79\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x0f\x0e\x00\x00\x00\x00\x00\x01\x00\x04\x96\x76\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x0f\x20\x00\x00\x00\x00\x00\x01\x00\x04\x97\xfc\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x0f\x32\x00\x00\x00\x00\x00\x01\x00\x04\x9d\xf6\ -\x00\x00\x01\x9b\x93\x1e\x08\x06\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x7f\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x02\x00\x00\x00\x80\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0f\x46\x00\x00\x00\x00\x00\x01\x00\x04\xa0\x4c\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x0f\x72\x00\x00\x00\x00\x00\x01\x00\x04\xa7\x33\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x83\ \x00\x00\x00\x00\x00\x00\x00\x00\ -<<<<<<< HEAD \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x04\x00\x00\x00\x84\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x00\x46\x00\x02\x00\x00\x00\x0c\x00\x00\x00\x88\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x0f\x96\x00\x01\x00\x00\x00\x01\x00\x04\xb0\x97\ -\x00\x00\x01\x9b\x93\x1e\x08\x06\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x0f\xba\x00\x00\x00\x00\x00\x01\x00\x04\xbb\xe8\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x0f\xdc\x00\x00\x00\x00\x00\x01\x00\x04\xc3\x8d\ -\x00\x00\x01\x9b\x93\x1e\x07\xf6\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x0f\xf8\x00\x00\x00\x00\x00\x01\x00\x04\xd4\x8d\ -\x00\x00\x01\x9c\xb4\x18\x40\xf7\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ \x00\x00\x10\x18\x00\x00\x00\x00\x00\x01\x00\x04\xda\x26\ -\x00\x00\x01\x9c\xb4\x18\x40\xf7\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ \x00\x00\x10\x38\x00\x00\x00\x00\x00\x01\x00\x04\xe0\x4d\ -\x00\x00\x01\x9c\xb4\x18\x40\xf7\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ \x00\x00\x10\x58\x00\x00\x00\x00\x00\x01\x00\x04\xe4\xa2\ -\x00\x00\x01\x9c\xb4\x18\x40\xf7\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ \x00\x00\x10\x78\x00\x00\x00\x00\x00\x01\x00\x04\xea\x26\ -\x00\x00\x01\x9c\xb4\x18\x40\xf7\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ \x00\x00\x10\x98\x00\x00\x00\x00\x00\x01\x00\x04\xef\xbf\ -\x00\x00\x01\x9c\xb4\x18\x40\xf7\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ \x00\x00\x10\xca\x00\x00\x00\x00\x00\x01\x00\x04\xf4\xc1\ -\x00\x00\x01\x9c\xb4\x18\x40\xf7\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ \x00\x00\x10\xea\x00\x00\x00\x00\x00\x01\x00\x04\xfa\x5a\ -\x00\x00\x01\x9c\xb4\x18\x40\xf7\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ \x00\x00\x11\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x04\xce\ -\x00\x00\x01\x9c\xb4\x18\x40\xf7\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ \x00\x00\x11\x52\x00\x00\x00\x00\x00\x01\x00\x05\x0f\x48\ -\x00\x00\x01\x9c\xb4\x18\x40\xf7\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ \x00\x00\x11\x86\x00\x00\x00\x00\x00\x01\x00\x05\x19\xa7\ -\x00\x00\x01\x9c\xb4\x18\x40\xf7\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ \x00\x00\x11\xba\x00\x00\x00\x00\x00\x01\x00\x05\x24\xf2\ -\x00\x00\x01\x9c\xb4\x18\x40\xf7\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x95\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x96\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x66\ -\x00\x00\x01\x9b\x93\x1e\x08\x06\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x38\xfc\ -\x00\x00\x01\x9b\x93\x1e\x08\x06\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x12\x4e\x00\x00\x00\x00\x00\x01\x00\x05\x44\xd8\ -\x00\x00\x01\x9b\x93\x1e\x08\x06\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x12\x72\x00\x00\x00\x00\x00\x01\x00\x05\x4b\x1c\ -\x00\x00\x01\x9b\x93\x1e\x08\x06\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x12\x9c\x00\x00\x00\x00\x00\x01\x00\x05\x52\xa5\ -\x00\x00\x01\x9b\x93\x1e\x07\xf6\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x12\xc8\x00\x00\x00\x00\x00\x01\x00\x05\x59\x03\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x12\xfe\x00\x00\x00\x00\x00\x01\x00\x05\x60\xf2\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x09\x66\x00\x00\x00\x00\x00\x01\x00\x05\x6e\x95\ -\x00\x00\x01\x9c\xb3\x36\xe2\x17\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ \x00\x00\x09\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x74\x76\ -\x00\x00\x01\x9c\xb3\x36\xe2\x17\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ \x00\x00\x13\x1c\x00\x00\x00\x00\x00\x01\x00\x05\x80\x09\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x9a\x27\x73\xa6\xfc\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa1\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x13\x44\x00\x00\x00\x00\x00\x01\x00\x05\x87\xd3\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa4\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x01\xb0\x00\x02\x00\x00\x00\x2b\x00\x00\x00\xa5\ \x00\x00\x00\x00\x00\x00\x00\x00\ \x00\x00\x13\x64\x00\x00\x00\x00\x00\x01\x00\x05\x8c\x99\ -\x00\x00\x01\x9b\x93\x1e\x08\x06\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x13\x7a\x00\x00\x00\x00\x00\x01\x00\x05\x94\x4d\ -\x00\x00\x01\x9b\x93\x1e\x07\xfe\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x13\x92\x00\x00\x00\x00\x00\x01\x00\x05\x96\x72\ -\x00\x00\x01\x9b\x93\x1e\x07\xf6\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x13\xca\x00\x00\x00\x00\x00\x01\x00\x05\x97\xf2\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x13\xe6\x00\x00\x00\x00\x00\x01\x00\x05\x9f\x9f\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x14\x06\x00\x00\x00\x00\x00\x01\x00\x05\xa4\x74\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x14\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xa5\x64\ -\x00\x00\x01\x9c\xb3\x36\xe2\x17\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ \x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xa8\x90\ -\x00\x00\x01\x9b\xfa\xbd\x22\x8a\ +\x00\x00\x01\x9b\xc6\xe6\xb1\x6c\ \x00\x00\x14\x58\x00\x00\x00\x00\x00\x01\x00\x05\xac\xb9\ -\x00\x00\x01\x9b\x93\x1e\x08\x06\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x14\x7e\x00\x00\x00\x00\x00\x01\x00\x05\xb2\xf0\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x99\x96\xf9\x85\x6f\ \x00\x00\x14\x98\x00\x00\x00\x00\x00\x01\x00\x05\xc7\xfd\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x9a\x27\x73\xa6\xf8\ \x00\x00\x14\xba\x00\x00\x00\x00\x00\x01\x00\x05\xcc\xed\ -\x00\x00\x01\x9b\x93\x1e\x07\xea\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x14\xd0\x00\x00\x00\x00\x00\x01\x00\x05\xcf\xec\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x14\xe6\x00\x00\x00\x00\x00\x01\x00\x05\xd5\xf6\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x15\x18\x00\x00\x00\x00\x00\x01\x00\x05\xd9\x45\ -\x00\x00\x01\x9c\xb3\x36\xe2\x17\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9b\ \x00\x00\x15\x30\x00\x00\x00\x00\x00\x01\x00\x05\xdc\x37\ -\x00\x00\x01\x9b\x93\x1e\x07\xea\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x15\x46\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x2e\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x99\x96\xf9\x85\x6f\ \x00\x00\x15\x5a\x00\x00\x00\x00\x00\x01\x00\x05\xe4\x3f\ -\x00\x00\x01\x9b\x93\x1e\x08\x06\ +\x00\x00\x01\x99\x96\xf9\x85\x6f\ \x00\x00\x15\x72\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x02\ -\x00\x00\x01\x9b\x93\x1e\x07\xea\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x15\x98\x00\x00\x00\x00\x00\x01\x00\x05\xf1\xb6\ -\x00\x00\x01\x9c\xb3\xfd\xca\xaf\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ \x00\x00\x15\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xf7\x7e\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x15\xe0\x00\x00\x00\x00\x00\x01\x00\x05\xfa\xcc\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x16\x02\x00\x00\x00\x00\x00\x01\x00\x05\xfe\x5a\ -\x00\x00\x01\x9b\x93\x1e\x07\xf2\ +\x00\x00\x01\x9a\x27\x73\xa6\xf8\ \x00\x00\x16\x28\x00\x00\x00\x00\x00\x01\x00\x06\x02\xfb\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x16\x3c\x00\x00\x00\x00\x00\x01\x00\x06\x0c\xcd\ -\x00\x00\x01\x9b\x93\x1e\x07\xfe\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x16\x68\x00\x00\x00\x00\x00\x01\x00\x06\x12\x17\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x18\x1e\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x16\xa6\x00\x00\x00\x00\x00\x01\x00\x06\x19\x02\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x2b\ \x00\x00\x16\xd2\x00\x00\x00\x00\x00\x01\x00\x06\x1b\x4b\ -\x00\x00\x01\x9b\x93\x1e\x08\x06\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x16\xe8\x00\x00\x00\x00\x00\x01\x00\x06\x21\xfb\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x17\x04\x00\x00\x00\x00\x00\x01\x00\x06\x25\x3f\ -\x00\x00\x01\x9c\xb3\x36\xe2\x17\ +\x00\x00\x01\x9c\xd4\xa5\xd3\x9f\ \x00\x00\x17\x38\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x25\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x17\x50\x00\x00\x00\x00\x00\x01\x00\x06\x2d\x51\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x17\x6a\x00\x00\x00\x00\x00\x01\x00\x06\x33\x12\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x17\x8c\x00\x00\x00\x00\x00\x01\x00\x06\x34\x34\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x17\xaa\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x27\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x17\xca\x00\x00\x00\x00\x00\x01\x00\x06\x3d\x2b\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x17\xec\x00\x00\x00\x00\x00\x01\x00\x06\x3e\x4c\ -\x00\x00\x01\x9b\x93\x1e\x08\x02\ +\x00\x00\x01\x98\x55\x96\x0d\x49\ \x00\x00\x18\x0c\x00\x00\x00\x00\x00\x01\x00\x06\x41\x20\ -\x00\x00\x01\x9b\x93\x1e\x07\xfe\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x18\x3a\x00\x00\x00\x00\x00\x01\x00\x06\x49\x8c\ -\x00\x00\x01\x9b\x93\x1e\x07\xea\ +\x00\x00\x01\x99\x7d\x04\xc3\x11\ \x00\x00\x18\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x51\x4c\ -\x00\x00\x01\x9b\x93\x1e\x07\xfa\ +\x00\x00\x01\x98\x55\x96\x0d\x3f\ \x00\x00\x18\x82\x00\x00\x00\x00\x00\x01\x00\x06\x56\x67\ -\x00\x00\x01\x9b\x93\x1e\x07\xee\ +\x00\x00\x01\x98\x55\x96\x0d\x35\ \x00\x00\x18\xaa\x00\x00\x00\x00\x00\x01\x00\x06\x57\xba\ -\x00\x00\x01\x9b\x93\x1e\x07\xea\ -======= -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x10\x00\x00\x00\x82\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x0f\x50\x00\x00\x00\x00\x00\x01\x00\x04\xaa\xfb\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x0f\x70\x00\x00\x00\x00\x00\x01\x00\x04\xb0\x94\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x0f\x90\x00\x01\x00\x00\x00\x01\x00\x04\xb6\xbb\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x0f\xb4\x00\x00\x00\x00\x00\x01\x00\x04\xc2\x0c\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x0f\xd6\x00\x00\x00\x00\x00\x01\x00\x04\xc9\xb1\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x0f\xf2\x00\x00\x00\x00\x00\x01\x00\x04\xda\xb1\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x10\x16\x00\x00\x00\x00\x00\x01\x00\x04\xe4\x32\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x10\x36\x00\x00\x00\x00\x00\x01\x00\x04\xe9\xb6\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x10\x56\x00\x00\x00\x00\x00\x01\x00\x04\xef\x4f\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x10\x7e\x00\x00\x00\x00\x00\x01\x00\x04\xf9\x18\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x10\x9e\x00\x00\x00\x00\x00\x01\x00\x04\xfe\xb1\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x10\xd2\x00\x00\x00\x00\x00\x01\x00\x05\x09\x25\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x10\xee\x00\x00\x00\x00\x00\x01\x00\x05\x0f\x60\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x11\x22\x00\x00\x00\x00\x00\x01\x00\x05\x19\xda\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x11\x56\x00\x00\x00\x00\x00\x01\x00\x05\x24\x39\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x11\x8a\x00\x00\x00\x00\x00\x01\x00\x05\x2f\x84\ -\x00\x00\x01\x9b\xbc\x55\x65\x5e\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x93\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x0a\x00\x00\x00\x94\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x11\xbe\x00\x00\x00\x00\x00\x01\x00\x05\x39\xf8\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x11\xee\x00\x00\x00\x00\x00\x01\x00\x05\x43\x8e\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x1e\x00\x00\x00\x00\x00\x01\x00\x05\x4f\x6a\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x42\x00\x00\x00\x00\x00\x01\x00\x05\x55\xae\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x12\x6c\x00\x00\x00\x00\x00\x01\x00\x05\x5d\x37\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x12\x98\x00\x00\x00\x00\x00\x01\x00\x05\x63\x95\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x12\xce\x00\x00\x00\x00\x00\x01\x00\x05\x6b\x84\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x09\x20\x00\x00\x00\x00\x00\x01\x00\x05\x79\x27\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x09\x34\x00\x00\x00\x00\x00\x01\x00\x05\x7e\x5c\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x12\xec\x00\x00\x00\x00\x00\x01\x00\x05\x88\x31\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\x9f\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa0\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x13\x14\x00\x00\x00\x00\x00\x01\x00\x05\x8f\xfb\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x01\xa0\x00\x02\x00\x00\x00\x01\x00\x00\x00\xa2\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01\xb0\x00\x02\x00\x00\x00\x28\x00\x00\x00\xa3\ -\x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x13\x34\x00\x00\x00\x00\x00\x01\x00\x05\x94\xc1\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x13\x4a\x00\x00\x00\x00\x00\x01\x00\x05\x9c\x75\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x13\x62\x00\x00\x00\x00\x00\x01\x00\x05\x9e\x9a\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x13\x9a\x00\x00\x00\x00\x00\x01\x00\x05\xa0\x1a\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\xb6\x00\x00\x00\x00\x00\x01\x00\x05\xa7\xc7\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x13\xd6\x00\x00\x00\x00\x00\x01\x00\x05\xac\x9c\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x13\xec\x00\x00\x00\x00\x00\x01\x00\x05\xad\x8c\ -\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ -\x00\x00\x14\x02\x00\x00\x00\x00\x00\x01\x00\x05\xb1\xb5\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x14\x28\x00\x00\x00\x00\x00\x01\x00\x05\xb7\xec\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x42\x00\x00\x00\x00\x00\x01\x00\x05\xcc\xf9\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x14\x64\x00\x00\x00\x00\x00\x01\x00\x05\xd1\xe9\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x14\x7a\x00\x00\x00\x00\x00\x01\x00\x05\xd4\xe8\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x14\x90\x00\x00\x00\x00\x00\x01\x00\x05\xda\xf2\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x14\xc2\x00\x00\x00\x00\x00\x01\x00\x05\xde\x41\ -\x00\x00\x01\x9b\xbc\x55\x7b\x5e\ -\x00\x00\x14\xda\x00\x00\x00\x00\x00\x01\x00\x05\xe2\x62\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x14\xf0\x00\x00\x00\x00\x00\x01\x00\x05\xe8\x59\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x04\x00\x00\x00\x00\x00\x01\x00\x05\xea\x6a\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x15\x1c\x00\x00\x00\x00\x00\x01\x00\x05\xee\x2d\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ -\x00\x00\x15\x42\x00\x00\x00\x00\x00\x01\x00\x05\xf7\xe1\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x6c\x00\x00\x00\x00\x00\x01\x00\x05\xfb\x2f\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\x8e\x00\x00\x00\x00\x00\x01\x00\x05\xfe\xbd\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x15\xb4\x00\x00\x00\x00\x00\x01\x00\x06\x03\x5e\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x15\xc8\x00\x00\x00\x00\x00\x01\x00\x06\x0d\x30\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x15\xf4\x00\x00\x00\x00\x00\x01\x00\x06\x12\x7a\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x1c\x00\x00\x00\x00\x00\x01\x00\x06\x18\x81\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x32\x00\x00\x00\x00\x00\x01\x00\x06\x19\x65\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x16\x5e\x00\x00\x00\x00\x00\x01\x00\x06\x1b\xae\ -\x00\x00\x01\x9a\x72\xe1\x94\x5b\ -\x00\x00\x16\x74\x00\x00\x00\x00\x00\x01\x00\x06\x22\x5e\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\x90\x00\x00\x00\x00\x00\x01\x00\x06\x25\xa2\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\xa8\x00\x00\x00\x00\x00\x01\x00\x06\x26\xce\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x16\xc2\x00\x00\x00\x00\x00\x01\x00\x06\x2c\x8f\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x16\xe4\x00\x00\x00\x00\x00\x01\x00\x06\x2d\xb1\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x02\x00\x00\x00\x00\x00\x01\x00\x06\x33\xa4\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\x22\x00\x00\x00\x00\x00\x01\x00\x06\x36\xa8\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x44\x00\x00\x00\x00\x00\x01\x00\x06\x37\xc9\ -\x00\x00\x01\x9a\x72\xe1\x94\x57\ -\x00\x00\x17\x64\x00\x00\x00\x00\x00\x01\x00\x06\x3a\x9d\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\x92\x00\x00\x00\x00\x00\x01\x00\x06\x43\x09\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x17\xb6\x00\x00\x00\x00\x00\x01\x00\x06\x4a\xc9\ -\x00\x00\x01\x9a\x72\xe1\x94\x53\ -\x00\x00\x17\xda\x00\x00\x00\x00\x00\x01\x00\x06\x4f\xe4\ -\x00\x00\x01\x9a\x72\xe1\x94\x4f\ -\x00\x00\x18\x02\x00\x00\x00\x00\x00\x01\x00\x06\x51\x37\ -\x00\x00\x01\x9a\x72\xe1\x94\x4b\ ->>>>>>> origin/main +\x00\x00\x01\x98\x55\x96\x0d\x2b\ " qt_version = [int(v) for v in QtCore.qVersion().split('.')] From 615059e943f3eb3033c088d85aab4c5de772daa0 Mon Sep 17 00:00:00 2001 From: HugoCLSC Date: Thu, 12 Mar 2026 10:49:07 +0000 Subject: [PATCH 66/70] Fix pyqt version --- BlocksScreen/lib/ui/resources/icon_resources_rc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlocksScreen/lib/ui/resources/icon_resources_rc.py b/BlocksScreen/lib/ui/resources/icon_resources_rc.py index 073887dd..45c93d8f 100644 --- a/BlocksScreen/lib/ui/resources/icon_resources_rc.py +++ b/BlocksScreen/lib/ui/resources/icon_resources_rc.py @@ -6,7 +6,7 @@ # # WARNING! All changes made in this file will be lost! -from PyQt5 import QtCore +from PyQt6 import QtCore qt_resource_data = b"\ \x00\x00\x08\x62\ From f1478cc56d87fd3d5254196843645290a62d5dce Mon Sep 17 00:00:00 2001 From: HugoCLSC Date: Thu, 12 Mar 2026 11:44:51 +0000 Subject: [PATCH 67/70] Add '-' to label prefix --- BlocksScreen/devices/storage/udisks2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/BlocksScreen/devices/storage/udisks2.py b/BlocksScreen/devices/storage/udisks2.py index 4f44c499..ba73ba9a 100644 --- a/BlocksScreen/devices/storage/udisks2.py +++ b/BlocksScreen/devices/storage/udisks2.py @@ -482,7 +482,7 @@ def add_symlink( """ if not _validated and label: label = validate_label(label, strict=True) - label = "USB " + label + label = "USB-" + label fallback: str = "USB DRIVE" if _index == 0 else str(f"USB DRIVE {_index}") dstb = pathlib.Path(dst_path).joinpath(label if label else fallback) try: From f2ebd8812723eb9fb75f78eec5c3caae0d92ca6f Mon Sep 17 00:00:00 2001 From: Guilherme Costa Date: Thu, 12 Mar 2026 11:54:30 +0000 Subject: [PATCH 68/70] bugfix(logger): starts logger before the mainwindow (#196) * bugfix(logger): starts logger before the mainwindow * fix formatting --------- Co-authored-by: Hugo Costa --- BlocksScreen/BlocksScreen.py | 26 ++++++++++++++++++++---- BlocksScreen/lib/panels/networkWindow.py | 1 - 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/BlocksScreen/BlocksScreen.py b/BlocksScreen/BlocksScreen.py index 22468a8b..ab198a7e 100644 --- a/BlocksScreen/BlocksScreen.py +++ b/BlocksScreen/BlocksScreen.py @@ -2,9 +2,27 @@ import sys import typing -from lib.panels.mainWindow import MainWindow -from logger import setup_logging, LogManager -from PyQt6 import QtCore, QtGui, QtWidgets +from logger import CrashHandler, LogManager, install_crash_handler, setup_logging + +install_crash_handler() + +from lib.panels.mainWindow import MainWindow # noqa: E402 +from PyQt6 import QtCore, QtGui, QtWidgets # noqa: E402 + + +class BlocksScreenApp(QtWidgets.QApplication): + """QApplication subclass that routes unhandled slot exceptions to CrashHandler.""" + + def notify(self, a0: QtCore.QObject, a1: QtCore.QEvent) -> bool: # type: ignore[override] + try: + return super().notify(a0, a1) + except Exception: + exc_type, exc_value, exc_tb = sys.exc_info() + handler = CrashHandler._instance + if handler is not None and exc_type is not None and exc_value is not None: + handler._exception_hook(exc_type, exc_value, exc_tb) + return False + QtGui.QGuiApplication.setAttribute( QtCore.Qt.ApplicationAttribute.AA_SynthesizeMouseForUnhandledTouchEvents, @@ -46,7 +64,7 @@ def on_quit() -> None: ) _logger = logging.getLogger(__name__) _logger.info("============ BlocksScreen Initializing ============") - BlocksScreen = QtWidgets.QApplication([]) + BlocksScreen = BlocksScreenApp([]) BlocksScreen.setApplicationName("BlocksScreen") BlocksScreen.setApplicationDisplayName("BlocksScreen") BlocksScreen.setDesktopFileName("BlocksScreen") diff --git a/BlocksScreen/lib/panels/networkWindow.py b/BlocksScreen/lib/panels/networkWindow.py index 916c6146..f9b17a52 100644 --- a/BlocksScreen/lib/panels/networkWindow.py +++ b/BlocksScreen/lib/panels/networkWindow.py @@ -3845,4 +3845,3 @@ def show_network_panel(self) -> None: self.repaint() self.show() self._nm.scan_networks() - From 3915a39cb2eaa92efa7078bc5b30969190473fd5 Mon Sep 17 00:00:00 2001 From: HugoCLSC Date: Thu, 12 Mar 2026 13:06:23 +0000 Subject: [PATCH 69/70] Add legacy dir cleanup on start --- BlocksScreen/devices/storage/udisks2.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/BlocksScreen/devices/storage/udisks2.py b/BlocksScreen/devices/storage/udisks2.py index ba73ba9a..a766656e 100644 --- a/BlocksScreen/devices/storage/udisks2.py +++ b/BlocksScreen/devices/storage/udisks2.py @@ -2,6 +2,7 @@ import logging import os import pathlib +import shutil import typing from collections.abc import Coroutine import unicodedata @@ -163,6 +164,7 @@ def __init__(self, parent: QtCore.QObject, gcodes_dir: str) -> None: self.listener_running: bool = False self.controlled_devs: dict[str, Device] = {} self._cleanup_broken_symlinks() + self._cleanup_legacy_dir() @property def active(self) -> bool: @@ -563,3 +565,13 @@ def _resolve_symlinks( # This `path` resolves to the `mount_path` return True return False + + def _cleanup_legacy_dir(self) -> None: + """Removes legacy directory that contained symlinks + for all mounted USB devices on the machine + """ + legacy_dir = self.gcodes_path.joinpath("USB") + if legacy_dir.is_dir() and not ( + legacy_dir.is_symlink() or legacy_dir.is_file() + ): + shutil.rmtree(legacy_dir) From ce1c06d189a0814f2c0fc3517036f04a213940b1 Mon Sep 17 00:00:00 2001 From: HugoCLSC Date: Thu, 12 Mar 2026 13:20:01 +0000 Subject: [PATCH 70/70] Replace pillow dependency with pure PyQt6 --- BlocksScreen/lib/qrcode_gen.py | 32 +++++++++--- pyproject.toml | 95 +++++++++++++++++----------------- 2 files changed, 72 insertions(+), 55 deletions(-) diff --git a/BlocksScreen/lib/qrcode_gen.py b/BlocksScreen/lib/qrcode_gen.py index 160ec6fd..1840f0ae 100644 --- a/BlocksScreen/lib/qrcode_gen.py +++ b/BlocksScreen/lib/qrcode_gen.py @@ -1,5 +1,7 @@ import qrcode -from PIL import ImageQt + +from PyQt6.QtGui import QImage, QColor, QPainter +from PyQt6.QtCore import Qt BLOCKS_URL = "https://blockstec.com" RF50_MANUAL_PAGE = "https://blockstec.com/RF50" @@ -8,8 +10,8 @@ RF50_USER_MANUAL_PAGE = "https://blockstec.com/assets/files/rf50_user_manual.pdf" -def make_qrcode(data) -> ImageQt.ImageQt: - """Generate a QR code image from *data* and return it as a Qt-compatible image.""" +def make_qrcode(data: str) -> QImage: + """Generate a QR code image from *data* and return it as a QImage.""" qr = qrcode.QRCode( version=1, error_correction=qrcode.ERROR_CORRECT_L, @@ -18,9 +20,25 @@ def make_qrcode(data) -> ImageQt.ImageQt: ) qr.add_data(data) qr.make(fit=True) - img = qr.make_image(fill_color="black", back_color="white") - pil_image = img.get_image() - return ImageQt.toqimage(pil_image) + + matrix = qr.get_matrix() + box_size = 10 + size = len(matrix) * box_size + + image = QImage(size, size, QImage.Format.Format_RGB32) + image.fill(QColor("white")) + + painter = QPainter(image) + painter.setPen(Qt.PenStyle.NoPen) + painter.setBrush(QColor("black")) + + for y, row in enumerate(matrix): + for x, cell in enumerate(row): + if cell: + painter.drawRect(x * box_size, y * box_size, box_size, box_size) + + painter.end() + return image _NM_TO_WIFI_QR_AUTH: dict[str, str] = { @@ -36,7 +54,7 @@ def make_qrcode(data) -> ImageQt.ImageQt: def generate_wifi_qrcode( ssid: str, password: str, auth_type: str, hidden: bool = False -) -> ImageQt.ImageQt: +) -> QImage: """Build a Wi-Fi QR code for the given SSID/password/auth combination. *auth_type* is a NetworkManager key-mgmt value (e.g. ``"wpa-psk"``, diff --git a/pyproject.toml b/pyproject.toml index bd07f501..09c1bbc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,29 +3,28 @@ name = "BlocksScreen" dynamic = ['version'] description = "GUI for BLOCKS Printers running Klipper" authors = [ - { name = "Hugo do Carmo Costa", email = "hugo.santos.costa@gmail.com" }, + { name = "Hugo do Carmo Costa", email = "hugo.santos.costa@gmail.com" }, ] maintainers = [ - { name = "Guilherme Costa", email = "guilherme.costa@blockstec.com" }, - { name = "Roberto Martins ", email = "roberto.martins@blockstec.com" }, + { name = "Guilherme Costa", email = "guilherme.costa@blockstec.com" }, + { name = "Roberto Martins ", email = "roberto.martins@blockstec.com" }, ] dependencies = [ - 'altgraph==0.17.4', - 'certifi==2025.10.5', - 'charset-normalizer==3.4.4', - 'idna==3.11', - 'numpy==2.3.4', - 'pefile==2024.8.26', - 'PyQt6==6.10.0', - 'PyQt6-Qt6==6.10.0', - 'PyQt6_sip==13.10.2', - 'requests>=2.32.5', - 'sdbus==0.14.1', - 'sdbus-networkmanager==2.0.0', - 'typing==3.7.4.3', - 'websocket-client==1.9.0', - 'qrcode==8.2', - 'pillow==12.1.1', + 'altgraph==0.17.4', + 'certifi==2025.10.5', + 'charset-normalizer==3.4.4', + 'idna==3.11', + 'numpy==2.3.4', + 'pefile==2024.8.26', + 'PyQt6==6.10.0', + 'PyQt6-Qt6==6.10.0', + 'PyQt6_sip==13.10.2', + 'requests>=2.32.5', + 'sdbus==0.14.1', + 'sdbus-networkmanager==2.0.0', + 'typing==3.7.4.3', + 'websocket-client==1.9.0', + 'qrcode==8.2', ] requires-python = "==3.11.2" readme = "README.md" @@ -46,35 +45,35 @@ Issues = "https://github.com/BlocksTechnology/BlocksScreen/issues" line-length = 88 indent-width = 4 exclude = [ - ".bzr", - ".direnv", - ".eggs", - ".git", - ".git-rewrite", - ".hg", - ".ipynb_checkpoints", - ".mypy_cache", - ".nox", - ".pants.d", - ".pyenv", - ".pytest_cache", - ".pytype", - ".ruff_cache", - ".svn", - ".tox", - ".venv", - ".vscode", - "__pypackages__", - "_build", - "buck-out", - "build", - "dist", - "node_modules", - "site-packages", - "venv", - "BlocksScreen/lib/ui", - "extras", - "tests" + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", + "BlocksScreen/lib/ui", + "extras", + "tests", ] [tool.ruff.lint]