pax_global_header00006660000000000000000000000064142767144070014526gustar00rootroot0000000000000052 comment=bdb110f952032a0a7cc3fb58a9fcd7e6064c7fb2 autokey-0.96.0/000077500000000000000000000000001427671440700133035ustar00rootroot00000000000000autokey-0.96.0/.coveragerc000066400000000000000000000013301427671440700154210ustar00rootroot00000000000000[paths] source = lib [run] branch = true parallel = true source = lib [html] directory = test_coverage_report_html show_contexts = true skip_empty = true [report] show_missing = true precision = 1 ; Eventually will want to fail if coverage is below this percentage. ; fail_under = 40 # Regexes for lines to exclude from consideration exclude_lines = # Have to re-enable the standard pragma pragma: no cover # Don't complain about missing debug-only code: def __repr__ if self\.debug # Don't complain if tests don't hit defensive assertion code: raise AssertionError raise NotImplementedError # Don't complain if non-runnable code isn't run: if 0: if __name__ == .__main__.: autokey-0.96.0/.github/000077500000000000000000000000001427671440700146435ustar00rootroot00000000000000autokey-0.96.0/.github/ISSUE_TEMPLATE.md000066400000000000000000000025651427671440700173600ustar00rootroot00000000000000## Classification: (Pick one: Bug, Crash/Hang/Data loss, Enhancement, Feature (new), Performance, UI/Usability) ## Reproducibility: (Pick one: Always, Sometimes, Rarely, Unable, I didn't try) ## AutoKey version: (Paste in your AutoKey version and, if the problem is known to be present in more than one version, please list them all.) ## Used GUI: (Pick one: Gtk, Qt, Both): ## Installed via: (Pick one: PPA, pip3, Git, package manager, etc.) ## Linux distribution: (Provide information about your Linux distribution and its release or version number.) ## Summary: (Provide a brief summary of the problem.) ## Steps to reproduce: - I do this. - Then I do that. ## Expected result: (Explain what should happen.) ## Actual result: (Explain what actually happens.) ## Screenshot (if applicable): (Include one or more screenshots of the issue by dragging the image file(s) here to help with debugging.) ## Verbose output (if applicable): (Include the output from launching AutoKey via the `autokey-gtk --verbose` or `autokey-qt --verbose` command to help with debugging. Please upload the output somewhere accessible or paste it into a code block here, enclosing it in triple backticks.) ``` Example code block. Replace this with your output content. ``` ## Notes: (Describe debugging steps you've taken, a workaround you've figured out, or any other information you think we might need.) autokey-0.96.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001427671440700170265ustar00rootroot00000000000000autokey-0.96.0/.github/ISSUE_TEMPLATE/bug.yaml000066400000000000000000000147561427671440700205040ustar00rootroot00000000000000--- name: "Open a new issue using our issue-reporting form:" description: > Having an issue with AutoKey? Answer a few questions in our report form to help us solve the issue as quickly as possible. Please click the "Get started" button when you're ready to begin. labels: new issue body: # HAS THIS ISSUE ALREADY BEEN REPORTED? - type: checkboxes id: existing-issue attributes: label: Has this issue already been reported? description: > If someone else has [already filled out a bug report for the issue you're having](https://github.com/autokey/autokey/issues), consider contributing to that report instead of creating a new one. If your issue hasn't been reported yet, check this box and continue filling out this form. options: - label: I have searched through the existing issues. required: true # IS THIS A QUESTION RATHER THAN AN ISSUE? - type: checkboxes id: is-a-question-not-an-issue attributes: label: Is this a question rather than an issue? description: >+ When creating AutoKey issues, whenever you have a question (as opposed to an issue), it's better to post it on our [Google list](https://groups.google.com/forum/#!forum/autokey-users) or in our [Gitter chat](https://gitter.im/autokey/autokey). The list is more user-oriented, whereas more technical questions will get a better answer on Gitter. Both of these venues have a number of active users, so you are likely to get more, better, and faster answers on them than in here on GitHub, where only our very busy developers and a few users see the posts. We also prefer to have the developers developing rather than taking time to answer questions and manage issues that the user-community could handle for them. options: - label: This is not a question. required: true # WHAT TYPE OF ISSUE IS THIS? - type: dropdown id: classification attributes: label: What type of issue is this? multiple: false options: - Bug - Crash/Hang/Data loss - Documentation - Enhancement - Performance - UI/Usability validations: required: false # WHICH LINUX DISTRIBUTION DID YOU USE? - type: textarea id: linux-distribution attributes: label: Which Linux distribution did you use? description: > Provide information about your Linux distribution and its release or version number. validations: required: false # WHICH AUTOKEY GUI DID YOU USE? - type: dropdown id: gui attributes: label: Which AutoKey GUI did you use? multiple: false options: - GTK - Qt - Both validations: required: false # WHICH AUTOKEY VERSION DID YOU USE? - type: textarea id: autokey-version attributes: label: Which AutoKey version did you use? description: > Paste in your AutoKey version and, if the problem is known to be present in more than one version, please list them all. validations: required: false # HOW DID YOU INSTALL AUTOKEY? - type: textarea id: installation-method attributes: label: How did you install AutoKey? description: > Describe where your copy of AutoKey came from. placeholder: From Git, pip3, a PPA, my distribution's repository, etc. validations: required: false # CAN YOU DESCRIBE THE ISSUE? - type: textarea id: summary attributes: label: Can you briefly describe the issue? description: Provide a short summary of the problem. validations: required: false # CAN THE ISSUE BE REPRODUCED? - type: dropdown id: reproducibility attributes: label: Can the issue be reproduced? description: null multiple: false options: - Always - I didn't try - N/A - Rarely - Sometimes - Unable validations: required: false # WHAT ARE THE STEPS TO RREPRODUCE THE ISSUE? - type: textarea id: steps-to-reproduce attributes: label: What are the steps to reproduce the issue? description: > Provide the steps that need to be taken to reproduce the behavior. For example: placeholder: | 1. Do this. 2. Then do that. 3. Etc. validations: required: false # WHAT SHOULD HAVE HAPPENED? - type: textarea id: expected-result attributes: label: What should have happened? description: Provide a description of what you expected to happen. validations: required: false # WHAT ACTUALLY HAPPENED? - type: textarea id: actual-result attributes: label: What actually happened? description: Provide a description of what actually happened. validations: required: false # DO YOU HAVE SCREENSHOTS? - type: textarea id: screenshot attributes: label: Do you have screenshots? description: > If you have one or more screenshots of the issue, include them by dragging the image file(s) below: validations: required: false # CAN YOU PROVIDE THE OUTPUT OF THE AUTOKEY COMMAND? - type: textarea id: verbose-output attributes: label: Can you provide the output of the AutoKey command? description: > This is only needed for some bugs. Sometimes, the output from starting AutoKey from a terminal window with either the ```autokey-gtk``` or ```autokey-qt``` command provides helpful messages that can be pasted below. If those aren't sufficient or if your issue involves a crash, throws an exception, or produces other unexpected results, [running an AutoKey trace](https://github.com/autokey/autokey-python2/wiki/Problem-Reporting-Guide) may be helpful. You can do this by running AutoKey from a terminal window with the ```autokey-gtk --verbose``` or ```autokey-qt --verbose``` command, then recreating your issue, then closing AutoKey, and then pasting any messages it produces below: render: bash validations: required: false # ANYTHING ELSE? - type: textarea id: notes attributes: label: Anything else? description: > Describe any debugging steps you've taken, a workaround you've figured out, or any other information you think we might need, including links or references. validations: required: false ... autokey-0.96.0/.github/workflows/000077500000000000000000000000001427671440700167005ustar00rootroot00000000000000autokey-0.96.0/.github/workflows/build.yml000066400000000000000000000115271427671440700205300ustar00rootroot00000000000000# This workflow will install Python dependencies, build debs, and upload to pypi. # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python build on: push: tags: - 'v*.*.*' # Always manually run on CI branches: [ CI ] jobs: build_deb: runs-on: ubuntu-latest strategy: matrix: python-version: [3.10.1] steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Cache pip uses: actions/cache@v2 with: # This path is specific to Ubuntu path: ~/.cache/pip # Look to see if there is a cache hit for the corresponding requirements file key: ${{ runner.os }}-pip-${{ hashFiles('pip-requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- - name: Install dependencies run: | sudo apt update sudo apt install $(cat apt-requirements.txt) python -m pip install --upgrade pip pip install flake8 pytest pytest-cov wheel pip install -r pip-requirements.txt - name: Build .deb # Developed from instructions on the wiki # The debian/ folder contains dependencies etc, so all this action needs to do is activate the process. run: | debian/build.sh mkdir debs mv ../*.deb debs - name: Upload built debs as artifacts uses: actions/upload-artifact@master with: name: debs path: debs build_pypi_wheel: runs-on: ubuntu-latest strategy: matrix: python-version: [3.10.1] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Cache pip uses: actions/cache@v2 with: # This path is specific to Ubuntu path: ~/.cache/pip # Look to see if there is a cache hit for the corresponding requirements file key: ${{ runner.os }}-pip-${{ hashFiles('pip-requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- - name: Install dependencies run: | sudo apt update sudo apt install $(cat apt-requirements.txt) python -m pip install --upgrade pip pip install build - name: Build a binary wheel and a source tarball run: >- python -m build --sdist --wheel --outdir dist/ - name: Upload built dist as artifact uses: actions/upload-artifact@master with: name: dist path: dist/ release: runs-on: ubuntu-latest strategy: matrix: python-version: [3.10.1] needs: [build_pypi_wheel, build_deb] steps: - uses: actions/checkout@v2 with: # Fetch all history so that we get tags. fetch-depth: 0 # Waits for checks to complete before releasing. - name: Wait on tests uses: lewagon/wait-on-check-action@master with: ref: ${{ github.ref }} repo-token: ${{ secrets.GITHUB_TOKEN }} wait-interval: 10 check-name: 'pytest (3.10.1)' - name: Download built dist as artifact uses: actions/download-artifact@master with: name: dist path: dist/ - name: Download built debs as artifacts uses: actions/download-artifact@master with: name: debs path: ../debs - name: Publish a Python distribution to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: user: __token__ password: ${{ secrets.PYPI_API_TOKEN }} - name: Create a release and attach files run: | git fetch --tags --force origin="$(git config --get remote.origin.url)" # Will get tag name only if a tag triggered this workflow. # tagname="${GITHUB_REF#refs/tags/}" # Use this to get the most recent tag on this branch rather than the tag triggering the current build. tagname="$(git describe --tags --abbrev=0 --match "v*.*.*")" prerelease="--prerelease" notes="[See here for changelog for this release]($origin/blob/$tagname/CHANGELOG.rst)" # If we are a tag and on master, should be a full release not a prerelease if git branch --all --contains tags/$tagname | grep --silent "master"; then prerelease="" fi # Include other files by adding them as additional arguments gh release create "$tagname" ../debs/*.deb --title "$tagname" --notes "$notes" $prerelease # ../autokey-common_${{ github.ref }}_all.deb env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} autokey-0.96.0/.github/workflows/python-test.yml000066400000000000000000000121621427671440700217230ustar00rootroot00000000000000# This workflow will install Python dependencies, run tests and lint with a single version of Python # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions name: Python test on: push: # Test on tags to ensure that the version metadata has been updated in tag as well. tags: - '*' branches: [ CI, master, develop, beta ] pull_request: branches: [ master, develop, beta ] jobs: Lint: runs-on: ubuntu-latest strategy: matrix: python-version: [3.6, 3.10.1] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Cache pip uses: actions/cache@v2 with: # This path is specific to Ubuntu path: ~/.cache/pip # Look to see if there is a cache hit for the corresponding requirements file key: ${{ runner.os }}-pip-${{ hashFiles('pip-requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- - name: Install dependencies run: | sudo apt update sudo apt install $(cat apt-requirements.txt) python -m pip install --upgrade pip pip install flake8 wheel pip install -r pip-requirements.txt - name: Python Code Quality and Lint uses: ricardochaves/python-lint@v1.3.0 with: python-root-list: "lib/autokey tests" use-pylint: false use-pycodestyle: false use-flake8: true use-black: false use-mypy: false use-isort: false extra-pylint-options: "" extra-pycodestyle-options: "" # select = stop the build if there are Python syntax errors or undefined names # exit-zero treats all errors as warnings. # "_" is part of gettext, not actually a built-in, but used almost # everywhere without explicit definition. # The GitHub editor is 127 chars wide. extra-flake8-options: > --count --select=E9,F63,F7,F82 --show-source --statistics --builtins=_ --max-complexity=10 --max-line-length=127 extra-black-options: "" extra-mypy-options: "" extra-isort-options: "" pytest: runs-on: ubuntu-latest strategy: matrix: python-version: [3.6, 3.10.1] steps: - uses: actions/checkout@v2 with: fetch-depth: 0 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Cache pip uses: actions/cache@v2 with: # This path is specific to Ubuntu path: ~/.cache/pip # Look to see if there is a cache hit for the corresponding requirements file key: ${{ runner.os }}-pip-${{ hashFiles('pip-requirements.txt') }} restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- - name: Install dependencies run: | sudo apt update sudo apt install $(cat apt-requirements.txt) python -m pip install --upgrade pip pip install flake8 tox pytest pytest-cov wheel pip install -r pip-requirements.txt - name: Test with tox and pytest run: | tox -e clean,coverage,report - name: Upload pytest test results uses: actions/upload-artifact@v2 with: name: pytest-results-${{ matrix.python-version }} path: junit/test-results-${{ matrix.python-version }}.xml # Use always() to always run this step to publish test results when there are test failures if: ${{ always() }} - name: Archive test coverage report run: | tar -cvzf test_coverage_report-${{ matrix.python-version }}.tar.gz test_coverage_report_html/ - name: Upload test coverage report uses: actions/upload-artifact@v2 with: name: test_coverage_report-${{ matrix.python-version }}.tar.gz path: test_coverage_report-${{ matrix.python-version }}.tar.gz # Use always() to always run this step to publish test results when there are test failures if: ${{ always() }} test-install: # Just runs basic app options to ensure pip installation has included relevant imports and put app in path. runs-on: ubuntu-latest strategy: matrix: python-version: [3.6, 3.10.1] steps: - uses: actions/checkout@v2 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: python-version: ${{ matrix.python-version }} - name: Install OS dependencies only run: | sudo apt update sudo apt install $(cat apt-requirements.txt) python -m pip install --upgrade pip pip install dbus-python gobject pygobject PyQt5 qscintilla - name: Test installation run: | pip install "${GITHUB_WORKSPACE}" autokey-gtk --help # qt xcb module requires a display to connect to, so won't work. # export QT_DEBUG_PLUGINS=1 # autokey-qt --help autokey-0.96.0/.gitignore000066400000000000000000000002111427671440700152650ustar00rootroot00000000000000*.html *.bak *.swp __pycache__ *.egg-info *.pyc .tox/ # IDE's .idea .vscode test_coverage_annotated_source/ test_coverage_report_html/ autokey-0.96.0/ACKNOWLEDGMENTS000066400000000000000000000042041427671440700154530ustar00rootroot00000000000000Thanks go to Sam Peterson (peabody17), the original developer of AutoKey, for allowing me to join his project and not objecting when I more or less took it over. My sincerest thanks to tiheum, the designer of the Faenza icon set for Linux. One of his designs is the basis of AutoKey's icon and his superb icon set makes my desktop a joy to use. I'd like to pass my gratitude to all who have made donations to the AutoKey project: theoa, jschall, mfseeker, jflevi, keycombat, rkcallahan, bkudria, riffian, and many others. Many thanks to the developers of xdotool, from which I copied the idea of dynamically modifying the keyboard map to send non-mapped characters. I also want to thank the developers of Phrase Express. Their application's UI was the inspiration for AutoKey's configuration window. The below are the original acknowledgements from Sam Peterson. --------------------------------------------------------------------- Above all, I wish to thank my wife Margaret Tam. You're always there when I need you. You're always there to support me throughout the good and bad in life. I love you my dear. I'd like to thank Eddie Bell for his keylogger example. It was the turning point in my early development of the program that gave me what I needed for monitoring keyboard input. I'd especially like to thank Peter Liljenberg for writing the python-xlib extension, without which this software wouldn't have been possible. I'd like to acknowledge Textpander, Texter and the Autohotkey developers whose software was an inspiration to this software. I'd like to thank all of the people on the Ubuntu forums who have given me their feedback and/or tried the program (in no particular order or preference): Vadi, Scarath, jackocleebrown, ugm6hr, RebounD11, Bungo Pony, forrestcupp, kevdog, conehead77, PartisanEntity, DjBones, antisocialist, Specter043, Linuxratty, SanskritFritz, Martje_001, tribaal, Vitamin-Carrot. I apologize profusely if I have forgotten to mention anyone. I'd like to thank SourceForge for hosting the project. Lastly I'd like to thank Guido van Rossum for authoring the Python programming language, and RMS for gcc and GNU Emacs. autokey-0.96.0/CHANGELOG.rst000066400000000000000000000775231427671440700153420ustar00rootroot00000000000000========= Changelog ========= Version 0.96.0 ============================ Important misc changes ---------------------- - Script and phrase metadata are no longer stored as hidden dotfiles. Existing scripts should be automatically converted, but if switch back to versions prior to this one, you will need to copy or symlink them back to dotfile form. - Scripting API files are now in Python packages, which may require adjusting imports if you have scripts that import them directly. - Change the default phrase send mode to `ctrl+v` (paste using clipboard) rather than sending keys one at a time. - This version represents some significant refactoring since the previous update, so bug reports will be highly appreciated. Features --------- Scripting API ^^^^^^^^^^^^^ **engine API object** - Deprecated: Confusingly named engine.create_abbreviation() and engine.create_hotkey() are deprecated and will be removed in the future. Use engine.create_phrase() with appropriate arguments instead. - Extended: engine.create_phrase() now supports multiple new optional arguments, allowing to fully configure the created phrase. It can set everything the GUI can do. - New: Scripts can use engine.get_triggered_abbreviation() to read which abbreviation triggered it’s execution. The function returns a tuple containing the abbreviation and the trigger character (the character that 'completed' or 'confirmed' the abbreviation. Both tuple elements are None if the script was not triggered by an abbreviation. The trigger character is None if the script was configured to 'trigger immediately'. The function always returns a tuple, so direct tuple unpacking like abbreviation, trigger = engine.get_triggered_abbreviation() will always work. - Allow creation of 'temporary' hotkeys and whole folders (which do not persist between sessions). - Allow overriding existing hotkeys when creating phrases with hotkeys. - Allow creation of folders. - Add `set_clipboard_image` methods for both Gtk and Qt. Takes a file path to an image to load into the clipboard. **keyboard API object** - keyboard.send_keys() got a new optional parameter send_mode, allowing to specify how the given text is sent. It basically offers the same pasting options as are available to AutoKey Phrases. - keyboard.send_keys() now raises a TypeError instead of a generic AssertionError, if parameters don’t match the expected types. **New clipboard API method** - Change the default phrase send mode to `ctrl+v` (paste using clipboard) rather than sending keys one at a time. **New mouse API object** - Add mouse drag, click and scroll options to the API. Command line interface ++++++++++++++++++++++ - Added a --version command line switch. It prints the current AutoKey version on the standard output and then exits. Graphical user interfaces +++++++++++++++++++++++++ - (GTK) Warn user about missing required and optional programs on startup. - (GTK) UI will now update when changes are detected to watched files. - (GTK) refresh UI if script files are modified externally - Use system monospace font - Add setting to change GtkSourceView theme, (defaults to classic). Other +++++ - Add `wait_for_keyevent` scripting function. - Rewrote script error logging system, with a neat Script Error Dialog to go with it. - `
 
[hide private]
[frames] | no frames]
[ Module Hierarchy | Class Hierarchy ]

Class Hierarchy

autokey-0.96.0/doc/scripting/crarr.png000066400000000000000000000005241427671440700176720ustar00rootroot00000000000000PNG  IHDR eE,tEXtCreation TimeTue 22 Aug 2006 00:43:10 -0500` XtIME)} pHYsnu>gAMA aEPLTEðf4sW ЊrD`@bCܖX{`,lNo@xdE螊dƴ~TwvtRNS@fMIDATxc`@0&+(;; /EXؑ? n  b;'+Y#(r<"IENDB`autokey-0.96.0/doc/scripting/epydoc.css000066400000000000000000000372271427671440700200620ustar00rootroot00000000000000 /* Epydoc CSS Stylesheet * * This stylesheet can be used to customize the appearance of epydoc's * HTML output. * */ /* Default Colors & Styles * - Set the default foreground & background color with 'body'; and * link colors with 'a:link' and 'a:visited'. * - Use bold for decision list terms. * - The heading styles defined here are used for headings *within* * docstring descriptions. All headings used by epydoc itself use * either class='epydoc' or class='toc' (CSS styles for both * defined below). */ body { background: #ffffff; color: #000000; } p { margin-top: 0.5em; margin-bottom: 0.5em; } a:link { color: #0000ff; } a:visited { color: #204080; } dt { font-weight: bold; } h1 { font-size: +140%; font-style: italic; font-weight: bold; } h2 { font-size: +125%; font-style: italic; font-weight: bold; } h3 { font-size: +110%; font-style: italic; font-weight: normal; } code { font-size: 100%; } /* N.B.: class, not pseudoclass */ a.link { font-family: monospace; } /* Page Header & Footer * - The standard page header consists of a navigation bar (with * pointers to standard pages such as 'home' and 'trees'); a * breadcrumbs list, which can be used to navigate to containing * classes or modules; options links, to show/hide private * variables and to show/hide frames; and a page title (using *

). The page title may be followed by a link to the * corresponding source code (using 'span.codelink'). * - The footer consists of a navigation bar, a timestamp, and a * pointer to epydoc's homepage. */ h1.epydoc { margin: 0; font-size: +140%; font-weight: bold; } h2.epydoc { font-size: +130%; font-weight: bold; } h3.epydoc { font-size: +115%; font-weight: bold; margin-top: 0.2em; } td h3.epydoc { font-size: +115%; font-weight: bold; margin-bottom: 0; } table.navbar { background: #a0c0ff; color: #000000; border: 2px groove #c0d0d0; } table.navbar table { color: #000000; } th.navbar-select { background: #70b0ff; color: #000000; } table.navbar a { text-decoration: none; } table.navbar a:link { color: #0000ff; } table.navbar a:visited { color: #204080; } span.breadcrumbs { font-size: 85%; font-weight: bold; } span.options { font-size: 70%; } span.codelink { font-size: 85%; } td.footer { font-size: 85%; } /* Table Headers * - Each summary table and details section begins with a 'header' * row. This row contains a section title (marked by * 'span.table-header') as well as a show/hide private link * (marked by 'span.options', defined above). * - Summary tables that contain user-defined groups mark those * groups using 'group header' rows. */ td.table-header { background: #70b0ff; color: #000000; border: 1px solid #608090; } td.table-header table { color: #000000; } td.table-header table a:link { color: #0000ff; } td.table-header table a:visited { color: #204080; } span.table-header { font-size: 120%; font-weight: bold; } th.group-header { background: #c0e0f8; color: #000000; text-align: left; font-style: italic; font-size: 115%; border: 1px solid #608090; } /* Summary Tables (functions, variables, etc) * - Each object is described by a single row of the table with * two cells. The left cell gives the object's type, and is * marked with 'code.summary-type'. The right cell gives the * object's name and a summary description. * - CSS styles for the table's header and group headers are * defined above, under 'Table Headers' */ table.summary { border-collapse: collapse; background: #e8f0f8; color: #000000; border: 1px solid #608090; margin-bottom: 0.5em; } td.summary { border: 1px solid #608090; } code.summary-type { font-size: 85%; } table.summary a:link { color: #0000ff; } table.summary a:visited { color: #204080; } /* Details Tables (functions, variables, etc) * - Each object is described in its own div. * - A single-row summary table w/ table-header is used as * a header for each details section (CSS style for table-header * is defined above, under 'Table Headers'). */ table.details { border-collapse: collapse; background: #e8f0f8; color: #000000; border: 1px solid #608090; margin: .2em 0 0 0; } table.details table { color: #000000; } table.details a:link { color: #0000ff; } table.details a:visited { color: #204080; } /* Fields */ dl.fields { margin-left: 2em; margin-top: 1em; margin-bottom: 1em; } dl.fields dd ul { margin-left: 0em; padding-left: 0em; } dl.fields dd ul li ul { margin-left: 2em; padding-left: 0em; } div.fields { margin-left: 2em; } div.fields p { margin-bottom: 0.5em; } /* Index tables (identifier index, term index, etc) * - link-index is used for indices containing lists of links * (namely, the identifier index & term index). * - index-where is used in link indices for the text indicating * the container/source for each link. * - metadata-index is used for indices containing metadata * extracted from fields (namely, the bug index & todo index). */ table.link-index { border-collapse: collapse; background: #e8f0f8; color: #000000; border: 1px solid #608090; } td.link-index { border-width: 0px; } table.link-index a:link { color: #0000ff; } table.link-index a:visited { color: #204080; } span.index-where { font-size: 70%; } table.metadata-index { border-collapse: collapse; background: #e8f0f8; color: #000000; border: 1px solid #608090; margin: .2em 0 0 0; } td.metadata-index { border-width: 1px; border-style: solid; } table.metadata-index a:link { color: #0000ff; } table.metadata-index a:visited { color: #204080; } /* Function signatures * - sig* is used for the signature in the details section. * - .summary-sig* is used for the signature in the summary * table, and when listing property accessor functions. * */ .sig-name { color: #006080; } .sig-arg { color: #008060; } .sig-default { color: #602000; } .summary-sig { font-family: monospace; } .summary-sig-name { color: #006080; font-weight: bold; } table.summary a.summary-sig-name:link { color: #006080; font-weight: bold; } table.summary a.summary-sig-name:visited { color: #006080; font-weight: bold; } .summary-sig-arg { color: #006040; } .summary-sig-default { color: #501800; } /* Subclass list */ ul.subclass-list { display: inline; } ul.subclass-list li { display: inline; } /* To render variables, classes etc. like functions */ table.summary .summary-name { color: #006080; font-weight: bold; font-family: monospace; } table.summary a.summary-name:link { color: #006080; font-weight: bold; font-family: monospace; } table.summary a.summary-name:visited { color: #006080; font-weight: bold; font-family: monospace; } /* Variable values * - In the 'variable details' sections, each varaible's value is * listed in a 'pre.variable' box. The width of this box is * restricted to 80 chars; if the value's repr is longer than * this it will be wrapped, using a backslash marked with * class 'variable-linewrap'. If the value's repr is longer * than 3 lines, the rest will be ellided; and an ellipsis * marker ('...' marked with 'variable-ellipsis') will be used. * - If the value is a string, its quote marks will be marked * with 'variable-quote'. * - If the variable is a regexp, it is syntax-highlighted using * the re* CSS classes. */ pre.variable { padding: .5em; margin: 0; background: #dce4ec; color: #000000; border: 1px solid #708890; } .variable-linewrap { color: #604000; font-weight: bold; } .variable-ellipsis { color: #604000; font-weight: bold; } .variable-quote { color: #604000; font-weight: bold; } .variable-group { color: #008000; font-weight: bold; } .variable-op { color: #604000; font-weight: bold; } .variable-string { color: #006030; } .variable-unknown { color: #a00000; font-weight: bold; } .re { color: #000000; } .re-char { color: #006030; } .re-op { color: #600000; } .re-group { color: #003060; } .re-ref { color: #404040; } /* Base tree * - Used by class pages to display the base class hierarchy. */ pre.base-tree { font-size: 80%; margin: 0; } /* Frames-based table of contents headers * - Consists of two frames: one for selecting modules; and * the other listing the contents of the selected module. * - h1.toc is used for each frame's heading * - h2.toc is used for subheadings within each frame. */ h1.toc { text-align: center; font-size: 105%; margin: 0; font-weight: bold; padding: 0; } h2.toc { font-size: 100%; font-weight: bold; margin: 0.5em 0 0 -0.3em; } /* Syntax Highlighting for Source Code * - doctest examples are displayed in a 'pre.py-doctest' block. * If the example is in a details table entry, then it will use * the colors specified by the 'table pre.py-doctest' line. * - Source code listings are displayed in a 'pre.py-src' block. * Each line is marked with 'span.py-line' (used to draw a line * down the left margin, separating the code from the line * numbers). Line numbers are displayed with 'span.py-lineno'. * The expand/collapse block toggle button is displayed with * 'a.py-toggle' (Note: the CSS style for 'a.py-toggle' should not * modify the font size of the text.) * - If a source code page is opened with an anchor, then the * corresponding code block will be highlighted. The code * block's header is highlighted with 'py-highlight-hdr'; and * the code block's body is highlighted with 'py-highlight'. * - The remaining py-* classes are used to perform syntax * highlighting (py-string for string literals, py-name for names, * etc.) */ pre.py-doctest { padding: .5em; margin: 1em; background: #e8f0f8; color: #000000; border: 1px solid #708890; } table pre.py-doctest { background: #dce4ec; color: #000000; } pre.py-src { border: 2px solid #000000; background: #f0f0f0; color: #000000; } .py-line { border-left: 2px solid #000000; margin-left: .2em; padding-left: .4em; } .py-lineno { font-style: italic; font-size: 90%; padding-left: .5em; } a.py-toggle { text-decoration: none; } div.py-highlight-hdr { border-top: 2px solid #000000; border-bottom: 2px solid #000000; background: #d8e8e8; } div.py-highlight { border-bottom: 2px solid #000000; background: #d0e0e0; } .py-prompt { color: #005050; font-weight: bold;} .py-more { color: #005050; font-weight: bold;} .py-string { color: #006030; } .py-comment { color: #003060; } .py-keyword { color: #600000; } .py-output { color: #404040; } .py-name { color: #000050; } .py-name:link { color: #000050 !important; } .py-name:visited { color: #000050 !important; } .py-number { color: #005000; } .py-defname { color: #000060; font-weight: bold; } .py-def-name { color: #000060; font-weight: bold; } .py-base-class { color: #000060; } .py-param { color: #000060; } .py-docstring { color: #006030; } .py-decorator { color: #804020; } /* Use this if you don't want links to names underlined: */ /*a.py-name { text-decoration: none; }*/ /* Graphs & Diagrams * - These CSS styles are used for graphs & diagrams generated using * Graphviz dot. 'img.graph-without-title' is used for bare * diagrams (to remove the border created by making the image * clickable). */ img.graph-without-title { border: none; } img.graph-with-title { border: 1px solid #000000; } span.graph-title { font-weight: bold; } span.graph-caption { } /* General-purpose classes * - 'p.indent-wrapped-lines' defines a paragraph whose first line * is not indented, but whose subsequent lines are. * - The 'nomargin-top' class is used to remove the top margin (e.g. * from lists). The 'nomargin' class is used to remove both the * top and bottom margin (but not the left or right margin -- * for lists, that would cause the bullets to disappear.) */ p.indent-wrapped-lines { padding: 0 0 0 7em; text-indent: -7em; margin: 0; } .nomargin-top { margin-top: 0; } .nomargin { margin-top: 0; margin-bottom: 0; } /* HTML Log */ div.log-block { padding: 0; margin: .5em 0 .5em 0; background: #e8f0f8; color: #000000; border: 1px solid #000000; } div.log-error { padding: .1em .3em .1em .3em; margin: 4px; background: #ffb0b0; color: #000000; border: 1px solid #000000; } div.log-warning { padding: .1em .3em .1em .3em; margin: 4px; background: #ffffb0; color: #000000; border: 1px solid #000000; } div.log-info { padding: .1em .3em .1em .3em; margin: 4px; background: #b0ffb0; color: #000000; border: 1px solid #000000; } h2.log-hdr { background: #70b0ff; color: #000000; margin: 0; padding: 0em 0.5em 0em 0.5em; border-bottom: 1px solid #000000; font-size: 110%; } p.log { font-weight: bold; margin: .5em 0 .5em 0; } tr.opt-changed { color: #000000; font-weight: bold; } tr.opt-default { color: #606060; } pre.log { margin: 0; padding: 0; padding-left: 1em; } autokey-0.96.0/doc/scripting/epydoc.js000066400000000000000000000245251427671440700177030ustar00rootroot00000000000000function toggle_private() { // Search for any private/public links on this page. Store // their old text in "cmd," so we will know what action to // take; and change their text to the opposite action. var cmd = "?"; var elts = document.getElementsByTagName("a"); for(var i=0; i...
"; elt.innerHTML = s; } } function toggle(id) { elt = document.getElementById(id+"-toggle"); if (elt.innerHTML == "-") collapse(id); else expand(id); return false; } function highlight(id) { var elt = document.getElementById(id+"-def"); if (elt) elt.className = "py-highlight-hdr"; var elt = document.getElementById(id+"-expanded"); if (elt) elt.className = "py-highlight"; var elt = document.getElementById(id+"-collapsed"); if (elt) elt.className = "py-highlight"; } function num_lines(s) { var n = 1; var pos = s.indexOf("\n"); while ( pos > 0) { n += 1; pos = s.indexOf("\n", pos+1); } return n; } // Collapse all blocks that mave more than `min_lines` lines. function collapse_all(min_lines) { var elts = document.getElementsByTagName("div"); for (var i=0; i 0) if (elt.id.substring(split, elt.id.length) == "-expanded") if (num_lines(elt.innerHTML) > min_lines) collapse(elt.id.substring(0, split)); } } function expandto(href) { var start = href.indexOf("#")+1; if (start != 0 && start != href.length) { if (href.substring(start, href.length) != "-") { collapse_all(4); pos = href.indexOf(".", start); while (pos != -1) { var id = href.substring(start, pos); expand(id); pos = href.indexOf(".", pos+1); } var id = href.substring(start, href.length); expand(id); highlight(id); } } } function kill_doclink(id) { var parent = document.getElementById(id); parent.removeChild(parent.childNodes.item(0)); } function auto_kill_doclink(ev) { if (!ev) var ev = window.event; if (!this.contains(ev.toElement)) { var parent = document.getElementById(this.parentID); parent.removeChild(parent.childNodes.item(0)); } } function doclink(id, name, targets_id) { var elt = document.getElementById(id); // If we already opened the box, then destroy it. // (This case should never occur, but leave it in just in case.) if (elt.childNodes.length > 1) { elt.removeChild(elt.childNodes.item(0)); } else { // The outer box: relative + inline positioning. var box1 = document.createElement("div"); box1.style.position = "relative"; box1.style.display = "inline"; box1.style.top = 0; box1.style.left = 0; // A shadow for fun var shadow = document.createElement("div"); shadow.style.position = "absolute"; shadow.style.left = "-1.3em"; shadow.style.top = "-1.3em"; shadow.style.background = "#404040"; // The inner box: absolute positioning. var box2 = document.createElement("div"); box2.style.position = "relative"; box2.style.border = "1px solid #a0a0a0"; box2.style.left = "-.2em"; box2.style.top = "-.2em"; box2.style.background = "white"; box2.style.padding = ".3em .4em .3em .4em"; box2.style.fontStyle = "normal"; box2.onmouseout=auto_kill_doclink; box2.parentID = id; // Get the targets var targets_elt = document.getElementById(targets_id); var targets = targets_elt.getAttribute("targets"); var links = ""; target_list = targets.split(","); for (var i=0; i" + target[0] + ""; } // Put it all together. elt.insertBefore(box1, elt.childNodes.item(0)); //box1.appendChild(box2); box1.appendChild(shadow); shadow.appendChild(box2); box2.innerHTML = "Which "+name+" do you want to see documentation for?" + ""; } return false; } function get_anchor() { var href = location.href; var start = href.indexOf("#")+1; if ((start != 0) && (start != href.length)) return href.substring(start, href.length); } function redirect_url(dottedName) { // Scan through each element of the "pages" list, and check // if "name" matches with any of them. for (var i=0; i-m" or "-c"; // extract the portion & compare it to dottedName. var pagename = pages[i].substring(0, pages[i].length-2); if (pagename == dottedName.substring(0,pagename.length)) { // We've found a page that matches `dottedName`; // construct its URL, using leftover `dottedName` // content to form an anchor. var pagetype = pages[i].charAt(pages[i].length-1); var url = pagename + ((pagetype=="m")?"-module.html": "-class.html"); if (dottedName.length > pagename.length) url += "#" + dottedName.substring(pagename.length+1, dottedName.length); return url; } } } autokey-0.96.0/doc/scripting/frames.html000066400000000000000000000011231427671440700202120ustar00rootroot00000000000000 API Documentation autokey-0.96.0/doc/scripting/help.html000066400000000000000000000247451427671440700177040ustar00rootroot00000000000000 Help
 
[hide private]
[frames] | no frames]

API Documentation

This document contains the API (Application Programming Interface) documentation for this project. Documentation for the Python objects defined by the project is divided into separate pages for each package, module, and class. The API documentation also includes two pages containing information about the project as a whole: a trees page, and an index page.

Object Documentation

Each Package Documentation page contains:

  • A description of the package.
  • A list of the modules and sub-packages contained by the package.
  • A summary of the classes defined by the package.
  • A summary of the functions defined by the package.
  • A summary of the variables defined by the package.
  • A detailed description of each function defined by the package.
  • A detailed description of each variable defined by the package.

Each Module Documentation page contains:

  • A description of the module.
  • A summary of the classes defined by the module.
  • A summary of the functions defined by the module.
  • A summary of the variables defined by the module.
  • A detailed description of each function defined by the module.
  • A detailed description of each variable defined by the module.

Each Class Documentation page contains:

  • A class inheritance diagram.
  • A list of known subclasses.
  • A description of the class.
  • A summary of the methods defined by the class.
  • A summary of the instance variables defined by the class.
  • A summary of the class (static) variables defined by the class.
  • A detailed description of each method defined by the class.
  • A detailed description of each instance variable defined by the class.
  • A detailed description of each class (static) variable defined by the class.

Project Documentation

The Trees page contains the module and class hierarchies:

  • The module hierarchy lists every package and module, with modules grouped into packages. At the top level, and within each package, modules and sub-packages are listed alphabetically.
  • The class hierarchy lists every class, grouped by base class. If a class has more than one base class, then it will be listed under each base class. At the top level, and under each base class, classes are listed alphabetically.

The Index page contains indices of terms and identifiers:

  • The term index lists every term indexed by any object's documentation. For each term, the index provides links to each place where the term is indexed.
  • The identifier index lists the (short) name of every package, module, class, method, function, variable, and parameter. For each identifier, the index provides a short description, and a link to its documentation.

The Table of Contents

The table of contents occupies the two frames on the left side of the window. The upper-left frame displays the project contents, and the lower-left frame displays the module contents:

Project
Contents
...
API
Documentation
Frame


Module
Contents
 
...
 

The project contents frame contains a list of all packages and modules that are defined by the project. Clicking on an entry will display its contents in the module contents frame. Clicking on a special entry, labeled "Everything," will display the contents of the entire project.

The module contents frame contains a list of every submodule, class, type, exception, function, and variable defined by a module or package. Clicking on an entry will display its documentation in the API documentation frame. Clicking on the name of the module, at the top of the frame, will display the documentation for the module itself.

The "frames" and "no frames" buttons below the top navigation bar can be used to control whether the table of contents is displayed or not.

The Navigation Bar

A navigation bar is located at the top and bottom of every page. It indicates what type of page you are currently viewing, and allows you to go to related pages. The following table describes the labels on the navigation bar. Note that not some labels (such as [Parent]) are not displayed on all pages.

Label Highlighted when... Links to...
[Parent] (never highlighted) the parent of the current package
[Package] viewing a package the package containing the current object
[Module] viewing a module the module containing the current object
[Class] viewing a class the class containing the current object
[Trees] viewing the trees page the trees page
[Index] viewing the index page the index page
[Help] viewing the help page the help page

The "show private" and "hide private" buttons below the top navigation bar can be used to control whether documentation for private objects is displayed. Private objects are usually defined as objects whose (short) names begin with a single underscore, but do not end with an underscore. For example, "_x", "__pprint", and "epydoc.epytext._tokenize" are private objects; but "re.sub", "__init__", and "type_" are not. However, if a module defines the "__all__" variable, then its contents are used to decide which objects are private.

A timestamp below the bottom navigation bar indicates when each page was last updated.

autokey-0.96.0/doc/scripting/identifier-index.html000066400000000000000000000721721427671440700222000ustar00rootroot00000000000000 Identifier Index
 
[hide private]
[frames] | no frames]

Identifier Index

[ A B C D E F G H I J K L M N O P Q R S T U V W X Y Z _ ]

A

C

E

F

G

I

K

L

M

O

P

Q

R

S

W

_



autokey-0.96.0/doc/scripting/index.html000066400000000000000000000011231427671440700200440ustar00rootroot00000000000000 API Documentation autokey-0.96.0/doc/scripting/lib.scripting-module.html000066400000000000000000000206541427671440700230010ustar00rootroot00000000000000 lib.scripting
Package lib :: Module scripting
[hide private]
[frames] | no frames]

Module scripting

source code

Classes [hide private]
  Keyboard
Provides access to the keyboard for event generation.
  Mouse
Provides access to send mouse clicks
  Store
Allows persistent storage of values between invocations of the script.
  QtDialog
Provides a simple interface for the display of some basic dialogs to collect information from the user.
  System
Simplified access to some system commands.
  GtkDialog
Provides a simple interface for the display of some basic dialogs to collect information from the user.
  QtClipboard
Read/write access to the X selection and clipboard - QT version
  GtkClipboard
Read/write access to the X selection and clipboard - GTK version
  Window
Basic window management using wmctrl
  Engine
Provides access to the internals of AutoKey.
Variables [hide private]
  __package__ = 'lib'
autokey-0.96.0/doc/scripting/lib.scripting-pysrc.html000066400000000000000000011230771427671440700226600ustar00rootroot00000000000000 lib.scripting
Package lib :: Module scripting
[hide private]
[frames] | no frames]

Source Code for Module lib.scripting

   1  # -*- coding: utf-8 -*- 
   2   
   3  # Copyright (C) 2011 Chris Dekter 
   4  # 
   5  # This program is free software: you can redistribute it and/or modify 
   6  # it under the terms of the GNU General Public License as published by 
   7  # the Free Software Foundation, either version 3 of the License, or 
   8  # (at your option) any later version. 
   9  # 
  10  # This program is distributed in the hope that it will be useful, 
  11  # but WITHOUT ANY WARRANTY; without even the implied warranty of 
  12  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
  13  # GNU General Public License for more details. 
  14  # 
  15  # You should have received a copy of the GNU General Public License 
  16  # along with this program.  If not, see <http://www.gnu.org/licenses/>. 
  17   
  18  import subprocess, threading, time, re 
  19  import common#, model, iomediator 
  20  #if common.USING_QT: 
  21  #    from PyQt4.QtGui import QClipboard, QApplication 
  22  #else: 
  23  #    from gi.repository import Gtk, Gdk 
  24   
25 -class Keyboard:
26 """ 27 Provides access to the keyboard for event generation. 28 """ 29
30 - def __init__(self, mediator):
31 self.mediator = mediator
32
33 - def send_keys(self, keyString):
34 """ 35 Send a sequence of keys via keyboard events 36 37 Usage: C{keyboard.send_keys(keyString)} 38 39 @param keyString: string of keys (including special keys) to send 40 """ 41 self.mediator.interface.begin_send() 42 self.mediator.send_string(keyString.decode("utf-8")) 43 self.mediator.interface.finish_send()
44
45 - def send_key(self, key, repeat=1):
46 """ 47 Send a keyboard event 48 49 Usage: C{keyboard.send_key(key, repeat=1)} 50 51 @param key: they key to be sent (e.g. "s" or "<enter>") 52 @param repeat: number of times to repeat the key event 53 """ 54 for x in xrange(repeat): 55 self.mediator.send_key(key.decode("utf-8")) 56 self.mediator.flush()
57
58 - def press_key(self, key):
59 """ 60 Send a key down event 61 62 Usage: C{keyboard.press_key(key)} 63 64 The key will be treated as down until a matching release_key() is sent. 65 @param key: they key to be pressed (e.g. "s" or "<enter>") 66 """ 67 self.mediator.press_key(key.decode("utf-8"))
68
69 - def release_key(self, key):
70 """ 71 Send a key up event 72 73 Usage: C{keyboard.release_key(key)} 74 75 If the specified key was not made down using press_key(), the event will be 76 ignored. 77 @param key: they key to be released (e.g. "s" or "<enter>") 78 """ 79 self.mediator.release_key(key.decode("utf-8"))
80
81 - def fake_keypress(self, key, repeat=1):
82 """ 83 Fake a keypress 84 85 Usage: C{keyboard.fake_keypress(key, repeat=1)} 86 87 Uses XTest to 'fake' a keypress. This is useful to send keypresses to some 88 applications which won't respond to keyboard.send_key() 89 90 @param key: they key to be sent (e.g. "s" or "<enter>") 91 @param repeat: number of times to repeat the key event 92 """ 93 for x in xrange(repeat): 94 self.mediator.fake_keypress(key.decode("utf-8"))
95
96 - def wait_for_keypress(self, key, modifiers=[], timeOut=10.0):
97 """ 98 Wait for a keypress or key combination 99 100 Usage: C{keyboard.wait_for_keypress(self, key, modifiers=[], timeOut=10.0)} 101 102 Note: this function cannot be used to wait for modifier keys on their own 103 104 @param key: they key to wait for 105 @param modifiers: list of modifiers that should be pressed with the key 106 @param timeOut: maximum time, in seconds, to wait for the keypress to occur 107 """ 108 w = iomediator.Waiter(key, modifiers, None, timeOut) 109 w.wait()
110 111
112 -class Mouse:
113 """ 114 Provides access to send mouse clicks 115 """
116 - def __init__(self, mediator):
117 self.mediator = mediator
118
119 - def click_relative(self, x, y, button):
120 """ 121 Send a mouse click relative to the active window 122 123 Usage: C{mouse.click_relative(x, y, button)} 124 125 @param x: x-coordinate in pixels, relative to upper left corner of window 126 @param y: y-coordinate in pixels, relative to upper left corner of window 127 @param button: mouse button to simulate (left=1, middle=2, right=3) 128 """ 129 self.mediator.send_mouse_click(x, y, button, True)
130
131 - def click_relative_self(self, x, y, button):
132 """ 133 Send a mouse click relative to the current mouse position 134 135 Usage: C{mouse.click_relative_self(x, y, button)} 136 137 @param x: x-offset in pixels, relative to current mouse position 138 @param y: y-offset in pixels, relative to current mouse position 139 @param button: mouse button to simulate (left=1, middle=2, right=3) 140 """ 141 self.mediator.send_mouse_click_relative(x, y, button)
142
143 - def click_absolute(self, x, y, button):
144 """ 145 Send a mouse click relative to the screen (absolute) 146 147 Usage: C{mouse.click_absolute(x, y, button)} 148 149 @param x: x-coordinate in pixels, relative to upper left corner of window 150 @param y: y-coordinate in pixels, relative to upper left corner of window 151 @param button: mouse button to simulate (left=1, middle=2, right=3) 152 """ 153 self.mediator.send_mouse_click(x, y, button, False)
154
155 - def wait_for_click(self, button, timeOut=10.0):
156 """ 157 Wait for a mouse click 158 159 Usage: C{mouse.wait_for_click(self, button, timeOut=10.0)} 160 161 @param button: they mouse button click to wait for as a button number, 1-9 162 @param timeOut: maximum time, in seconds, to wait for the keypress to occur 163 """ 164 button = int(button) 165 w = iomediator.Waiter(None, None, button, timeOut) 166 w.wait()
167 168
169 -class Store(dict):
170 """ 171 Allows persistent storage of values between invocations of the script. 172 """ 173
174 - def set_value(self, key, value):
175 """ 176 Store a value 177 178 Usage: C{store.set_value(key, value)} 179 """ 180 self[key] = value
181
182 - def get_value(self, key):
183 """ 184 Get a value 185 186 Usage: C{store.get_value(key)} 187 """ 188 return self[key]
189
190 - def remove_value(self, key):
191 """ 192 Remove a value 193 194 Usage: C{store.remove_value(key)} 195 """ 196 del self[key]
197
198 - def set_global_value(self, key, value):
199 """ 200 Store a global value 201 202 Usage: C{store.set_global_value(key, value)} 203 204 The value stored with this method will be available to all scripts. 205 """ 206 Store.GLOBALS[key] = value
207
208 - def get_global_value(self, key):
209 """ 210 Get a global value 211 212 Usage: C{store.get_global_value(key)} 213 """ 214 return self.GLOBALS[key]
215
216 - def remove_global_value(self, key):
217 """ 218 Remove a global value 219 220 Usage: C{store.remove_global_value(key)} 221 """ 222 del self.GLOBALS[key]
223 224
225 -class QtDialog:
226 """ 227 Provides a simple interface for the display of some basic dialogs to collect information from the user. 228 229 This version uses KDialog to integrate well with KDE. To pass additional arguments to KDialog that are 230 not specifically handled, use keyword arguments. For example, to pass the --geometry argument to KDialog 231 to specify the desired size of the dialog, pass C{geometry="700x400"} as one of the parameters. All 232 keyword arguments must be given as strings. 233 234 A note on exit codes: an exit code of 0 indicates that the user clicked OK. 235 """ 236
237 - def _run_kdialog(self, title, args, kwargs):
238 for k, v in kwargs.iteritems(): 239 args.append("--" + k) 240 args.append(v) 241 242 p = subprocess.Popen(["kdialog", "--title", title] + args, stdout=subprocess.PIPE) 243 retCode = p.wait() 244 output = p.stdout.read()[:-1] # Drop trailing newline 245 246 return (retCode, output)
247
248 - def info_dialog(self, title="Information", message="", **kwargs):
249 """ 250 Show an information dialog 251 252 Usage: C{dialog.info_dialog(title="Information", message="", **kwargs)} 253 254 @param title: window title for the dialog 255 @param message: message displayed in the dialog 256 @return: a tuple containing the exit code and user input 257 @rtype: C{tuple(int, str)} 258 """ 259 return self._run_kdialog(title, ["--msgbox", message], kwargs)
260
261 - def input_dialog(self, title="Enter a value", message="Enter a value", default="", **kwargs):
262 """ 263 Show an input dialog 264 265 Usage: C{dialog.input_dialog(title="Enter a value", message="Enter a value", default="", **kwargs)} 266 267 @param title: window title for the dialog 268 @param message: message displayed above the input box 269 @param default: default value for the input box 270 @return: a tuple containing the exit code and user input 271 @rtype: C{tuple(int, str)} 272 """ 273 return self._run_kdialog(title, ["--inputbox", message, default], kwargs)
274
275 - def password_dialog(self, title="Enter password", message="Enter password", **kwargs):
276 """ 277 Show a password input dialog 278 279 Usage: C{dialog.password_dialog(title="Enter password", message="Enter password", **kwargs)} 280 281 @param title: window title for the dialog 282 @param message: message displayed above the password input box 283 @return: a tuple containing the exit code and user input 284 @rtype: C{tuple(int, str)} 285 """ 286 return self._run_kdialog(title, ["--password", message], kwargs)
287
288 - def combo_menu(self, options, title="Choose an option", message="Choose an option", **kwargs):
289 """ 290 Show a combobox menu 291 292 Usage: C{dialog.combo_menu(options, title="Choose an option", message="Choose an option", **kwargs)} 293 294 @param options: list of options (strings) for the dialog 295 @param title: window title for the dialog 296 @param message: message displayed above the combobox 297 @return: a tuple containing the exit code and user choice 298 @rtype: C{tuple(int, str)} 299 """ 300 return self._run_kdialog(title, ["--combobox", message] + options, kwargs)
301
302 - def list_menu(self, options, title="Choose a value", message="Choose a value", default=None, **kwargs):
303 """ 304 Show a single-selection list menu 305 306 Usage: C{dialog.list_menu(options, title="Choose a value", message="Choose a value", default=None, **kwargs)} 307 308 @param options: list of options (strings) for the dialog 309 @param title: window title for the dialog 310 @param message: message displayed above the list 311 @param default: default value to be selected 312 @return: a tuple containing the exit code and user choice 313 @rtype: C{tuple(int, str)} 314 """ 315 316 choices = [] 317 optionNum = 0 318 for option in options: 319 choices.append(str(optionNum)) 320 choices.append(option) 321 if option == default: 322 choices.append("on") 323 else: 324 choices.append("off") 325 optionNum += 1 326 327 retCode, result = self._run_kdialog(title, ["--radiolist", message] + choices, kwargs) 328 choice = options[int(result)] 329 330 return retCode, choice
331
332 - def list_menu_multi(self, options, title="Choose one or more values", message="Choose one or more values", defaults=[], **kwargs):
333 """ 334 Show a multiple-selection list menu 335 336 Usage: C{dialog.list_menu_multi(options, title="Choose one or more values", message="Choose one or more values", defaults=[], **kwargs)} 337 338 @param options: list of options (strings) for the dialog 339 @param title: window title for the dialog 340 @param message: message displayed above the list 341 @param defaults: list of default values to be selected 342 @return: a tuple containing the exit code and user choice 343 @rtype: C{tuple(int, str)} 344 """ 345 346 choices = [] 347 optionNum = 0 348 for option in options: 349 choices.append(str(optionNum)) 350 choices.append(option) 351 if option in defaults: 352 choices.append("on") 353 else: 354 choices.append("off") 355 optionNum += 1 356 357 retCode, output = self._run_kdialog(title, ["--separate-output", "--checklist", message] + choices, kwargs) 358 results = output.split() 359 360 choices = [] 361 for index in results: 362 choices.append(options[int(index)]) 363 364 return retCode, choices
365
366 - def open_file(self, title="Open File", initialDir="~", fileTypes="*|All Files", rememberAs=None, **kwargs):
367 """ 368 Show an Open File dialog 369 370 Usage: C{dialog.open_file(title="Open File", initialDir="~", fileTypes="*|All Files", rememberAs=None, **kwargs)} 371 372 @param title: window title for the dialog 373 @param initialDir: starting directory for the file dialog 374 @param fileTypes: file type filter expression 375 @param rememberAs: gives an ID to this file dialog, allowing it to open at the last used path next time 376 @return: a tuple containing the exit code and file path 377 @rtype: C{tuple(int, str)} 378 """ 379 if rememberAs is not None: 380 return self._run_kdialog(title, ["--getopenfilename", initialDir, fileTypes, ":" + rememberAs], kwargs) 381 else: 382 return self._run_kdialog(title, ["--getopenfilename", initialDir, fileTypes], kwargs)
383
384 - def save_file(self, title="Save As", initialDir="~", fileTypes="*|All Files", rememberAs=None, **kwargs):
385 """ 386 Show a Save As dialog 387 388 Usage: C{dialog.save_file(title="Save As", initialDir="~", fileTypes="*|All Files", rememberAs=None, **kwargs)} 389 390 @param title: window title for the dialog 391 @param initialDir: starting directory for the file dialog 392 @param fileTypes: file type filter expression 393 @param rememberAs: gives an ID to this file dialog, allowing it to open at the last used path next time 394 @return: a tuple containing the exit code and file path 395 @rtype: C{tuple(int, str)} 396 """ 397 if rememberAs is not None: 398 return self._run_kdialog(title, ["--getsavefilename", initialDir, fileTypes, ":" + rememberAs], kwargs) 399 else: 400 return self._run_kdialog(title, ["--getsavefilename", initialDir, fileTypes], kwargs)
401
402 - def choose_directory(self, title="Select Directory", initialDir="~", rememberAs=None, **kwargs):
403 """ 404 Show a Directory Chooser dialog 405 406 Usage: C{dialog.choose_directory(title="Select Directory", initialDir="~", rememberAs=None, **kwargs)} 407 408 @param title: window title for the dialog 409 @param initialDir: starting directory for the directory chooser dialog 410 @param rememberAs: gives an ID to this file dialog, allowing it to open at the last used path next time 411 @return: a tuple containing the exit code and chosen path 412 @rtype: C{tuple(int, str)} 413 """ 414 if rememberAs is not None: 415 return self._run_kdialog(title, ["--getexistingdirectory", initialDir, ":" + rememberAs], kwargs) 416 else: 417 return self._run_kdialog(title, ["--getexistingdirectory", initialDir], kwargs)
418
419 - def choose_colour(self, title="Select Colour", **kwargs):
420 """ 421 Show a Colour Chooser dialog 422 423 Usage: C{dialog.choose_colour(title="Select Colour")} 424 425 @param title: window title for the dialog 426 @return: a tuple containing the exit code and colour 427 @rtype: C{tuple(int, str)} 428 """ 429 return self._run_kdialog(title, ["--getcolor"], kwargs)
430
431 - def calendar(self, title="Choose a date", format="%Y-%m-%d", date="today", **kwargs):
432 """ 433 Show a calendar dialog 434 435 Usage: C{dialog.calendar_dialog(title="Choose a date", format="%Y-%m-%d", date="YYYY-MM-DD", **kwargs)} 436 437 Note: the format and date parameters are not currently used 438 439 @param title: window title for the dialog 440 @param format: format of date to be returned 441 @param date: initial date as YYYY-MM-DD, otherwise today 442 @return: a tuple containing the exit code and date 443 @rtype: C{tuple(int, str)} 444 """ 445 return self._run_kdialog(title, ["--calendar"], kwargs)
446 447
448 -class System:
449 """ 450 Simplified access to some system commands. 451 """ 452
453 - def exec_command(self, command, getOutput=True):
454 """ 455 Execute a shell command 456 457 Usage: C{system.exec_command(command, getOutput=True)} 458 459 Set getOutput to False if the command does not exit and return immediately. Otherwise 460 AutoKey will not respond to any hotkeys/abbreviations etc until the process started 461 by the command exits. 462 463 @param command: command to be executed (including any arguments) - e.g. "ls -l" 464 @param getOutput: whether to capture the (stdout) output of the command 465 @raise subprocess.CalledProcessError: if the command returns a non-zero exit code 466 """ 467 if getOutput: 468 p = subprocess.Popen(command, shell=True, bufsize=-1, stdout=subprocess.PIPE) 469 retCode = p.wait() 470 output = p.stdout.read()[:-1] 471 if retCode != 0: 472 raise subprocess.CalledProcessError(retCode, output) 473 else: 474 return output 475 else: 476 subprocess.Popen(command, shell=True, bufsize=-1)
477
478 - def create_file(self, fileName, contents=""):
479 """ 480 Create a file with contents 481 482 Usage: C{system.create_file(fileName, contents="")} 483 484 @param fileName: full path to the file to be created 485 @param contents: contents to insert into the file 486 """ 487 f = open(fileName, "w") 488 f.write(contents) 489 f.close()
490 491
492 -class GtkDialog:
493 """ 494 Provides a simple interface for the display of some basic dialogs to collect information from the user. 495 496 This version uses Zenity to integrate well with GNOME. To pass additional arguments to Zenity that are 497 not specifically handled, use keyword arguments. For example, to pass the --timeout argument to Zenity 498 pass C{timeout="15"} as one of the parameters. All keyword arguments must be given as strings. 499 500 A note on exit codes: an exit code of 0 indicates that the user clicked OK. 501 """ 502
503 - def _run_zenity(self, title, args, kwargs):
504 for k, v in kwargs.iteritems(): 505 args.append("--" + k) 506 args.append(v) 507 508 p = subprocess.Popen(["zenity", "--title", title] + args, stdout=subprocess.PIPE) 509 retCode = p.wait() 510 output = p.stdout.read()[:-1] # Drop trailing newline 511 512 return (retCode, output)
513
514 - def info_dialog(self, title="Information", message="", **kwargs):
515 """ 516 Show an information dialog 517 518 Usage: C{dialog.info_dialog(title="Information", message="", **kwargs)} 519 520 @param title: window title for the dialog 521 @param message: message displayed in the dialog 522 @return: a tuple containing the exit code and user input 523 @rtype: C{tuple(int, str)} 524 """ 525 return self._run_zenity(title, ["--info", "--text", message], kwargs)
526
527 - def input_dialog(self, title="Enter a value", message="Enter a value", default="", **kwargs):
528 """ 529 Show an input dialog 530 531 Usage: C{dialog.input_dialog(title="Enter a value", message="Enter a value", default="", **kwargs)} 532 533 @param title: window title for the dialog 534 @param message: message displayed above the input box 535 @param default: default value for the input box 536 @return: a tuple containing the exit code and user input 537 @rtype: C{tuple(int, str)} 538 """ 539 return self._run_zenity(title, ["--entry", "--text", message, "--entry-text", default], kwargs)
540
541 - def password_dialog(self, title="Enter password", message="Enter password", **kwargs):
542 """ 543 Show a password input dialog 544 545 Usage: C{dialog.password_dialog(title="Enter password", message="Enter password")} 546 547 @param title: window title for the dialog 548 @param message: message displayed above the password input box 549 @return: a tuple containing the exit code and user input 550 @rtype: C{tuple(int, str)} 551 """ 552 return self._run_zenity(title, ["--entry", "--text", message, "--hide-text"], kwargs) 553 554 #def combo_menu(self, options, title="Choose an option", message="Choose an option"): 555 """ 556 Show a combobox menu - not supported by zenity 557 558 Usage: C{dialog.combo_menu(options, title="Choose an option", message="Choose an option")} 559 560 @param options: list of options (strings) for the dialog 561 @param title: window title for the dialog 562 @param message: message displayed above the combobox 563 """
564 #return self._run_zenity(title, ["--combobox", message] + options) 565
566 - def list_menu(self, options, title="Choose a value", message="Choose a value", default=None, **kwargs):
567 """ 568 Show a single-selection list menu 569 570 Usage: C{dialog.list_menu(options, title="Choose a value", message="Choose a value", default=None, **kwargs)} 571 572 @param options: list of options (strings) for the dialog 573 @param title: window title for the dialog 574 @param message: message displayed above the list 575 @param default: default value to be selected 576 @return: a tuple containing the exit code and user choice 577 @rtype: C{tuple(int, str)} 578 """ 579 580 choices = [] 581 #optionNum = 0 582 for option in options: 583 if option == default: 584 choices.append("TRUE") 585 else: 586 choices.append("FALSE") 587 588 #choices.append(str(optionNum)) 589 choices.append(option) 590 #optionNum += 1 591 592 return self._run_zenity(title, ["--list", "--radiolist", "--text", message, "--column", " ", "--column", "Options"] + choices, kwargs)
593 594 #return retCode, choice 595
596 - def list_menu_multi(self, options, title="Choose one or more values", message="Choose one or more values", defaults=[], **kwargs):
597 """ 598 Show a multiple-selection list menu 599 600 Usage: C{dialog.list_menu_multi(options, title="Choose one or more values", message="Choose one or more values", defaults=[], **kwargs)} 601 602 @param options: list of options (strings) for the dialog 603 @param title: window title for the dialog 604 @param message: message displayed above the list 605 @param defaults: list of default values to be selected 606 @return: a tuple containing the exit code and user choice 607 @rtype: C{tuple(int, str)} 608 """ 609 610 choices = [] 611 #optionNum = 0 612 for option in options: 613 if option in defaults: 614 choices.append("TRUE") 615 else: 616 choices.append("FALSE") 617 618 #choices.append(str(optionNum)) 619 choices.append(option) 620 #optionNum += 1 621 622 retCode, output = self._run_zenity(title, ["--list", "--checklist", "--text", message, "--column", " ", "--column", "Options"] + choices, kwargs) 623 results = output.split('|') 624 625 #choices = [] 626 #for choice in results: 627 # choices.append(choice) 628 629 return retCode, results
630
631 - def open_file(self, title="Open File", **kwargs):
632 """ 633 Show an Open File dialog 634 635 Usage: C{dialog.open_file(title="Open File", **kwargs)} 636 637 @param title: window title for the dialog 638 @return: a tuple containing the exit code and file path 639 @rtype: C{tuple(int, str)} 640 """ 641 #if rememberAs is not None: 642 # return self._run_zenity(title, ["--getopenfilename", initialDir, fileTypes, ":" + rememberAs]) 643 #else: 644 return self._run_zenity(title, ["--file-selection"], kwargs)
645
646 - def save_file(self, title="Save As", **kwargs):
647 """ 648 Show a Save As dialog 649 650 Usage: C{dialog.save_file(title="Save As", **kwargs)} 651 652 @param title: window title for the dialog 653 @return: a tuple containing the exit code and file path 654 @rtype: C{tuple(int, str)} 655 """ 656 #if rememberAs is not None: 657 # return self._run_zenity(title, ["--getsavefilename", initialDir, fileTypes, ":" + rememberAs]) 658 #else: 659 return self._run_zenity(title, ["--file-selection", "--save"], kwargs)
660
661 - def choose_directory(self, title="Select Directory", initialDir="~", **kwargs):
662 """ 663 Show a Directory Chooser dialog 664 665 Usage: C{dialog.choose_directory(title="Select Directory", **kwargs)} 666 667 @param title: window title for the dialog 668 @return: a tuple containing the exit code and path 669 @rtype: C{tuple(int, str)} 670 """ 671 #if rememberAs is not None: 672 # return self._run_zenity(title, ["--getexistingdirectory", initialDir, ":" + rememberAs]) 673 #else: 674 return self._run_zenity(title, ["--file-selection", "--directory"], kwargs) 675 676 #def choose_colour(self, title="Select Colour"): 677 """ 678 Show a Colour Chooser dialog - not supported by zenity 679 680 Usage: C{dialog.choose_colour(title="Select Colour")} 681 682 @param title: window title for the dialog 683 """
684 #return self._run_zenity(title, ["--getcolor"]) 685
686 - def calendar(self, title="Choose a date", format="%Y-%m-%d", date="today", **kwargs):
687 """ 688 Show a calendar dialog 689 690 Usage: C{dialog.calendar_dialog(title="Choose a date", format="%Y-%m-%d", date="YYYY-MM-DD", **kwargs)} 691 692 @param title: window title for the dialog 693 @param format: format of date to be returned 694 @param date: initial date as YYYY-MM-DD, otherwise today 695 @return: a tuple containing the exit code and date 696 @rtype: C{tuple(int, str)} 697 """ 698 if re.match(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", date): 699 year = date[0:4] 700 month = date[5:7] 701 day = date[8:10] 702 date_args = ["--year=" + year, "--month=" + month, "--day=" + day] 703 else: 704 date_args = [] 705 return self._run_zenity(title, ["--calendar", "--date-format=" + format] + date_args, kwargs)
706 707
708 -class QtClipboard:
709 """ 710 Read/write access to the X selection and clipboard - QT version 711 """ 712
713 - def __init__(self, app):
714 self.clipBoard = QApplication.clipboard() 715 self.app = app
716
717 - def fill_selection(self, contents):
718 """ 719 Copy text into the X selection 720 721 Usage: C{clipboard.fill_selection(contents)} 722 723 @param contents: string to be placed in the selection 724 """ 725 self.__execAsync(self.__fillSelection, contents)
726
727 - def __fillSelection(self, string):
728 self.clipBoard.setText(string, QClipboard.Selection) 729 self.sem.release()
730
731 - def get_selection(self):
732 """ 733 Read text from the X selection 734 735 Usage: C{clipboard.get_selection()} 736 737 @return: text contents of the mouse selection 738 @rtype: C{str} 739 """ 740 self.__execAsync(self.__getSelection) 741 return unicode(self.text)
742
743 - def __getSelection(self):
744 self.text = self.clipBoard.text(QClipboard.Selection) 745 self.sem.release()
746
747 - def fill_clipboard(self, contents):
748 """ 749 Copy text into the clipboard 750 751 Usage: C{clipboard.fill_clipboard(contents)} 752 753 @param contents: string to be placed in the selection 754 """ 755 self.__execAsync(self.__fillClipboard, contents)
756
757 - def __fillClipboard(self, string):
758 self.clipBoard.setText(string, QClipboard.Clipboard) 759 self.sem.release()
760
761 - def get_clipboard(self):
762 """ 763 Read text from the clipboard 764 765 Usage: C{clipboard.get_clipboard()} 766 767 @return: text contents of the clipboard 768 @rtype: C{str} 769 """ 770 self.__execAsync(self.__getClipboard) 771 return unicode(self.text)
772
773 - def __getClipboard(self):
774 self.text = self.clipBoard.text(QClipboard.Clipboard) 775 self.sem.release()
776
777 - def __execAsync(self, callback, *args):
778 self.sem = threading.Semaphore(0) 779 self.app.exec_in_main(callback, *args) 780 self.sem.acquire()
781 782
783 -class GtkClipboard:
784 """ 785 Read/write access to the X selection and clipboard - GTK version 786 """ 787
788 - def __init__(self, app):
789 self.clipBoard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) 790 self.selection = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY) 791 self.app = app
792
793 - def fill_selection(self, contents):
794 """ 795 Copy text into the X selection 796 797 Usage: C{clipboard.fill_selection(contents)} 798 799 @param contents: string to be placed in the selection 800 """ 801 #self.__execAsync(self.__fillSelection, contents) 802 self.__fillSelection(contents)
803
804 - def __fillSelection(self, string):
805 Gdk.threads_enter() 806 self.selection.set_text(string.encode("utf-8")) 807 Gdk.threads_leave()
808 #self.sem.release() 809
810 - def get_selection(self):
811 """ 812 Read text from the X selection 813 814 Usage: C{clipboard.get_selection()} 815 816 @return: text contents of the mouse selection 817 @rtype: C{str} 818 @raise Exception: if no text was found in the selection 819 """ 820 self.__execAsync(self.selection.request_text, self.__receive) 821 if self.text is not None: 822 return self.text.decode("utf-8") 823 else: 824 raise Exception("No text found in X selection")
825
826 - def __receive(self, cb, text, data=None):
827 self.text = text 828 self.sem.release()
829
830 - def fill_clipboard(self, contents):
831 """ 832 Copy text into the clipboard 833 834 Usage: C{clipboard.fill_clipboard(contents)} 835 836 @param contents: string to be placed in the selection 837 """ 838 self.__fillClipboard(contents)
839
840 - def __fillClipboard(self, string):
841 Gdk.threads_enter() 842 self.clipBoard.set_text(string.encode("utf-8")) 843 Gdk.threads_leave()
844 #self.sem.release() 845
846 - def get_clipboard(self):
847 """ 848 Read text from the clipboard 849 850 Usage: C{clipboard.get_clipboard()} 851 852 @return: text contents of the clipboard 853 @rtype: C{str} 854 @raise Exception: if no text was found on the clipboard 855 """ 856 self.__execAsync(self.clipBoard.request_text, self.__receive) 857 if self.text is not None: 858 return self.text.decode("utf-8") 859 else: 860 raise Exception("No text found on clipboard")
861
862 - def __execAsync(self, callback, *args):
863 self.sem = threading.Semaphore(0) 864 Gdk.threads_enter() 865 callback(*args) 866 Gdk.threads_leave() 867 self.sem.acquire()
868 869
870 -class Window:
871 """ 872 Basic window management using wmctrl 873 874 Note: in all cases where a window title is required (with the exception of wait_for_focus()), 875 two special values of window title are permitted: 876 877 :ACTIVE: - select the currently active window 878 :SELECT: - select the desired window by clicking on it 879 """ 880
881 - def __init__(self, mediator):
882 self.mediator = mediator
883
884 - def wait_for_focus(self, title, timeOut=5):
885 """ 886 Wait for window with the given title to have focus 887 888 Usage: C{window.wait_for_focus(title, timeOut=5)} 889 890 If the window becomes active, returns True. Otherwise, returns False if 891 the window has not become active by the time the timeout has elapsed. 892 893 @param title: title to match against (as a regular expression) 894 @param timeOut: period (seconds) to wait before giving up 895 @rtype: boolean 896 """ 897 regex = re.compile(title) 898 waited = 0 899 while waited <= timeOut: 900 if regex.match(self.mediator.interface.get_window_title()): 901 return True 902 903 if timeOut == 0: 904 break # zero length timeout, if not matched go straight to end 905 906 time.sleep(0.3) 907 waited += 0.3 908 909 return False
910
911 - def wait_for_exist(self, title, timeOut=5):
912 """ 913 Wait for window with the given title to be created 914 915 Usage: C{window.wait_for_exist(title, timeOut=5)} 916 917 If the window is in existence, returns True. Otherwise, returns False if 918 the window has not been created by the time the timeout has elapsed. 919 920 @param title: title to match against (as a regular expression) 921 @param timeOut: period (seconds) to wait before giving up 922 @rtype: boolean 923 """ 924 regex = re.compile(title) 925 waited = 0 926 while waited <= timeOut: 927 retCode, output = self._run_wmctrl(["-l"]) 928 for line in output.split('\n'): 929 if regex.match(line[14:].split(' ', 1)[-1]): 930 return True 931 932 if timeOut == 0: 933 break # zero length timeout, if not matched go straight to end 934 935 time.sleep(0.3) 936 waited += 0.3 937 938 return False
939
940 - def activate(self, title, switchDesktop=False, matchClass=False):
941 """ 942 Activate the specified window, giving it input focus 943 944 Usage: C{window.activate(title, switchDesktop=False, matchClass=False)} 945 946 If switchDesktop is False (default), the window will be moved to the current desktop 947 and activated. Otherwise, switch to the window's current desktop and activate it there. 948 949 @param title: window title to match against (as case-insensitive substring match) 950 @param switchDesktop: whether or not to switch to the window's current desktop 951 @param matchClass: if True, match on the window class instead of the title 952 """ 953 if switchDesktop: 954 args = ["-a", title] 955 else: 956 args = ["-R", title] 957 if matchClass: 958 args += ["-x"] 959 self._run_wmctrl(args)
960
961 - def close(self, title, matchClass=False):
962 """ 963 Close the specified window gracefully 964 965 Usage: C{window.close(title, matchClass=False)} 966 967 @param title: window title to match against (as case-insensitive substring match) 968 @param matchClass: if True, match on the window class instead of the title 969 """ 970 if matchClass: 971 self._run_wmctrl(["-c", title, "-x"]) 972 else: 973 self._run_wmctrl(["-c", title])
974
975 - def resize_move(self, title, xOrigin=-1, yOrigin=-1, width=-1, height=-1, matchClass=False):
976 """ 977 Resize and/or move the specified window 978 979 Usage: C{window.close(title, xOrigin=-1, yOrigin=-1, width=-1, height=-1, matchClass=False)} 980 981 Leaving and of the position/dimension values as the default (-1) will cause that 982 value to be left unmodified. 983 984 @param title: window title to match against (as case-insensitive substring match) 985 @param xOrigin: new x origin of the window (upper left corner) 986 @param yOrigin: new y origin of the window (upper left corner) 987 @param width: new width of the window 988 @param height: new height of the window 989 @param matchClass: if True, match on the window class instead of the title 990 """ 991 mvArgs = ["0", str(xOrigin), str(yOrigin), str(width), str(height)] 992 if matchClass: 993 xArgs = ["-x"] 994 else: 995 xArgs = [] 996 self._run_wmctrl(["-r", title, "-e", ','.join(mvArgs)] + xArgs)
997 998
999 - def move_to_desktop(self, title, deskNum, matchClass=False):
1000 """ 1001 Move the specified window to the given desktop 1002 1003 Usage: C{window.move_to_desktop(title, deskNum, matchClass=False)} 1004 1005 @param title: window title to match against (as case-insensitive substring match) 1006 @param deskNum: desktop to move the window to (note: zero based) 1007 @param matchClass: if True, match on the window class instead of the title 1008 """ 1009 if matchClass: 1010 xArgs = ["-x"] 1011 else: 1012 xArgs = [] 1013 self._run_wmctrl(["-r", title, "-t", str(deskNum)] + xArgs)
1014 1015
1016 - def switch_desktop(self, deskNum):
1017 """ 1018 Switch to the specified desktop 1019 1020 Usage: C{window.switch_desktop(deskNum)} 1021 1022 @param deskNum: desktop to switch to (note: zero based) 1023 """ 1024 self._run_wmctrl(["-s", str(deskNum)])
1025
1026 - def set_property(self, title, action, prop, matchClass=False):
1027 """ 1028 Set a property on the given window using the specified action 1029 1030 Usage: C{window.set_property(title, action, prop, matchClass=False)} 1031 1032 Allowable actions: C{add, remove, toggle} 1033 Allowable properties: C{modal, sticky, maximized_vert, maximized_horz, shaded, skip_taskbar, 1034 skip_pager, hidden, fullscreen, above} 1035 1036 @param title: window title to match against (as case-insensitive substring match) 1037 @param action: one of the actions listed above 1038 @param prop: one of the properties listed above 1039 @param matchClass: if True, match on the window class instead of the title 1040 """ 1041 if matchClass: 1042 xArgs = ["-x"] 1043 else: 1044 xArgs = [] 1045 self._run_wmctrl(["-r", title, "-b" + action + ',' + prop] + xArgs)
1046
1047 - def get_active_geometry(self):
1048 """ 1049 Get the geometry of the currently active window 1050 1051 Usage: C{window.get_active_geometry()} 1052 1053 @return: a 4-tuple containing the x-origin, y-origin, width and height of the window (in pixels) 1054 @rtype: C{tuple(int, int, int, int)} 1055 """ 1056 active = self.mediator.interface.get_window_title() 1057 result, output = self._run_wmctrl(["-l", "-G"]) 1058 matchingLine = None 1059 for line in output.split('\n'): 1060 if active in line[34:].split(' ', 1)[-1]: 1061 matchingLine = line 1062 1063 if matchingLine is not None: 1064 output = matchingLine.split()[2:6] 1065 return map(int, output) 1066 else: 1067 return None
1068
1069 - def get_active_title(self):
1070 """ 1071 Get the visible title of the currently active window 1072 1073 Usage: C{window.get_active_title()} 1074 1075 @return: the visible title of the currentle active window 1076 @rtype: C{str} 1077 """ 1078 return self.mediator.interface.get_window_title()
1079
1080 - def get_active_class(self):
1081 """ 1082 Get the class of the currently active window 1083 1084 Usage: C{window.get_active_class()} 1085 1086 @return: the class of the currentle active window 1087 @rtype: C{str} 1088 """ 1089 return self.mediator.interface.get_window_class()
1090
1091 - def _run_wmctrl(self, args):
1092 p = subprocess.Popen(["wmctrl"] + args, stdout=subprocess.PIPE) 1093 retCode = p.wait() 1094 output = p.stdout.read()[:-1] # Drop trailing newline 1095 1096 return (retCode, output)
1097 1098
1099 -class Engine:
1100 """ 1101 Provides access to the internals of AutoKey. 1102 1103 Note that any configuration changes made using this API while the configuration window 1104 is open will not appear until it is closed and re-opened. 1105 """ 1106
1107 - def __init__(self, configManager, runner):
1108 self.configManager = configManager 1109 self.runner = runner 1110 self.monitor = configManager.app.monitor 1111 self.__returnValue = ''
1112
1113 - def get_folder(self, title):
1114 """ 1115 Retrieve a folder by its title 1116 1117 Usage: C{engine.get_folder(title)} 1118 1119 Note that if more than one folder has the same title, only the first match will be 1120 returned. 1121 """ 1122 for folder in self.configManager.allFolders: 1123 if folder.title == title: 1124 return folder 1125 return None
1126
1127 - def create_phrase(self, folder, description, contents):
1128 """ 1129 Create a text phrase 1130 1131 Usage: C{engine.create_phrase(folder, description, contents)} 1132 1133 A new phrase with no abbreviation or hotkey is created in the specified folder 1134 1135 @param folder: folder to place the abbreviation in, retrieved using C{engine.get_folder()} 1136 @param description: description for the phrase 1137 @param contents: the expansion text 1138 """ 1139 self.monitor.suspend() 1140 p = model.Phrase(description, contents) 1141 folder.add_item(p) 1142 p.persist() 1143 self.monitor.unsuspend() 1144 self.configManager.config_altered(False)
1145
1146 - def create_abbreviation(self, folder, description, abbr, contents):
1147 """ 1148 Create a text abbreviation 1149 1150 Usage: C{engine.create_abbreviation(folder, description, abbr, contents)} 1151 1152 When the given abbreviation is typed, it will be replaced with the given 1153 text. 1154 1155 @param folder: folder to place the abbreviation in, retrieved using C{engine.get_folder()} 1156 @param description: description for the phrase 1157 @param abbr: the abbreviation that will trigger the expansion 1158 @param contents: the expansion text 1159 @raise Exception: if the specified abbreviation is not unique 1160 """ 1161 if not self.configManager.check_abbreviation_unique(abbr, None, None): 1162 raise Exception("The specified abbreviation is already in use") 1163 1164 self.monitor.suspend() 1165 p = model.Phrase(description, contents) 1166 p.modes.append(model.TriggerMode.ABBREVIATION) 1167 p.abbreviations = [abbr] 1168 folder.add_item(p) 1169 p.persist() 1170 self.monitor.unsuspend() 1171 self.configManager.config_altered(False)
1172
1173 - def create_hotkey(self, folder, description, modifiers, key, contents):
1174 """ 1175 Create a text hotkey 1176 1177 Usage: C{engine.create_hotkey(folder, description, modifiers, key, contents)} 1178 1179 When the given hotkey is pressed, it will be replaced with the given 1180 text. Modifiers must be given as a list of strings, with the following 1181 values permitted: 1182 1183 <ctrl> 1184 <alt> 1185 <super> 1186 <hyper> 1187 <shift> 1188 1189 The key must be an unshifted character (i.e. lowercase) 1190 1191 @param folder: folder to place the abbreviation in, retrieved using C{engine.get_folder()} 1192 @param description: description for the phrase 1193 @param modifiers: modifiers to use with the hotkey (as a list) 1194 @param key: the hotkey 1195 @param contents: the expansion text 1196 @raise Exception: if the specified hotkey is not unique 1197 """ 1198 modifiers.sort() 1199 if not self.configManager.check_hotkey_unique(modifiers, key, None, None): 1200 raise Exception("The specified hotkey and modifier combination is already in use") 1201 1202 self.monitor.suspend() 1203 p = model.Phrase(description, contents) 1204 p.modes.append(model.TriggerMode.HOTKEY) 1205 p.set_hotkey(modifiers, key) 1206 folder.add_item(p) 1207 p.persist() 1208 self.monitor.unsuspend() 1209 self.configManager.config_altered(False)
1210
1211 - def run_script(self, description):
1212 """ 1213 Run an existing script using its description to look it up 1214 1215 Usage: C{engine.run_script(description)} 1216 1217 @param description: description of the script to run 1218 @raise Exception: if the specified script does not exist 1219 """ 1220 targetScript = None 1221 for item in self.configManager.allItems: 1222 if item.description == description and isinstance(item, model.Script): 1223 targetScript = item 1224 1225 if targetScript is not None: 1226 self.runner.run_subscript(targetScript) 1227 else: 1228 raise Exception("No script with description '%s' found" % description)
1229
1230 - def run_script_from_macro(self, args):
1231 """ 1232 Used internally by AutoKey for phrase macros 1233 """ 1234 self.__macroArgs = args["args"].split(',') 1235 1236 try: 1237 self.run_script(args["name"]) 1238 except Exception, e: 1239 self.set_return_value("{ERROR: %s}" % str(e))
1240
1241 - def get_macro_arguments(self):
1242 """ 1243 Get the arguments supplied to the current script via its macro 1244 1245 Usage: C{engine.get_macro_arguments()} 1246 1247 @return: the arguments 1248 @rtype: C{list(str())} 1249 """ 1250 return self.__macroArgs
1251
1252 - def set_return_value(self, val):
1253 """ 1254 Store a return value to be used by a phrase macro 1255 1256 Usage: C{engine.set_return_value(val)} 1257 1258 @param val: value to be stored 1259 """ 1260 self.__returnValue = val
1261
1262 - def _get_return_value(self):
1263 """ 1264 Used internally by AutoKey for phrase macros 1265 """ 1266 ret = self.__returnValue 1267 self.__returnValue = '' 1268 return ret
1269

autokey-0.96.0/doc/scripting/lib.scripting.Engine-class.html000066400000000000000000000537171427671440700240330ustar00rootroot00000000000000 lib.scripting.Engine
Package lib :: Module scripting :: Class Engine
[hide private]
[frames] | no frames]

Class Engine

source code

Provides access to the internals of AutoKey.

Note that any configuration changes made using this API while the configuration window is open will not appear until it is closed and re-opened.

Instance Methods [hide private]
 
__init__(self, configManager, runner) source code
 
get_folder(self, title)
Retrieve a folder by its title
source code
 
create_phrase(self, folder, description, contents)
Create a text phrase
source code
 
create_abbreviation(self, folder, description, abbr, contents)
Create a text abbreviation
source code
 
create_hotkey(self, folder, description, modifiers, key, contents)
Create a text hotkey
source code
 
run_script(self, description)
Run an existing script using its description to look it up
source code
 
run_script_from_macro(self, args)
Used internally by AutoKey for phrase macros
source code
list(str())
get_macro_arguments(self)
Get the arguments supplied to the current script via its macro
source code
 
set_return_value(self, val)
Store a return value to be used by a phrase macro
source code
 
_get_return_value(self)
Used internally by AutoKey for phrase macros
source code
Method Details [hide private]

get_folder(self, title)

source code 

Retrieve a folder by its title

Usage: engine.get_folder(title)

Note that if more than one folder has the same title, only the first match will be returned.

create_phrase(self, folder, description, contents)

source code 

Create a text phrase

Usage: engine.create_phrase(folder, description, contents)

A new phrase with no abbreviation or hotkey is created in the specified folder

Parameters:
  • folder - folder to place the abbreviation in, retrieved using engine.get_folder()
  • description - description for the phrase
  • contents - the expansion text

create_abbreviation(self, folder, description, abbr, contents)

source code 

Create a text abbreviation

Usage: engine.create_abbreviation(folder, description, abbr, contents)

When the given abbreviation is typed, it will be replaced with the given text.

Parameters:
  • folder - folder to place the abbreviation in, retrieved using engine.get_folder()
  • description - description for the phrase
  • abbr - the abbreviation that will trigger the expansion
  • contents - the expansion text
Raises:
  • Exception - if the specified abbreviation is not unique

create_hotkey(self, folder, description, modifiers, key, contents)

source code 

Create a text hotkey

Usage: engine.create_hotkey(folder, description, modifiers, key, contents)

When the given hotkey is pressed, it will be replaced with the given text. Modifiers must be given as a list of strings, with the following values permitted:

<ctrl> <alt> <super> <hyper> <shift>

The key must be an unshifted character (i.e. lowercase)

Parameters:
  • folder - folder to place the abbreviation in, retrieved using engine.get_folder()
  • description - description for the phrase
  • modifiers - modifiers to use with the hotkey (as a list)
  • key - the hotkey
  • contents - the expansion text
Raises:
  • Exception - if the specified hotkey is not unique

run_script(self, description)

source code 

Run an existing script using its description to look it up

Usage: engine.run_script(description)

Parameters:
  • description - description of the script to run
Raises:
  • Exception - if the specified script does not exist

get_macro_arguments(self)

source code 

Get the arguments supplied to the current script via its macro

Usage: engine.get_macro_arguments()

Returns: list(str())
the arguments

set_return_value(self, val)

source code 

Store a return value to be used by a phrase macro

Usage: engine.set_return_value(val)

Parameters:
  • val - value to be stored

autokey-0.96.0/doc/scripting/lib.scripting.GtkClipboard-class.html000066400000000000000000000370021427671440700251600ustar00rootroot00000000000000 lib.scripting.GtkClipboard
Package lib :: Module scripting :: Class GtkClipboard
[hide private]
[frames] | no frames]

Class GtkClipboard

source code

Read/write access to the X selection and clipboard - GTK version

Instance Methods [hide private]
 
__init__(self, app) source code
 
fill_selection(self, contents)
Copy text into the X selection
source code
 
__fillSelection(self, string) source code
str
get_selection(self)
Read text from the X selection
source code
 
__receive(self, cb, text, data=None) source code
 
fill_clipboard(self, contents)
Copy text into the clipboard
source code
 
__fillClipboard(self, string) source code
str
get_clipboard(self)
Read text from the clipboard
source code
 
__execAsync(self, callback, *args) source code
Method Details [hide private]

fill_selection(self, contents)

source code 

Copy text into the X selection

Usage: clipboard.fill_selection(contents)

Parameters:
  • contents - string to be placed in the selection

get_selection(self)

source code 

Read text from the X selection

Usage: clipboard.get_selection()

Returns: str
text contents of the mouse selection
Raises:
  • Exception - if no text was found in the selection

fill_clipboard(self, contents)

source code 

Copy text into the clipboard

Usage: clipboard.fill_clipboard(contents)

Parameters:
  • contents - string to be placed in the selection

get_clipboard(self)

source code 

Read text from the clipboard

Usage: clipboard.get_clipboard()

Returns: str
text contents of the clipboard
Raises:
  • Exception - if no text was found on the clipboard

autokey-0.96.0/doc/scripting/lib.scripting.GtkDialog-class.html000066400000000000000000001004361427671440700244620ustar00rootroot00000000000000 lib.scripting.GtkDialog
Package lib :: Module scripting :: Class GtkDialog
[hide private]
[frames] | no frames]

Class GtkDialog

source code

Provides a simple interface for the display of some basic dialogs to collect information from the user.

This version uses Zenity to integrate well with GNOME. To pass additional arguments to Zenity that are not specifically handled, use keyword arguments. For example, to pass the --timeout argument to Zenity pass timeout="15" as one of the parameters. All keyword arguments must be given as strings.

A note on exit codes: an exit code of 0 indicates that the user clicked OK.

Instance Methods [hide private]
 
_run_zenity(self, title, args, kwargs) source code
tuple(int, str)
info_dialog(self, title='Information', message='', **kwargs)
Show an information dialog
source code
tuple(int, str)
input_dialog(self, title='Enter a value', message='Enter a value', default='', **kwargs)
Show an input dialog
source code
tuple(int, str)
password_dialog(self, title='Enter password', message='Enter password', **kwargs)
Show a password input dialog
source code
tuple(int, str)
list_menu(self, options, title='Choose a value', message='Choose a value', default=None, **kwargs)
Show a single-selection list menu
source code
tuple(int, str)
list_menu_multi(self, options, title='Choose one or more values', message='Choose one or more values', defaults=[], **kwargs)
Show a multiple-selection list menu
source code
tuple(int, str)
open_file(self, title='Open File', **kwargs)
Show an Open File dialog
source code
tuple(int, str)
save_file(self, title='Save As', **kwargs)
Show a Save As dialog
source code
tuple(int, str)
choose_directory(self, title='Select Directory', initialDir='~', **kwargs)
Show a Directory Chooser dialog
source code
tuple(int, str)
calendar(self, title='Choose a date', format='%Y-%m-%d', date='today', **kwargs)
Show a calendar dialog
source code
Method Details [hide private]

info_dialog(self, title='Information', message='', **kwargs)

source code 

Show an information dialog

Usage: dialog.info_dialog(title="Information", message="", **kwargs)

Parameters:
  • title - window title for the dialog
  • message - message displayed in the dialog
Returns: tuple(int, str)
a tuple containing the exit code and user input

input_dialog(self, title='Enter a value', message='Enter a value', default='', **kwargs)

source code 

Show an input dialog

Usage: dialog.input_dialog(title="Enter a value", message="Enter a value", default="", **kwargs)

Parameters:
  • title - window title for the dialog
  • message - message displayed above the input box
  • default - default value for the input box
Returns: tuple(int, str)
a tuple containing the exit code and user input

password_dialog(self, title='Enter password', message='Enter password', **kwargs)

source code 

Show a password input dialog

Usage: dialog.password_dialog(title="Enter password", message="Enter password")

Parameters:
  • title - window title for the dialog
  • message - message displayed above the password input box
Returns: tuple(int, str)
a tuple containing the exit code and user input

list_menu(self, options, title='Choose a value', message='Choose a value', default=None, **kwargs)

source code 

Show a single-selection list menu

Usage: dialog.list_menu(options, title="Choose a value", message="Choose a value", default=None, **kwargs)

Parameters:
  • options - list of options (strings) for the dialog
  • title - window title for the dialog
  • message - message displayed above the list
  • default - default value to be selected
Returns: tuple(int, str)
a tuple containing the exit code and user choice

list_menu_multi(self, options, title='Choose one or more values', message='Choose one or more values', defaults=[], **kwargs)

source code 

Show a multiple-selection list menu

Usage: dialog.list_menu_multi(options, title="Choose one or more values", message="Choose one or more values", defaults=[], **kwargs)

Parameters:
  • options - list of options (strings) for the dialog
  • title - window title for the dialog
  • message - message displayed above the list
  • defaults - list of default values to be selected
Returns: tuple(int, str)
a tuple containing the exit code and user choice

open_file(self, title='Open File', **kwargs)

source code 

Show an Open File dialog

Usage: dialog.open_file(title="Open File", **kwargs)

Parameters:
  • title - window title for the dialog
Returns: tuple(int, str)
a tuple containing the exit code and file path

save_file(self, title='Save As', **kwargs)

source code 

Show a Save As dialog

Usage: dialog.save_file(title="Save As", **kwargs)

Parameters:
  • title - window title for the dialog
Returns: tuple(int, str)
a tuple containing the exit code and file path

choose_directory(self, title='Select Directory', initialDir='~', **kwargs)

source code 

Show a Directory Chooser dialog

Usage: dialog.choose_directory(title="Select Directory", **kwargs)

Parameters:
  • title - window title for the dialog
Returns: tuple(int, str)
a tuple containing the exit code and path

calendar(self, title='Choose a date', format='%Y-%m-%d', date='today', **kwargs)

source code 

Show a calendar dialog

Usage: dialog.calendar_dialog(title="Choose a date", format="%Y-%m-%d", date="YYYY-MM-DD", **kwargs)

Parameters:
  • title - window title for the dialog
  • format - format of date to be returned
  • date - initial date as YYYY-MM-DD, otherwise today
Returns: tuple(int, str)
a tuple containing the exit code and date

autokey-0.96.0/doc/scripting/lib.scripting.Keyboard-class.html000066400000000000000000000430001427671440700243460ustar00rootroot00000000000000 lib.scripting.Keyboard
Package lib :: Module scripting :: Class Keyboard
[hide private]
[frames] | no frames]

Class Keyboard

source code

Provides access to the keyboard for event generation.

Instance Methods [hide private]
 
__init__(self, mediator) source code
 
send_keys(self, keyString)
Send a sequence of keys via keyboard events
source code
 
send_key(self, key, repeat=1)
Send a keyboard event
source code
 
press_key(self, key)
Send a key down event
source code
 
release_key(self, key)
Send a key up event
source code
 
fake_keypress(self, key, repeat=1)
Fake a keypress
source code
 
wait_for_keypress(self, key, modifiers=[], timeOut=10.0)
Wait for a keypress or key combination
source code
Method Details [hide private]

send_keys(self, keyString)

source code 

Send a sequence of keys via keyboard events

Usage: keyboard.send_keys(keyString)

Parameters:
  • keyString - string of keys (including special keys) to send

send_key(self, key, repeat=1)

source code 

Send a keyboard event

Usage: keyboard.send_key(key, repeat=1)

Parameters:
  • key - they key to be sent (e.g. "s" or "<enter>")
  • repeat - number of times to repeat the key event

press_key(self, key)

source code 

Send a key down event

Usage: keyboard.press_key(key)

The key will be treated as down until a matching release_key() is sent.

Parameters:
  • key - they key to be pressed (e.g. "s" or "<enter>")

release_key(self, key)

source code 

Send a key up event

Usage: keyboard.release_key(key)

If the specified key was not made down using press_key(), the event will be ignored.

Parameters:
  • key - they key to be released (e.g. "s" or "<enter>")

fake_keypress(self, key, repeat=1)

source code 

Fake a keypress

Usage: keyboard.fake_keypress(key, repeat=1)

Uses XTest to 'fake' a keypress. This is useful to send keypresses to some applications which won't respond to keyboard.send_key()

Parameters:
  • key - they key to be sent (e.g. "s" or "<enter>")
  • repeat - number of times to repeat the key event

wait_for_keypress(self, key, modifiers=[], timeOut=10.0)

source code 

Wait for a keypress or key combination

Usage: keyboard.wait_for_keypress(self, key, modifiers=[], timeOut=10.0)

Note: this function cannot be used to wait for modifier keys on their own

Parameters:
  • key - they key to wait for
  • modifiers - list of modifiers that should be pressed with the key
  • timeOut - maximum time, in seconds, to wait for the keypress to occur

autokey-0.96.0/doc/scripting/lib.scripting.Mouse-class.html000066400000000000000000000342451427671440700237110ustar00rootroot00000000000000 lib.scripting.Mouse
Package lib :: Module scripting :: Class Mouse
[hide private]
[frames] | no frames]

Class Mouse

source code

Provides access to send mouse clicks

Instance Methods [hide private]
 
__init__(self, mediator) source code
 
click_relative(self, x, y, button)
Send a mouse click relative to the active window
source code
 
click_relative_self(self, x, y, button)
Send a mouse click relative to the current mouse position
source code
 
click_absolute(self, x, y, button)
Send a mouse click relative to the screen (absolute)
source code
 
wait_for_click(self, button, timeOut=10.0)
Wait for a mouse click
source code
Method Details [hide private]

click_relative(self, x, y, button)

source code 

Send a mouse click relative to the active window

Usage: mouse.click_relative(x, y, button)

Parameters:
  • x - x-coordinate in pixels, relative to upper left corner of window
  • y - y-coordinate in pixels, relative to upper left corner of window
  • button - mouse button to simulate (left=1, middle=2, right=3)

click_relative_self(self, x, y, button)

source code 

Send a mouse click relative to the current mouse position

Usage: mouse.click_relative_self(x, y, button)

Parameters:
  • x - x-offset in pixels, relative to current mouse position
  • y - y-offset in pixels, relative to current mouse position
  • button - mouse button to simulate (left=1, middle=2, right=3)

click_absolute(self, x, y, button)

source code 

Send a mouse click relative to the screen (absolute)

Usage: mouse.click_absolute(x, y, button)

Parameters:
  • x - x-coordinate in pixels, relative to upper left corner of window
  • y - y-coordinate in pixels, relative to upper left corner of window
  • button - mouse button to simulate (left=1, middle=2, right=3)

wait_for_click(self, button, timeOut=10.0)

source code 

Wait for a mouse click

Usage: mouse.wait_for_click(self, button, timeOut=10.0)

Parameters:
  • button - they mouse button click to wait for as a button number, 1-9
  • timeOut - maximum time, in seconds, to wait for the keypress to occur

autokey-0.96.0/doc/scripting/lib.scripting.QtClipboard-class.html000066400000000000000000000371511427671440700250240ustar00rootroot00000000000000 lib.scripting.QtClipboard
Package lib :: Module scripting :: Class QtClipboard
[hide private]
[frames] | no frames]

Class QtClipboard

source code

Read/write access to the X selection and clipboard - QT version

Instance Methods [hide private]
 
__init__(self, app) source code
 
fill_selection(self, contents)
Copy text into the X selection
source code
 
__fillSelection(self, string) source code
str
get_selection(self)
Read text from the X selection
source code
 
__getSelection(self) source code
 
fill_clipboard(self, contents)
Copy text into the clipboard
source code
 
__fillClipboard(self, string) source code
str
get_clipboard(self)
Read text from the clipboard
source code
 
__getClipboard(self) source code
 
__execAsync(self, callback, *args) source code
Method Details [hide private]

fill_selection(self, contents)

source code 

Copy text into the X selection

Usage: clipboard.fill_selection(contents)

Parameters:
  • contents - string to be placed in the selection

get_selection(self)

source code 

Read text from the X selection

Usage: clipboard.get_selection()

Returns: str
text contents of the mouse selection

fill_clipboard(self, contents)

source code 

Copy text into the clipboard

Usage: clipboard.fill_clipboard(contents)

Parameters:
  • contents - string to be placed in the selection

get_clipboard(self)

source code 

Read text from the clipboard

Usage: clipboard.get_clipboard()

Returns: str
text contents of the clipboard

autokey-0.96.0/doc/scripting/lib.scripting.QtDialog-class.html000066400000000000000000001220221427671440700243140ustar00rootroot00000000000000 lib.scripting.QtDialog
Package lib :: Module scripting :: Class QtDialog
[hide private]
[frames] | no frames]

Class QtDialog

source code

Provides a simple interface for the display of some basic dialogs to collect information from the user.

This version uses KDialog to integrate well with KDE. To pass additional arguments to KDialog that are not specifically handled, use keyword arguments. For example, to pass the --geometry argument to KDialog to specify the desired size of the dialog, pass geometry="700x400" as one of the parameters. All keyword arguments must be given as strings.

A note on exit codes: an exit code of 0 indicates that the user clicked OK.

Instance Methods [hide private]
 
_run_kdialog(self, title, args, kwargs) source code
tuple(int, str)
info_dialog(self, title='Information', message='', **kwargs)
Show an information dialog
source code
tuple(int, str)
input_dialog(self, title='Enter a value', message='Enter a value', default='', **kwargs)
Show an input dialog
source code
tuple(int, str)
password_dialog(self, title='Enter password', message='Enter password', **kwargs)
Show a password input dialog
source code
tuple(int, str)
combo_menu(self, options, title='Choose an option', message='Choose an option', **kwargs)
Show a combobox menu
source code
tuple(int, str)
list_menu(self, options, title='Choose a value', message='Choose a value', default=None, **kwargs)
Show a single-selection list menu
source code
tuple(int, str)
list_menu_multi(self, options, title='Choose one or more values', message='Choose one or more values', defaults=[], **kwargs)
Show a multiple-selection list menu
source code
tuple(int, str)
open_file(self, title='Open File', initialDir='~', fileTypes='*|All Files', rememberAs=None, **kwargs)
Show an Open File dialog
source code
tuple(int, str)
save_file(self, title='Save As', initialDir='~', fileTypes='*|All Files', rememberAs=None, **kwargs)
Show a Save As dialog
source code
tuple(int, str)
choose_directory(self, title='Select Directory', initialDir='~', rememberAs=None, **kwargs)
Show a Directory Chooser dialog
source code
tuple(int, str)
choose_colour(self, title='Select Colour', **kwargs)
Show a Colour Chooser dialog
source code
tuple(int, str)
calendar(self, title='Choose a date', format='%Y-%m-%d', date='today', **kwargs)
Show a calendar dialog
source code
Method Details [hide private]

info_dialog(self, title='Information', message='', **kwargs)

source code 

Show an information dialog

Usage: dialog.info_dialog(title="Information", message="", **kwargs)

Parameters:
  • title - window title for the dialog
  • message - message displayed in the dialog
Returns: tuple(int, str)
a tuple containing the exit code and user input

input_dialog(self, title='Enter a value', message='Enter a value', default='', **kwargs)

source code 

Show an input dialog

Usage: dialog.input_dialog(title="Enter a value", message="Enter a value", default="", **kwargs)

Parameters:
  • title - window title for the dialog
  • message - message displayed above the input box
  • default - default value for the input box
Returns: tuple(int, str)
a tuple containing the exit code and user input

password_dialog(self, title='Enter password', message='Enter password', **kwargs)

source code 

Show a password input dialog

Usage: dialog.password_dialog(title="Enter password", message="Enter password", **kwargs)

Parameters:
  • title - window title for the dialog
  • message - message displayed above the password input box
Returns: tuple(int, str)
a tuple containing the exit code and user input

combo_menu(self, options, title='Choose an option', message='Choose an option', **kwargs)

source code 

Show a combobox menu

Usage: dialog.combo_menu(options, title="Choose an option", message="Choose an option", **kwargs)

Parameters:
  • options - list of options (strings) for the dialog
  • title - window title for the dialog
  • message - message displayed above the combobox
Returns: tuple(int, str)
a tuple containing the exit code and user choice

list_menu(self, options, title='Choose a value', message='Choose a value', default=None, **kwargs)

source code 

Show a single-selection list menu

Usage: dialog.list_menu(options, title="Choose a value", message="Choose a value", default=None, **kwargs)

Parameters:
  • options - list of options (strings) for the dialog
  • title - window title for the dialog
  • message - message displayed above the list
  • default - default value to be selected
Returns: tuple(int, str)
a tuple containing the exit code and user choice

list_menu_multi(self, options, title='Choose one or more values', message='Choose one or more values', defaults=[], **kwargs)

source code 

Show a multiple-selection list menu

Usage: dialog.list_menu_multi(options, title="Choose one or more values", message="Choose one or more values", defaults=[], **kwargs)

Parameters:
  • options - list of options (strings) for the dialog
  • title - window title for the dialog
  • message - message displayed above the list
  • defaults - list of default values to be selected
Returns: tuple(int, str)
a tuple containing the exit code and user choice

open_file(self, title='Open File', initialDir='~', fileTypes='*|All Files', rememberAs=None, **kwargs)

source code 

Show an Open File dialog

Usage: dialog.open_file(title="Open File", initialDir="~", fileTypes="*|All Files", rememberAs=None, **kwargs)

Parameters:
  • title - window title for the dialog
  • initialDir - starting directory for the file dialog
  • fileTypes - file type filter expression
  • rememberAs - gives an ID to this file dialog, allowing it to open at the last used path next time
Returns: tuple(int, str)
a tuple containing the exit code and file path

save_file(self, title='Save As', initialDir='~', fileTypes='*|All Files', rememberAs=None, **kwargs)

source code 

Show a Save As dialog

Usage: dialog.save_file(title="Save As", initialDir="~", fileTypes="*|All Files", rememberAs=None, **kwargs)

Parameters:
  • title - window title for the dialog
  • initialDir - starting directory for the file dialog
  • fileTypes - file type filter expression
  • rememberAs - gives an ID to this file dialog, allowing it to open at the last used path next time
Returns: tuple(int, str)
a tuple containing the exit code and file path

choose_directory(self, title='Select Directory', initialDir='~', rememberAs=None, **kwargs)

source code 

Show a Directory Chooser dialog

Usage: dialog.choose_directory(title="Select Directory", initialDir="~", rememberAs=None, **kwargs)

Parameters:
  • title - window title for the dialog
  • initialDir - starting directory for the directory chooser dialog
  • rememberAs - gives an ID to this file dialog, allowing it to open at the last used path next time
Returns: tuple(int, str)
a tuple containing the exit code and chosen path

choose_colour(self, title='Select Colour', **kwargs)

source code 

Show a Colour Chooser dialog

Usage: dialog.choose_colour(title="Select Colour")

Parameters:
  • title - window title for the dialog
Returns: tuple(int, str)
a tuple containing the exit code and colour

calendar(self, title='Choose a date', format='%Y-%m-%d', date='today', **kwargs)

source code 

Show a calendar dialog

Usage: dialog.calendar_dialog(title="Choose a date", format="%Y-%m-%d", date="YYYY-MM-DD", **kwargs)

Note: the format and date parameters are not currently used

Parameters:
  • title - window title for the dialog
  • format - format of date to be returned
  • date - initial date as YYYY-MM-DD, otherwise today
Returns: tuple(int, str)
a tuple containing the exit code and date

autokey-0.96.0/doc/scripting/lib.scripting.Store-class.html000066400000000000000000000425431427671440700237150ustar00rootroot00000000000000 lib.scripting.Store
Package lib :: Module scripting :: Class Store
[hide private]
[frames] | no frames]

Class Store

source code

object --+    
         |    
      dict --+
             |
            Store

Allows persistent storage of values between invocations of the script.

Instance Methods [hide private]
 
set_value(self, key, value)
Store a value
source code
 
get_value(self, key)
Get a value
source code
 
remove_value(self, key)
Remove a value
source code
 
set_global_value(self, key, value)
Store a global value
source code
 
get_global_value(self, key)
Get a global value
source code
 
remove_global_value(self, key)
Remove a global value
source code

Inherited from dict: __cmp__, __contains__, __delitem__, __eq__, __ge__, __getattribute__, __getitem__, __gt__, __init__, __iter__, __le__, __len__, __lt__, __ne__, __new__, __repr__, __setitem__, __sizeof__, clear, copy, fromkeys, get, has_key, items, iteritems, iterkeys, itervalues, keys, pop, popitem, setdefault, update, values, viewitems, viewkeys, viewvalues

Inherited from object: __delattr__, __format__, __reduce__, __reduce_ex__, __setattr__, __str__, __subclasshook__

Class Variables [hide private]

Inherited from dict: __hash__

Properties [hide private]

Inherited from object: __class__

Method Details [hide private]

set_value(self, key, value)

source code 

Store a value

Usage: store.set_value(key, value)

get_value(self, key)

source code 

Get a value

Usage: store.get_value(key)

remove_value(self, key)

source code 

Remove a value

Usage: store.remove_value(key)

set_global_value(self, key, value)

source code 

Store a global value

Usage: store.set_global_value(key, value)

The value stored with this method will be available to all scripts.

get_global_value(self, key)

source code 

Get a global value

Usage: store.get_global_value(key)

remove_global_value(self, key)

source code 

Remove a global value

Usage: store.remove_global_value(key)


autokey-0.96.0/doc/scripting/lib.scripting.System-class.html000066400000000000000000000231731427671440700241030ustar00rootroot00000000000000 lib.scripting.System
Package lib :: Module scripting :: Class System
[hide private]
[frames] | no frames]

Class System

source code

Simplified access to some system commands.

Instance Methods [hide private]
 
exec_command(self, command, getOutput=True)
Execute a shell command
source code
 
create_file(self, fileName, contents='')
Create a file with contents
source code
Method Details [hide private]

exec_command(self, command, getOutput=True)

source code 

Execute a shell command

Usage: system.exec_command(command, getOutput=True)

Set getOutput to False if the command does not exit and return immediately. Otherwise AutoKey will not respond to any hotkeys/abbreviations etc until the process started by the command exits.

Parameters:
  • command - command to be executed (including any arguments) - e.g. "ls -l"
  • getOutput - whether to capture the (stdout) output of the command
Raises:
  • subprocess.CalledProcessError - if the command returns a non-zero exit code

create_file(self, fileName, contents='')

source code 

Create a file with contents

Usage: system.create_file(fileName, contents="")

Parameters:
  • fileName - full path to the file to be created
  • contents - contents to insert into the file

autokey-0.96.0/doc/scripting/lib.scripting.Window-class.html000066400000000000000000000744711427671440700240750ustar00rootroot00000000000000 lib.scripting.Window
Package lib :: Module scripting :: Class Window
[hide private]
[frames] | no frames]

Class Window

source code

Basic window management using wmctrl

Note: in all cases where a window title is required (with the exception of wait_for_focus()), two special values of window title are permitted:

:ACTIVE: - select the currently active window :SELECT: - select the desired window by clicking on it

Instance Methods [hide private]
 
__init__(self, mediator) source code
boolean
wait_for_focus(self, title, timeOut=5)
Wait for window with the given title to have focus
source code
boolean
wait_for_exist(self, title, timeOut=5)
Wait for window with the given title to be created
source code
 
activate(self, title, switchDesktop=False, matchClass=False)
Activate the specified window, giving it input focus
source code
 
close(self, title, matchClass=False)
Close the specified window gracefully
source code
 
resize_move(self, title, xOrigin=-1, yOrigin=-1, width=-1, height=-1, matchClass=False)
Resize and/or move the specified window
source code
 
move_to_desktop(self, title, deskNum, matchClass=False)
Move the specified window to the given desktop
source code
 
switch_desktop(self, deskNum)
Switch to the specified desktop
source code
 
set_property(self, title, action, prop, matchClass=False)
Set a property on the given window using the specified action
source code
tuple(int, int, int, int)
get_active_geometry(self)
Get the geometry of the currently active window
source code
str
get_active_title(self)
Get the visible title of the currently active window
source code
str
get_active_class(self)
Get the class of the currently active window
source code
 
_run_wmctrl(self, args) source code
Method Details [hide private]

wait_for_focus(self, title, timeOut=5)

source code 

Wait for window with the given title to have focus

Usage: window.wait_for_focus(title, timeOut=5)

If the window becomes active, returns True. Otherwise, returns False if the window has not become active by the time the timeout has elapsed.

Parameters:
  • title - title to match against (as a regular expression)
  • timeOut - period (seconds) to wait before giving up
Returns: boolean

wait_for_exist(self, title, timeOut=5)

source code 

Wait for window with the given title to be created

Usage: window.wait_for_exist(title, timeOut=5)

If the window is in existence, returns True. Otherwise, returns False if the window has not been created by the time the timeout has elapsed.

Parameters:
  • title - title to match against (as a regular expression)
  • timeOut - period (seconds) to wait before giving up
Returns: boolean

activate(self, title, switchDesktop=False, matchClass=False)

source code 

Activate the specified window, giving it input focus

Usage: window.activate(title, switchDesktop=False, matchClass=False)

If switchDesktop is False (default), the window will be moved to the current desktop and activated. Otherwise, switch to the window's current desktop and activate it there.

Parameters:
  • title - window title to match against (as case-insensitive substring match)
  • switchDesktop - whether or not to switch to the window's current desktop
  • matchClass - if True, match on the window class instead of the title

close(self, title, matchClass=False)

source code 

Close the specified window gracefully

Usage: window.close(title, matchClass=False)

Parameters:
  • title - window title to match against (as case-insensitive substring match)
  • matchClass - if True, match on the window class instead of the title

resize_move(self, title, xOrigin=-1, yOrigin=-1, width=-1, height=-1, matchClass=False)

source code 

Resize and/or move the specified window

Usage: window.close(title, xOrigin=-1, yOrigin=-1, width=-1, height=-1, matchClass=False)

Leaving and of the position/dimension values as the default (-1) will cause that value to be left unmodified.

Parameters:
  • title - window title to match against (as case-insensitive substring match)
  • xOrigin - new x origin of the window (upper left corner)
  • yOrigin - new y origin of the window (upper left corner)
  • width - new width of the window
  • height - new height of the window
  • matchClass - if True, match on the window class instead of the title

move_to_desktop(self, title, deskNum, matchClass=False)

source code 

Move the specified window to the given desktop

Usage: window.move_to_desktop(title, deskNum, matchClass=False)

Parameters:
  • title - window title to match against (as case-insensitive substring match)
  • deskNum - desktop to move the window to (note: zero based)
  • matchClass - if True, match on the window class instead of the title

switch_desktop(self, deskNum)

source code 

Switch to the specified desktop

Usage: window.switch_desktop(deskNum)

Parameters:
  • deskNum - desktop to switch to (note: zero based)

set_property(self, title, action, prop, matchClass=False)

source code 

Set a property on the given window using the specified action

Usage: window.set_property(title, action, prop, matchClass=False)

Allowable actions: add, remove, toggle Allowable properties: modal, sticky, maximized_vert, maximized_horz, shaded, skip_taskbar, skip_pager, hidden, fullscreen, above

Parameters:
  • title - window title to match against (as case-insensitive substring match)
  • action - one of the actions listed above
  • prop - one of the properties listed above
  • matchClass - if True, match on the window class instead of the title

get_active_geometry(self)

source code 

Get the geometry of the currently active window

Usage: window.get_active_geometry()

Returns: tuple(int, int, int, int)
a 4-tuple containing the x-origin, y-origin, width and height of the window (in pixels)

get_active_title(self)

source code 

Get the visible title of the currently active window

Usage: window.get_active_title()

Returns: str
the visible title of the currentle active window

get_active_class(self)

source code 

Get the class of the currently active window

Usage: window.get_active_class()

Returns: str
the class of the currentle active window

autokey-0.96.0/doc/scripting/module-tree.html000066400000000000000000000070031427671440700211620ustar00rootroot00000000000000 Module Hierarchy
 
[hide private]
[frames] | no frames]
[ Module Hierarchy | Class Hierarchy ]

Module Hierarchy

autokey-0.96.0/doc/scripting/redirect.html000066400000000000000000000024011427671440700205360ustar00rootroot00000000000000Epydoc Redirect Page

Epydoc Auto-redirect page

When javascript is enabled, this page will redirect URLs of the form redirect.html#dotted.name to the documentation for the object with the given fully-qualified dotted name.

 

autokey-0.96.0/doc/scripting/toc-everything.html000066400000000000000000000042201427671440700217050ustar00rootroot00000000000000 Everything

Everything


All Classes

lib.scripting.Engine
lib.scripting.GtkClipboard
lib.scripting.GtkDialog
lib.scripting.Keyboard
lib.scripting.Mouse
lib.scripting.QtClipboard
lib.scripting.QtDialog
lib.scripting.Store
lib.scripting.System
lib.scripting.Window

All Variables

lib.scripting.__package__

[hide private] autokey-0.96.0/doc/scripting/toc-lib.scripting-module.html000066400000000000000000000037631427671440700235660ustar00rootroot00000000000000 scripting

Module scripting


Classes

Engine
GtkClipboard
GtkDialog
Keyboard
Mouse
QtClipboard
QtDialog
Store
System
Window

Variables

__package__

[hide private] autokey-0.96.0/doc/scripting/toc.html000066400000000000000000000023771427671440700175360ustar00rootroot00000000000000 Table of Contents

Table of Contents


Everything

Modules

lib.scripting

[hide private] autokey-0.96.0/extractDoc.py000066400000000000000000000030701427671440700157550ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2008 Chris Dekter # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, but # WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. import sys, inspect sys.path.append("./src/lib") import scripting if __name__ == "__main__": outFile = open("src/lib/qtui/data/api.txt", "w") for name, attrib in inspect.getmembers(scripting): if inspect.isclass(attrib) and not (name.startswith("_") or name.startswith("Gtk")): for name, attrib in inspect.getmembers(attrib): if inspect.ismethod(attrib) and not name.startswith("_"): doc = attrib.__doc__ lines = doc.split('\n') try: apiLine = lines[3].strip() docLine = lines[1].strip() except: continue outFile.write(apiLine[9:-1] + " " + docLine + '\n') outFile.close()autokey-0.96.0/lib/000077500000000000000000000000001427671440700140515ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/000077500000000000000000000000001427671440700155325ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/UI_common_functions.py000066400000000000000000000200641427671440700220630ustar00rootroot00000000000000import dbus import importlib import os.path from shutil import which import re import subprocess import sys import time from . import common import autokey.model.helpers logger = __import__("autokey.logger").logger.get_logger(__name__) common_modules = ['argparse', 'collections', 'enum', 'faulthandler', 'gettext', 'inspect', 'itertools', 'logging', 'os', 'select', 'shlex', 'shutil', 'subprocess', 'sys', 'threading', 'time', 'traceback', 'typing', 'warnings', 'webbrowser', 'dbus', 'pyinotify'] gtk_modules = ['gi', 'gi.repository.Gtk', 'gi.repository.Gdk', 'gi.repository.Pango', 'gi.repository.Gio', 'gi.repository.GtkSource'] qt_modules = ['PyQt5', 'PyQt5.QtGui', 'PyQt5.QtWidgets', 'PyQt5.QtCore', 'PyQt5.Qsci'] common_programs = ['wmctrl', 'ps'] # Checking some of these appears to be redundant as some are provided by the same packages on my system but # better safe than sorry. optional_programs = ['visgrep', 'import', 'png2pat', 'xte', 'xmousepos'] gtk_programs = ['zenity'] qt_programs = ['kdialog'] def checkModuleImports(modules): missing_modules = [] for module in modules: spec = importlib.util.find_spec(module) if spec is None: #module has not been imported/found correctly logger.error("Python module \""+module+"\" was not found/able to be imported correctly. Check that it is installed correctly") missing_modules.append(module) return missing_modules def checkProgramImports(programs, optional=False): missing_programs = [] for program in programs: if which(program) is None: # file not found by shell status="Commandline program \""+program+"\" was not found/able to be used correctly by AutoKey." suggestion="Check that \""+program+"\" exists on your system and is in the $PATH seen by Autokey." if optional: logger.info(status + " This program is optional for Autokey operation, but if you wish to use functionality associated with it, " + suggestion) else: logger.error(status + " " + suggestion) missing_programs.append(program) return missing_programs def checkOptionalPrograms(): if common.USING_QT: checkProgramImports(optional_programs, optional=True) else: checkProgramImports(optional_programs, optional=True) def getErrorMessage(item_type, missing_items): error_message = "" for item in missing_items: error_message+= item_type+": "+item+"\n" return error_message def checkRequirements(): errorMessage = "" if common.USING_QT: missing_programs = checkProgramImports(common_programs+qt_programs) missing_modules = checkModuleImports(common_modules+qt_modules) else: missing_programs = checkProgramImports(common_programs+gtk_programs) missing_modules = checkModuleImports(common_modules+gtk_modules) errorMessage += getErrorMessage("Python Modules",missing_modules) errorMessage += getErrorMessage("Programs",missing_programs) return errorMessage def create_storage_directories(): """Create various storage directories, if those do not exist.""" # Create configuration directory makedir_if_not_exists(common.CONFIG_DIR) # Create data directory (for log file) makedir_if_not_exists(common.DATA_DIR) # Create run directory (for lock file) makedir_if_not_exists(common.RUN_DIR) def makedir_if_not_exists(d): if not os.path.exists(d): os.makedirs(d) def create_lock_file(): with open(common.LOCK_FILE, "w") as lock_file: lock_file.write(str(os.getpid())) def read_pid_from_lock_file() -> str: with open(common.LOCK_FILE, "r") as lock_file: pid = lock_file.read() try: # Check if the pid file contains garbage int(pid) except ValueError: logger.exception("AutoKey pid file contains garbage instead of a usable process id: " + pid) sys.exit(1) return pid def get_process_details(pid): with subprocess.Popen(["ps", "-p", pid, "-o", "command"], stdout=subprocess.PIPE) as p: output = p.communicate()[0].decode() return output def check_pid_is_a_running_autokey(pid): output = get_process_details(pid) def is_existing_running_autokey(): if os.path.exists(common.LOCK_FILE): pid = read_pid_from_lock_file() # Check that the found PID is running and is autokey output = get_process_details(pid) if "autokey" in output: logger.debug("AutoKey is already running as pid %s", pid) return True return False def test_Dbus_response(app): bus = dbus.SessionBus() try: dbus_service = bus.get_object("org.autokey.Service", "/AppService") dbus_service.show_configure(dbus_interface="org.autokey.Service") sys.exit(0) except dbus.DBusException as e: pid = read_pid_from_lock_file() message="AutoKey is already running as pid {} but is not responding".format(pid) logger.exception( "Error communicating with Dbus service. {}".format(message)) app.show_error_dialog( message=message, details=str(e)) sys.exit(1) # def init_global_hotkeys(app, configManager): # logger.info("Initialise global hotkeys") # configManager.toggleServiceHotkey.set_closure(app.toggle_service) # configManager.configHotkey.set_closure(app.show_configure_signal.emit) # This line replaces the above line in the gtk app. Need to find out # what the difference is before continuing. # configManager.configHotkey.set_closure(app.show_configure_async) def hotkey_created(app_service, item): logger.debug("Created hotkey: %r %s", item.modifiers, item.hotKey) app_service.mediator.interface.grab_hotkey(item) def hotkey_removed(app_service, item): logger.debug("Removed hotkey: %r %s", item.modifiers, item.hotKey) app_service.mediator.interface.ungrab_hotkey(item) def path_created_or_modified(configManager, configWindow, path): time.sleep(0.5) changed = configManager.path_created_or_modified(path) set_file_watched(configManager.app.monitor, path, True) if changed and configWindow is not None: configWindow.config_modified() def set_file_watched(appmonitor, path, watch): if not appmonitor.has_watch(path) and os.path.isdir(path): appmonitor.suspend() if watch: appmonitor.add_watch(path) else: appmonitor.remove_watch(path) appmonitor.unsuspend() def path_removed(configManager, configWindow, path): time.sleep(0.5) changed = configManager.path_removed(path) set_file_watched(configManager.app.monitor, path, False) if changed and configWindow is not None: configWindow.config_modified() def save_item_filter(app, item): filter_regex = app.get_filter_text() try: item.set_window_titles(filter_regex) except re.error: logger.error( "Invalid window filter regex: '{}'. Discarding without saving.".format(filter_regex) ) item.set_filter_recursive(app.get_is_recursive()) def get_hotkey_text(app, key): if key in app.KEY_MAP: keyText = app.KEY_MAP[key] else: keyText = key return keyText def save_hotkey_settings_dialog(app, item): mode = autokey.model.helpers.TriggerMode.HOTKEY if mode not in item.modes: item.modes.append(mode) modifiers = app.build_modifiers() if app.key in app.REVERSE_KEY_MAP: key = app.REVERSE_KEY_MAP[app.key] else: key = app.key if key is None: raise RuntimeError("Attempt to set hotkey with no key") logger.info("Item {} updated with hotkey {} and modifiers {}".format(item, key, modifiers)) item.set_hotkey(modifiers, key) def load_hotkey_settings_dialog(app, item): if autokey.model.helpers.TriggerMode.HOTKEY in item.modes: app.populate_hotkey_details(item) else: app.reset() def load_global_hotkey_dialog(app, item): if item.enabled: app.populate_hotkey_details(item) else: app.reset() autokey-0.96.0/lib/autokey/__init__.py000066400000000000000000000000001427671440700176310ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/argument_parser.py000066400000000000000000000052171427671440700213070ustar00rootroot00000000000000# Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import argparse from typing import NamedTuple import autokey.common __all__ = [ "Namespace", "parse_args" ] Namespace = NamedTuple("Namespace", [ # Mock Namespace that mimics the object returned by parse_args() and should have the same signature. # Used to provide better static type checking inside the IDE. Can also be used for unit testing. # TODO: Convert to a class when the minimum Python version is risen to >= 3.6. ("verbose", bool), ("configure", bool), ("cutelog_integration", bool), ]) def _generate_argument_parser() -> argparse.ArgumentParser: """Generates an ArgumentParser for AutoKey""" parser = argparse.ArgumentParser(description="Desktop automation ") parser.add_argument( "-l", "--verbose", action="store_true", help="Enable verbose logging" ) parser.add_argument( "-c", "--configure", action="store_true", dest="show_config_window", help="Show the configuration window on startup" ) # %(prog)s substitution only works for installations. It shows "__main__.py", if run from the source tree. parser.add_argument( "-V", "--version", action="version", version="%(prog)s Version {}".format(autokey.common.VERSION) ) parser.add_argument( "--cutelog-integration", action="store_true", help="Connect to a locally running cutelog instance with default settings to display the full program log. " "See https://github.com/busimus/cutelog" ) parser.add_argument( "-m", "--mouse", action="store_true", dest="mouse_logging", help="Similar to -l/--verbose but includes mouse button events" ) return parser def parse_args() -> Namespace: """ Parses the command line arguments. :return: argparse Namespace object containing the parsed command line arguments """ parser = _generate_argument_parser() args = parser.parse_args() return args autokey-0.96.0/lib/autokey/common.py000066400000000000000000000040131427671440700173720ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os XDG_CONFIG_HOME = os.environ.get('XDG_CONFIG_HOME', os.path.expanduser('~/.config')) # Runtime dir falls back to cache dir, as a fallback is suggested by the spec XDG_CACHE_HOME = os.environ.get('XDG_CACHE_HOME', os.path.expanduser('~/.cache')) XDG_DATA_HOME = os.environ.get('XDG_DATA_HOME', os.path.expanduser("~/.local/share")) CONFIG_DIR = os.path.join(XDG_CONFIG_HOME, "autokey") RUN_DIR = os.path.join(os.environ.get('XDG_RUNTIME_DIR', XDG_CACHE_HOME), "autokey") DATA_DIR = os.path.join(XDG_DATA_HOME, "autokey") # The desktop file to start autokey during login is placed here AUTOSTART_DIR = os.path.join(XDG_CONFIG_HOME, "autostart") LOCK_FILE = os.path.join(RUN_DIR, "autokey.pid") APP_NAME = "autokey" CATALOG = "" VERSION = "0.96.0" HOMEPAGE = "https://github.com/autokey/autokey" AUTHOR = 'Chris Dekter' AUTHOR_EMAIL = 'cdekter@gmail.com' MAINTAINER = 'GuoCi' MAINTAINER_EMAIL = 'guociz@gmail.com' BUG_EMAIL = "guociz@gmail.com" FAQ_URL = "https://github.com/autokey/autokey/wiki/FAQ" API_URL = "https://autokey.github.io/" HELP_URL = "https://github.com/autokey/autokey/wiki/Troubleshooting" BUG_URL = HOMEPAGE + "/issues" ICON_FILE = "autokey" ICON_FILE_NOTIFICATION = "autokey-status" ICON_FILE_NOTIFICATION_DARK = "autokey-status-dark" ICON_FILE_NOTIFICATION_ERROR = "autokey-status-error" USING_QT = False autokey-0.96.0/lib/autokey/configmanager/000077500000000000000000000000001427671440700203325ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/configmanager/__init__.py000066400000000000000000000000001427671440700224310ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/configmanager/autostart.py000066400000000000000000000141451427671440700227370ustar00rootroot00000000000000# Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ This module contains the autostart handling code. This setting is not handled in the autokey.json configuration file. Instead, automatically starting autokey at login is handled by the presence of a autokey.desktop file in “$XDG_CONFIG_DIR/autostart/”. """ import typing import logging from pathlib import Path from autokey import common _logger = logging.getLogger("config-manager").getChild("autostart") # type: logging.Logger AutostartSettings = typing.NamedTuple("AutostartSettings", [ ("desktop_file_name", typing.Optional[str]), ("switch_show_configure", bool) ]) def get_autostart() -> AutostartSettings: """Returns the autostart settings as read from the system.""" autostart_file = Path(common.AUTOSTART_DIR) / "autokey.desktop" if not autostart_file.exists(): return AutostartSettings(None, False) else: return _extract_data_from_desktop_file(autostart_file) def _extract_data_from_desktop_file(desktop_file: Path) -> AutostartSettings: with open(str(desktop_file), "r") as file: for line in file.readlines(): line = line.rstrip("\n") if line.startswith("Exec="): program_name = line.split("=")[1].split(" ")[0] return AutostartSettings(program_name + ".desktop", line.endswith("-c")) raise ValueError("Autostart autokey.desktop file does not contain any Exec line. File: {}".format(desktop_file)) def set_autostart_entry(autostart_data: AutostartSettings): """ Activates or deactivates autostarting autokey during user login. Autostart is handled by placing a .desktop file into '$XDG_CONFIG_HOME/autostart', typically '~/.config/autostart' """ _logger.info("Save autostart settings: {}".format(autostart_data)) autostart_file = Path(common.AUTOSTART_DIR) / "autokey.desktop" if autostart_data.desktop_file_name is None: # Choosing None as the GUI signals deleting the entry. delete_autostart_entry() else: autostart_file.parent.mkdir(exist_ok=True) # make sure that the parent autostart directory exists. _create_autostart_entry(autostart_data, autostart_file) def _create_autostart_entry(autostart_data: AutostartSettings, autostart_file: Path): """Create an autostart .desktop file in the autostart directory, if possible.""" try: source_desktop_file = get_source_desktop_file(autostart_data.desktop_file_name) except FileNotFoundError: _logger.exception("Failed to find a usable .desktop file! Unable to find: {}".format( autostart_data.desktop_file_name)) else: _logger.debug("Found source desktop file that will be placed into the autostart directory: {}".format( source_desktop_file)) with open(str(source_desktop_file), "r") as opened_source_desktop_file: desktop_file_content = opened_source_desktop_file.read() desktop_file_content = "\n".join(_manage_autostart_desktop_file_launch_flags( desktop_file_content, autostart_data.switch_show_configure )) + "\n" with open(str(autostart_file), "w", encoding="UTF-8") as opened_autostart_file: opened_autostart_file.write(desktop_file_content) _logger.debug("Written desktop file: {}".format(autostart_file)) def delete_autostart_entry(): """Remove a present autostart entry. If none is found, nothing happens.""" autostart_file = Path(common.AUTOSTART_DIR) / "autokey.desktop" if autostart_file.exists(): autostart_file.unlink() _logger.info("Deleted old autostart entry: {}".format(autostart_file)) def get_source_desktop_file(desktop_file_name: str) -> Path: """ Try to get the source .desktop file with the given name. :raises FileNotFoundError: If no desktop file was found in the searched directories. """ possible_paths = ( # Copy from local installation. Also used if the user explicitely customized the launcher .desktop file. Path(common.XDG_DATA_HOME) / "applications", # Copy from system-wide installation Path("/", "usr", "share", "applications"), # Copy from git source tree. This will probably not work when used, because the application won’t be in the PATH Path(__file__).parent.parent.parent / "config" ) for possible_path in possible_paths: desktop_file = possible_path / desktop_file_name if desktop_file.exists(): return desktop_file raise FileNotFoundError("Desktop file for autokey could not be found. Searched paths: {}".format(possible_paths)) def _manage_autostart_desktop_file_launch_flags(desktop_file_content: str, show_configure: bool) -> typing.Iterable[str]: """Iterate over the desktop file contents. Yields all lines except for the "Exec=" line verbatim. Modifies the Exec line to include the user desired command line switches (currently only one implemented).""" for line in desktop_file_content.splitlines(keepends=False): if line.startswith("Exec="): exec_line = _modify_exec_line(line, show_configure) _logger.info("Used 'Exec' line in desktop file: {}".format(exec_line)) yield exec_line else: yield line def _modify_exec_line(line: str, show_configure: bool) -> str: if show_configure: if line.endswith(" -c"): return line else: return line + " -c" else: if line.endswith(" -c"): return line[:-3] else: return line autokey-0.96.0/lib/autokey/configmanager/configmanager.py000066400000000000000000000747121427671440700235170ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import os.path import shutil import glob import threading import typing import re import json import itertools import autokey.model.abstract_hotkey import autokey.model.folder import autokey.model.helpers import autokey.model.phrase import autokey.model.script from autokey.model import key from autokey import common from autokey.configmanager.configmanager_constants import CONFIG_FILE, CONFIG_DEFAULT_FOLDER, CONFIG_FILE_BACKUP, \ RECENT_ENTRIES_FOLDER, IS_FIRST_RUN, SERVICE_RUNNING, MENU_TAKES_FOCUS, SHOW_TRAY_ICON, SORT_BY_USAGE_COUNT, \ PROMPT_TO_SAVE, ENABLE_QT4_WORKAROUND, UNDO_USING_BACKSPACE, WINDOW_DEFAULT_SIZE, HPANE_POSITION, COLUMN_WIDTHS, \ SHOW_TOOLBAR, NOTIFICATION_ICON, WORKAROUND_APP_REGEX, TRIGGER_BY_INITIAL, SCRIPT_GLOBALS, INTERFACE_TYPE, \ DISABLED_MODIFIERS, GTK_THEME, GTK_TREE_VIEW_EXPANDED_ROWS, PATH_LAST_OPEN import autokey.configmanager.version_upgrading as version_upgrade import autokey.configmanager.predefined_user_files from autokey.iomediator.constants import X_RECORD_INTERFACE from autokey.model.key import MODIFIERS logger = __import__("autokey.logger").logger.get_logger(__name__) def create_config_manager_instance(auto_key_app, had_error=False): if not os.path.exists(CONFIG_DEFAULT_FOLDER): os.mkdir(CONFIG_DEFAULT_FOLDER) try: config_manager = ConfigManager(auto_key_app) except Exception as e: if had_error or not os.path.exists(CONFIG_FILE_BACKUP) or not os.path.exists(CONFIG_FILE): logger.exception("Error while loading configuration. Cannot recover.") raise logger.exception("Error while loading configuration. Backup has been restored.") os.remove(CONFIG_FILE) shutil.copy2(CONFIG_FILE_BACKUP, CONFIG_FILE) return create_config_manager_instance(auto_key_app, True) logger.debug("Global settings: %r", ConfigManager.SETTINGS) return config_manager def save_config(config_manager): logger.info("Persisting configuration") config_manager.app.monitor.suspend() # Back up configuration if it exists # TODO: maybe use with-statement instead of try-except? if os.path.exists(CONFIG_FILE): logger.info("Backing up existing config file") shutil.copy2(CONFIG_FILE, CONFIG_FILE_BACKUP) try: _persist_settings(config_manager) logger.info("Finished persisting configuration - no errors") except Exception as e: if os.path.exists(CONFIG_FILE_BACKUP): shutil.copy2(CONFIG_FILE_BACKUP, CONFIG_FILE) logger.exception("Error while saving configuration. Backup has been restored (if found).") raise Exception("Error while saving configuration. Backup has been restored (if found).") finally: config_manager.app.monitor.unsuspend() def _persist_settings(config_manager): """ Write the settings, including the persistent global script Store. The Store instance might contain arbitrary user data, like function objects, OpenCL contexts, or whatever other non-serializable objects, both as keys or values. Try to serialize the data, and if it fails, fall back to checking the store and removing all non-serializable data. """ serializable_data = config_manager.get_serializable() try: _try_persist_settings(serializable_data) except (TypeError, ValueError): # The user added non-serializable data to the store, so remove all non-serializable keys or values. _remove_non_serializable_store_entries(serializable_data["settings"][SCRIPT_GLOBALS]) _try_persist_settings(serializable_data) def _try_persist_settings(serializable_data: dict): """ Write the settings as JSON to the configuration file :raises TypeError: If the user tries to store non-serializable types :raises ValueError: If the user tries to store circular referenced (recursive) structures. """ with open(CONFIG_FILE, "w") as json_file: json.dump(serializable_data, json_file, indent=4) def _remove_non_serializable_store_entries(store: dict): """ This function is called if there are non-serializable items in the global script storage. This function removes all such items. """ removed_key_list = [] for key, value in store.items(): if not (_is_serializable(key) and _is_serializable(value)): logger.info("Remove non-serializable item from the global script store. Key: '{}', Value: '{}'. " "This item cannot be saved and therefore will be lost.".format(key, value)) removed_key_list.append(key) for key in removed_key_list: del store[key] def _is_serializable(data): """Check, if data is json serializable.""" try: json.dumps(data) except (TypeError, ValueError): # TypeError occurs with non-serializable types (type, function, etc.) # ValueError occurs when circular references are found. Example: `l=[]; l.append(l)` return False else: return True def apply_settings(settings): """ Allows new settings to be added without users having to lose all their configuration """ for key, value in settings.items(): ConfigManager.SETTINGS[key] = value class ConfigManager: """ Contains all application configuration, and provides methods for updating and maintaining consistency of the configuration. """ """ Static member for global application settings. """ CLASS_VERSION = common.VERSION SETTINGS = { IS_FIRST_RUN: True, SERVICE_RUNNING: True, MENU_TAKES_FOCUS: False, SHOW_TRAY_ICON: True, SORT_BY_USAGE_COUNT: True, #DETECT_UNWANTED_ABBR: False, PROMPT_TO_SAVE:False, #PREDICTIVE_LENGTH: 5, ENABLE_QT4_WORKAROUND: False, INTERFACE_TYPE: X_RECORD_INTERFACE, UNDO_USING_BACKSPACE: True, WINDOW_DEFAULT_SIZE: (600, 400), HPANE_POSITION: 150, COLUMN_WIDTHS: [150, 50, 100], SHOW_TOOLBAR: True, NOTIFICATION_ICON: common.ICON_FILE_NOTIFICATION, WORKAROUND_APP_REGEX: ".*VirtualBox.*|krdc.Krdc", TRIGGER_BY_INITIAL: False, DISABLED_MODIFIERS: [], # TODO - Future functionality #TRACK_RECENT_ENTRY: True, # RECENT_ENTRY_COUNT: 5, #RECENT_ENTRY_MINLENGTH: 10, #RECENT_ENTRY_SUGGEST: True SCRIPT_GLOBALS: {}, GTK_THEME: "classic", GTK_TREE_VIEW_EXPANDED_ROWS: [], PATH_LAST_OPEN: "0" } def __init__(self, app): """ Create initial default configuration """ self.VERSION = self.__class__.CLASS_VERSION self.lock = threading.Lock() self.app = app self.folders = [] self.userCodeDir = None # type: str self.configHotkey = GlobalHotkey() self.configHotkey.set_hotkey([""], "k") self.configHotkey.enabled = True self.toggleServiceHotkey = GlobalHotkey() self.toggleServiceHotkey.set_hotkey(["", ""], "k") self.toggleServiceHotkey.enabled = True # Set the attribute to the default first. Without this, AK breaks, if started for the first time. See #274 self.workAroundApps = re.compile(self.SETTINGS[WORKAROUND_APP_REGEX]) app.init_global_hotkeys(self) self.load_global_config() self.app.monitor.add_watch(CONFIG_DEFAULT_FOLDER) self.app.monitor.add_watch(common.CONFIG_DIR) if self.folders: return # --- Code below here only executed if no persisted config data provided logger.info("No configuration found - creating new one") self.folders.append(autokey.configmanager.predefined_user_files.create_my_phrases_folder()) self.folders.append(autokey.configmanager.predefined_user_files.create_sample_scripts_folder()) logger.debug("Initial folders generated and populated with example data.") # TODO - future functionality self.recentEntries = [] self.config_altered(True) def get_serializable(self): extraFolders = [] for folder in self.folders: if not folder.path.startswith(CONFIG_DEFAULT_FOLDER): extraFolders.append(folder.path) d = { "version": self.VERSION, "userCodeDir": self.userCodeDir, "settings": ConfigManager.SETTINGS, "folders": extraFolders, "toggleServiceHotkey": self.toggleServiceHotkey.get_serializable(), "configHotkey": self.configHotkey.get_serializable() } return d def load_global_config(self): if os.path.exists(CONFIG_FILE): logger.info("Loading config from existing file: " + CONFIG_FILE) with open(CONFIG_FILE, 'r') as pFile: data = json.load(pFile) version_upgrade.upgrade_configuration_format(self, data) self.VERSION = data["version"] self.userCodeDir = data["userCodeDir"] apply_settings(data["settings"]) self.load_disabled_modifiers() self.workAroundApps = re.compile(self.SETTINGS[WORKAROUND_APP_REGEX]) self.__load_folders(data) self.toggleServiceHotkey.load_from_serialized(data["toggleServiceHotkey"]) self.configHotkey.load_from_serialized(data["configHotkey"]) if self.VERSION < self.CLASS_VERSION: version_upgrade.upgrade_configuration_after_load(self, data) self.config_altered(False) logger.info("Successfully loaded configuration") def __load_folders(self, data): for path in self.get_all_config_folder_paths(data): f = autokey.model.folder.Folder("", path=path) f.load() logger.debug("Loading folder at '%s'", path) self.folders.append(f) def get_all_config_folder_paths(self, data): for path in glob.glob(CONFIG_DEFAULT_FOLDER + "/*"): if os.path.isdir(path): yield path for path in data["folders"]: yield path def get_all_folders(self): out = [] for folder in self.folders: out.append(folder) out.extend(folder.get_child_folders()) return out def __checkExisting(self, path): # Check if we already know about the path, and return object if found for item in self.allItems: if item.path == path: return item return None def __checkExistingFolder(self, path): for folder in self.allFolders: if folder.path == path: return folder return None def path_created_or_modified(self, path): directory, baseName = os.path.split(path) loaded = False if path == CONFIG_FILE: self.reload_global_config() elif directory != common.CONFIG_DIR: # ignore all other changes in top dir # --- handle directories added if os.path.isdir(path): f = autokey.model.folder.Folder("", path=path) if directory == CONFIG_DEFAULT_FOLDER: self.folders.append(f) f.load() loaded = True else: folder = self.__checkExistingFolder(directory) if folder is not None: f.load(folder) folder.add_folder(f) loaded = True # -- handle txt or py files added or modified elif os.path.isfile(path): i = self.__checkExisting(path) isNew = False if i is None: isNew = True if baseName.endswith(".txt"): i = autokey.model.phrase.Phrase("", "", path=path) elif baseName.endswith(".py"): i = autokey.model.script.Script("", "", path=path) if i is not None: folder = self.__checkExistingFolder(directory) if folder is not None: i.load(folder) if isNew: folder.add_item(i) loaded = True # --- handle changes to folder settings if baseName == "folder.json": folder = self.__checkExistingFolder(directory) if folder is not None: folder.load_from_serialized() loaded = True # --- handle changes to item settings if baseName.endswith(".json"): for item in self.allItems: if item.get_json_path() == path: item.load_from_serialized() loaded = True if not loaded: logger.warning("No action taken for create/update event at %s", path) else: self.config_altered(False) return loaded def path_removed(self, path): directory, baseName = os.path.split(path) deleted = False if directory == common.CONFIG_DIR: # ignore all deletions in top dir return folder = self.__checkExistingFolder(path) item = self.__checkExisting(path) if folder is not None: if folder.parent is None: self.folders.remove(folder) else: folder.parent.remove_folder(folder) deleted = True elif item is not None: item.parent.remove_item(item) #item.remove_data() deleted = True if not deleted: logger.warning("No action taken for delete event at %s", path) else: self.config_altered(False) return deleted def load_disabled_modifiers(self): """ Load all disabled modifier keys from the configuration file. Called during startup, after the configuration is read into the SETTINGS dictionary. :return: """ try: self.SETTINGS[DISABLED_MODIFIERS] = [key.Key(value) for value in self.SETTINGS[DISABLED_MODIFIERS]] except ValueError: logger.error("Unknown value in the disabled modifier list found. Unexpected: {}".format( self.SETTINGS[DISABLED_MODIFIERS])) self.SETTINGS[DISABLED_MODIFIERS] = [] for possible_modifier in self.SETTINGS[DISABLED_MODIFIERS]: self._check_if_modifier(possible_modifier) logger.info("Disabling modifier key {} based on the stored configuration file.".format(possible_modifier)) MODIFIERS.remove(possible_modifier) @staticmethod def is_modifier_disabled(modifier: key.Key) -> bool: """Checks, if the given modifier key is disabled. """ ConfigManager._check_if_modifier(modifier) return modifier in ConfigManager.SETTINGS[DISABLED_MODIFIERS] @staticmethod def disable_modifier(modifier: typing.Union[key.Key, str]): """ Permanently disable a modifier key. This can be used to disable unwanted modifier keys, like CAPSLOCK, if the user remapped the physical key to something else. :param modifier: Modifier key to disable. :return: """ if isinstance(modifier, str): modifier = key.Key(modifier) ConfigManager._check_if_modifier(modifier) try: logger.info("Disabling modifier key {} on user request.".format(modifier)) MODIFIERS.remove(modifier) except ValueError: logger.warning("Disabling already disabled modifier key. Affected key: {}".format(modifier)) else: ConfigManager.SETTINGS[DISABLED_MODIFIERS].append(modifier) @staticmethod def enable_modifier(modifier: typing.Union[key.Key, str]): """ Enable a previously disabled modifier key. :param modifier: Modifier key to re-enable :return: """ if isinstance(modifier, str): modifier = key.Key(modifier) ConfigManager._check_if_modifier(modifier) if modifier not in MODIFIERS: logger.info("Re-eabling modifier key {} on user request.".format(modifier)) MODIFIERS.append(modifier) ConfigManager.SETTINGS[DISABLED_MODIFIERS].remove(modifier) else: logger.warning("Enabling already enabled modifier key. Affected key: {}".format(modifier)) @staticmethod def _check_if_modifier(modifier: key.Key): if not isinstance(modifier, key.Key): raise TypeError("The given value must be an AutoKey Key instance, got {}".format(type(modifier))) if not modifier in key._ALL_MODIFIERS_: raise ValueError("The given key '{}' is not a modifier. Expected one of {}.".format( modifier, key._ALL_MODIFIERS_)) def reload_global_config(self): logger.info("Reloading global configuration") with open(CONFIG_FILE, 'r') as pFile: data = json.load(pFile) self.userCodeDir = data["userCodeDir"] apply_settings(data["settings"]) self.workAroundApps = re.compile(self.SETTINGS[WORKAROUND_APP_REGEX]) existingPaths = [] for folder in self.folders: if folder.parent is None and not folder.path.startswith(CONFIG_DEFAULT_FOLDER): existingPaths.append(folder.path) for folderPath in data["folders"]: if folderPath not in existingPaths: f = autokey.model.folder.Folder("", path=folderPath) f.load() self.folders.append(f) self.toggleServiceHotkey.load_from_serialized(data["toggleServiceHotkey"]) self.configHotkey.load_from_serialized(data["configHotkey"]) self.config_altered(False) logger.info("Successfully reloaded global configuration") def config_altered(self, persistGlobal): """ Called when some element of configuration has been altered, to update the lists of phrases/folders. @param persistGlobal: save the global configuration at the end of the process """ logger.info("Configuration changed - rebuilding in-memory structures") self.lock.acquire() # Rebuild root folder list #rootFolders = self.folders #self.folders = [] #for folder in rootFolders: # self.folders.append(folder) self.hotKeyFolders = [] self.hotKeys = [] self.abbreviations = [] self.allFolders = [] self.allItems = [] for folder in self.folders: if autokey.model.helpers.TriggerMode.HOTKEY in folder.modes: self.hotKeyFolders.append(folder) self.allFolders.append(folder) if not self.app.monitor.has_watch(folder.path): self.app.monitor.add_watch(folder.path) self.__processFolder(folder) self.globalHotkeys = [] self.globalHotkeys.append(self.configHotkey) self.globalHotkeys.append(self.toggleServiceHotkey) #_logger.debug("Global hotkeys: %s", self.globalHotkeys) #_logger.debug("Hotkey folders: %s", self.hotKeyFolders) #_logger.debug("Hotkey phrases: %s", self.hotKeys) #_logger.debug("Abbreviation phrases: %s", self.abbreviations) #_logger.debug("All folders: %s", self.allFolders) #_logger.debug("All phrases: %s", self.allItems) if persistGlobal: save_config(self) self.lock.release() def __processFolder(self, parentFolder): if not self.app.monitor.has_watch(parentFolder.path): self.app.monitor.add_watch(parentFolder.path) for folder in parentFolder.folders: if autokey.model.helpers.TriggerMode.HOTKEY in folder.modes: self.hotKeyFolders.append(folder) self.allFolders.append(folder) if not self.app.monitor.has_watch(folder.path): self.app.monitor.add_watch(folder.path) self.__processFolder(folder) for item in parentFolder.items: if autokey.model.helpers.TriggerMode.HOTKEY in item.modes: self.hotKeys.append(item) if autokey.model.helpers.TriggerMode.ABBREVIATION in item.modes: self.abbreviations.append(item) self.allItems.append(item) # TODO Future functionality def add_recent_entry(self, entry): if RECENT_ENTRIES_FOLDER not in self.folders: folder = autokey.model.folder.Folder(RECENT_ENTRIES_FOLDER) folder.set_hotkey([""], "") folder.set_modes([autokey.model.helpers.TriggerMode.HOTKEY]) self.folders[RECENT_ENTRIES_FOLDER] = folder self.recentEntries = [] folder = self.folders[RECENT_ENTRIES_FOLDER] if entry not in self.recentEntries: self.recentEntries.append(entry) while len(self.recentEntries) > self.SETTINGS[RECENT_ENTRY_COUNT]: # noqa: F821 self.recentEntries.pop(0) folder.items = [] for theEntry in self.recentEntries: if len(theEntry) > 17: description = theEntry[:17] + "..." else: description = theEntry p = autokey.model.phrase.Phrase(description, theEntry) if self.SETTINGS[RECENT_ENTRY_SUGGEST]: # noqa: F821 p.set_modes([autokey.model.helpers.TriggerMode.PREDICTIVE]) folder.add_item(p) self.config_altered(False) def check_abbreviation_unique(self, abbreviation, filterPattern, targetItem): """ Checks that the given abbreviation is not already in use. @param abbreviation: the abbreviation to check @param filterPattern: The filter pattern associated with the abbreviation @param targetItem: the phrase for which the abbreviation to be used """ for item in itertools.chain(self.allFolders, self.allItems): if ConfigManager.item_has_abbreviation(item, abbreviation) and \ item.filter_matches(filterPattern): return item is targetItem, item return True, None @staticmethod def item_has_abbreviation(item, abbreviation): return autokey.model.helpers.TriggerMode.ABBREVIATION in item.modes and \ abbreviation in item.abbreviations """def check_abbreviation_substring(self, abbreviation, targetItem): for item in self.allFolders: if model.TriggerMode.ABBREVIATION in item.modes: if abbreviation in item.abbreviation or item.abbreviation in abbreviation: return item is targetItem, item.title for item in self.allItems: if model.TriggerMode.ABBREVIATION in item.modes: if abbreviation in item.abbreviation or item.abbreviation in abbreviation: return item is targetItem, item.description return True, "" def __checkSubstringAbbr(self, item1, item2, abbr): # Check if the given abbreviation is a substring match for the given item # If it is, check a few other rules to see if it matters print ("substring check {} against {}".format(item.abbreviation, abbr)) try: index = item.abbreviation.index(abbr) print (index) if index == 0 and len(abbr) < len(item.abbreviation): return item.immediate elif (index + len(abbr)) == len(item.abbreviation): return item.triggerInside elif len(abbr) != len(item.abbreviation): return item.triggerInside and item.immediate else: return False except ValueError: return False""" def check_hotkey_unique(self, modifiers, hotKey, newFilterPattern, targetItem): """ Checks that the given hotkey is not already in use. Also checks the special hotkeys configured from the advanced settings dialog. @param modifiers: modifiers for the hotkey @param hotKey: the hotkey to check @param newFilterPattern: @param targetItem: the phrase for which the hotKey to be used """ item = self.get_item_with_hotkey(modifiers, hotKey, newFilterPattern) if item: return item is targetItem, item else: return True, None def get_item_with_hotkey(self, modifiers, hotKey, newFilterPattern=None): """ Gets first item with the specified hotkey. Also checks the special hotkeys configured from the advanced settings dialog. Checks folders first, then phrases, then special hotkeys. @param modifiers: modifiers for the hotkey @param hotKey: the hotkey to check @param newFilterPattern: """ for item in itertools.chain(self.allFolders, self.allItems): if autokey.model.helpers.TriggerMode.HOTKEY in item.modes and \ ConfigManager.item_has_same_hotkey(item, modifiers, hotKey, newFilterPattern): return item for item in self.globalHotkeys: if item.enabled and ConfigManager.item_has_same_hotkey(item, modifiers, hotKey, newFilterPattern): return item return None @staticmethod def item_has_same_hotkey(item, modifiers, hotKey, newFilterPattern): return item.modifiers == modifiers and item.hotKey == hotKey and item.filter_matches(newFilterPattern) def remove_all_temporary(self, folder=None, in_temp_parent=False): """ Removes all temporary folders and phrases, as well as any within temporary folders. Useful for rc-style scripts that want to change a set of keys. """ if folder is None: searchFolders = self.allFolders searchItems = self.allItems else: searchFolders = folder.folders searchItems = folder.items for item in searchItems: try: if item.temporary or in_temp_parent: self.__deleteHotkeys(item) searchItems.remove(item) # Items created before this update don't have a 'temporary' field. except AttributeError: pass for subfolder in searchFolders: self.__deleteHotkeys(subfolder) try: if subfolder.temporary or in_temp_parent: in_temp_parent = True if folder is not None: folder.remove_folder(subfolder) else: searchFolders.remove(subfolder) # Items created before this update don't have a 'temporary' field. except AttributeError: pass self.remove_all_temporary(subfolder, in_temp_parent) def delete_hotkeys(self, removed_item): return self.__deleteHotkeys(removed_item) def __deleteHotkeys(self, removed_item): removed_item.unset_hotkey() app = self.app if autokey.model.helpers.TriggerMode.HOTKEY in removed_item.modes: app.hotkey_removed(removed_item) if isinstance(removed_item, autokey.model.folder.Folder): for subFolder in removed_item.folders: self.delete_hotkeys(subFolder) for item in removed_item.items: if autokey.model.helpers.TriggerMode.HOTKEY in item.modes: app.hotkey_removed(item) class GlobalHotkey(autokey.model.abstract_hotkey.AbstractHotkey): """ A global application hotkey, configured from the advanced settings dialog. Allows a method call to be attached to the hotkey. """ def __init__(self): autokey.model.abstract_hotkey.AbstractHotkey.__init__(self) self.enabled = False self.windowInfoRegex = None self.isRecursive = False self.parent = None self.modes = [] def get_serializable(self): d = { "enabled": self.enabled } d.update(autokey.model.abstract_hotkey.AbstractHotkey.get_serializable(self)) return d def load_from_serialized(self, data): autokey.model.abstract_hotkey.AbstractHotkey.load_from_serialized(self, data) self.enabled = data["enabled"] def set_closure(self, closure): """ Set the callable to be executed when the hotkey is triggered. """ self.closure = closure def check_hotkey(self, modifiers, key, windowTitle): # TODO: Doesn’t this always return False? (as long as no exceptions are thrown) if autokey.model.abstract_hotkey.AbstractHotkey.check_hotkey(self, modifiers, key, windowTitle) and self.enabled: logger.debug("Triggered global hotkey using modifiers: %r key: %r", modifiers, key) self.closure() return False def get_hotkey_string(self, key=None, modifiers=None): if key is None and modifiers is None: if not self.enabled: return "" key = self.hotKey modifiers = self.modifiers ret = "" for modifier in modifiers: ret += modifier ret += "+" if key == ' ': ret += "" else: ret += key return ret def __str__(self): return "AutoKey global hotkeys" # TODO: i18n autokey-0.96.0/lib/autokey/configmanager/configmanager_constants.py000066400000000000000000000036501427671440700256040ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . """This file holds constants used in the configuration manager.""" import os.path from autokey import common # Configuration file location CONFIG_FILE = os.path.join(common.CONFIG_DIR, "autokey.json") CONFIG_DEFAULT_FOLDER = os.path.join(common.CONFIG_DIR, "data") CONFIG_FILE_BACKUP = CONFIG_FILE + '~' DEFAULT_ABBR_FOLDER = "Imported Abbreviations" RECENT_ENTRIES_FOLDER = "Recently Typed" # JSON Key names used in the configuration file INTERFACE_TYPE = "interfaceType" IS_FIRST_RUN = "isFirstRun" SERVICE_RUNNING = "serviceRunning" MENU_TAKES_FOCUS = "menuTakesFocus" SHOW_TRAY_ICON = "showTrayIcon" SORT_BY_USAGE_COUNT = "sortByUsageCount" PROMPT_TO_SAVE = "promptToSave" INPUT_SAVINGS = "inputSavings" ENABLE_QT4_WORKAROUND = "enableQT4Workaround" UNDO_USING_BACKSPACE = "undoUsingBackspace" WINDOW_DEFAULT_SIZE = "windowDefaultSize" HPANE_POSITION = "hPanePosition" COLUMN_WIDTHS = "columnWidths" SHOW_TOOLBAR = "showToolbar" NOTIFICATION_ICON = "notificationIcon" WORKAROUND_APP_REGEX = "workAroundApps" DISABLED_MODIFIERS = "disabledModifiers" TRIGGER_BY_INITIAL = "triggerItemByInitial" SCRIPT_GLOBALS = "scriptGlobals" GTK_THEME = "gtkTheme" GTK_TREE_VIEW_EXPANDED_ROWS = "gtkExpandedRows" PATH_LAST_OPEN = "pathLastOpen" autokey-0.96.0/lib/autokey/configmanager/predefined_user_files.py000066400000000000000000000164221427671440700252360ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ This module creates and loads the pre-defined scripts and phrases users find after the first application start. Script content is stored as Python files inside the predefined_user_scripts directory. This eases maintenance of predefined user scripts, because those are not stored inside string variables any more. """ from typing import NamedTuple, List, Optional import pathlib from autokey.model.folder import Folder from autokey.model.script import Script from autokey.model.phrase import Phrase from autokey.model.helpers import TriggerMode logger = __import__("autokey.logger").logger.get_logger(__name__) # A Hotkey is defined by a list of modifier keys and a printable key. HotkeyData = NamedTuple("HotkeyData", [("modifiers", List[str]), ("key", str)]) # ItemData holds everything needed to define a standalone Script or Phrase. For Scripts, content contains the file name # pointing to the actual data (without the .pyi extension). ItemData = NamedTuple( "ItemData", [ ("name", str), ("hotkey", Optional[HotkeyData]), ("abbreviations", List[str]), ("trigger_modes", Optional[List[TriggerMode]]), ("window_filter", Optional[str]), ("show_in_tray_menu", bool), ("content", str)] ) adress_phrases_data = [ ItemData( name="Home Address", hotkey=None, abbreviations=["adr"], trigger_modes=[TriggerMode.ABBREVIATION], window_filter=None, show_in_tray_menu=False, content="22 Avenue Street\nBrisbane\nQLD\n4000") ] my_phrases_data = [ ItemData( name="First phrase", hotkey=None, abbreviations=[], trigger_modes=[], window_filter=".* - gedit", show_in_tray_menu=False, content="Test phrase number one!"), ItemData( name="Second phrase", hotkey=None, abbreviations=[], trigger_modes=[], window_filter=None, show_in_tray_menu=False, content="Test phrase number two!"), ItemData( name="Third phrase", hotkey=None, abbreviations=[], trigger_modes=[], window_filter=None, show_in_tray_menu=False, content="Test phrase number three!") ] sample_scripts_data = [ ItemData( name="Insert Date", hotkey=None, abbreviations=[], trigger_modes=[], window_filter=None, show_in_tray_menu=False, content="insert_date"), ItemData( name="List Menu", hotkey=None, abbreviations=[], trigger_modes=[], window_filter=None, show_in_tray_menu=False, content="list_menu"), ItemData( name="Selection Test", hotkey=None, abbreviations=[], trigger_modes=[], window_filter=None, show_in_tray_menu=False, content="selection_test"), ItemData( name="Abbreviation from selection", hotkey=None, abbreviations=[], trigger_modes=[], window_filter=None, show_in_tray_menu=False, content="new_abbreviation_from_selection"), ItemData( name="Phrase from selection", hotkey=None, abbreviations=[], trigger_modes=[], window_filter=None, show_in_tray_menu=False, content="create_phrase_from_selection"), ItemData( name="Display window info", hotkey=None, abbreviations=[], trigger_modes=[], window_filter=None, show_in_tray_menu=True, content="display_window_info") ] def _create_script(data: ItemData, parent: Folder) -> Script: """ Create a script from data, reading the actual content from a python file in the predefined_user_scripts directory. Place the script into the parent folder. """ content_path = pathlib.Path(__file__).parent / "predefined_user_scripts" / (data.content + ".pyi") logger.debug("Creating Script: name={}, path_to_content={}".format(data.name, content_path)) with open(str(content_path), "r", encoding="utf-8") as source_file: source_code = source_file.read() item = Script(data.name, source_code) if data.hotkey: item.set_hotkey(*data.hotkey) for abbreviation in data.abbreviations: item.add_abbreviation(abbreviation) item.set_modes(data.trigger_modes) if data.window_filter: item.set_window_titles(data.window_filter) item.show_in_tray_menu = data.show_in_tray_menu parent.add_item(item) item.persist() return item def _create_phrase(data: ItemData, parent: Folder) -> Phrase: """Create a Phrase from data. Place it into the parent folder.""" logger.debug("Creating Phrase: name={}".format(data.name)) item = Phrase(data.name, data.content) if data.hotkey: item.set_hotkey(*data.hotkey) for abbreviation in data.abbreviations: item.add_abbreviation(abbreviation) item.set_modes(data.trigger_modes) if data.window_filter: item.set_window_titles(data.window_filter) item.show_in_tray_menu = data.show_in_tray_menu parent.add_item(item) item.persist() return item def _create_folder(name: str, parent: Folder=None) -> Folder: """Creates a folder with the given name. If parent is given, create it inside parent.""" logger.debug("About to create folder '{}'".format(name)) folder = Folder(name) if parent is not None: parent.add_folder(folder) return folder def _create_addresses_folder(parent: Folder) -> Folder: """Creates the "Adresses" folder inside the "My Phrases" folder.""" addresses = _create_folder("Addresses", parent) addresses.persist() for item_data in adress_phrases_data: _create_phrase(item_data, addresses) return addresses def create_my_phrases_folder() -> Folder: """Creates the "My Phrases" folder. It will contain some simple test phrases""" my_phrases = _create_folder("My Phrases") my_phrases.set_hotkey([""], "") my_phrases.set_modes([TriggerMode.HOTKEY]) my_phrases.persist() _create_addresses_folder(my_phrases) for item_data in my_phrases_data: _create_phrase(item_data, my_phrases) return my_phrases def create_sample_scripts_folder(): """ Creates the "Sample Scripts" folder. It contains a bunch of pre-defined example scripts. The exact script content is read from the predefined_user_scripts directory inside this Python package. """ sample_scripts = _create_folder("Sample Scripts") sample_scripts.persist() for item_data in sample_scripts_data: _create_script(item_data, sample_scripts) return sample_scripts autokey-0.96.0/lib/autokey/configmanager/predefined_user_scripts/000077500000000000000000000000001427671440700252445ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/configmanager/predefined_user_scripts/_README.txt000066400000000000000000000012701427671440700271010ustar00rootroot00000000000000This directory contains the default user scripts that ship with a new AutoKey installation. - There is no __init__.py. This is not a AutoKey source package. This is package data, that happens to be Python code. - Files have the Python interface file ending (.pyi) to emphasise that files are not part of the running AutoKey source. - The files are not executable, because of missing imports. This is intended. The missing imports belong to the AutoKey API and are injected at script runtime, when executed by AutoKey. - Script files here are not automatically detected. To add a script, place the code here and define the meta-data in predefined_user_files.py in the parent AutoKey package. autokey-0.96.0/lib/autokey/configmanager/predefined_user_scripts/create_phrase_from_selection.pyi000066400000000000000000000006561427671440700336730ustar00rootroot00000000000000# Create a new phrase from the currently selected text, without having any abbreviation or hotkey assigned. contents = clipboard.get_selection() if len(contents) > 20: # The title is abbreviated, if it is longer than 20 characters title = contents[0:19] + "…" else: title = contents folder = engine.get_folder("My Phrases") # The phrase will be created in this folder. engine.create_phrase(folder, title, contents) autokey-0.96.0/lib/autokey/configmanager/predefined_user_scripts/display_window_info.pyi000066400000000000000000000005261427671440700320410ustar00rootroot00000000000000# Displays the information of the next window to be left-clicked import time mouse.wait_for_click(5) time.sleep(0.2) window_title = window.get_active_title() window_class = window.get_active_class() dialog.info_dialog( "Window information", "Active window information:\nTitle: '{}'\nClass: '{}'".format(window_title, window_class) ) autokey-0.96.0/lib/autokey/configmanager/predefined_user_scripts/insert_date.pyi000066400000000000000000000001001427671440700302570ustar00rootroot00000000000000output = system.exec_command("date") keyboard.send_keys(output) autokey-0.96.0/lib/autokey/configmanager/predefined_user_scripts/list_menu.pyi000066400000000000000000000003331427671440700277650ustar00rootroot00000000000000choices = ["something", "something else", "a third thing"] return_code, choice = dialog.list_menu(choices) if not return_code: # return code is 0 (thus false) on success. keyboard.send_keys("You chose " + choice) autokey-0.96.0/lib/autokey/configmanager/predefined_user_scripts/new_abbreviation_from_selection.pyi000066400000000000000000000011351427671440700343750ustar00rootroot00000000000000# Create a new Phrase from the current text selection. Ask for an abbreviation and then create a new Phrase having # this abbreviation assigned. import time time.sleep(0.25) contents = clipboard.get_selection() return_code, abbreviation = dialog.input_dialog("New Abbreviation", "Choose an abbreviation for the new phrase") if not return_code: # return code is 0 (thus false) on success. if len(contents) > 20: title = contents[0:19] + "…" else: title = contents folder = engine.get_folder("My Phrases") engine.create_abbreviation(folder, title, abbreviation, contents) autokey-0.96.0/lib/autokey/configmanager/predefined_user_scripts/selection_test.pyi000066400000000000000000000002021427671440700310050ustar00rootroot00000000000000text = clipboard.get_selection() keyboard.send_key("") keyboard.send_keys("The text {} was here previously".format(text)) autokey-0.96.0/lib/autokey/configmanager/version_upgrading.py000066400000000000000000000203231427671440700244310ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Sometimes, changes in code require updating the format of existing user data. This module handles upgrading user data, if required. The stored user data contains a version field with the autokey version that created it. This is used to determine if any patches must be applied. Each converter function altering the configuration data MUST NOT change the "version" item in the configuration data to a different version (so NEVER set to common.VERSION), because this might skip additional conversion steps. Example: Let the current version be 0.97.0. If 0.96.1 introduces a conversion step, and old 0.70.x data is found, setting config_data["version"] to common.VERSION ("0.97.0") inside the conversion function convert_v0_70_to_v0_80 will skip the required conversion task for 0.96.1. Additionally, it will skip further conversion tasks that require the model to be present, which are executed in the ConfigManager later. The ConfigManager is responsible for updating the data version. Do not require the user to install all versions one after another in order to get all data patches. Such skips might happen with LTS distribution releases that skip several autokey versions during their lifetime. """ import os from pathlib import Path import glob import re from packaging.version import parse as vparse import autokey.model.folder import autokey.model.phrase import autokey.model.script from autokey import common import autokey.configmanager.configmanager_constants as cm_constants from autokey.iomediator.constants import X_RECORD_INTERFACE logger = __import__("autokey.logger").logger.get_logger(__name__) def upgrade_configuration_format(configuration_manager, config_data: dict): """ Updates the global configuration data to the latest version. Run before configuration loaded. """ version = vparse(config_data["version"]) logger.info("Checking if upgrade is needed from version %s", version) if not version < vparse(common.VERSION): return if version < vparse("0.80.0"): convert_v0_70_to_v0_80(config_data) if version < vparse("0.95.3"): convert_autostart_entries_for_v0_95_3() if version < vparse("0.96"): convertDotFiles_v96(configuration_manager, config_data) convert_folder_attributes_0_96(configuration_manager, config_data) def upgrade_configuration_after_load(configuration_manager, config_data: dict): """ Updates the global configuration data to the latest version. Run after configuration loaded. """ version = vparse(config_data["version"]) logger.info("Checking if upgrade is needed from version %s", version) if not version < vparse(common.VERSION): return # Always reset interface type when upgrading configuration_manager.SETTINGS[cm_constants.INTERFACE_TYPE] = X_RECORD_INTERFACE logger.info("Resetting interface type, new type: %s", configuration_manager.SETTINGS[cm_constants.INTERFACE_TYPE]) if version < vparse("0.82.3"): convert_to_v0_82_3(configuration_manager) if version < vparse("0.70.0"): convert_to_v0_70(configuration_manager) configuration_manager.VERSION = common.VERSION configuration_manager.config_altered(True) def convert_to_v0_70(config_manager): logger.info("Doing upgrade to 0.70.0") for item in config_manager.allItems: if isinstance(item, autokey.model.phrase.Phrase): item.sendMode = autokey.model.phrase.SendMode.KEYBOARD def convert_to_v0_82_3(configuration_manager): logger.info("Doing upgrade to 0.82.3") configuration_manager.SETTINGS[cm_constants.WORKAROUND_APP_REGEX] += "|krdc.Krdc" configuration_manager.workAroundApps = re.compile(configuration_manager.SETTINGS[cm_constants.WORKAROUND_APP_REGEX]) configuration_manager.SETTINGS[cm_constants.SCRIPT_GLOBALS] = {} def convert_v0_70_to_v0_80(config_data): old_version = config_data["version"] try: _convert_v0_70_to_v0_80(config_data, old_version) except Exception: logger.exception( "Problem occurred during conversion of configuration data format from v0.70 to v0.80" "Existing config file has been saved as {}{}".format(cm_constants.CONFIG_FILE, old_version) ) raise def _convert_v0_70_to_v0_80(config_data, old_version: str): os.rename(cm_constants.CONFIG_FILE, cm_constants.CONFIG_FILE + old_version) logger.info("Converting v{} configuration data to v0.80.0".format(old_version)) for folder_data in config_data["folders"]: _convert_v0_70_to_v0_80_folder(folder_data, None) config_data["folders"] = [] config_data["settings"][cm_constants.NOTIFICATION_ICON] = common.ICON_FILE_NOTIFICATION # Remove old backup file so we never retry the conversion if os.path.exists(cm_constants.CONFIG_FILE_BACKUP): os.remove(cm_constants.CONFIG_FILE_BACKUP) logger.info("Conversion succeeded") def _convert_v0_70_to_v0_80_folder(folder_data, parent): f = autokey.model.folder.Folder("") f.inject_json_data(folder_data) f.parent = parent f.persist() for subfolder in folder_data["folders"]: _convert_v0_70_to_v0_80_folder(subfolder, f) for itemData in folder_data["items"]: i = None if itemData["type"] == "script": i = autokey.model.script.Script("", "") i.code = itemData["code"] elif itemData["type"] == "phrase": i = autokey.model.phrase.Phrase("", "") i.phrase = itemData["phrase"] if i is not None: i.inject_json_data(itemData) i.parent = f i.persist() def convert_autostart_entries_for_v0_95_3(): """ In versions <= 0.95.2, the autostart option in autokey-gtk copied the default autokey-gtk.desktop file into $XDG_CONFIG_DIR/autostart (with minor, unrelated modifications). For versions >= 0.95.3, the autostart file is renamed to autokey.desktop. In 0.95.3, the autostart functionality is implemented for autokey-qt. Thus, it becomes possible to have an autostart file for both GUIs in the autostart directory simultaneously. Because of the singleton nature of autokey, this becomes an issue and race-conditions determine which GUI starts first. To prevent this, both GUIs will share a single autokey.desktop autostart entry, allowing only one GUI to be started during login. This allows for much simpler code. """ logger.info("Version update: Converting autostart entry for 0.95.3") old_autostart_file = Path(common.AUTOSTART_DIR) / "autokey-gtk.desktop" if old_autostart_file.exists(): new_file_name = Path(common.AUTOSTART_DIR) / "autokey.desktop" logger.info("Found old autostart entry: '{}'. Rename to: '{}'".format( old_autostart_file, new_file_name) ) old_autostart_file.rename(new_file_name) def convertDotFiles_v95_11_folder(p: Path): for name in p.glob('.*.json'): new_json = p / name.name[1:] name.rename(new_json) logger.debug("Converted to {}".format(new_json)) for name in p.iterdir(): if name.is_dir(): convertDotFiles_v95_11_folder(name) def convertDotFiles_v96(cm, configData): logger.info("Version update: Unhiding sidecar dotfiles for versions > 0.95.10") for name in cm.get_all_config_folder_paths(configData): convertDotFiles_v95_11_folder(Path(name)) def convert_folder_attributes_0_96(cm, config_data): for folder in cm.get_all_folders(): logger.debug(folder) try: _ = folder.temporary except AttributeError: folder.temporary = False autokey-0.96.0/lib/autokey/dbus_service.py000066400000000000000000000043461427671440700205700ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2019 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import dbus.service logger = __import__("autokey.logger").logger.get_logger(__name__) class AppService(dbus.service.Object): """ This class is used by the GTK GUI and provides the DBus interface. It can be used by external programs to communicate with the autokey process. """ def __init__(self, app): busName = dbus.service.BusName('org.autokey.Service', bus=dbus.SessionBus()) dbus.service.Object.__init__(self, busName, "/AppService") self.app = app logger.debug("Created DBus service") @dbus.service.method(dbus_interface='org.autokey.Service', in_signature='', out_signature='') def show_configure(self): self.app.show_configure() @dbus.service.method(dbus_interface='org.autokey.Service', in_signature='s', out_signature='') def run_script(self, name): self.app.service.run_script(name) @dbus.service.method(dbus_interface='org.autokey.Service', in_signature='s', out_signature='') def run_phrase(self, name): self.app.service.run_phrase(name) @dbus.service.method(dbus_interface='org.autokey.Service', in_signature='s', out_signature='') def run_folder(self, name): self.app.service.run_folder(name) @dbus.service.method(dbus_interface='org.autokey.Service', in_signature='', out_signature='') def pause_service(self): self.app.pause_service() @dbus.service.method(dbus_interface='org.autokey.Service', in_signature='', out_signature='') def unpause_service(self): self.app.unpause_service() autokey-0.96.0/lib/autokey/gtkapp.py000066400000000000000000000225071427671440700174000ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import autokey.dbus_service import autokey.model.script from . import common common.USING_QT = False import sys import os.path import time import threading import gettext import dbus import dbus.service import dbus.mainloop.glib import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk, GObject, GLib gettext.install("autokey") import autokey.argument_parser from autokey import service, monitor from autokey.gtkui.notifier import get_notifier from autokey.gtkui.popupmenu import PopupMenu from autokey.gtkui.configwindow import ConfigWindow from autokey.gtkui.dialogs import ShowScriptErrorsDialog import autokey.configmanager.configmanager as cm import autokey.configmanager.configmanager_constants as cm_constants import autokey.UI_common_functions as UI_common from autokey.logger import get_logger, configure_root_logger from autokey.UI_common_functions import checkRequirements, checkOptionalPrograms, create_storage_directories logger = get_logger(__name__) # TODO: this _ named function is initialised by gettext.install(), which is for # localisation. It marks the string as a candidate for translation, but I don't # know what else. PROGRAM_NAME = _("AutoKey") DESCRIPTION = _("Desktop automation utility") COPYRIGHT = _("(c) 2008-2011 Chris Dekter") class Application: """ Main application class; starting and stopping of the application is controlled from here, together with some interactions from the tray icon. """ def __init__(self): GLib.threads_init() Gdk.threads_init() args = autokey.argument_parser.parse_args() configure_root_logger(args) checkOptionalPrograms() missing_reqs = checkRequirements() if len(missing_reqs)>0: Gdk.threads_enter() self.show_error_dialog("AutoKey Requires the following programs or python modules to be installed to function properly", missing_reqs) Gdk.threads_leave() sys.exit("Missing required programs and/or python modules, exiting") try: create_storage_directories() if self.__verifyNotRunning(): UI_common.create_lock_file() self.initialise(args.show_config_window) except Exception as e: self.show_error_dialog(_("Fatal error starting AutoKey.\n") + str(e)) logger.exception("Fatal error starting AutoKey: " + str(e)) sys.exit(1) def __verifyNotRunning(self): if UI_common.is_existing_running_autokey(): UI_common.test_Dbus_response(self) return True def initialise(self, configure): logger.info("Initialising application") self.monitor = monitor.FileMonitor(self) self.configManager = cm.create_config_manager_instance(self) self.service = service.Service(self) self.serviceDisabled = False # Initialise user code dir if self.configManager.userCodeDir is not None: sys.path.append(self.configManager.userCodeDir) try: self.service.start() except Exception as e: logger.exception("Error starting interface: " + str(e)) self.serviceDisabled = True self.show_error_dialog(_("Error starting interface. Keyboard monitoring will be disabled.\n" + "Check your system/configuration."), str(e)) self.notifier = get_notifier(self) self.configWindow = None self.monitor.start() dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) self.dbusService = autokey.dbus_service.AppService(self) if configure: self.show_configure() def init_global_hotkeys(self, configManager): logger.info("Initialise global hotkeys") configManager.toggleServiceHotkey.set_closure(self.toggle_service) configManager.configHotkey.set_closure(self.show_configure_async) def config_altered(self, persistGlobal): self.configManager.config_altered(persistGlobal) self.notifier.rebuild_menu() def hotkey_created(self, item): UI_common.hotkey_created(self.service, item) def hotkey_removed(self, item): UI_common.hotkey_removed(self.service, item) def path_created_or_modified(self, path): UI_common.path_created_or_modified(self.configManager, self.configWindow, path) def path_removed(self, path): UI_common.path_removed(self.configManager, self.configWindow, path) def unpause_service(self): """ Unpause the expansion service (start responding to keyboard and mouse events). """ self.service.unpause() self.notifier.update_tool_tip() def pause_service(self): """ Pause the expansion service (stop responding to keyboard and mouse events). """ self.service.pause() self.notifier.update_tool_tip() def toggle_service(self): """ Convenience method for toggling the expansion service on or off. """ if self.service.is_running(): self.pause_service() else: self.unpause_service() def shutdown(self): """ Shut down the entire application. """ if self.configWindow is not None: if self.configWindow.promptToSave(): return self.configWindow.hide() self.notifier.hide_icon() t = threading.Thread(target=self.__completeShutdown) t.start() def __completeShutdown(self): logger.info("Shutting down") self.service.shutdown() self.monitor.stop() Gdk.threads_enter() Gtk.main_quit() Gdk.threads_leave() os.remove(common.LOCK_FILE) logger.debug("All shutdown tasks complete... quitting") def notify_error(self, error: autokey.model.script.ScriptErrorRecord): """ Show an error notification popup. @param error: The error that occurred in a Script """ message = "The script '{}' encountered an error".format(error.script_name) self.notifier.notify_error(message) if self.configWindow is not None: self.configWindow.set_has_errors(True) def update_notifier_visibility(self): self.notifier.update_visible_status() def show_configure(self): """ Show the configuration window, or deiconify (un-minimise) it if it's already open. """ logger.info("Displaying configuration window") if self.configWindow is None: self.configWindow = ConfigWindow(self) self.configWindow.show() else: self.configWindow.deiconify() def show_configure_async(self): Gdk.threads_enter() self.show_configure() Gdk.threads_leave() def main(self): logger.info("Entering main()") Gdk.threads_enter() Gtk.main() Gdk.threads_leave() def show_error_dialog(self, message, details=None, dialog_type=Gtk.MessageType.ERROR): """ Convenience method for showing an error dialog. @param dialog_type: One of Gtk.MessageType.ERROR, Gtk.MessageType.WARNING , Gtk.MessageType.INFO, Gtk.MessageType.OTHER, Gtk.MessageType.QUESTION defaults to Gtk.MessageType.ERROR """ logger.debug("Displaying "+dialog_type.value_name+" Dialog") dlg = Gtk.MessageDialog(type=dialog_type, buttons=Gtk.ButtonsType.OK, message_format=message) if details is not None: dlg.format_secondary_text(details) dlg.run() dlg.destroy() def show_script_error(self, parent): """ Show the last script error (if any) """ if self.service.scriptRunner.error_records: dlg = ShowScriptErrorsDialog(self) # revert the tray icon self.notifier.set_icon(cm.ConfigManager.SETTINGS[cm_constants.NOTIFICATION_ICON]) self.notifier.errorItem.hide() self.notifier.update_visible_status() else: dlg = Gtk.MessageDialog(type=Gtk.MessageType.INFO, buttons=Gtk.ButtonsType.OK, message_format=_("No error information available")) dlg.set_title(_("View script error")) dlg.set_transient_for(parent) dlg.run() dlg.destroy() def show_popup_menu(self, folders: list=None, items: list=None, onDesktop=True, title=None): if items is None: items = [] if folders is None: folders = [] self.menu = PopupMenu(self.service, folders, items, onDesktop, title) self.menu.show_on_desktop() def hide_menu(self): self.menu.remove_from_desktop() autokey-0.96.0/lib/autokey/gtkui/000077500000000000000000000000001427671440700166555ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/gtkui/__init__.py000066400000000000000000000000001427671440700207540ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/gtkui/__main__.py000066400000000000000000000002531427671440700207470ustar00rootroot00000000000000import faulthandler faulthandler.enable() from autokey.gtkapp import Application def main(): a = Application() a.main() if __name__ == '__main__': main() autokey-0.96.0/lib/autokey/gtkui/configwindow.py000066400000000000000000002057001427671440700217300ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import locale import os import threading import time import webbrowser from gi import require_version import autokey.model.folder import autokey.model.helpers import autokey.model.phrase import autokey.model.script require_version('Gtk', '3.0') require_version('GtkSource', '3.0') from gi.repository import Gtk, Pango, GtkSource, Gdk, Gio GETTEXT_DOMAIN = 'autokey' locale.setlocale(locale.LC_ALL, '') from . import dialogs from .settingsdialog import SettingsDialog import autokey.configmanager.configmanager as cm import autokey.configmanager.configmanager_constants as cm_constants import autokey.iomediator.keygrabber from autokey import common CONFIG_WINDOW_TITLE = "AutoKey" UI_DESCRIPTION_FILE = os.path.join(os.path.dirname(__file__), "data/menus.xml") logger = __import__("autokey.logger").logger.get_logger(__name__) PROBLEM_MSG_PRIMARY = _("Some problems were found") PROBLEM_MSG_SECONDARY = _("%s\n\nYour changes have not been saved.") from .shared import get_ui def set_linkbutton(button, path, filename_only=False): label = button.get_child() label.set_sensitive(True) if path.startswith(cm_constants.CONFIG_DEFAULT_FOLDER): text = path.replace(cm_constants.CONFIG_DEFAULT_FOLDER, _("(Default folder)")) else: text = path.replace(os.path.expanduser("~"), "~") if filename_only: filename = os.path.basename(path) label.set_label(filename) else: label.set_label(text) button.set_uri("file://" + path) label.set_ellipsize(Pango.EllipsizeMode.START) class RenameDialog: def __init__(self, parentWindow, oldName, isNew, title=_("Rename '%s'")): builder = get_ui("renamedialog.xml") self.ui = builder.get_object("dialog") builder.connect_signals(self) self.ui.set_transient_for(parentWindow) self.nameEntry = builder.get_object("nameEntry") self.checkButton = builder.get_object("checkButton") self.image = builder.get_object("image") self.nameEntry.set_text(oldName) self.checkButton.set_active(True) if isNew: self.checkButton.hide() self.set_title(title) else: self.set_title(title % oldName) def get_name(self): return self.nameEntry.get_text()#.decode("utf-8") def get_update_fs(self): return self.checkButton.get_active() def set_image(self, stockId): self.image.set_from_stock(stockId, Gtk.IconSize.DIALOG) def __getattr__(self, attr): # Magic fudge to allow us to pretend to be the ui class we encapsulate return getattr(self.ui, attr) class SettingsWidget: KEY_MAP = dialogs.HotkeySettingsDialog.KEY_MAP REVERSE_KEY_MAP = dialogs.HotkeySettingsDialog.REVERSE_KEY_MAP def __init__(self, parentWindow): self.parentWindow = parentWindow builder = get_ui("settingswidget.xml") self.ui = builder.get_object("settingswidget") builder.connect_signals(self) self.abbrDialog = dialogs.AbbrSettingsDialog(parentWindow.ui, parentWindow.app.configManager, self.on_abbr_response) self.hotkeyDialog = dialogs.HotkeySettingsDialog(parentWindow.ui, parentWindow.app.configManager, self.on_hotkey_response) self.filterDialog = dialogs.WindowFilterSettingsDialog(parentWindow.ui, self.on_filter_dialog_response) self.abbrLabel = builder.get_object("abbrLabel") self.clearAbbrButton = builder.get_object("clearAbbrButton") self.hotkeyLabel = builder.get_object("hotkeyLabel") self.clearHotkeyButton = builder.get_object("clearHotkeyButton") self.windowFilterLabel = builder.get_object("windowFilterLabel") self.clearFilterButton = builder.get_object("clearFilterButton") def load(self, item): self.currentItem = item self.abbrDialog.load(self.currentItem) if autokey.model.helpers.TriggerMode.ABBREVIATION in item.modes: self.abbrLabel.set_text(item.get_abbreviations()) self.clearAbbrButton.set_sensitive(True) self.abbrEnabled = True else: self.abbrLabel.set_text(_("(None configured)")) self.clearAbbrButton.set_sensitive(False) self.abbrEnabled = False self.hotkeyDialog.load(self.currentItem) if autokey.model.helpers.TriggerMode.HOTKEY in item.modes: self.hotkeyLabel.set_text(item.get_hotkey_string()) self.clearHotkeyButton.set_sensitive(True) self.hotkeyEnabled = True else: self.hotkeyLabel.set_text(_("(None configured)")) self.clearHotkeyButton.set_sensitive(False) self.hotkeyEnabled = False self.filterDialog.load(self.currentItem) self.filterEnabled = False self.clearFilterButton.set_sensitive(False) if item.has_filter() or item.inherits_filter(): self.windowFilterLabel.set_text(item.get_filter_regex()) if not item.inherits_filter(): self.clearFilterButton.set_sensitive(True) self.filterEnabled = True else: self.windowFilterLabel.set_text(_("(None configured)")) def save(self): # Perform hotkey ungrab if autokey.model.helpers.TriggerMode.HOTKEY in self.currentItem.modes: self.parentWindow.app.hotkey_removed(self.currentItem) self.currentItem.set_modes([]) if self.abbrEnabled: self.abbrDialog.save(self.currentItem) if self.hotkeyEnabled: self.hotkeyDialog.save(self.currentItem) else: self.currentItem.unset_hotkey() if self.filterEnabled: self.filterDialog.save(self.currentItem) else: self.currentItem.set_window_titles(None) if self.hotkeyEnabled: self.parentWindow.app.hotkey_created(self.currentItem) def set_dirty(self): self.parentWindow.set_dirty(True) def validate(self): # Start by getting all applicable information abbreviations, modifiers, key, filterExpression = self.get_item_details() # Validate ret = [] configManager = self.parentWindow.app.configManager for abbr in abbreviations: unique, conflicting = configManager.check_abbreviation_unique(abbr, filterExpression, self.currentItem) if not unique: ret.append(self.build_msg_for_item_in_use(conflicting, "abbreviation")) unique, conflicting = configManager.check_hotkey_unique(modifiers, key, filterExpression, self.currentItem) if not unique: ret.append(self.build_msg_for_item_in_use(conflicting, "hotkey")) return ret def get_item_details(self): if self.abbrEnabled: abbreviations = self.abbrDialog.get_abbrs() else: abbreviations = [] if self.hotkeyEnabled: modifiers = self.hotkeyDialog.build_modifiers() key = self.hotkeyDialog.key else: modifiers = [] key = None filterExpression = None if self.filterEnabled: filterExpression = self.filterDialog.get_filter_text() elif self.currentItem.parent is not None: r = self.currentItem.parent.get_applicable_regex(True) if r is not None: filterExpression = r.pattern return abbreviations, modifiers, key, filterExpression def build_msg_for_item_in_use(self, conflicting, itemtype): msg = _("The %s '%s' is already in use by the %s") % (itemtype, conflicting.get_hotkey_string(), str(conflicting)) f = conflicting.get_applicable_regex() if f is not None: msg += _(" for windows matching '%s'.") % f.pattern return msg # ---- Signal handlers def on_setAbbrButton_clicked(self, widget, data=None): self.abbrDialog.reset_focus() self.abbrDialog.show() def on_abbr_response(self, res): if res == Gtk.ResponseType.OK: self.set_dirty() self.abbrEnabled = True self.abbrLabel.set_text(self.abbrDialog.get_abbrs_readable()) self.clearAbbrButton.set_sensitive(True) def on_clearAbbrButton_clicked(self, widget, data=None): self.set_dirty() self.abbrEnabled = False self.clearAbbrButton.set_sensitive(False) self.abbrLabel.set_text(_("(None configured)")) self.abbrDialog.reset() def on_setHotkeyButton_clicked(self, widget, data=None): self.hotkeyDialog.show() def on_hotkey_response(self, res): if res == Gtk.ResponseType.OK: self.set_dirty() self.hotkeyEnabled = True key = self.hotkeyDialog.key modifiers = self.hotkeyDialog.build_modifiers() self.hotkeyLabel.set_text(self.currentItem.get_hotkey_string(key, modifiers)) self.clearHotkeyButton.set_sensitive(True) def on_clearHotkeyButton_clicked(self, widget, data=None): self.set_dirty() self.hotkeyEnabled = False self.clearHotkeyButton.set_sensitive(False) self.hotkeyLabel.set_text(_("(None configured)")) self.hotkeyDialog.reset() def on_setFilterButton_clicked(self, widget, data=None): self.filterDialog.reset_focus() self.filterDialog.show() def on_clearFilterButton_clicked(self, widget, data=None): self.set_dirty() self.filterEnabled = False self.clearFilterButton.set_sensitive(False) if self.currentItem.inherits_filter(): text = self.currentItem.parent.get_child_filter() else: text = _("(None configured)") self.windowFilterLabel.set_text(text) self.filterDialog.reset() def on_filter_dialog_response(self, res): if res == Gtk.ResponseType.OK: self.set_dirty() filterText = self.filterDialog.get_filter_text() if filterText != "": self.filterEnabled = True self.clearFilterButton.set_sensitive(True) self.windowFilterLabel.set_text(filterText) else: self.filterEnabled = False self.clearFilterButton.set_sensitive(False) if self.currentItem.inherits_filter(): text = self.currentItem.parent.get_child_filter() else: text = _("(None configured)") self.windowFilterLabel.set_text(text) def __getattr__(self, attr): # Magic fudge to allow us to pretend to be the ui class we encapsulate return getattr(self.ui, attr) class BlankPage: def __init__(self, parentWindow): self.parentWindow = parentWindow builder = get_ui("blankpage.xml") self.ui = builder.get_object("blankpage") def load(self, theFolder): pass def save(self): pass def set_item_title(self, newTitle): pass def reset(self): pass def validate(self): return True def on_modified(self, widget, data=None): pass def set_dirty(self): self.parentWindow.set_dirty(True) class FolderPage: def __init__(self, parentWindow): self.parentWindow = parentWindow builder = get_ui("folderpage.xml") self.ui = builder.get_object("folderpage") builder.connect_signals(self) self.showInTrayCheckbox = builder.get_object("showInTrayCheckbox") self.linkButton = builder.get_object("linkButton") self.jsonLinkButton = builder.get_object("jsonLinkButton") label = self.linkButton.get_child() label.set_ellipsize(Pango.EllipsizeMode.MIDDLE) label1 = self.jsonLinkButton.get_child() label1.set_ellipsize(Pango.EllipsizeMode.MIDDLE) vbox = builder.get_object("settingsVbox") self.settingsWidget = SettingsWidget(parentWindow) vbox.pack_start(self.settingsWidget.ui, True, True, 0) def load(self, theFolder): self.currentFolder = theFolder self.showInTrayCheckbox.set_active(theFolder.show_in_tray_menu) self.settingsWidget.load(theFolder) if self.is_new_item(): self.linkButton.set_sensitive(False) self.linkButton.set_label(_("(Unsaved)")) self.jsonLinkButton.set_sensitive(False) self.jsonLinkButton.set_label(_("(Unsaved)")) else: set_linkbutton(self.linkButton, self.currentFolder.path) set_linkbutton(self.jsonLinkButton, self.currentFolder.get_json_path(), True) def save(self): self.currentFolder.show_in_tray_menu = self.showInTrayCheckbox.get_active() self.settingsWidget.save() self.currentFolder.persist() set_linkbutton(self.linkButton, self.currentFolder.path) return not self.currentFolder.path.startswith(cm_constants.CONFIG_DEFAULT_FOLDER) def set_item_title(self, newTitle): self.currentFolder.title = newTitle def rebuild_item_path(self): self.currentFolder.rebuild_path() def is_new_item(self): return self.currentFolder.path is None def reset(self): self.load(self.currentFolder) def validate(self): # Check settings errors = self.settingsWidget.validate() if errors: msg = PROBLEM_MSG_SECONDARY % '\n'.join(errors) dlg = Gtk.MessageDialog( self.parentWindow.ui, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, PROBLEM_MSG_PRIMARY ) dlg.format_secondary_text(msg) dlg.run() dlg.destroy() return len(errors) == 0 def on_modified(self, widget, data=None): self.set_dirty() def set_dirty(self): self.parentWindow.set_dirty(True) class ScriptPage: def __init__(self, parentWindow): self.parentWindow = parentWindow builder = get_ui("scriptpage.xml") self.ui = builder.get_object("scriptpage") builder.connect_signals(self) self.buffer = GtkSource.Buffer() self.buffer.connect("changed", self.on_modified) self.editor = GtkSource.View.new_with_buffer(self.buffer) scrolledWindow = builder.get_object("scrolledWindow") scrolledWindow.add(self.editor) # Editor font settings = Gio.Settings.new("org.gnome.desktop.interface") fontDesc = Pango.font_description_from_string(settings.get_string("monospace-font-name")) self.editor.modify_font(fontDesc) self.promptCheckbox = builder.get_object("promptCheckbox") self.showInTrayCheckbox = builder.get_object("showInTrayCheckbox") self.linkButton = builder.get_object("linkButton") self.jsonLinkButton = builder.get_object("jsonLinkButton") label = self.linkButton.get_child() label.set_ellipsize(Pango.EllipsizeMode.MIDDLE) label1 = self.jsonLinkButton.get_child() label1.set_ellipsize(Pango.EllipsizeMode.MIDDLE) vbox = builder.get_object("settingsVbox") self.settingsWidget = SettingsWidget(parentWindow) vbox.pack_start(self.settingsWidget.ui, False, False, 0) # Configure script editor self.__m = GtkSource.LanguageManager() self.__sm = GtkSource.StyleSchemeManager() self.buffer.set_language(self.__m.get_language("python")) self.buffer.set_style_scheme(self.__sm.get_scheme(cm.ConfigManager.SETTINGS[cm_constants.GTK_THEME])) self.editor.set_auto_indent(True) self.editor.set_smart_home_end(True) self.editor.set_insert_spaces_instead_of_tabs(True) self.editor.set_tab_width(4) self.ui.show_all() def load(self, theScript): self.currentItem = theScript self.buffer.begin_not_undoable_action() self.buffer.set_text(theScript.code) # self.buffer.set_text(theScript.code.encode("utf-8")) self.buffer.end_not_undoable_action() self.buffer.place_cursor(self.buffer.get_start_iter()) self.promptCheckbox.set_active(theScript.prompt) self.showInTrayCheckbox.set_active(theScript.show_in_tray_menu) self.settingsWidget.load(theScript) if self.is_new_item(): self.linkButton.set_sensitive(False) self.linkButton.set_label(_("(Unsaved)")) self.jsonLinkButton.set_sensitive(False) self.jsonLinkButton.set_label(_("(Unsaved)")) else: set_linkbutton(self.linkButton, self.currentItem.path) set_linkbutton(self.jsonLinkButton, self.currentItem.get_json_path(), True) def save(self): self.currentItem.code = self.buffer.get_text(self.buffer.get_start_iter(), self.buffer.get_end_iter(), False) self.currentItem.prompt = self.promptCheckbox.get_active() self.currentItem.show_in_tray_menu = self.showInTrayCheckbox.get_active() self.settingsWidget.save() self.currentItem.persist() set_linkbutton(self.linkButton, self.currentItem.path) return False def set_item_title(self, newTitle): self.currentItem.description = newTitle def rebuild_item_path(self): self.currentItem.rebuild_path() def is_new_item(self): return self.currentItem.path is None def reset(self): self.load(self.currentItem) self.parentWindow.set_undo_available(False) self.parentWindow.set_redo_available(False) def validate(self): errors = [] # Check script code text = self.buffer.get_text(self.buffer.get_start_iter(), self.buffer.get_end_iter(), False) if dialogs.EMPTY_FIELD_REGEX.match(text): errors.append(_("The script code can't be empty")) # Check settings errors += self.settingsWidget.validate() if errors: msg = PROBLEM_MSG_SECONDARY % '\n'.join(errors) dlg = Gtk.MessageDialog( self.parentWindow.ui, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, PROBLEM_MSG_PRIMARY ) dlg.format_secondary_text(msg) dlg.run() dlg.destroy() return len(errors) == 0 def record_keystrokes(self, isActive): if isActive: self.recorder = autokey.iomediator.keygrabber.Recorder(self) dlg = dialogs.RecordDialog(self.parentWindow.ui, self.on_rec_response) dlg.run() else: self.recorder.stop() def on_rec_response(self, response, recKb, recMouse, delay): if response == Gtk.ResponseType.OK: self.recorder.set_record_keyboard(recKb) self.recorder.set_record_mouse(recMouse) self.recorder.start(delay) elif response == Gtk.ResponseType.CANCEL: self.parentWindow.record_stopped() def cancel_record(self): self.recorder.stop() def start_record(self): self.buffer.insert(self.buffer.get_end_iter(), "\n") def start_key_sequence(self): self.buffer.insert(self.buffer.get_end_iter(), "keyboard.send_keys(\"") def end_key_sequence(self): self.buffer.insert(self.buffer.get_end_iter(), "\")\n") def append_key(self, key): #line, pos = self.buffer.getCursorPosition() self.buffer.insert(self.buffer.get_end_iter(), key) #self.scriptCodeEditor.setCursorPosition(line, pos + len(key)) def append_hotkey(self, key, modifiers): #line, pos = self.scriptCodeEditor.getCursorPosition() keyString = self.currentItem.get_hotkey_string(key, modifiers) self.buffer.insert(self.buffer.get_end_iter(), keyString) #self.scriptCodeEditor.setCursorPosition(line, pos + len(keyString)) def append_mouseclick(self, xCoord, yCoord, button, windowTitle): self.buffer.insert(self.buffer.get_end_iter(), "mouse.click_relative(%d, %d, %d) # %s\n" % (xCoord, yCoord, int(button), windowTitle)) def undo(self): self.buffer.undo() self.parentWindow.set_undo_available(self.buffer.can_undo()) self.parentWindow.set_redo_available(self.buffer.can_redo()) def redo(self): self.buffer.redo() self.parentWindow.set_undo_available(self.buffer.can_undo()) self.parentWindow.set_redo_available(self.buffer.can_redo()) def on_modified(self, widget, data=None): self.set_dirty() self.parentWindow.set_undo_available(self.buffer.can_undo()) self.parentWindow.set_redo_available(self.buffer.can_redo()) def set_dirty(self): self.parentWindow.set_dirty(True) class PhrasePage(ScriptPage): def __init__(self, parentWindow): self.parentWindow = parentWindow builder = get_ui("phrasepage.xml") self.ui = builder.get_object("phrasepage") builder.connect_signals(self) self.buffer = GtkSource.Buffer() self.buffer.connect("changed", self.on_modified) self.editor = GtkSource.View.new_with_buffer(self.buffer) scrolledWindow = builder.get_object("scrolledWindow") scrolledWindow.add(self.editor) self.promptCheckbox = builder.get_object("promptCheckbox") self.showInTrayCheckbox = builder.get_object("showInTrayCheckbox") self.sendModeCombo = Gtk.ComboBoxText.new() self.sendModeCombo.connect("changed", self.on_modified) sendModeHbox = builder.get_object("sendModeHbox") sendModeHbox.pack_start(self.sendModeCombo, False, False, 0) self.linkButton = builder.get_object("linkButton") self.jsonLinkButton = builder.get_object("jsonLinkButton") vbox = builder.get_object("settingsVbox") self.settingsWidget = SettingsWidget(parentWindow) vbox.pack_start(self.settingsWidget.ui, False, False, 0) # Populate combo l = list(autokey.model.phrase.SEND_MODES.keys()) l.sort() for val in l: self.sendModeCombo.append_text(val) # Configure script editor #self.__m = GtkSource.LanguageManager() self.__sm = GtkSource.StyleSchemeManager() self.buffer.set_language(None) self.buffer.set_style_scheme(self.__sm.get_scheme("kate")) self.buffer.set_highlight_matching_brackets(False) self.editor.set_auto_indent(False) self.editor.set_smart_home_end(False) self.editor.set_insert_spaces_instead_of_tabs(True) self.editor.set_tab_width(4) self.ui.show_all() def insert_text(self, text): self.buffer.insert_at_cursor(text) # self.buffer.insert_at_cursor(text.encode("utf-8")) def load(self, thePhrase): self.currentItem = thePhrase self.buffer.begin_not_undoable_action() self.buffer.set_text(thePhrase.phrase) # self.buffer.set_text(thePhrase.phrase.encode("utf-8")) self.buffer.end_not_undoable_action() self.buffer.place_cursor(self.buffer.get_start_iter()) self.promptCheckbox.set_active(thePhrase.prompt) self.showInTrayCheckbox.set_active(thePhrase.show_in_tray_menu) self.settingsWidget.load(thePhrase) if self.is_new_item(): self.linkButton.set_sensitive(False) self.linkButton.set_label(_("(Unsaved)")) self.jsonLinkButton.set_sensitive(False) self.jsonLinkButton.set_label(_("(Unsaved)")) else: set_linkbutton(self.linkButton, self.currentItem.path) set_linkbutton(self.jsonLinkButton, self.currentItem.get_json_path(), True) l = list(autokey.model.phrase.SEND_MODES.keys()) l.sort() for k, v in autokey.model.phrase.SEND_MODES.items(): if v == thePhrase.sendMode: self.sendModeCombo.set_active(l.index(k)) break def save(self): self.currentItem.phrase = self.buffer.get_text(self.buffer.get_start_iter(), self.buffer.get_end_iter(), False)#.decode("utf-8") self.currentItem.prompt = self.promptCheckbox.get_active() self.currentItem.show_in_tray_menu = self.showInTrayCheckbox.get_active() self.currentItem.sendMode = autokey.model.phrase.SEND_MODES[self.sendModeCombo.get_active_text()] self.settingsWidget.save() self.currentItem.persist() set_linkbutton(self.linkButton, self.currentItem.path) return False def validate(self): errors = [] # Check phrase content text = self.buffer.get_text(self.buffer.get_start_iter(), self.buffer.get_end_iter(), False)#.decode("utf-8") if dialogs.EMPTY_FIELD_REGEX.match(text): errors.append(_("The phrase content can't be empty")) # Check settings errors += self.settingsWidget.validate() if errors: msg = PROBLEM_MSG_SECONDARY % '\n'.join(errors) dlg = Gtk.MessageDialog(self.parentWindow.ui, Gtk.DialogFlags.MODAL|Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.ERROR, Gtk.ButtonsType.OK, PROBLEM_MSG_PRIMARY) dlg.format_secondary_text(msg) dlg.run() dlg.destroy() return len(errors) == 0 def record_keystrokes(self, isActive): if isActive: msg = _("AutoKey will now take exclusive use of the keyboard.\n\nClick the mouse anywhere to release the keyboard when you are done.") dlg = Gtk.MessageDialog(self.parentWindow.ui, Gtk.DialogFlags.MODAL, Gtk.MessageType.INFO, Gtk.ButtonsType.OK, msg) dlg.set_title(_("Record Keystrokes")) dlg.run() dlg.destroy() self.editor.set_sensitive(False) self.recorder = autokey.iomediator.keygrabber.Recorder(self) self.recorder.set_record_keyboard(True) self.recorder.set_record_mouse(True) self.recorder.start_withgrab() else: self.recorder.stop() self.editor.set_sensitive(True) def start_record(self): pass def start_key_sequence(self): pass def end_key_sequence(self): pass def append_key(self, key): #line, pos = self.buffer.getCursorPosition() self.buffer.insert(self.buffer.get_end_iter(), key) #self.scriptCodeEditor.setCursorPosition(line, pos + len(key)) def append_hotkey(self, key, modifiers): #line, pos = self.scriptCodeEditor.getCursorPosition() keyString = self.currentItem.get_hotkey_string(key, modifiers) self.buffer.insert(self.buffer.get_end_iter(), keyString) #self.scriptCodeEditor.setCursorPosition(line, pos + len(keyString)) def append_mouseclick(self, xCoord, yCoord, button, windowTitle): self.cancel_record() self.parentWindow.record_stopped() def cancel_record(self): self.recorder.stop_withgrab() self.editor.set_sensitive(True) class ConfigWindow: def __init__(self, app): self.app = app self.cutCopiedItems = [] self.__warnedOfChanges = False builder = get_ui("mainwindow.xml") self.ui = builder.get_object("mainwindow") self.ui.set_title(CONFIG_WINDOW_TITLE) # Menus and Actions self.uiManager = Gtk.UIManager() self.add_accel_group(self.uiManager.get_accel_group()) # Menu Bar actionGroup = Gtk.ActionGroup("menu") actions = [ ("File", None, _("_File")), ("create", None, _("New")), ("new-top-folder", "folder-new", _("_Folder"), "", _("Create a new top-level folder"), self.on_new_topfolder), ("new-folder", "folder-new", _("Subf_older"), "", _("Create a new folder in the current folder"), self.on_new_folder), ("new-phrase", "text-x-generic", _("_Phrase"), "n", _("Create a new phrase in the current folder"), self.on_new_phrase), ("new-script", "text-x-python", _("Scrip_t"), "n", _("Create a new script in the current folder"), self.on_new_script), ("save", Gtk.STOCK_SAVE, _("_Save"), None, _("Save changes to current item"), self.on_save), ("revert", Gtk.STOCK_REVERT_TO_SAVED, _("_Revert"), None, _("Drop all unsaved changes to current item"), self.on_revert), ("close-window", Gtk.STOCK_CLOSE, _("_Close window"), None, _("Close the configuration window"), self.on_close), ("quit", Gtk.STOCK_QUIT, _("_Quit"), None, _("Completely exit AutoKey"), self.on_quit), ("Edit", None, _("_Edit")), ("cut-item", Gtk.STOCK_CUT, _("Cu_t Item"), "", _("Cut the selected item"), self.on_cut_item), ("copy-item", Gtk.STOCK_COPY, _("_Copy Item"), "", _("Copy the selected item"), self.on_copy_item), ("paste-item", Gtk.STOCK_PASTE, _("_Paste Item"), "", _("Paste the last cut/copied item"), self.on_paste_item), ("clone-item", Gtk.STOCK_COPY, _("C_lone Item"), "c", _("Clone the selected item"), self.on_clone_item), ("delete-item", Gtk.STOCK_DELETE, _("_Delete Item"), "d", _("Delete the selected item"), self.on_delete_item), ("rename", None, _("_Rename"), "F2", _("Rename the selected item"), self.on_rename), ("undo", Gtk.STOCK_UNDO, _("_Undo"), "z", _("Undo the last edit"), self.on_undo), ("redo", Gtk.STOCK_REDO, _("_Redo"), "z", _("Redo the last undone edit"), self.on_redo), ("insert-macro", None, _("_Insert Macro"), None, _("Insert a phrase macro"), None), ("preferences", Gtk.STOCK_PREFERENCES, _("_Preferences"), "", _("Additional options"), self.on_advanced_settings), ("Tools", None, _("_Tools")), ("script-error", Gtk.STOCK_DIALOG_ERROR, _("Vie_w script error"), None, _("View script error information"), self.on_show_error), ("run", Gtk.STOCK_MEDIA_PLAY, _("_Run current script"), None, _("Run the currently selected script"), self.on_run_script), ("Help", None, _("_Help")), ("faq", None, _("_F.A.Q."), None, _("Display Frequently Asked Questions"), self.on_show_faq), ("help", Gtk.STOCK_HELP, _("Online _Help"), None, _("Display Online Help"), self.on_show_help), ("api", None, _("_Scripting Help"), None, _("Display Scripting API"), self.on_show_api), ("report-bug", None, _("Report a Bug"), "", _("Report a Bug"), self.on_report_bug), ("about", Gtk.STOCK_ABOUT, _("About AutoKey"), None, _("Show program information"), self.on_show_about), ] actionGroup.add_actions(actions) toggleActions = [ ("toolbar", None, _("_Show Toolbar"), None, _("Show/hide the toolbar"), self.on_toggle_toolbar), ("record", Gtk.STOCK_MEDIA_RECORD, _("R_ecord keyboard/mouse"), None, _("Record keyboard/mouse actions"), self.on_record_keystrokes), ] actionGroup.add_toggle_actions(toggleActions) self.uiManager.insert_action_group(actionGroup, 0) self.uiManager.add_ui_from_file(UI_DESCRIPTION_FILE) self.vbox = builder.get_object("vbox") self.vbox.pack_end(self.uiManager.get_widget("/MenuBar"), False, False, 0) # Macro menu menu = self.app.service.phraseRunner.macroManager.get_menu(self.on_insert_macro) self.uiManager.get_widget("/MenuBar/Edit/insert-macro").set_submenu(menu) # Toolbar 'create' button create = Gtk.MenuToolButton.new_from_stock(Gtk.STOCK_NEW) create.show() create.set_is_important(True) create.connect("clicked", self.on_new_clicked) menu = self.uiManager.get_widget('/NewDropdown') create.set_menu(menu) toolbar = self.uiManager.get_widget('/Toolbar') toolbar.insert(create, 0) self.uiManager.get_action("/MenuBar/Tools/toolbar").set_active(cm.ConfigManager.SETTINGS[cm_constants.SHOW_TOOLBAR]) toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_PRIMARY_TOOLBAR) self.expanded_rows = cm.ConfigManager.SETTINGS[cm_constants.GTK_TREE_VIEW_EXPANDED_ROWS] self.last_open = cm.ConfigManager.SETTINGS[cm_constants.PATH_LAST_OPEN] self.treeView = builder.get_object("treeWidget") self.__initTreeWidget() self.stack = builder.get_object("stack") self.__initStack() self.hpaned = builder.get_object("hpaned") self.uiManager.get_widget("/Toolbar/save").set_is_important(True) self.uiManager.get_widget("/Toolbar/undo").set_is_important(True) builder.connect_signals(self) rootIter = self.treeView.get_model().get_iter_first() if rootIter is not None: self.treeView.get_selection().select_path(self.last_open) self.on_tree_selection_changed(self.treeView) self.treeView.columns_autosize() width, height = cm.ConfigManager.SETTINGS[cm_constants.WINDOW_DEFAULT_SIZE] self.set_default_size(width, height) self.hpaned.set_position(cm.ConfigManager.SETTINGS[cm_constants.HPANE_POSITION]) def __addToolbar(self): toolbar = self.uiManager.get_widget('/Toolbar') self.vbox.pack_end(toolbar, False, False, 0) self.vbox.reorder_child(toolbar, 1) def record_stopped(self): self.uiManager.get_widget("/MenuBar/Tools/record").set_active(False) def cancel_record(self): if self.uiManager.get_widget("/MenuBar/Tools/record").get_active(): self.record_stopped() self.__getCurrentPage().cancel_record() def save_completed(self, persistGlobal): self.uiManager.get_action("/MenuBar/File/save").set_sensitive(False) self.app.config_altered(persistGlobal) def set_dirty(self, dirty): self.dirty = dirty self.uiManager.get_action("/MenuBar/File/save").set_sensitive(dirty) self.uiManager.get_action("/MenuBar/File/revert").set_sensitive(dirty) def config_modified(self): logger.info("Modifications detected to open files. Reloading...") #save tree view selection selection = self.treeView.get_selection() selection.get_selected_rows() self.rebuild_tree() #get selection for new treeview selection = self.treeView.get_selection() path = Gtk.TreePath() for row in self.expanded_rows: self.treeView.expand_to_path(path.new_from_string(row)) selection.select_path(path.new_from_string(self.last_open)) self.on_tree_selection_changed(self.treeView) def update_actions(self, items, changed): if len(items) == 0: canCreate = False canCopy = False canRecord = False canMacro = False canPlay = False enableAny = False hasError = False else: canCreate = isinstance(items[0], autokey.model.folder.Folder) and len(items) == 1 canCopy = True canRecord = (not isinstance(items[0], autokey.model.folder.Folder)) and len(items) == 1 canMacro = isinstance(items[0], autokey.model.phrase.Phrase) and len(items) == 1 canPlay = isinstance(items[0], autokey.model.script.Script) and len(items) == 1 enableAny = True hasError = self.app.service.scriptRunner.error_records for item in items: if isinstance(item, autokey.model.folder.Folder): canCopy = False break self.uiManager.get_action("/MenuBar/Edit/copy-item").set_sensitive(canCopy) self.uiManager.get_action("/MenuBar/Edit/cut-item").set_sensitive(enableAny) self.uiManager.get_action("/MenuBar/Edit/clone-item").set_sensitive(canCopy) self.uiManager.get_action("/MenuBar/Edit/paste-item").set_sensitive(len(self.cutCopiedItems) > 0) self.uiManager.get_action("/MenuBar/Edit/delete-item").set_sensitive(enableAny) self.uiManager.get_action("/MenuBar/Edit/rename").set_sensitive(enableAny) self.uiManager.get_action("/MenuBar/Edit/insert-macro").set_sensitive(canMacro) self.uiManager.get_action("/MenuBar/Tools/record").set_sensitive(canRecord) self.uiManager.get_action("/MenuBar/Tools/run").set_sensitive(canPlay) self.uiManager.get_action("/MenuBar/Tools/script-error").set_sensitive(hasError) if changed: self.uiManager.get_action("/MenuBar/File/save").set_sensitive(False) self.uiManager.get_action("/MenuBar/Edit/undo").set_sensitive(False) self.uiManager.get_action("/MenuBar/Edit/redo").set_sensitive(False) def set_has_errors(self, state): self.uiManager.get_action("/MenuBar/Tools/script-error").set_sensitive(state) def set_undo_available(self, state): self.uiManager.get_action("/MenuBar/Edit/undo").set_sensitive(state) def set_redo_available(self, state): self.uiManager.get_action("/MenuBar/Edit/redo").set_sensitive(state) def rebuild_tree(self): self.treeView.set_model(AkTreeModel(self.app.configManager.folders)) def refresh_tree(self): model, selectedPaths = self.treeView.get_selection().get_selected_rows() for path in selectedPaths: model.update_item(model[path].iter, self.__getTreeSelection()) # ---- Signal handlers ---- def on_new_clicked(self, widget, data=None): widget.get_menu().popup(None, None, None, None, 1, Gtk.get_current_event_time()) def on_save(self, widget, data=None): if self.__getCurrentPage().validate(): self.app.monitor.suspend() persistGlobal = self.__getCurrentPage().save() self.save_completed(persistGlobal) self.set_dirty(False) self.refresh_tree() self.app.monitor.unsuspend() return False return True def on_revert(self, widget, data=None): self.__getCurrentPage().reset() self.set_dirty(False) self.cancel_record() def queryClose(self): if self.dirty: return self.promptToSave() return False def on_close(self, widget, data=None): cm.ConfigManager.SETTINGS[cm_constants.WINDOW_DEFAULT_SIZE] = self.get_size() cm.ConfigManager.SETTINGS[cm_constants.HPANE_POSITION] = self.hpaned.get_position() cm.ConfigManager.SETTINGS[cm_constants.GTK_TREE_VIEW_EXPANDED_ROWS] = self.expanded_rows cm.ConfigManager.SETTINGS[cm_constants.PATH_LAST_OPEN] = self.last_open self.cancel_record() if self.queryClose(): return True else: self.hide() self.destroy() self.app.configWindow = None self.app.config_altered(True) def on_quit(self, widget, data=None): #if not self.queryClose(): cm.ConfigManager.SETTINGS[cm_constants.WINDOW_DEFAULT_SIZE] = self.get_size() cm.ConfigManager.SETTINGS[cm_constants.HPANE_POSITION] = self.hpaned.get_position() cm.ConfigManager.SETTINGS[cm_constants.GTK_TREE_VIEW_EXPANDED_ROWS] = self.expanded_rows cm.ConfigManager.SETTINGS[cm_constants.PATH_LAST_OPEN] = self.last_open self.app.shutdown() # File Menu def on_new_topfolder(self, widget, data=None): dlg = Gtk.FileChooserDialog(_("Create New Folder"), self.ui) dlg.set_action(Gtk.FileChooserAction.CREATE_FOLDER) dlg.set_local_only(True) dlg.add_buttons(_("Use Default"), Gtk.ResponseType.NONE, Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, Gtk.STOCK_OK, Gtk.ResponseType.OK) response = dlg.run() if response == Gtk.ResponseType.OK: path = dlg.get_filename() self.__createFolder(os.path.basename(path), None, path) self.app.monitor.add_watch(path) dlg.destroy() self.app.config_altered(True) elif response == Gtk.ResponseType.NONE: dlg.destroy() name = self.__getNewItemName("Folder") self.__createFolder(name, None) self.app.config_altered(True) else: dlg.destroy() def __getRealParent(self, parentIter): theModel = self.treeView.get_model() parentModelItem = theModel.get_value(parentIter, AkTreeModel.OBJECT_COLUMN) if not isinstance(parentModelItem, autokey.model.folder.Folder): return theModel.iter_parent(parentIter) return parentIter def on_new_folder(self, widget, data=None): name = self.__getNewItemName("Folder") if name is not None: theModel, selectedPaths = self.treeView.get_selection().get_selected_rows() parentIter = self.__getRealParent(theModel[selectedPaths[0]].iter) self.__createFolder(name, parentIter) self.app.config_altered(False) def __createFolder(self, title, parentIter, path=None): self.app.monitor.suspend() theModel = self.treeView.get_model() newFolder = autokey.model.folder.Folder(title, path=path) newIter = theModel.append_item(newFolder, parentIter) newFolder.persist() self.app.monitor.unsuspend() self.treeView.expand_to_path(theModel.get_path(newIter)) self.treeView.get_selection().unselect_all() self.treeView.get_selection().select_iter(newIter) self.on_tree_selection_changed(self.treeView) def __getNewItemName(self, itemType): dlg = RenameDialog(self.ui, "New %s" % itemType, True, _("Create New %s") % itemType) dlg.set_image(Gtk.STOCK_NEW) if dlg.run() == 1: newText = dlg.get_name() if dialogs.validate(not dialogs.EMPTY_FIELD_REGEX.match(newText), _("The name can't be empty"), None, self.ui): dlg.destroy() return newText else: dlg.destroy() return None dlg.destroy() return None def on_new_phrase(self, widget, data=None): name = self.__getNewItemName("Phrase") if name is not None: self.app.monitor.suspend() theModel, selectedPaths = self.treeView.get_selection().get_selected_rows() parentIter = self.__getRealParent(theModel[selectedPaths[0]].iter) newPhrase = autokey.model.phrase.Phrase(name, "Enter phrase contents") newIter = theModel.append_item(newPhrase, parentIter) newPhrase.persist() self.app.monitor.unsuspend() self.treeView.expand_to_path(theModel.get_path(newIter)) self.treeView.get_selection().unselect_all() self.treeView.get_selection().select_iter(newIter) self.on_tree_selection_changed(self.treeView) #self.on_rename(self.treeView) def on_new_script(self, widget, data=None): name = self.__getNewItemName("Script") if name is not None: self.app.monitor.suspend() theModel, selectedPaths = self.treeView.get_selection().get_selected_rows() parentIter = self.__getRealParent(theModel[selectedPaths[0]].iter) newScript = autokey.model.script.Script(name, "# Enter script code") newIter = theModel.append_item(newScript, parentIter) newScript.persist() self.app.monitor.unsuspend() self.treeView.expand_to_path(theModel.get_path(newIter)) self.treeView.get_selection().unselect_all() self.treeView.get_selection().select_iter(newIter) self.on_tree_selection_changed(self.treeView) # self.on_rename(self.treeView) # Edit Menu def on_cut_item(self, widget, data=None): self.cutCopiedItems = self.__getTreeSelection() selection = self.treeView.get_selection() model, selectedPaths = selection.get_selected_rows() refs = [] for path in selectedPaths: refs.append(Gtk.TreeRowReference(model, path)) for ref in refs: if ref.valid(): self.__removeItem(model, model[ref.get_path()].iter) if len(selectedPaths) > 1: self.treeView.get_selection().unselect_all() self.treeView.get_selection().select_iter(model.get_iter_first()) self.on_tree_selection_changed(self.treeView) self.app.config_altered(True) def on_copy_item(self, widget, data=None): sourceObjects = self.__getTreeSelection() for source in sourceObjects: if isinstance(source, autokey.model.phrase.Phrase): newObj = autokey.model.phrase.Phrase('', '') else: newObj = autokey.model.script.Script('', '') newObj.copy(source) self.cutCopiedItems.append(newObj) def on_paste_item(self, widget, data=None): theModel, selectedPaths = self.treeView.get_selection().get_selected_rows() parentIter = self.__getRealParent(theModel[selectedPaths[0]].iter) self.app.monitor.suspend() newIters = [] for item in self.cutCopiedItems: newIter = theModel.append_item(item, parentIter) if isinstance(item, autokey.model.folder.Folder): theModel.populate_store(newIter, item) newIters.append(newIter) item.path = None item.persist() self.app.monitor.unsuspend() self.treeView.expand_to_path(theModel.get_path(newIters[-1])) self.treeView.get_selection().unselect_all() self.treeView.get_selection().select_iter(newIters[0]) self.cutCopiedItems = [] self.on_tree_selection_changed(self.treeView) for iterator in newIters: self.treeView.get_selection().select_iter(iterator) self.app.config_altered(True) def on_clone_item(self, widget, data=None): source = self.__getTreeSelection()[0] theModel, selectedPaths = self.treeView.get_selection().get_selected_rows() sourceIter = theModel[selectedPaths[0]].iter parentIter = theModel.iter_parent(sourceIter) self.app.monitor.suspend() if isinstance(source, autokey.model.phrase.Phrase): newObj = autokey.model.phrase.Phrase('', '') else: newObj = autokey.model.script.Script('', '') newObj.copy(source) newObj.persist() self.app.monitor.unsuspend() newIter = theModel.append_item(newObj, parentIter) self.app.config_altered(False) def on_delete_item(self, widget, data=None): selection = self.treeView.get_selection() theModel, selectedPaths = selection.get_selected_rows() refs = [] for path in selectedPaths: refs.append(Gtk.TreeRowReference.new(theModel, path)) modified = False if len(refs) == 1: item = theModel[refs[0].get_path()].iter modelItem = theModel.get_value(item, AkTreeModel.OBJECT_COLUMN) if isinstance(modelItem, autokey.model.folder.Folder): msg = _("Are you sure you want to delete the %s and all the items in it?") % str(modelItem) else: msg = _("Are you sure you want to delete the %s?") % str(modelItem) else: msg = _("Are you sure you want to delete the %d selected items?") % len(refs) dlg = Gtk.MessageDialog(self.ui, Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, msg) dlg.set_title(_("Delete")) if dlg.run() == Gtk.ResponseType.YES: self.app.monitor.suspend() for ref in refs: if ref.valid(): item = theModel[ref.get_path()].iter modelItem = theModel.get_value(item, AkTreeModel.OBJECT_COLUMN) self.__removeItem(theModel, item) modified = True self.app.monitor.unsuspend() dlg.destroy() if modified: if len(selectedPaths) > 1: self.treeView.get_selection().unselect_all() self.treeView.get_selection().select_iter(theModel.get_iter_first()) self.on_tree_selection_changed(self.treeView) self.app.config_altered(True) def __removeItem(self, model, item): #selection = self.treeView.get_selection() #model, selectedPaths = selection.get_selected_rows() #parentIter = model.iter_parent(model[selectedPaths[0]].iter) parentIter = model.iter_parent(item) nextIter = model.iter_next(item) data = model.get_value(item, AkTreeModel.OBJECT_COLUMN) self.__deleteHotkeys(data) model.remove_item(item) if nextIter is not None: self.treeView.get_selection().select_iter(nextIter) elif parentIter is not None: self.treeView.get_selection().select_iter(parentIter) elif model.iter_n_children(None) > 0: selectIter = model.iter_nth_child(None, model.iter_n_children(None) - 1) self.treeView.get_selection().select_iter(selectIter) self.on_tree_selection_changed(self.treeView) def __deleteHotkeys(self, theItem): self.app.configManager.delete_hotkeys(theItem) def on_undo(self, widget, data=None): self.__getCurrentPage().undo() def on_redo(self, widget, data=None): self.__getCurrentPage().redo() def on_insert_macro(self, widget, macro): token = macro.get_token() self.phrasePage.insert_text(token) def on_advanced_settings(self, widget, data=None): s = SettingsDialog(self.ui, self.app.configManager) s.show() def on_record_keystrokes(self, widget, data=None): self.__getCurrentPage().record_keystrokes(widget.get_active()) # Tools Menu def on_toggle_toolbar(self, widget, data=None): if widget.get_active(): self.__addToolbar() else: self.vbox.remove(self.uiManager.get_widget('/Toolbar')) cm.ConfigManager.SETTINGS[cm_constants.SHOW_TOOLBAR] = widget.get_active() def on_show_error(self, widget, data=None): self.app.show_script_error(self.ui) def on_run_script(self, widget, data=None): t = threading.Thread(target=self.__runScript) t.start() def __runScript(self): script = self.__getTreeSelection()[0] time.sleep(2) self.app.service.scriptRunner.execute_script(script) # Help Menu def on_show_faq(self, widget, data=None): webbrowser.open(common.FAQ_URL, False, True) def on_show_help(self, widget, data=None): webbrowser.open(common.HELP_URL, False, True) def on_show_api(self, widget, data=None): webbrowser.open(common.API_URL, False, True) def on_report_bug(self, widget, data=None): webbrowser.open(common.BUG_URL, False, True) def on_show_about(self, widget, data=None): dlg = Gtk.AboutDialog() dlg.set_name("AutoKey") dlg.set_comments(_("A desktop automation utility for Linux and X11.")) dlg.set_version(common.VERSION) p = Gtk.IconTheme.get_default().load_icon(common.ICON_FILE, 100, 0) dlg.set_logo(p) dlg.set_website(common.HOMEPAGE) dlg.set_authors(["GuoCi (Python 3 port maintainer) ", "Chris Dekter (Developer) ", "Sam Peterson (Original developer) "]) dlg.set_transient_for(self.ui) dlg.run() dlg.destroy() # Tree widget def on_rename(self, widget, data=None): selection = self.treeView.get_selection() theModel, selectedPaths = selection.get_selected_rows() selection.unselect_all() self.treeView.set_cursor(selectedPaths[0], self.treeView.get_column(0), False) selectedObject = self.__getTreeSelection()[0] if isinstance(selectedObject, autokey.model.folder.Folder): oldName = selectedObject.title else: oldName = selectedObject.description dlg = RenameDialog(self.ui, oldName, False) dlg.set_image(Gtk.STOCK_EDIT) if dlg.run() == 1: newText = dlg.get_name() if dialogs.validate(not dialogs.EMPTY_FIELD_REGEX.match(newText), _("The name can't be empty"), None, self.ui): self.__getCurrentPage().set_item_title(newText) self.app.monitor.suspend() if dlg.get_update_fs(): self.__getCurrentPage().rebuild_item_path() persistGlobal = self.__getCurrentPage().save() self.refresh_tree() self.app.monitor.unsuspend() self.app.config_altered(persistGlobal) dlg.destroy() def on_treeWidget_row_activated(self, widget, path, viewColumn, data=None): """This function is called when a row is double clicked""" if widget.row_expanded(path): widget.collapse_row(path) else: widget.expand_row(path, False) def on_treeWidget_row_collapsed(self, widget, tIter, path, data=None): widget.columns_autosize() p = path.to_string() if len(p) == 1: #closing one of the base dirs self.__hide_row_with_path_up_to(p, 1) return self.__hide_row_with_path_up_to(p, len(p)) def __hide_row_with_path_up_to(self, pathStr, up_to): to_remove = [] for row in self.expanded_rows: if row[:up_to] == pathStr: # print("Removing ", row) to_remove.append(row) # print(self.expanded_rows) for row in to_remove: self.expanded_rows.remove(row) def on_treeWidget_row_expanded(self, widget, tIter, path, data=None): pathS = path.to_string() for row in self.expanded_rows: if row == pathS: #don't add already existing return self.expanded_rows.append(pathS) def on_treeview_buttonpress(self, widget, event, data=None): return self.dirty def on_treeview_buttonrelease(self, widget, event, data=None): if self.promptToSave(): # True result indicates user selected Cancel. Stop event propagation return True else: x = int(event.x) y = int(event.y) time = event.time pthinfo = widget.get_path_at_pos(x, y) if pthinfo is not None: path, col, cellx, celly = pthinfo currentPath, currentCol = widget.get_cursor() if currentPath != path: widget.set_cursor(path, col, 0) if event.button == 3: self.__popupMenu(event) return False def on_drag_begin(self, *args): selection = self.treeView.get_selection() theModel, self.__sourceRows = selection.get_selected_rows() self.__sourceObjects = self.__getTreeSelection() def on_tree_selection_changed(self, widget, data=None): selectedObjects = self.__getTreeSelection() if len(selectedObjects) == 0: self.stack.set_current_page(0) self.set_dirty(False) self.cancel_record() self.update_actions(selectedObjects, True) self.selectedObject = None elif len(selectedObjects) == 1: selectedObject = selectedObjects[0] self.last_open = self.treeView.get_selection().get_selected_rows()[1][0].to_string() if isinstance(selectedObject, autokey.model.folder.Folder): self.stack.set_current_page(1) self.folderPage.load(selectedObject) elif isinstance(selectedObject, autokey.model.phrase.Phrase): self.stack.set_current_page(2) self.phrasePage.load(selectedObject) else: self.stack.set_current_page(3) self.scriptPage.load(selectedObject) self.set_dirty(False) self.cancel_record() self.update_actions(selectedObjects, True) self.selectedObject = selectedObject else: self.update_actions(selectedObjects, False) def on_drag_data_received(self, treeview, context, x, y, selection, info, etime): selection = self.treeView.get_selection() theModel, sourcePaths = selection.get_selected_rows() drop_info = treeview.get_dest_row_at_pos(x, y) if drop_info: path, position = drop_info targetIter = theModel.get_iter(path) else: targetIter = None #targetModelItem = theModel.get_value(targetIter, AkTreeModel.OBJECT_COLUMN) self.app.monitor.suspend() for path in self.__sourceRows: self.__removeItem(theModel, theModel[path].iter) newIters = [] for item in self.__sourceObjects: newIter = theModel.append_item(item, targetIter) if isinstance(item, autokey.model.folder.Folder): theModel.populate_store(newIter, item) self.__dropRecurseUpdate(item) else: item.path = None item.persist() newIters.append(newIter) self.app.monitor.unsuspend() self.treeView.expand_to_path(theModel.get_path(newIters[-1])) selection.unselect_all() for iterator in newIters: selection.select_iter(iterator) self.on_tree_selection_changed(self.treeView) self.app.config_altered(True) def __dropRecurseUpdate(self, folder): folder.path = None folder.persist() for subfolder in folder.folders: self.__dropRecurseUpdate(subfolder) for child in folder.items: child.path = None child.persist() def on_drag_drop(self, widget, drag_context, x, y, timestamp): drop_info = widget.get_dest_row_at_pos(x, y) if drop_info: selection = widget.get_selection() theModel, sourcePaths = selection.get_selected_rows() path, position = drop_info if position not in (Gtk.TreeViewDropPosition.INTO_OR_BEFORE, Gtk.TreeViewDropPosition.INTO_OR_AFTER): return True targetIter = theModel.get_iter(path) targetModelItem = theModel.get_value(targetIter, AkTreeModel.OBJECT_COLUMN) if isinstance(targetModelItem, autokey.model.folder.Folder): # prevent dropping a folder onto itself return path in self.__sourceRows elif targetModelItem is None: # Target is top level for item in self.__sourceObjects: if not isinstance(item, autokey.model.folder.Folder): # drop not permitted for top level because not folder return True # prevent dropping a folder onto itself return path in self.__sourceRows else: # target is top level with no drop info for item in self.__sourceObjects: if not isinstance(item, autokey.model.folder.Folder): # drop not permitted for no drop info because not folder return True # drop permitted for no drop info which is a folder return False # drop not permitted return True def __initTreeWidget(self): self.treeView.set_model(AkTreeModel(self.app.configManager.folders)) self.treeView.set_headers_visible(True) self.treeView.set_reorderable(False) self.treeView.set_rubber_banding(True) self.treeView.set_search_column(1) self.treeView.set_enable_search(True) targets = [] self.treeView.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, targets, Gdk.DragAction.DEFAULT|Gdk.DragAction.MOVE) self.treeView.enable_model_drag_dest(targets, Gdk.DragAction.DEFAULT) self.treeView.drag_source_add_text_targets() self.treeView.drag_dest_add_text_targets() self.treeView.get_selection().set_mode(Gtk.SelectionMode.MULTIPLE) # Treeview columns column1 = Gtk.TreeViewColumn(_("Name")) iconRenderer = Gtk.CellRendererPixbuf() textRenderer = Gtk.CellRendererText() column1.pack_start(iconRenderer, False) column1.pack_end(textRenderer, True) column1.add_attribute(iconRenderer, "icon-name", 0) column1.add_attribute(textRenderer, "text", 1) column1.set_expand(True) column1.set_min_width(150) column1.set_sort_column_id(1) self.treeView.append_column(column1) column2 = Gtk.TreeViewColumn(_("Abbr.")) textRenderer = Gtk.CellRendererText() textRenderer.set_property("editable", False) column2.pack_start(textRenderer, True) column2.add_attribute(textRenderer, "text", 2) column2.set_expand(False) column2.set_min_width(50) self.treeView.append_column(column2) column3 = Gtk.TreeViewColumn(_("Hotkey")) textRenderer = Gtk.CellRendererText() textRenderer.set_property("editable", False) column3.pack_start(textRenderer, True) column3.add_attribute(textRenderer, "text", 3) column3.set_expand(False) column3.set_min_width(100) self.treeView.append_column(column3) path = Gtk.TreePath() for row in self.expanded_rows: p = path.new_from_string(row) if not p is None: self.treeView.expand_to_path(p) def __popupMenu(self, event): menu = self.uiManager.get_widget("/Context") menu.popup(None, None, None, None, event.button, event.time) def __getattr__(self, attr): # Magic fudge to allow us to pretend to be the ui class we encapsulate return getattr(self.ui, attr) def __getTreeSelection(self): selection = self.treeView.get_selection() if selection is None: return [] model, items = selection.get_selected_rows() ret = [] if items: for item in items: value = model.get_value(model[item].iter, AkTreeModel.OBJECT_COLUMN) if value.parent not in ret: # Filter out any child objects that belong to a parent already in the list ret.append(value) return ret def __initStack(self): self.blankPage = BlankPage(self) self.folderPage = FolderPage(self) self.phrasePage = PhrasePage(self) self.scriptPage = ScriptPage(self) self.stack.append_page(self.blankPage.ui, None) self.stack.append_page(self.folderPage.ui, None) self.stack.append_page(self.phrasePage.ui, None) self.stack.append_page(self.scriptPage.ui, None) def promptToSave(self): selectedObject = self.__getTreeSelection() current = self.__getCurrentPage() result = False if self.dirty: if cm.ConfigManager.SETTINGS[cm_constants.PROMPT_TO_SAVE]: dlg = Gtk.MessageDialog( self.ui, Gtk.DialogFlags.MODAL, Gtk.MessageType.QUESTION, Gtk.ButtonsType.YES_NO, _("There are unsaved changes. Would you like to save them?") ) dlg.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) response = dlg.run() if response == Gtk.ResponseType.YES: self.on_save(None) elif response == Gtk.ResponseType.CANCEL: result = True dlg.destroy() else: result = self.on_save(None) return result def __getCurrentPage(self): #selectedObject = self.__getTreeSelection() if isinstance(self.selectedObject, autokey.model.folder.Folder): return self.folderPage elif isinstance(self.selectedObject, autokey.model.phrase.Phrase): return self.phrasePage elif isinstance(self.selectedObject, autokey.model.script.Script): return self.scriptPage else: return None class AkTreeModel(Gtk.TreeStore): OBJECT_COLUMN = 4 def __init__(self, folders): Gtk.TreeStore.__init__(self, str, str, str, str, object) for folder in folders: iterator = self.append(None, folder.get_tuple()) self.populate_store(iterator, folder) self.folders = folders self.set_sort_func(1, self.compare) self.set_sort_column_id(1, Gtk.SortType.ASCENDING) def populate_store(self, parent, parentFolder): for folder in parentFolder.folders: iterator = self.append(parent, folder.get_tuple()) self.populate_store(iterator, folder) for item in parentFolder.items: self.append(parent, item.get_tuple()) def append_item(self, item, parentIter): if parentIter is None: self.folders.append(item) item.parent = None return self.append(None, item.get_tuple()) else: parentFolder = self.get_value(parentIter, self.OBJECT_COLUMN) if isinstance(item, autokey.model.folder.Folder): parentFolder.add_folder(item) else: parentFolder.add_item(item) return self.append(parentIter, item.get_tuple()) def remove_item(self, iterator): item = self.get_value(iterator, self.OBJECT_COLUMN) item.remove_data() if item.parent is None: self.folders.remove(item) else: if isinstance(item, autokey.model.folder.Folder): item.parent.remove_folder(item) else: item.parent.remove_item(item) self.remove(iterator) def update_item(self, targetIter, items): for item in items: itemTuple = item.get_tuple() updateList = [] for n in range(len(itemTuple)): updateList.append(n) updateList.append(itemTuple[n]) self.set(targetIter, *updateList) def compare(self, theModel, iter1, iter2, data=None): item1 = theModel.get_value(iter1, AkTreeModel.OBJECT_COLUMN) item2 = theModel.get_value(iter2, AkTreeModel.OBJECT_COLUMN) if isinstance(item1, autokey.model.folder.Folder) and (isinstance(item2, autokey.model.phrase.Phrase) or isinstance(item2, autokey.model.script.Script)): return -1 elif isinstance(item2, autokey.model.folder.Folder) and (isinstance(item1, autokey.model.phrase.Phrase) or isinstance(item1, autokey.model.script.Script)): return 1 else: # return cmp(str(item1), str(item2)) a, b = str(item1), str(item2) return (a > b) - (a < b) autokey-0.96.0/lib/autokey/gtkui/data/000077500000000000000000000000001427671440700175665ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/gtkui/data/abbrsettings.xml000066400000000000000000000424341427671440700230060ustar00rootroot00000000000000 False 8 Set Abbreviations True center-on-parent dialog True False vertical True False end gtk-cancel False True True True False True False True 0 gtk-ok False True True True True True False True False True 1 False True end 0 True False 8 True False 5 True True in 180 True True False False True False True True 0 True False 5 True gtk-add False True True True False False True False True 0 gtk-remove False True True True False True False True 1 False True 1 True True 0 True False True False True False Trigger on: False True 5 0 True False True 0 1 True True True 1 False True 0 Remove typed abbreviation False True True False start False True False True 1 Omit trigger character False True True False start False True False True 2 Match phrase case to typed abbreviation False True True False start False True False True 3 Ignore case of typed abbreviation False True True False start False True False True 4 Trigger when typed as part of a word False True True False start False True False True 5 Trigger immediately (don't require a trigger character) False True True False start False True False True 6 False False 1 True True 2 cancelButton okButton All non-word Space and Enter Tab autokey-0.96.0/lib/autokey/gtkui/data/autokey.svg000066400000000000000000000322171427671440700217750ustar00rootroot00000000000000 image/svg+xml autokey-0.96.0/lib/autokey/gtkui/data/blankpage.xml000066400000000000000000000010541427671440700222340ustar00rootroot00000000000000 True 0 0 5 5 True autokey-0.96.0/lib/autokey/gtkui/data/detectdialog.xml000066400000000000000000000260621427671440700227460ustar00rootroot00000000000000 False 8 Set Window Filter False True center-on-parent dialog True False vertical 5 True False end gtk-cancel False True True True False True False False 0 gtk-ok False True True True True True False True False False 1 False True end 0 True False 15 True False 0 none True False 10 12 True False vertical 5 True False 0 label False True 0 True False 0 label False True 1 True False <b>Properties of selected window</b> True True True 0 True False 0 none True False 5 12 True False vertical Window class (entire application) False True True False False 0 True True False True 0 Window title False True True False False 0 True True classRadioButton False True 1 True False <b>Property to use</b> True True True 1 True False 2 cancelButton okButton autokey-0.96.0/lib/autokey/gtkui/data/folderpage.xml000066400000000000000000000134611427671440700224250ustar00rootroot00000000000000 True False 0 0 5 5 True False True False (Unsaved) False True True True True none True True 0 (Unsaved) False True True True True none True True 1 False False 0 True False 0 none True False 5 5 10 True False 5 Show in notification icon menu False True True False False True True True 0 True False False True 5 1 True False <b>Folder Settings</b> True True True 1 autokey-0.96.0/lib/autokey/gtkui/data/hotkeysettings.xml000066400000000000000000000273751427671440700234120ustar00rootroot00000000000000 False 8 Set Hotkey False True center-on-parent dialog True False vertical 5 True False end gtk-cancel False True True True False True False True 0 gtk-ok False True True True False True False True 1 False True end 0 True False 5 10 True False 5 True Control False True True False False False True 0 Alt False True True False False False True 1 Alt GR False True True False False False True 2 Shift False True True False False False True 3 Super False True True False False False True 4 Hyper False True True False False False True 5 Meta False True True False False False True 6 True True 0 True False True False 0 Key: %s True True 0 Press to Set False True True False False False True 1 True True 1 False False 2 cancelButton okButton autokey-0.96.0/lib/autokey/gtkui/data/mainwindow.xml000066400000000000000000000074661427671440700225010ustar00rootroot00000000000000 False AutoKey - Configuration 600 400 autokey.svg True False True True 150 True True True in True True True True True False 8 True True False False True True True True end 0 autokey-0.96.0/lib/autokey/gtkui/data/menus.xml000066400000000000000000000050671427671440700214470ustar00rootroot00000000000000 autokey-0.96.0/lib/autokey/gtkui/data/phrasepage.xml000066400000000000000000000215331427671440700224330ustar00rootroot00000000000000 True False 5 5 True False 5 True False (Unsaved) False True True True True False none True True 0 (Unsaved) False True True True True False none True True 1 False False 0 True True in True True 1 True False 0 none True False 5 10 True False Always prompt before pasting this phrase False True True False False 0 True False True 0 Show in notification icon menu False True True False False 0 True False True 1 True False 5 True False Paste using False True 0 False True 2 True False False True 10 3 True False <b>Phrase Settings</b> True False True 2 autokey-0.96.0/lib/autokey/gtkui/data/recorddialog.xml000066400000000000000000000214651427671440700227560ustar00rootroot00000000000000 1 20 5 1 10 False 8 Record Script False True center-on-parent dialog True False vertical 2 True False end gtk-cancel False True True True False True False True 0 gtk-ok False True True True True True False True False True 1 False True end 0 True False True False 0 <b>Record a keyboard/mouse script</b> True True True 10 0 Record keyboard events False True True False start False True True True True 1 Record mouse events (experimental) False True True False start False True True True 2 True False 5 True False Start recording after False True 0 True True adjustment1 False False 1 True False seconds False True 2 False True 5 3 False True 2 cancelButton okButton autokey-0.96.0/lib/autokey/gtkui/data/renamedialog.xml000066400000000000000000000155571427671440700227540ustar00rootroot00000000000000 False end 8 False True center-on-parent dialog False True False vertical 2 True False end gtk-cancel True True False False True False False 0 gtk-ok True True True True True False True False False 1 False True end 0 True False True False 10 10 gtk-missing-image False True 0 True False 5 True False 0 Enter a new name: False True 0 100 True True True False False False True 1 Update name on file system True True False False True True True 2 True True 1 True True 1 button1 button2 autokey-0.96.0/lib/autokey/gtkui/data/scriptpage.xml000066400000000000000000000167531427671440700224650ustar00rootroot00000000000000 True False 5 5 True False 5 True False (Unsaved) False True True True True False none True True 0 (Unsaved) False True True True True False none True True 1 False False 0 True True in True True 1 True False 0 none True False 5 10 True False Always prompt before executing this script False True True False False 0 True False False 0 Show in notification icon menu False True True False False 0 True False False 1 True False False True 10 2 True False <b>Script Settings</b> True False True 2 autokey-0.96.0/lib/autokey/gtkui/data/settingsdialog.xml000066400000000000000000001165671427671440700233500ustar00rootroot00000000000000 False 8 AutoKey - Preferences True center-on-parent dialog True False vertical 2 True False end gtk-cancel False True True True True False False 0 gtk-ok False True True True True True True False False 1 False True end 0 True True True False 0 0 10 5 5 5 True False 5 True False 0 none True False 12 True False Automatically start AutoKey at login False True True False True True True 0 Automatically save changes without confirmation False True True False False True True True 1 Show a notification icon False True True False False True True True 2 Disable handling of the Capslock key False True True False True True True 3 True False True False Notification icon style (requires restart): False True 5 0 True True 4 True False True False GTKSource Theme (requires restart): False True 5 0 True True 4 True False <b>Application</b> True True True 0 True False 0 none True False 12 True False Allow keyboard navigation of popup menu False True True False True True True 0 Sort menu items with most frequently used first False True True False True True True 1 Trigger menu item by first initial False True True False True True True 2 True False <b>Popup Menu</b> True True True 1 True False 0 none True False 12 Enable undo by pressing backspace False True True False True True False <b>Expansions</b> True True True 2 True False General False True False 0 0 10 5 5 5 True False 10 True False 0 none True False 5 5 12 True False 5 True False Hotkey: 0 False True 0 True False $hotkey 0 True True 1 Set False 80 True True True False True 2 gtk-clear False 80 True True True True False True 3 True False <b>Toggle monitoring using a hotkey</b> True True True 0 True False 0 none True False 5 5 12 True False 5 True False Hotkey: 0 False True 0 True False $hotkey 0 True True 1 Set False 80 True True True False True 2 gtk-clear False 80 True True True True False True 3 True False <b>Show configuration window using a hotkey</b> True True True 1 1 True False Special Hotkeys 1 False True False 10 5 5 5 True False 0 none True False 12 True False True False 5 Any Python modules placed in this folder will be available for import by scripts. 0 False True 0 True False select-folder Select A Folder False True 1 True False <b>User Module Folder</b> True 2 True False Script Engine 2 False False True 2 button1 button2 autokey-0.96.0/lib/autokey/gtkui/data/settingswidget.xml000066400000000000000000000233711427671440700233620ustar00rootroot00000000000000 True False 5 True False 3 4 5 5 True False 0 Abbreviations: GTK_FILL GTK_FILL True False 0 Hotkey: 1 2 GTK_FILL GTK_FILL True False 0 Window Filter: 2 3 GTK_FILL GTK_FILL True False 0 $abbr 1 2 GTK_FILL True False 0 $hotkey 1 2 1 2 GTK_FILL True False 0 $filter 1 2 2 3 GTK_FILL Set False 80 True True True False 2 3 GTK_FILL GTK_FILL gtk-clear False 80 True True True False True 3 4 GTK_FILL GTK_FILL Set False True True True False 2 3 1 2 GTK_FILL GTK_FILL Set False True True True False 2 3 2 3 GTK_FILL GTK_FILL gtk-clear False True True True False True 3 4 1 2 GTK_FILL GTK_FILL gtk-clear False True True True False True 3 4 2 3 GTK_FILL GTK_FILL autokey-0.96.0/lib/autokey/gtkui/data/show_script_errors_dialog.xml000066400000000000000000000441111427671440700255700ustar00rootroot00000000000000 False Show recorded errors from Scripts dialog True True False vertical 2 False end gtk-clear True True True Clear the whole error list and close this window True True True 0 gtk-delete True True True Delete the currently shown error record True True True 1 gtk-close True True True Close this window, keeping all error records. True True True True 2 False False 0 True False Shown below are the recorded errors that occurred in Scripts run by AutoKey during this session. False True 0 True False True False Currently showing error: False True 0 True False {} False True 1 True False / False True 3 True False {} False True 4 True False True False True 5 gtk-goto-first True True True Show the oldest recorded script error True False True 6 gtk-go-back True True True Show the previous recorded script error True False True 7 gtk-go-forward True True True Show the next recorded script error True False True 8 gtk-goto-last True True True Show the newest recorded script error True False True 9 False True 1 True False 8 False True 2 True False 2 2 3 2 True False Crashed Script name: 0 0 True False Script start time: 0 1 True False Error occured at: 0 2 True False Script was triggered by: 0 3 True True The name of the failed Script. 2 2 True False 1 0 True True Timestamp at which the Script execution started 2 2 True False number 1 1 True True Timestamp at which the error occured 2 2 True False number 1 2 True True 2 2 True False 1 3 False True 3 True False The Python Stacktrace: False True 4 True True True True in True True True True False True False True 5 autokey-0.96.0/lib/autokey/gtkui/data/windowfiltersettings.xml000066400000000000000000000200121427671440700246010ustar00rootroot00000000000000 False 8 Set Window Filter False True center-on-parent dialog True False vertical 5 True False end gtk-cancel False True True True False True False False 0 gtk-ok False True True True True True False True False False 1 False True end 0 True False True False 0 10 Detect Window Properties False True True True False False False 0 True False 5 True False Regular expression to match: False True 0 True True Regular expression to match the title or class of windows True 30 True False False True True 1 True True 1 Apply recursively to subfolders and items False True True False False 0 True False True 2 True False 2 cancelButton okButton autokey-0.96.0/lib/autokey/gtkui/dialogs.py000066400000000000000000000613221427671440700206550ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import locale import re import typing from gi.repository import Gtk, Gdk, Pango, Gio import autokey.model.folder import autokey.model.helpers import autokey.model.phrase import autokey.iomediator.keygrabber import autokey.iomediator.windowgrabber GETTEXT_DOMAIN = 'autokey' locale.setlocale(locale.LC_ALL, '') __all__ = ["validate", "EMPTY_FIELD_REGEX", "AbbrSettingsDialog", "HotkeySettingsDialog", "WindowFilterSettingsDialog", "RecordDialog"] from autokey import model from autokey import UI_common_functions as UI_common from autokey.model.key import Key from .shared import get_ui logger = __import__("autokey.logger").logger.get_logger(__name__) WORD_CHAR_OPTIONS = { "All non-word": autokey.model.helpers.DEFAULT_WORDCHAR_REGEX, "Space and Enter": r"[^ \n]", "Tab": r"[^\t]" } WORD_CHAR_OPTIONS_ORDERED = ["All non-word", "Space and Enter", "Tab"] EMPTY_FIELD_REGEX = re.compile(r"^ *$", re.UNICODE) def validate(expression, message, widget, parent): if not expression: dlg = Gtk.MessageDialog( parent, Gtk.DialogFlags.MODAL | Gtk.DialogFlags.DESTROY_WITH_PARENT, Gtk.MessageType.WARNING, Gtk.ButtonsType.OK, message ) dlg.run() dlg.destroy() if widget is not None: widget.grab_focus() return expression class DialogBase: def __init__(self): self.connect("close", self.on_close) self.connect("delete_event", self.on_close) def on_close(self, widget, data=None): self.hide() return True def on_cancel(self, widget, data=None): self.load(self.targetItem) self.ui.response(Gtk.ResponseType.CANCEL) self.hide() def on_ok(self, widget, data=None): if self.valid(): self.response(Gtk.ResponseType.OK) self.hide() def __getattr__(self, attr): # Magic fudge to allow us to pretend to be the ui class we encapsulate return getattr(self.ui, attr) def on_response(self, widget, responseId): self.closure(responseId) if responseId < 0: self.hide() self.emit_stop_by_name('response') class ShowScriptErrorsDialog(DialogBase): def __init__(self, app): builder = get_ui("show_script_errors_dialog.xml") self.ui = builder.get_object("show_script_errors_dialog") self.show_first_error_button = builder.get_object("first_error_button") self.show_previous_error_button = builder.get_object("previous_error_button") self.show_next_error_button = builder.get_object("next_error_button") self.show_last_error_button = builder.get_object("last_error_button") self.current_error_label = builder.get_object("current_error_label") self.total_error_count_label = builder.get_object("total_error_count_label") self.script_name_field = builder.get_object("script_name_field") self.script_start_time_field = builder.get_object("script_start_time_field") self.script_crash_time_field = builder.get_object("script_crash_time_field") self.script_triggered_by_field = builder.get_object("script_triggered_by_field") self.crash_stack_trace_field = builder.get_object("crash_stack_trace_field") # TODO: When setting the compatibility to GTK >= 3.16, remove the next three code lines and replace them with: # self.crash_stack_trace_field.set_monospace(True) settings = Gio.Settings.new("org.gnome.desktop.interface") monospace_font_description = Pango.font_description_from_string(settings.get_string("monospace-font-name")) self.crash_stack_trace_field.modify_font(monospace_font_description) builder.connect_signals(self) self.set_default_size(800, 600) self.script_runner = app.service.scriptRunner self.error_list = self.script_runner.error_records # type: typing.List[model.ScriptErrorRecord] self.currently_shown_error_index = 0 self.parent = app.configWindow if self.parent is not None: self.ui.set_transient_for(self.parent.ui) self.show_error_at_current_index() super(ShowScriptErrorsDialog, self).__init__() def show_first_error(self, button): self.currently_shown_error_index = 0 self.show_error_at_current_index() def show_previous_error(self, button): self.currently_shown_error_index -= 1 self.show_error_at_current_index() def show_next_error(self, button): self.currently_shown_error_index += 1 self.show_error_at_current_index() def show_last_error(self, button): self.currently_shown_error_index = self.get_error_count() - 1 self.show_error_at_current_index() def clear_all_errors(self, button): self.error_list.clear() self.parent.set_has_errors(False) self.on_close(button) def delete_currently_shown_error(self, button): if self.get_error_count() == 1: self.clear_all_errors(button) else: del self.error_list[self.currently_shown_error_index] if self.currently_shown_error_index == self.get_error_count(): # Go to previous error if at the end of the error list. Else shows the next error in the list. self.currently_shown_error_index -= 1 self.show_error_at_current_index() def get_error_count(self) -> int: return len(self.error_list) def _update_navigation_gui_states(self): self._update_total_error_count() self._update_navigation_button_states() def _update_total_error_count(self): current_error_str = str(self.currently_shown_error_index + 1) error_count_str = str(self.get_error_count()) self.current_error_label.set_text(current_error_str) self.total_error_count_label.set_text(error_count_str) def _update_navigation_button_states(self): self.show_first_error_button.set_sensitive(True) self.show_previous_error_button.set_sensitive(True) self.show_next_error_button.set_sensitive(True) self.show_last_error_button.set_sensitive(True) if self.currently_shown_error_index == 0: self.show_first_error_button.set_sensitive(False) self.show_previous_error_button.set_sensitive(False) if self.currently_shown_error_index == self.get_error_count() - 1: self.show_next_error_button.set_sensitive(False) self.show_last_error_button.set_sensitive(False) def show_error_at_current_index(self): self._update_navigation_gui_states() print(f"About to show error at index {self.currently_shown_error_index}") error = self.error_list[self.currently_shown_error_index] self.script_name_field.get_buffer().set_text(error.script_name) self.script_start_time_field.get_buffer().set_text(str(error.start_time)) self.script_crash_time_field.get_buffer().set_text(str(error.error_time)) self.crash_stack_trace_field.get_buffer().set_text(error.error_traceback) class AbbrSettingsDialog(DialogBase): def __init__(self, parent, configManager, closure): builder = get_ui("abbrsettings.xml") self.ui = builder.get_object("abbrsettings") builder.connect_signals(self) self.ui.set_transient_for(parent) self.configManager = configManager self.closure = closure self.abbrList = builder.get_object("abbrList") self.addButton = builder.get_object("addButton") self.removeButton = builder.get_object("removeButton") self.wordCharCombo = builder.get_object("wordCharCombo") self.removeTypedCheckbox = builder.get_object("removeTypedCheckbox") self.omitTriggerCheckbox = builder.get_object("omitTriggerCheckbox") self.matchCaseCheckbox = builder.get_object("matchCaseCheckbox") self.ignoreCaseCheckbox = builder.get_object("ignoreCaseCheckbox") self.triggerInsideCheckbox = builder.get_object("triggerInsideCheckbox") self.immediateCheckbox = builder.get_object("immediateCheckbox") DialogBase.__init__(self) # set up list view store = Gtk.ListStore(str) self.abbrList.set_model(store) column1 = Gtk.TreeViewColumn(_("Abbreviations")) textRenderer = Gtk.CellRendererText() textRenderer.set_property("editable", True) textRenderer.connect("edited", self.on_cell_modified) textRenderer.connect("editing-canceled", self.on_cell_editing_cancelled) column1.pack_end(textRenderer, True) column1.add_attribute(textRenderer, "text", 0) column1.set_sizing(Gtk.TreeViewColumnSizing.FIXED) self.abbrList.append_column(column1) for item in WORD_CHAR_OPTIONS_ORDERED: self.wordCharCombo.append_text(item) def load(self, item): self.targetItem = item self.abbrList.get_model().clear() if autokey.model.helpers.TriggerMode.ABBREVIATION in item.modes: for abbr in item.abbreviations: self.abbrList.get_model().append((abbr,)) self.removeButton.set_sensitive(True) firstIter = self.abbrList.get_model().get_iter_first() self.abbrList.get_selection().select_iter(firstIter) else: self.removeButton.set_sensitive(False) self.removeTypedCheckbox.set_active(item.backspace) self.__resetWordCharCombo() wordCharRegex = item.get_word_chars() if wordCharRegex in list(WORD_CHAR_OPTIONS.values()): # Default wordchar regex used for desc, regex in WORD_CHAR_OPTIONS.items(): if item.get_word_chars() == regex: self.wordCharCombo.set_active(WORD_CHAR_OPTIONS_ORDERED.index(desc)) break else: # Custom wordchar regex used self.wordCharCombo.append_text(autokey.model.helpers.extract_wordchars(wordCharRegex)) self.wordCharCombo.set_active(len(WORD_CHAR_OPTIONS)) if isinstance(item, autokey.model.folder.Folder): self.omitTriggerCheckbox.hide() else: self.omitTriggerCheckbox.show() self.omitTriggerCheckbox.set_active(item.omitTrigger) if isinstance(item, autokey.model.phrase.Phrase): self.matchCaseCheckbox.show() self.matchCaseCheckbox.set_active(item.matchCase) else: self.matchCaseCheckbox.hide() self.ignoreCaseCheckbox.set_active(item.ignoreCase) self.triggerInsideCheckbox.set_active(item.triggerInside) self.immediateCheckbox.set_active(item.immediate) def save(self, item): item.modes.append(autokey.model.helpers.TriggerMode.ABBREVIATION) item.clear_abbreviations() item.abbreviations = self.get_abbrs() item.backspace = self.removeTypedCheckbox.get_active() option = self.wordCharCombo.get_active_text() if option in WORD_CHAR_OPTIONS: item.set_word_chars(WORD_CHAR_OPTIONS[option]) else: item.set_word_chars(autokey.model.helpers.make_wordchar_re(option)) if not isinstance(item, autokey.model.folder.Folder): item.omitTrigger = self.omitTriggerCheckbox.get_active() if isinstance(item, autokey.model.phrase.Phrase): item.matchCase = self.matchCaseCheckbox.get_active() item.ignoreCase = self.ignoreCaseCheckbox.get_active() item.triggerInside = self.triggerInsideCheckbox.get_active() item.immediate = self.immediateCheckbox.get_active() def reset(self): self.abbrList.get_model().clear() self.__resetWordCharCombo() self.removeButton.set_sensitive(False) self.wordCharCombo.set_active(0) self.omitTriggerCheckbox.set_active(False) self.removeTypedCheckbox.set_active(True) self.matchCaseCheckbox.set_active(False) self.ignoreCaseCheckbox.set_active(False) self.triggerInsideCheckbox.set_active(False) self.immediateCheckbox.set_active(False) def __resetWordCharCombo(self): self.wordCharCombo.remove_all() for item in WORD_CHAR_OPTIONS_ORDERED: self.wordCharCombo.append_text(item) self.wordCharCombo.set_active(0) def get_abbrs(self): ret = [] model = self.abbrList.get_model() i = iter(model) # TODO: list comprehension or for loop, instead of manual loop try: while True: text = model.get_value(i.next().iter, 0) ret.append(text) # ret.append(text.decode("utf-8")) except StopIteration: pass return list(set(ret)) def get_abbrs_readable(self): abbrs = self.get_abbrs() if len(abbrs) == 1: return abbrs[0] else: return "[" + ",".join(abbrs) + "]" def valid(self): if not validate( len(self.get_abbrs()) > 0, _("You must specify at least one abbreviation"), self.addButton, self.ui): return False return True def reset_focus(self): self.addButton.grab_focus() # Signal handlers def on_cell_editing_cancelled(self, renderer, data=None): model, curIter = self.abbrList.get_selection().get_selected() oldText = model.get_value(curIter, 0) or "" self.on_cell_modified(renderer, None, oldText) def on_cell_modified(self, renderer, path, newText, data=None): model, curIter = self.abbrList.get_selection().get_selected() oldText = model.get_value(curIter, 0) or "" if EMPTY_FIELD_REGEX.match(newText) and EMPTY_FIELD_REGEX.match(oldText): self.on_removeButton_clicked(renderer) else: model.set(curIter, 0, newText) def on_addButton_clicked(self, widget, data=None): model = self.abbrList.get_model() newIter = model.append() self.abbrList.set_cursor(model.get_path(newIter), self.abbrList.get_column(0), True) self.removeButton.set_sensitive(True) def on_removeButton_clicked(self, widget, data=None): model, curIter = self.abbrList.get_selection().get_selected() model.remove(curIter) if model.get_iter_first() is None: self.removeButton.set_sensitive(False) else: self.abbrList.get_selection().select_iter(model.get_iter_first()) def on_abbrList_cursorchanged(self, widget, data=None): pass def on_ignoreCaseCheckbox_stateChanged(self, widget, data=None): if not self.ignoreCaseCheckbox.get_active(): self.matchCaseCheckbox.set_active(False) def on_matchCaseCheckbox_stateChanged(self, widget, data=None): if self.matchCaseCheckbox.get_active(): self.ignoreCaseCheckbox.set_active(True) def on_immediateCheckbox_stateChanged(self, widget, data=None): if self.immediateCheckbox.get_active(): self.omitTriggerCheckbox.set_active(False) self.omitTriggerCheckbox.set_sensitive(False) self.wordCharCombo.set_sensitive(False) else: self.omitTriggerCheckbox.set_sensitive(True) self.wordCharCombo.set_sensitive(True) class HotkeySettingsDialog(DialogBase): KEY_MAP = { ' ': "", } REVERSE_KEY_MAP = {} for key, value in KEY_MAP.items(): REVERSE_KEY_MAP[value] = key def __init__(self, parent, configManager, closure): builder = get_ui("hotkeysettings.xml") self.ui = builder.get_object("hotkeysettings") builder.connect_signals(self) self.ui.set_transient_for(parent) self.configManager = configManager self.closure = closure self.key = None self.controlButton = builder.get_object("controlButton") self.altButton = builder.get_object("altButton") self.altgrButton = builder.get_object("altgrButton") self.shiftButton = builder.get_object("shiftButton") self.superButton = builder.get_object("superButton") self.hyperButton = builder.get_object("hyperButton") self.metaButton = builder.get_object("metaButton") self.setButton = builder.get_object("setButton") self.keyLabel = builder.get_object("keyLabel") self.MODIFIER_BUTTONS = { self.controlButton: Key.CONTROL, self.altButton: Key.ALT, self.altgrButton: Key.ALT_GR, self.shiftButton: Key.SHIFT, self.superButton: Key.SUPER, self.hyperButton: Key.HYPER, self.metaButton: Key.META, } DialogBase.__init__(self) def load(self, item): self.setButton.set_sensitive(True) self.targetItem = item UI_common.load_hotkey_settings_dialog(self, item) def populate_hotkey_details(self, item): self.activate_modifier_buttons(item.modifiers) key = item.hotKey keyText = UI_common.get_hotkey_text(self, key) self._setKeyLabel(keyText) self.key = keyText logger.debug("Loaded item {}, key: {}, modifiers: {}".format(item, keyText, item.modifiers)) def activate_modifier_buttons(self, modifiers): for button, key in self.MODIFIER_BUTTONS.items(): button.set_active(key in modifiers) def save(self, item): UI_common.save_hotkey_settings_dialog(self, item) def reset(self): for button in self.MODIFIER_BUTTONS: button.set_active(False) self._setKeyLabel(_("(None)")) self.key = None self.setButton.set_sensitive(True) def set_key(self, key, modifiers: list=None): if modifiers is None: modifiers = [] Gdk.threads_enter() if key in self.KEY_MAP: key = self.KEY_MAP[key] self._setKeyLabel(key) self.key = key self.activate_modifier_buttons(modifiers) self.setButton.set_sensitive(True) Gdk.threads_leave() def cancel_grab(self): Gdk.threads_enter() self.setButton.set_sensitive(True) self._setKeyLabel(self.key) Gdk.threads_leave() def build_modifiers(self): modifiers = [] for button, key in self.MODIFIER_BUTTONS.items(): if button.get_active(): modifiers.append(key) modifiers.sort() return modifiers def _setKeyLabel(self, key): self.keyLabel.set_text(_("Key: ") + key) def valid(self): if not self.check_nonempty_key(): return False return True def check_nonempty_key(self): return validate( self.key is not None, _("You must specify a key for the hotkey."), None, self.ui) def on_setButton_pressed(self, widget, data=None): self.setButton.set_sensitive(False) self.keyLabel.set_text(_("Press a key...")) self.grabber = autokey.iomediator.keygrabber.KeyGrabber(self) self.grabber.start() class GlobalHotkeyDialog(HotkeySettingsDialog): def load(self, item): self.targetItem = item UI_common.load_global_hotkey_dialog(self, item) def save(self, item): UI_common.save_hotkey_settings_dialog(self, item) def valid(self): configManager = self.configManager modifiers = self.build_modifiers() regex = self.targetItem.get_applicable_regex() pattern = None if regex is not None: pattern = regex.pattern unique, conflicting = configManager.check_hotkey_unique(modifiers, self.key, pattern, self.targetItem) if not validate(unique, _("The hotkey is already in use for %s.") % conflicting, None, self.ui): return False if not self.check_nonempty_key(): return False return True class WindowFilterSettingsDialog(DialogBase): def __init__(self, parent, closure): builder = get_ui("windowfiltersettings.xml") self.ui = builder.get_object("windowfiltersettings") builder.connect_signals(self) self.ui.set_transient_for(parent) self.closure = closure self.triggerRegexEntry = builder.get_object("triggerRegexEntry") self.recursiveButton = builder.get_object("recursiveButton") self.detectButton = builder.get_object("detectButton") DialogBase.__init__(self) def load(self, item): self.targetItem = item if not isinstance(item, autokey.model.folder.Folder): self.recursiveButton.hide() else: self.recursiveButton.show() if not item.has_filter(): self.reset() else: self.triggerRegexEntry.set_text(item.get_filter_regex()) self.recursiveButton.set_active(item.isRecursive) def save(self, item): UI_common.save_item_filter(self, item) def reset(self): self.triggerRegexEntry.set_text("") self.recursiveButton.set_active(False) def get_filter_text(self): return self.triggerRegexEntry.get_text() def get_is_recursive(self): return self.recursiveButton.get_active() def valid(self): return True def reset_focus(self): self.triggerRegexEntry.grab_focus() def on_response(self, widget, responseId): self.closure(responseId) def receive_window_info(self, info): Gdk.threads_enter() dlg = DetectDialog(self.ui) dlg.populate(info) response = dlg.run() if response == Gtk.ResponseType.OK: self.triggerRegexEntry.set_text(dlg.get_choice()) self.detectButton.set_sensitive(True) Gdk.threads_leave() def on_detectButton_pressed(self, widget, data=None): #self.__dlg = widget.set_sensitive(False) self.grabber = autokey.iomediator.windowgrabber.WindowGrabber(self) self.grabber.start() class DetectDialog(DialogBase): def __init__(self, parent): builder = get_ui("detectdialog.xml") self.ui = builder.get_object("detectdialog") builder.connect_signals(self) self.ui.set_transient_for(parent) self.classLabel = builder.get_object("classLabel") self.titleLabel = builder.get_object("titleLabel") self.classRadioButton = builder.get_object("classRadioButton") self.titleRadioButton = builder.get_object("titleRadioButton") DialogBase.__init__(self) def populate(self, windowInfo): self.titleLabel.set_text(_("Window title: %s") % windowInfo.wm_title) self.classLabel.set_text(_("Window class: %s") % windowInfo.wm_class) self.windowInfo = windowInfo def get_choice(self): if self.classRadioButton.get_active(): return self.windowInfo.wm_class else: return self.windowInfo.wm_title def on_cancel(self, widget, data=None): self.ui.response(Gtk.ResponseType.CANCEL) self.hide() def on_ok(self, widget, data=None): self.response(Gtk.ResponseType.OK) self.hide() class RecordDialog(DialogBase): def __init__(self, parent, closure): self.closure = closure builder = get_ui("recorddialog.xml") self.ui = builder.get_object("recorddialog") builder.connect_signals(self) self.ui.set_transient_for(parent) self.keyboardButton = builder.get_object("keyboardButton") self.mouseButton = builder.get_object("mouseButton") self.spinButton = builder.get_object("spinButton") DialogBase.__init__(self) def get_record_keyboard(self): return self.keyboardButton.get_active() def get_record_mouse(self): return self.mouseButton.get_active() def get_delay(self): return self.spinButton.get_value_as_int() def on_response(self, widget, responseId): self.closure(responseId, self.get_record_keyboard(), self.get_record_mouse(), self.get_delay()) def on_cancel(self, widget, data=None): self.ui.response(Gtk.ResponseType.CANCEL) self.hide() def valid(self): return True autokey-0.96.0/lib/autokey/gtkui/notifier.py000066400000000000000000000233271427671440700210550ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see .. import datetime import threading import gi gi.require_version('Gtk', '3.0') gi.require_version('Notify', '0.7') # AppIndicator is apparently deprecated, and on Debian the namespace is now # `AyatanaAppIndicator3`, which is a maintained verison. try: gi.require_version('AyatanaAppIndicator3', '0.1') except ValueError: gi.require_version('AppIndicator3', '0.1') from gi.repository import Gtk, Gdk, Notify try: from gi.repository import AyatanaAppIndicator3 as AppIndicator except ImportError: from gi.repository import AppIndicator3 as AppIndicator import gettext from . import popupmenu import autokey.configmanager.configmanager as cm import autokey.configmanager.configmanager_constants as cm_constants from .. import common gettext.install("autokey") TOOLTIP_RUNNING = _("AutoKey - running") TOOLTIP_PAUSED = _("AutoKey - paused") def get_notifier(app): return IndicatorNotifier(app) class IndicatorNotifier: def __init__(self, autokeyApp): Notify.init("AutoKey") # Used to rate-limit error notifications to 1 per second. Without this, two notifications per second cause the # following exception, which in turn completely locks up the GUI: # gi.repository.GLib.GError: g-io-error-quark: # GDBus.Error:org.freedesktop.Notifications.Error.ExcessNotificationGeneration: # Created too many similar notifications in quick succession (36) self.last_notification_timestamp = datetime.datetime.now() self.app = autokeyApp self.configManager = autokeyApp.service.configManager self.indicator = AppIndicator.Indicator.new( "AutoKey", cm.ConfigManager.SETTINGS[cm_constants.NOTIFICATION_ICON], AppIndicator.IndicatorCategory.APPLICATION_STATUS) self.indicator.set_attention_icon(common.ICON_FILE_NOTIFICATION_ERROR) self.update_visible_status() self.rebuild_menu() def update_visible_status(self): if cm.ConfigManager.SETTINGS[cm_constants.SHOW_TRAY_ICON]: self.indicator.set_status(AppIndicator.IndicatorStatus.ACTIVE) else: self.indicator.set_status(AppIndicator.IndicatorStatus.PASSIVE) def hide_icon(self): self.indicator.set_status(AppIndicator.IndicatorStatus.PASSIVE) def set_icon(self,name): self.indicator.set_icon(name) def rebuild_menu(self): # Main Menu items self.errorItem = Gtk.MenuItem(_("View script error")) enableMenuItem = Gtk.CheckMenuItem(_("Enable Expansions")) enableMenuItem.set_active(self.app.service.is_running()) enableMenuItem.set_sensitive(not self.app.serviceDisabled) configureMenuItem = Gtk.ImageMenuItem(_("Show Main Window")) configureMenuItem.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_PREFERENCES, Gtk.IconSize.MENU)) removeMenuItem = Gtk.ImageMenuItem(_("Remove icon")) removeMenuItem.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.MENU)) quitMenuItem = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_QUIT, None) # Menu signals enableMenuItem.connect("toggled", self.on_enable_toggled) configureMenuItem.connect("activate", self.on_show_configure) removeMenuItem.connect("activate", self.on_remove_icon) quitMenuItem.connect("activate", self.on_destroy_and_exit) self.errorItem.connect("activate", self.on_show_error) # Get phrase folders to add to main menu folders = [] items = [] for folder in self.configManager.allFolders: if folder.show_in_tray_menu: folders.append(folder) for item in self.configManager.allItems: if item.show_in_tray_menu: items.append(item) # Construct main menu self.menu = popupmenu.PopupMenu(self.app.service, folders, items, False) if len(items) > 0: self.menu.append(Gtk.SeparatorMenuItem()) self.menu.append(self.errorItem) self.menu.append(enableMenuItem) self.menu.append(configureMenuItem) self.menu.append(removeMenuItem) self.menu.append(quitMenuItem) self.menu.show_all() self.errorItem.hide() self.indicator.set_menu(self.menu) def notify_error(self, message): now = datetime.datetime.now() if self.last_notification_timestamp + datetime.timedelta(seconds=1) < now: self.show_notify(message, Gtk.STOCK_DIALOG_ERROR) self.last_notification_timestamp = now self.errorItem.show() self.indicator.set_status(AppIndicator.IndicatorStatus.ATTENTION) def show_notify(self, message, iconName): Gdk.threads_enter() n = Notify.Notification.new("AutoKey", message, iconName) n.set_urgency(Notify.Urgency.LOW) n.show() Gdk.threads_leave() def update_tool_tip(self): pass def on_show_error(self, widget, data=None): # Work around the current GUI design: the UI is destroyed when the main window is closed. # This causes the show_script_error method below to fail because self.app.configWindow.ui doesn’t exist if self.app.configWindow is not None: self.app.show_script_error(self.app.configWindow.ui) else: self.app.show_script_error(None) self.errorItem.hide() self.update_visible_status() def on_enable_toggled(self, widget, data=None): if widget.active: self.app.unpause_service() else: self.app.pause_service() def on_show_configure(self, widget, data=None): self.app.show_configure() def on_remove_icon(self, widget, data=None): self.indicator.set_status(AppIndicator.IndicatorStatus.PASSIVE) cm.ConfigManager.SETTINGS[cm_constants.SHOW_TRAY_ICON] = False def on_destroy_and_exit(self, widget, data=None): self.app.shutdown() class UnityLauncher(IndicatorNotifier): SHOW_ITEM_STRING = _("Add to quicklist/notification menu") #def __init__(self, autokeyApp): # IndicatorNotifier.__init__(self, autokeyApp) def __getQuickItem(self, label): from gi.repository import Dbusmenu item = Dbusmenu.Menuitem.new() item.property_set(Dbusmenu.MENUITEM_PROP_LABEL, label) item.property_set_bool(Dbusmenu.MENUITEM_PROP_VISIBLE, True) return item def rebuild_menu(self): IndicatorNotifier.rebuild_menu(self) print(threading.currentThread().name) #try: from gi.repository import Unity, Dbusmenu HAVE_UNITY = True print("have unity") #except ImportError: # return print("rebuild unity menu") self.launcher = Unity.LauncherEntry.get_for_desktop_id ("autokey-gtk.desktop") # Main Menu items enableMenuItem = self.__getQuickItem(_("Enable Expansions")) enableMenuItem.property_set(Dbusmenu.MENUITEM_PROP_TOGGLE_TYPE, Dbusmenu.MENUITEM_TOGGLE_CHECK) #if self.app.service.is_running(): # enableMenuItem.property_set_int(Dbusmenu.MENUITEM_PROP_TOGGLE_STATE, Dbusmenu.MENUITEM_TOGGLE_STATE_CHECKED) #else: # enableMenuItem.property_set_int(Dbusmenu.MENUITEM_PROP_TOGGLE_STATE, Dbusmenu.MENUITEM_TOGGLE_STATE_UNCHECKED) enableMenuItem.property_set_int(Dbusmenu.MENUITEM_PROP_TOGGLE_STATE, int(self.app.service.is_running())) enableMenuItem.property_set_bool(Dbusmenu.MENUITEM_PROP_ENABLED, not self.app.serviceDisabled) configureMenuItem = self.__getQuickItem(_("Show Main Window")) # Menu signals enableMenuItem.connect("item-activated", self.on_ql_enable_toggled, None) configureMenuItem.connect("item-activated", self.on_show_configure, None) # Get phrase folders to add to main menu # folders = [] # items = [] # for folder in self.configManager.allFolders: # if folder.show_in_tray_menu: # folders.append(folder) # # for item in self.configManager.allItems: # if item.show_in_tray_menu: # items.append(item) # Construct main menu quicklist = Dbusmenu.Menuitem.new() #if len(items) > 0: # self.menu.append(Gtk.SeparatorMenuItem()) quicklist.child_append(enableMenuItem) quicklist.child_append(configureMenuItem) self.launcher.set_property ("quicklist", quicklist) def on_ql_enable_toggled(self, menuitem, data=None): from gi.repository import Dbusmenu if menuitem.property_get_int(Dbusmenu.Menuitem.MENUITEM_PROP_TOGGLE_STATE) == Dbusmenu.Menuitem.MENUITEM_TOGGLE_STATE_CHECKED: self.app.unpause_service() else: self.app.pause_service() autokey-0.96.0/lib/autokey/gtkui/popupmenu.py000066400000000000000000000110671427671440700212640ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import time from gi.repository import Gtk, Gdk import autokey.configmanager.configmanager as cm import autokey.configmanager.configmanager_constants as cm_constants logger = __import__("autokey.logger").logger.get_logger(__name__) class PopupMenu(Gtk.Menu): """ A popup menu that allows the user to select a phrase. """ def __init__(self, service, folders: list=None, items: list=None, onDesktop=True, title=None): Gtk.Menu.__init__(self) #self.set_take_focus(cm.ConfigManager.SETTINGS[MENU_TAKES_FOCUS]) if items is None: items = [] if folders is None: folders = [] self.__i = 1 self.service = service folders, items = self.sort_popup_items(folders, items) self.set_up_trigger_position() single_desktop_folder = len(folders) == 1 and not items and onDesktop if single_desktop_folder: # Only one folder - create menu with just its folders and items self.create_menu_item(folders[0].folders, service, onDesktop) self.__addItemsToSelf(folders[0].items, onDesktop) else: # Create phrase folder section self.create_menu_item(folders, service, onDesktop, multiple_folders=True) self.__addItemsToSelf(items, onDesktop) self.show_all() def create_menu_item(self, folders, service, onDesktop, multiple_folders=False): for folder in folders: menuItem = Gtk.MenuItem(label=self.__getMnemonic(folder.title, onDesktop)) if multiple_folders: onDesktop=False menuItem.set_submenu(PopupMenu(service, folder.folders, folder.items, onDesktop)) menuItem.set_use_underline(True) self.append(menuItem) if len(folders) > 0: self.append(Gtk.SeparatorMenuItem()) def set_up_trigger_position(self): if cm.ConfigManager.SETTINGS[cm_constants.TRIGGER_BY_INITIAL]: self.triggerInitial = 1 else: logger.debug("Triggering menu item by position in list") self.triggerInitial = 0 def sort_popup_items(self, folders, items): if cm.ConfigManager.SETTINGS[cm_constants.SORT_BY_USAGE_COUNT]: logger.debug("Sorting phrase menu by usage count") folders.sort(key=lambda obj: obj.usageCount, reverse=True) items.sort(key=lambda obj: obj.usageCount, reverse=True) else: logger.debug("Sorting phrase menu by item name/title") folders.sort(key=lambda obj: str(obj)) items.sort(key=lambda obj: str(obj)) return folders, items def __getMnemonic(self, desc, onDesktop): if 1 < 10 and '_' not in desc and onDesktop: # TODO: if 1 < 10 ?? if self.triggerInitial: ret = str(desc) else: ret = "_{} - {}".format(self.__i, desc) self.__i += 1 return ret else: return desc def show_on_desktop(self): Gdk.threads_enter() time.sleep(0.2) self.popup(None, None, None, None, 1, 0) Gdk.threads_leave() def remove_from_desktop(self): Gdk.threads_enter() self.popdown() Gdk.threads_leave() def __addItemsToSelf(self, items, onDesktop): # Create phrase section if cm.ConfigManager.SETTINGS[cm_constants.SORT_BY_USAGE_COUNT]: items.sort(key=lambda obj: obj.usageCount, reverse=True) else: items.sort(key=lambda obj: str(obj)) for item in items: menuItem = Gtk.MenuItem(label=self.__getMnemonic(item.description, onDesktop)) menuItem.connect("activate", self.__itemSelected, item) menuItem.set_use_underline(True) self.append(menuItem) def __itemSelected(self, widget, item): self.service.item_selected(item) autokey-0.96.0/lib/autokey/gtkui/settingsdialog.py000066400000000000000000000251731427671440700222570ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys from gi.repository import Gtk, GtkSource import autokey.configmanager.autostart import autokey.configmanager.configmanager as cm import autokey.configmanager.configmanager_constants as cm_constants from autokey import common from autokey.model.key import Key from .dialogs import GlobalHotkeyDialog from .shared import get_ui ICON_NAME_MAP = { _("Light"): common.ICON_FILE_NOTIFICATION, _("Dark"): common.ICON_FILE_NOTIFICATION_DARK } ICON_NAME_LIST = [] class SettingsDialog: KEY_MAP = GlobalHotkeyDialog.KEY_MAP REVERSE_KEY_MAP = GlobalHotkeyDialog.REVERSE_KEY_MAP def __init__(self, parent, configManager): builder = get_ui("settingsdialog.xml") self.ui = builder.get_object("settingsdialog") builder.connect_signals(self) self.ui.set_transient_for(parent) self.configManager = configManager # General Settings self.autoStartCheckbox = builder.get_object("autoStartCheckbox") self.autosaveCheckbox = builder.get_object("autosaveCheckbox") self.showTrayCheckbox = builder.get_object("showTrayCheckbox") self.disableCapslockCheckbox = builder.get_object("disableCapslockCheckbox") self.allowKbNavCheckbox = builder.get_object("allowKbNavCheckbox") self.allowKbNavCheckbox.hide() self.sortByUsageCheckbox = builder.get_object("sortByUsageCheckbox") # Added by Trey Blancher (ectospasm) 2015-09-16 self.triggerItemByInitial = builder.get_object("triggerItemByInitial") self.enableUndoCheckbox = builder.get_object("enableUndoCheckbox") self.gtkThemeCombo = Gtk.ComboBoxText.new() hboxgtktheme = builder.get_object("hboxgtktheme") hboxgtktheme.pack_start(self.gtkThemeCombo, False, True, 0) hboxgtktheme.show_all() self.themeList = GtkSource.StyleSchemeManager().get_scheme_ids() for item in self.themeList: self.gtkThemeCombo.append_text(item) self.gtkThemeCombo.set_sensitive(cm.ConfigManager.SETTINGS[cm_constants.GTK_THEME]) self.gtkThemeCombo.set_active(self.themeList.index(cm.ConfigManager.SETTINGS[cm_constants.GTK_THEME])) self.iconStyleCombo = Gtk.ComboBoxText.new() hbox = builder.get_object("hbox4") hbox.pack_start(self.iconStyleCombo, False, True, 0) hbox.show_all() for key, value in list(ICON_NAME_MAP.items()): self.iconStyleCombo.append_text(key) ICON_NAME_LIST.append(value) self.iconStyleCombo.set_sensitive(cm.ConfigManager.SETTINGS[cm_constants.SHOW_TRAY_ICON]) self.iconStyleCombo.set_active(ICON_NAME_LIST.index(cm.ConfigManager.SETTINGS[cm_constants.NOTIFICATION_ICON])) self.autoStartCheckbox.set_active(autokey.configmanager.autostart.get_autostart().desktop_file_name is not None) self.autosaveCheckbox.set_active(not cm.ConfigManager.SETTINGS[cm_constants.PROMPT_TO_SAVE]) self.showTrayCheckbox.set_active(cm.ConfigManager.SETTINGS[cm_constants.SHOW_TRAY_ICON]) self.disableCapslockCheckbox.set_active(cm.ConfigManager.is_modifier_disabled(Key.CAPSLOCK)) # self.allowKbNavCheckbox.set_active(cm.ConfigManager.SETTINGS[MENU_TAKES_FOCUS]) # Added by Trey Blancher (ectospasm) 2015-09-16 self.triggerItemByInitial.set_active(cm.ConfigManager.SETTINGS[cm_constants.TRIGGER_BY_INITIAL]) self.sortByUsageCheckbox.set_active(cm.ConfigManager.SETTINGS[cm_constants.SORT_BY_USAGE_COUNT]) self.enableUndoCheckbox.set_active(cm.ConfigManager.SETTINGS[cm_constants.UNDO_USING_BACKSPACE]) # Hotkeys self.showConfigDlg = GlobalHotkeyDialog(parent, configManager, self.on_config_response) self.toggleMonitorDlg = GlobalHotkeyDialog(parent, configManager, self.on_monitor_response) self.configKeyLabel = builder.get_object("configKeyLabel") self.clearConfigButton = builder.get_object("clearConfigButton") self.monitorKeyLabel = builder.get_object("monitorKeyLabel") self.clearMonitorButton = builder.get_object("clearMonitorButton") self.useConfigHotkey = self.__loadHotkey(configManager.configHotkey, self.configKeyLabel, self.showConfigDlg, self.clearConfigButton) self.useServiceHotkey = self.__loadHotkey(configManager.toggleServiceHotkey, self.monitorKeyLabel, self.toggleMonitorDlg, self.clearMonitorButton) # Script Engine Settings self.userModuleChooserButton = builder.get_object("userModuleChooserButton") if configManager.userCodeDir is not None: self.userModuleChooserButton.set_current_folder(configManager.userCodeDir) if configManager.userCodeDir in sys.path: sys.path.remove(configManager.userCodeDir) def on_save(self, widget, data=None): if self.autoStartCheckbox.get_active(): autokey.configmanager.autostart.set_autostart_entry( autokey.configmanager.autostart.AutostartSettings("autokey-gtk.desktop", False)) else: autokey.configmanager.autostart.delete_autostart_entry() #promptToSaveCheckbox no longer exists? This prevented saving in the Preferences window from what I can tell #cm.ConfigManager.SETTINGS[cm_constants.PROMPT_TO_SAVE] = not self.promptToSaveCheckbox.get_active() cm.ConfigManager.SETTINGS[cm_constants.SHOW_TRAY_ICON] = self.showTrayCheckbox.get_active() cm.ConfigManager.SETTINGS[cm_constants.GTK_THEME] = self.gtkThemeCombo.get_active_text() #cm.ConfigManager.SETTINGS[MENU_TAKES_FOCUS] = self.allowKbNavCheckbox.get_active() cm.ConfigManager.SETTINGS[cm_constants.SORT_BY_USAGE_COUNT] = self.sortByUsageCheckbox.get_active() # Added by Trey Blancher (ectospasm) 2015-09-16 cm.ConfigManager.SETTINGS[cm_constants.TRIGGER_BY_INITIAL] = self.triggerItemByInitial.get_active() cm.ConfigManager.SETTINGS[cm_constants.UNDO_USING_BACKSPACE] = self.enableUndoCheckbox.get_active() cm.ConfigManager.SETTINGS[cm_constants.NOTIFICATION_ICON] = ICON_NAME_MAP[self.iconStyleCombo.get_active_text()] self._save_disable_capslock_setting() self.configManager.userCodeDir = self.userModuleChooserButton.get_current_folder() sys.path.append(self.configManager.userCodeDir) configHotkey = self.configManager.configHotkey toggleHotkey = self.configManager.toggleServiceHotkey app = self.configManager.app if configHotkey.enabled: app.hotkey_removed(configHotkey) configHotkey.enabled = self.useConfigHotkey if self.useConfigHotkey: self.showConfigDlg.save(configHotkey) app.hotkey_created(configHotkey) if toggleHotkey.enabled: app.hotkey_removed(toggleHotkey) toggleHotkey.enabled = self.useServiceHotkey if self.useServiceHotkey: self.toggleMonitorDlg.save(toggleHotkey) app.hotkey_created(toggleHotkey) app.update_notifier_visibility() self.configManager.config_altered(True) self.hide() self.destroy() def _save_disable_capslock_setting(self): # Only update the modifier key handling if the value changed. if self.disableCapslockCheckbox.get_active() and not cm.ConfigManager.is_modifier_disabled(Key.CAPSLOCK): cm.ConfigManager.disable_modifier(Key.CAPSLOCK) elif not self.disableCapslockCheckbox.get_active() and cm.ConfigManager.is_modifier_disabled(Key.CAPSLOCK): cm.ConfigManager.enable_modifier(Key.CAPSLOCK) def on_cancel(self, widget, data=None): self.hide() self.destroy() def __getattr__(self, attr): # Magic fudge to allow us to pretend to be the ui class we encapsulate return getattr(self.ui, attr) def __loadHotkey(self, item, label, dialog, clearButton): dialog.load(item) if item.enabled: key = item.hotKey.encode("utf-8") label.set_text(item.get_hotkey_string()) clearButton.set_sensitive(True) return True else: label.set_text(_("(None configured)")) clearButton.set_sensitive(False) return False # ---- Signal handlers def on_showTrayCheckbox_toggled(self, widget, data=None): self.iconStyleCombo.set_sensitive(widget.get_active()) def on_setConfigButton_pressed(self, widget, data=None): self.showConfigDlg.run() def on_config_response(self, res): if res == Gtk.ResponseType.OK: self.useConfigHotkey = True key = self.showConfigDlg.key modifiers = self.showConfigDlg.build_modifiers() self.configKeyLabel.set_text(self.build_hotkey_string(key, modifiers)) self.clearConfigButton.set_sensitive(True) def on_clearConfigButton_pressed(self, widget, data=None): self.useConfigHotkey = False self.clearConfigButton.set_sensitive(False) self.configKeyLabel.set_text(_("(None configured)")) self.showConfigDlg.reset() def on_setMonitorButton_pressed(self, widget, data=None): self.toggleMonitorDlg.run() def on_monitor_response(self, res): if res == Gtk.ResponseType.OK: self.useServiceHotkey = True key = self.toggleMonitorDlg.key modifiers = self.toggleMonitorDlg.build_modifiers() self.monitorKeyLabel.set_text(self.build_hotkey_string(key, modifiers)) self.clearMonitorButton.set_sensitive(True) def on_clearMonitorButton_pressed(self, widget, data=None): self.useServiceHotkey = False self.clearMonitorButton.set_sensitive(False) self.monitorKeyLabel.set_text(_("(None configured)")) self.toggleMonitorDlg.reset() autokey-0.96.0/lib/autokey/gtkui/shared.py000066400000000000000000000003531427671440700204760ustar00rootroot00000000000000import os from gi.repository import Gtk def get_ui(fileName): builder = Gtk.Builder() uiFile = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data/" + fileName) builder.add_from_file(uiFile) return builder autokey-0.96.0/lib/autokey/interface.py000066400000000000000000001622511427671440700200530ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . __all__ = ["XRecordInterface", "AtSpiInterface", "WindowInfo"] from abc import abstractmethod import logging import typing import threading import select import queue import subprocess import time import autokey.model.phrase if typing.TYPE_CHECKING: from autokey.iomediator.iomediator import IoMediator import autokey.configmanager.configmanager_constants as cm_constants # Imported to enable threading in Xlib. See module description. Not an unused import statement. import Xlib.threaded as xlib_threaded # Delete again, as the reference is not needed anymore after the import side-effect has done it’s work. # This (hopefully) also prevents automatic code cleanup software from deleting an "unused" import and re-introduce # issues. del xlib_threaded from Xlib.error import ConnectionClosedError from . import common from autokey.model.button import Button if common.USING_QT: from PyQt5.QtGui import QClipboard from PyQt5.QtWidgets import QApplication else: import gi gi.require_version('Gtk', '3.0') from gi.repository import Gtk, Gdk try: gi.require_version('Atspi', '2.0') import pyatspi HAS_ATSPI = True except ImportError: HAS_ATSPI = False except ValueError: HAS_ATSPI = False except SyntaxError: # pyatspi 2.26 fails when used with Python 3.7 HAS_ATSPI = False from Xlib import X, XK, display, error try: from Xlib.ext import record, xtest HAS_RECORD = True except ImportError: HAS_RECORD = False from Xlib.protocol import rq, event logger = __import__("autokey.logger").logger.get_logger(__name__) MASK_INDEXES = [ (X.ShiftMapIndex, X.ShiftMask), (X.ControlMapIndex, X.ControlMask), (X.LockMapIndex, X.LockMask), (X.Mod1MapIndex, X.Mod1Mask), (X.Mod2MapIndex, X.Mod2Mask), (X.Mod3MapIndex, X.Mod3Mask), (X.Mod4MapIndex, X.Mod4Mask), (X.Mod5MapIndex, X.Mod5Mask), ] CAPSLOCK_LEDMASK = 1<<0 NUMLOCK_LEDMASK = 1<<1 def str_or_bytes_to_bytes(x: typing.Union[str, bytes, memoryview]) -> bytes: if type(x) == bytes: # logger.info("using LiuLang's python3-xlib") return x if type(x) == str: logger.debug("using official python-xlib") return x.encode("utf8") if type(x) == memoryview: logger.debug("using official python-xlib") return x.tobytes() raise RuntimeError("x must be str or bytes or memoryview object, type(x)={}, repr(x)={}".format(type(x), repr(x))) # This tuple is used to return requested window properties. WindowInfo = typing.NamedTuple("WindowInfo", [("wm_title", str), ("wm_class", str)]) class AbstractClipboard: """ Abstract interface for clipboard interactions. This is an abstraction layer for platform dependent clipboard handling. It unifies clipboard handling for Qt and GTK. """ @property @abstractmethod def text(self): """Get and set the keyboard clipboard content.""" return @property @abstractmethod def selection(self): """Get and set the mouse selection clipboard content.""" return if common.USING_QT: class Clipboard(AbstractClipboard): def __init__(self): self._clipboard = QApplication.clipboard() @property def text(self): return self._clipboard.text(QClipboard.Clipboard) @text.setter def text(self, new_content: str): self._clipboard.setText(new_content, QClipboard.Clipboard) @property def selection(self): return self._clipboard.text(QClipboard.Selection) @selection.setter def selection(self, new_content: str): self._clipboard.setText(new_content, QClipboard.Selection) else: class Clipboard(AbstractClipboard): def __init__(self): self._clipboard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) self._selection = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY) @property def text(self): Gdk.threads_enter() text = self._clipboard.wait_for_text() Gdk.threads_leave() return text @text.setter def text(self, new_content: str): Gdk.threads_enter() try: # This call might fail and raise an Exception. # If it does, make sure to release the mutex and not deadlock AutoKey. self._clipboard.set_text(new_content, -1) finally: Gdk.threads_leave() @property def selection(self): Gdk.threads_enter() text = self._selection.wait_for_text() Gdk.threads_leave() return text @selection.setter def selection(self, new_content: str): Gdk.threads_enter() try: # This call might fail and raise an Exception. # If it does, make sure to release the mutex and not deadlock AutoKey. self._selection.set_text(new_content, -1) finally: Gdk.threads_leave() class XInterfaceBase(threading.Thread): """ Encapsulates the common functionality for the two X interface classes. """ def __init__(self, mediator, app): threading.Thread.__init__(self) self.setDaemon(True) self.setName("XInterface-thread") self.mediator = mediator # type: IoMediator self.app = app self.lastChars = [] # QT4 Workaround self.__enableQT4Workaround = False # QT4 Workaround self.shutdown = False # Event loop self.eventThread = threading.Thread(target=self.__eventLoop) self.queue = queue.Queue() # Event listener self.listenerThread = threading.Thread(target=self.__flushEvents) self.clipboard = Clipboard() self.__initMappings() # Set initial lock state ledMask = self.localDisplay.get_keyboard_control().led_mask mediator.set_modifier_state(Key.CAPSLOCK, (ledMask & CAPSLOCK_LEDMASK) != 0) mediator.set_modifier_state(Key.NUMLOCK, (ledMask & NUMLOCK_LEDMASK) != 0) # Window name atoms self.__NameAtom = self.localDisplay.intern_atom("_NET_WM_NAME", True) self.__VisibleNameAtom = self.localDisplay.intern_atom("_NET_WM_VISIBLE_NAME", True) #move detection of key map changes to X event thread in order to have QT and GTK detection # if not common.USING_QT: # self.keyMap = Gdk.Keymap.get_default() # self.keyMap.connect("keys-changed", self.on_keys_changed) self.__ignoreRemap = False self.eventThread.start() self.listenerThread.start() def __eventLoop(self): while True: method, args = self.queue.get() if method is None and args is None: break elif method is not None and args is None: logger.debug("__eventLoop: Got method {} with None arguments!".format(method)) try: method(*args) except Exception as e: logger.exception("Error in X event loop thread") self.queue.task_done() def __enqueue(self, method: typing.Callable, *args): self.queue.put_nowait((method, args)) def on_keys_changed(self, data=None): if not self.__ignoreRemap: logger.debug("Recorded keymap change event") self.__ignoreRemap = True time.sleep(0.2) self.__enqueue(self.__ungrabAllHotkeys) self.__enqueue(self.__delayedInitMappings) else: logger.debug("Ignored keymap change event") def __delayedInitMappings(self): self.__initMappings() self.__ignoreRemap = False def __initMappings(self): self.localDisplay = display.Display() self.rootWindow = self.localDisplay.screen().root self.rootWindow.change_attributes(event_mask=X.SubstructureNotifyMask|X.StructureNotifyMask) altList = self.localDisplay.keysym_to_keycodes(XK.XK_ISO_Level3_Shift) self.__usableOffsets = (0, 1) for code, offset in altList: if code == 108 and offset == 0: self.__usableOffsets += (4, 5) logger.debug("Enabling sending using Alt-Grid") break # Build modifier mask mapping self.modMasks = {} mapping = self.localDisplay.get_modifier_mapping() for keySym, ak in XK_TO_AK_MAP.items(): if ak in MODIFIERS: keyCodeList = self.localDisplay.keysym_to_keycodes(keySym) found = False for keyCode, lvl in keyCodeList: for index, mask in MASK_INDEXES: if keyCode in mapping[index]: self.modMasks[ak] = mask found = True break if found: break logger.debug("Modifier masks: %r", self.modMasks) self.__grabHotkeys() self.localDisplay.flush() # --- get list of keycodes that are unused in the current keyboard mapping keyCode = 8 avail = [] for keyCodeMapping in self.localDisplay.get_keyboard_mapping(keyCode, 200): codeAvail = True for offset in keyCodeMapping: if offset != 0: codeAvail = False break if codeAvail: avail.append(keyCode) keyCode += 1 self.__availableKeycodes = avail self.remappedChars = {} if logger.getEffectiveLevel() == logging.DEBUG: self.keymap_test() def keymap_test(self): code = self.localDisplay.keycode_to_keysym(108, 0) for attr in XK.__dict__.items(): if attr[0].startswith("XK"): if attr[1] == code: logger.debug("Alt-Grid: %s, %s", attr[0], attr[1]) logger.debug("X Server Keymap, listing unmapped keys.") for char in "\\|`1234567890-=~!@#$%^&*()qwertyuiop[]asdfghjkl;'zxcvbnm,./QWERTYUIOP{}ASDFGHJKL:\"ZXCVBNM<>?": keyCodeList = list(self.localDisplay.keysym_to_keycodes(ord(char))) if not keyCodeList: logger.debug("No mapping for [%s]", char) def __needsMutterWorkaround(self, item): if Key.SUPER not in item.modifiers: return False try: output = subprocess.check_output(["ps", "-eo", "command"]).decode() except subprocess.CalledProcessError: pass # since this is just a nasty workaround, if anything goes wrong just disable it else: lines = output.splitlines() for line in lines: if "gnome-shell" in line or "cinnamon" in line or "unity" in line: return True return False def __grabHotkeys(self): """ Run during startup to grab global and specific hotkeys in all open windows """ c = self.app.configManager hotkeys = c.hotKeys + c.hotKeyFolders # Grab global hotkeys in root window for item in c.globalHotkeys: if item.enabled: self.__enqueue(self.__grabHotkey, item.hotKey, item.modifiers, self.rootWindow) if self.__needsMutterWorkaround(item): self.__enqueue(self.__grabRecurse, item, self.rootWindow, False) # Grab hotkeys without a filter in root window for item in hotkeys: if item.get_applicable_regex() is None: self.__enqueue(self.__grabHotkey, item.hotKey, item.modifiers, self.rootWindow) if self.__needsMutterWorkaround(item): self.__enqueue(self.__grabRecurse, item, self.rootWindow, False) self.__enqueue(self.__recurseTree, self.rootWindow, hotkeys) def __recurseTree(self, parent, hotkeys): # Grab matching hotkeys in all open child windows try: children = parent.query_tree().children except: return # window has been destroyed for window in children: try: window_info = self.get_window_info(window, False) if window_info.wm_title or window_info.wm_class: for item in hotkeys: if item.get_applicable_regex() is not None and item._should_trigger_window_title(window_info): self.__grabHotkey(item.hotKey, item.modifiers, window) self.__grabRecurse(item, window, False) self.__enqueue(self.__recurseTree, window, hotkeys) except: logger.exception("grab on window failed") def __ungrabAllHotkeys(self): """ Ungrab all hotkeys in preparation for keymap change """ c = self.app.configManager hotkeys = c.hotKeys + c.hotKeyFolders # Ungrab global hotkeys in root window, recursively for item in c.globalHotkeys: if item.enabled: self.__ungrabHotkey(item.hotKey, item.modifiers, self.rootWindow) if self.__needsMutterWorkaround(item): self.__ungrabRecurse(item, self.rootWindow, False) # Ungrab hotkeys without a filter in root window, recursively for item in hotkeys: if item.get_applicable_regex() is None: self.__ungrabHotkey(item.hotKey, item.modifiers, self.rootWindow) if self.__needsMutterWorkaround(item): self.__ungrabRecurse(item, self.rootWindow, False) self.__recurseTreeUngrab(self.rootWindow, hotkeys) def __recurseTreeUngrab(self, parent, hotkeys): # Ungrab matching hotkeys in all open child windows try: children = parent.query_tree().children except: return # window has been destroyed for window in children: try: window_info = self.get_window_info(window, False) if window_info.wm_title or window_info.wm_class: for item in hotkeys: if item.get_applicable_regex() is not None and item._should_trigger_window_title(window_info): self.__ungrabHotkey(item.hotKey, item.modifiers, window) self.__ungrabRecurse(item, window, False) self.__enqueue(self.__recurseTreeUngrab, window, hotkeys) except: logger.exception("ungrab on window failed") def __grabHotkeysForWindow(self, window): """ Grab all hotkeys relevant to the window Used when a new window is created """ c = self.app.configManager hotkeys = c.hotKeys + c.hotKeyFolders window_info = self.get_window_info(window) for item in hotkeys: if item.get_applicable_regex() is not None and item._should_trigger_window_title(window_info): self.__enqueue(self.__grabHotkey, item.hotKey, item.modifiers, window) elif self.__needsMutterWorkaround(item): self.__enqueue(self.__grabHotkey, item.hotKey, item.modifiers, window) def __grabHotkey(self, key, modifiers, window): """ Grab a specific hotkey in the given window """ logger.debug("Grabbing hotkey: %r %r", modifiers, key) try: keycode = self.__lookupKeyCode(key) mask = 0 for mod in modifiers: mask |= self.modMasks[mod] window.grab_key(keycode, mask, True, X.GrabModeAsync, X.GrabModeAsync) if Key.NUMLOCK in self.modMasks: window.grab_key(keycode, mask|self.modMasks[Key.NUMLOCK], True, X.GrabModeAsync, X.GrabModeAsync) if Key.CAPSLOCK in self.modMasks: window.grab_key(keycode, mask|self.modMasks[Key.CAPSLOCK], True, X.GrabModeAsync, X.GrabModeAsync) if Key.CAPSLOCK in self.modMasks and Key.NUMLOCK in self.modMasks: window.grab_key(keycode, mask|self.modMasks[Key.CAPSLOCK]|self.modMasks[Key.NUMLOCK], True, X.GrabModeAsync, X.GrabModeAsync) except Exception as e: logger.warning("Failed to grab hotkey %r %r: %s", modifiers, key, str(e)) def grab_hotkey(self, item): """ Grab a hotkey. If the hotkey has no filter regex, it is global and is grabbed recursively from the root window If it has a filter regex, iterate over all children of the root and grab from matching windows """ if item.get_applicable_regex() is None: self.__enqueue(self.__grabHotkey, item.hotKey, item.modifiers, self.rootWindow) if self.__needsMutterWorkaround(item): self.__enqueue(self.__grabRecurse, item, self.rootWindow, False) else: self.__enqueue(self.__grabRecurse, item, self.rootWindow) def __grabRecurse(self, item, parent, checkWinInfo=True): try: children = parent.query_tree().children except: return # window has been destroyed for window in children: shouldTrigger = False if checkWinInfo: window_info = self.get_window_info(window, False) shouldTrigger = item._should_trigger_window_title(window_info) if shouldTrigger or not checkWinInfo: self.__grabHotkey(item.hotKey, item.modifiers, window) self.__grabRecurse(item, window, False) else: self.__grabRecurse(item, window) def ungrab_hotkey(self, item): """ Ungrab a hotkey. If the hotkey has no filter regex, it is global and is grabbed recursively from the root window If it has a filter regex, iterate over all children of the root and ungrab from matching windows """ import copy newItem = copy.copy(item) if item.get_applicable_regex() is None: self.__enqueue(self.__ungrabHotkey, newItem.hotKey, newItem.modifiers, self.rootWindow) if self.__needsMutterWorkaround(item): self.__enqueue(self.__ungrabRecurse, newItem, self.rootWindow, False) else: self.__enqueue(self.__ungrabRecurse, newItem, self.rootWindow) def __ungrabRecurse(self, item, parent, checkWinInfo=True): try: children = parent.query_tree().children except: return # window has been destroyed for window in children: shouldTrigger = False if checkWinInfo: window_info = self.get_window_info(window, False) shouldTrigger = item._should_trigger_window_title(window_info) if shouldTrigger or not checkWinInfo: self.__ungrabHotkey(item.hotKey, item.modifiers, window) self.__ungrabRecurse(item, window, False) else: self.__ungrabRecurse(item, window) def __ungrabHotkey(self, key, modifiers, window): """ Ungrab a specific hotkey in the given window """ logger.debug("Ungrabbing hotkey: %r %r", modifiers, key) try: keycode = self.__lookupKeyCode(key) mask = 0 for mod in modifiers: mask |= self.modMasks[mod] window.ungrab_key(keycode, mask) if Key.NUMLOCK in self.modMasks: window.ungrab_key(keycode, mask|self.modMasks[Key.NUMLOCK]) if Key.CAPSLOCK in self.modMasks: window.ungrab_key(keycode, mask|self.modMasks[Key.CAPSLOCK]) if Key.CAPSLOCK in self.modMasks and Key.NUMLOCK in self.modMasks: window.ungrab_key(keycode, mask|self.modMasks[Key.CAPSLOCK]|self.modMasks[Key.NUMLOCK]) except Exception as e: logger.warning("Failed to ungrab hotkey %r %r: %s", modifiers, key, str(e)) def lookup_string(self, keyCode, shifted, numlock, altGrid): if keyCode == 0: return "" keySym = self.localDisplay.keycode_to_keysym(keyCode, 0) if keySym in XK_TO_AK_NUMLOCKED and numlock and not (numlock and shifted): return XK_TO_AK_NUMLOCKED[keySym] elif keySym in XK_TO_AK_MAP: return XK_TO_AK_MAP[keySym] else: index = 0 if shifted: index += 1 if altGrid: index += 4 try: return chr(self.localDisplay.keycode_to_keysym(keyCode, index)) except ValueError: return "" % keyCode def send_string_clipboard(self, string: str, paste_command: autokey.model.phrase.SendMode): """ This method is called from the IoMediator for Phrase expansion using one of the clipboard method. :param string: The to-be pasted string :param paste_command: Optional paste command. If None, the mouse selection is used. Otherwise, it contains a keyboard combination string, like '+v', or '+' that is sent to the target application, causing a paste operation to happen. """ logger.debug("Sending string via clipboard: " + string) if common.USING_QT: if paste_command in (None, autokey.model.phrase.SendMode.SELECTION): self.__enqueue(self.app.exec_in_main, self._send_string_selection, string) else: self.__enqueue(self.app.exec_in_main, self._send_string_clipboard, string, paste_command) else: if paste_command in (None, autokey.model.phrase.SendMode.SELECTION): self.__enqueue(self._send_string_selection, string) else: self.__enqueue(self._send_string_clipboard, string, paste_command) logger.debug("Sending via clipboard enqueued.") def _send_string_clipboard(self, string: str, paste_command: autokey.model.phrase.SendMode): """ Use the clipboard to send a string. """ backup = self.clipboard.text # Keep a backup of current content, to restore the original afterwards. if backup is None: logger.warning("Tried to backup the X clipboard content, but got None instead of a string.") self.clipboard.text = string try: self.mediator.send_string(paste_command.value) finally: self.ungrab_keyboard() # Because send_string is queued, also enqueue the clipboard restore, to keep the proper action ordering. self.__enqueue(self._restore_clipboard_text, backup) def _restore_clipboard_text(self, backup: str): """Restore the clipboard content.""" # Pasting takes some time, so wait a bit before restoring the content. Otherwise the restore is done before # the pasting happens, causing the backup to be pasted instead of the desired clipboard content. time.sleep(0.2) self.clipboard.text = backup if backup is not None else "" def _send_string_selection(self, string: str): """Use the mouse selection clipboard to send a string.""" backup = self.clipboard.selection # Keep a backup of current content, to restore the original afterwards. if backup is None: logger.warning("Tried to backup the X PRIMARY selection content, but got None instead of a string.") self.clipboard.selection = string self.__enqueue(self._paste_using_mouse_button_2) self.__enqueue(self._restore_clipboard_selection, backup) def _restore_clipboard_selection(self, backup: str): """Restore the selection clipboard content.""" # Pasting takes some time, so wait a bit before restoring the content. Otherwise the restore is done before # the pasting happens, causing the backup to be pasted instead of the desired clipboard content. # Programmatically pressing the middle mouse button seems VERY slow, so wait rather long. # It might be a good idea to make this delay configurable. There might be systems that need even longer. time.sleep(1) self.clipboard.selection = backup if backup is not None else "" def _paste_using_mouse_button_2(self): """Paste using the mouse: Press the second mouse button, then release it again.""" focus = self.localDisplay.get_input_focus().focus xtest.fake_input(focus, X.ButtonPress, X.Button2) xtest.fake_input(focus, X.ButtonRelease, X.Button2) logger.debug("Mouse Button2 event sent.") def begin_send(self): self.__enqueue(self.__grab_keyboard) def finish_send(self): self.__enqueue(self.__ungrabKeyboard) def grab_keyboard(self): self.__enqueue(self.__grab_keyboard) def __grab_keyboard(self): focus = self.localDisplay.get_input_focus().focus focus.grab_keyboard(True, X.GrabModeAsync, X.GrabModeAsync, X.CurrentTime) self.localDisplay.flush() def ungrab_keyboard(self): self.__enqueue(self.__ungrabKeyboard) def __ungrabKeyboard(self): self.localDisplay.ungrab_keyboard(X.CurrentTime) self.localDisplay.flush() def __findUsableKeycode(self, codeList): for code, offset in codeList: if offset in self.__usableOffsets: return code, offset return None, None def send_string(self, string): self.__enqueue(self.__sendString, string) def __sendString(self, string): """ Send a string of printable characters. """ logger.debug("Sending string: %r", string) # Determine if workaround is needed if not cm.ConfigManager.SETTINGS[cm_constants.ENABLE_QT4_WORKAROUND]: self.__checkWorkaroundNeeded() # First find out if any chars need remapping remapNeeded = False for char in string: keyCodeList = self.localDisplay.keysym_to_keycodes(ord(char)) usableCode, offset = self.__findUsableKeycode(keyCodeList) if usableCode is None and char not in self.remappedChars: remapNeeded = True break # Now we know chars need remapping, do it if remapNeeded: self.__ignoreRemap = True self.remappedChars = {} remapChars = [] for char in string: keyCodeList = self.localDisplay.keysym_to_keycodes(ord(char)) usableCode, offset = self.__findUsableKeycode(keyCodeList) if usableCode is None: remapChars.append(char) logger.debug("Characters requiring remapping: %r", remapChars) availCodes = self.__availableKeycodes logger.debug("Remapping with keycodes in the range: %r", availCodes) mapping = self.localDisplay.get_keyboard_mapping(8, 200) firstCode = 8 for i in range(len(availCodes) - 1): code = availCodes[i] sym1 = 0 sym2 = 0 if len(remapChars) > 0: char = remapChars.pop(0) self.remappedChars[char] = (code, 0) sym1 = ord(char) if len(remapChars) > 0: char = remapChars.pop(0) self.remappedChars[char] = (code, 1) sym2 = ord(char) if sym1 != 0: mapping[code - firstCode][0] = sym1 mapping[code - firstCode][1] = sym2 mapping = [tuple(l) for l in mapping] self.localDisplay.change_keyboard_mapping(firstCode, mapping) self.localDisplay.flush() focus = self.localDisplay.get_input_focus().focus for char in string: try: keyCodeList = self.localDisplay.keysym_to_keycodes(ord(char)) keyCode, offset = self.__findUsableKeycode(keyCodeList) if keyCode is not None: if offset == 0: if self.localDisplay.lookup_string(ord(char)) is None: # No reasonable translation of key to string found # Try typing this as Unicode: u + hex ukeyCodeList = self.localDisplay.keysym_to_keycodes(ord('u')) ukeyCode, uOffset = self.__findUsableKeycode(ukeyCodeList) self.__sendKeyCode(ukeyCode, self.modMasks[Key.CONTROL] | self.modMasks[Key.SHIFT], focus) self.__releaseKey(Key.CONTROL) self.__releaseKey(Key.SHIFT) char_as_hex_string = '{:X}'.format(ord(char)) for hex_char in char_as_hex_string: hexKeyCodeList = self.localDisplay.keysym_to_keycodes(ord(hex_char)) hexKeyCode, hexOffset = self.__findUsableKeycode(hexKeyCodeList) self.__sendKeyCode(hexKeyCode) self.__pressKey(Key.ENTER) else: self.__sendKeyCode(keyCode, theWindow=focus) if offset == 1: self.__pressKey(Key.SHIFT) self.__sendKeyCode(keyCode, self.modMasks[Key.SHIFT], focus) self.__releaseKey(Key.SHIFT) if offset == 4: self.__pressKey(Key.ALT_GR) self.__sendKeyCode(keyCode, self.modMasks[Key.ALT_GR], focus) self.__releaseKey(Key.ALT_GR) if offset == 5: self.__pressKey(Key.ALT_GR) self.__pressKey(Key.SHIFT) self.__sendKeyCode(keyCode, self.modMasks[Key.ALT_GR]|self.modMasks[Key.SHIFT], focus) self.__releaseKey(Key.SHIFT) self.__releaseKey(Key.ALT_GR) elif char in self.remappedChars: keyCode, offset = self.remappedChars[char] if offset == 0: self.__sendKeyCode(keyCode, theWindow=focus) if offset == 1: self.__pressKey(Key.SHIFT) self.__sendKeyCode(keyCode, self.modMasks[Key.SHIFT], focus) self.__releaseKey(Key.SHIFT) else: logger.warning("Unable to send character %r", char) except Exception as e: logger.exception("Error sending char %r: %s", char, str(e)) self.__ignoreRemap = False def send_key(self, keyName): """ Send a specific non-printing key, eg Up, Left, etc """ self.__enqueue(self.__sendKey, keyName) def __sendKey(self, keyName): logger.debug("Send special key: [%r]", keyName) self.__sendKeyCode(self.__lookupKeyCode(keyName)) def fake_keypress(self, keyName): self.__enqueue(self.__fakeKeypress, keyName) def __fakeKeypress(self, keyName): keyCode = self.__lookupKeyCode(keyName) xtest.fake_input(self.rootWindow, X.KeyPress, keyCode) xtest.fake_input(self.rootWindow, X.KeyRelease, keyCode) def fake_keydown(self, keyName): self.__enqueue(self.__fakeKeydown, keyName) def __fakeKeydown(self, keyName): keyCode = self.__lookupKeyCode(keyName) xtest.fake_input(self.rootWindow, X.KeyPress, keyCode) def fake_keyup(self, keyName): self.__enqueue(self.__fakeKeyup, keyName) def __fakeKeyup(self, keyName): keyCode = self.__lookupKeyCode(keyName) xtest.fake_input(self.rootWindow, X.KeyRelease, keyCode) def send_modified_key(self, keyName, modifiers): """ Send a modified key (e.g. when emulating a hotkey) """ self.__enqueue(self.__sendModifiedKey, keyName, modifiers) def __sendModifiedKey(self, keyName, modifiers): logger.debug("Send modified key: modifiers: %s key: %s", modifiers, keyName) try: mask = 0 for mod in modifiers: mask |= self.modMasks[mod] keyCode = self.__lookupKeyCode(keyName) for mod in modifiers: self.__pressKey(mod) self.__sendKeyCode(keyCode, mask) for mod in modifiers: self.__releaseKey(mod) except Exception as e: logger.warning("Error sending modified key %r %r: %s", modifiers, keyName, str(e)) def send_mouse_click(self, xCoord, yCoord, button, relative): self.__enqueue(self.__sendMouseClick, xCoord, yCoord, button, relative) def __sendMouseClick(self, xCoord, yCoord, button, relative): # Get current pointer position so we can return it there pos = self.rootWindow.query_pointer() if relative: focus = self.localDisplay.get_input_focus().focus focus.warp_pointer(xCoord, yCoord) xtest.fake_input(focus, X.ButtonPress, button, x=xCoord, y=yCoord) xtest.fake_input(focus, X.ButtonRelease, button, x=xCoord, y=yCoord) else: self.rootWindow.warp_pointer(xCoord, yCoord) xtest.fake_input(self.rootWindow, X.ButtonPress, button, x=xCoord, y=yCoord) xtest.fake_input(self.rootWindow, X.ButtonRelease, button, x=xCoord, y=yCoord) self.rootWindow.warp_pointer(pos.root_x, pos.root_y) self.__flush() def mouse_press(self, xCoord, yCoord, button): self.__enqueue(self.__mousePress, xCoord, yCoord, button) def __mousePress(self, xCoord, yCoord, button): focus = self.localDisplay.get_input_focus().focus xtest.fake_input(focus, X.ButtonPress, button, x=xCoord, y=yCoord) self.__flush() def mouse_release(self, xCoord, yCoord, button): self.__enqueue(self.__mouseRelease, xCoord, yCoord, button) def __mouseRelease(self, xCoord, yCoord, button): focus = self.localDisplay.get_input_focus().focus xtest.fake_input(focus, X.ButtonRelease, button, x=xCoord, y=yCoord) self.__flush() def mouse_location(self): pos = self.rootWindow.query_pointer() return (pos.root_x, pos.root_y) def relative_mouse_location(self, window=None): #return relative mouse location within given window if window==None: window = self.localDisplay.get_input_focus().focus pos = window.query_pointer() return (pos.win_x, pos.win_y) def scroll_down(self, number): for i in range(0, number): self.__enqueue(self.__scroll, Button.SCROLL_DOWN) def scroll_up(self, number): for i in range(0, number): self.__enqueue(self.__scroll, Button.SCROLL_UP) def __scroll(self, button): focus = self.localDisplay.get_input_focus().focus x,y = self.mouse_location() xtest.fake_input(self=focus, event_type=X.ButtonPress, detail=button, x=x, y=y) xtest.fake_input(self=focus, event_type=X.ButtonRelease, detail=button, x=x, y=y) self.__flush() def move_cursor(self, xCoord, yCoord, relative=False, relative_self=False): self.__enqueue(self.__moveCursor, xCoord, yCoord, relative, relative_self) def __moveCursor(self, xCoord, yCoord, relative=False, relative_self=False): if relative: focus = self.localDisplay.get_input_focus().focus focus.warp_pointer(xCoord, yCoord) self.__flush() return if relative_self: pos = self.rootWindow.query_pointer() xCoord += pos.root_x yCoord += pos.root_y self.rootWindow.warp_pointer(xCoord,yCoord) self.__flush() def send_mouse_click_relative(self, xoff, yoff, button): self.__enqueue(self.__sendMouseClickRelative, xoff, yoff, button) def __sendMouseClickRelative(self, xoff, yoff, button): # Get current pointer position pos = self.rootWindow.query_pointer() xCoord = pos.root_x + xoff yCoord = pos.root_y + yoff self.rootWindow.warp_pointer(xCoord, yCoord) xtest.fake_input(self.rootWindow, X.ButtonPress, button, x=xCoord, y=yCoord) xtest.fake_input(self.rootWindow, X.ButtonRelease, button, x=xCoord, y=yCoord) self.rootWindow.warp_pointer(pos.root_x, pos.root_y) self.__flush() def flush(self): self.__enqueue(self.__flush) def __flush(self): self.localDisplay.flush() self.lastChars = [] def press_key(self, keyName): self.__enqueue(self.__pressKey, keyName) def __pressKey(self, keyName): self.__sendKeyPressEvent(self.__lookupKeyCode(keyName), 0) def release_key(self, keyName): self.__enqueue(self.__releaseKey, keyName) def __releaseKey(self, keyName): self.__sendKeyReleaseEvent(self.__lookupKeyCode(keyName), 0) def __flushEvents(self): logger.debug("__flushEvents: Entering event loop.") while True: try: readable, w, e = select.select([self.localDisplay], [], [], 1) time.sleep(1) if self.localDisplay in readable: createdWindows = [] destroyedWindows = [] for x in range(self.localDisplay.pending_events()): event = self.localDisplay.next_event() if event.type == X.CreateNotify: createdWindows.append(event.window) if event.type == X.DestroyNotify: destroyedWindows.append(event.window) if event.type == X.MappingNotify: logger.debug("X Mapping Event Detected") self.on_keys_changed() for window in createdWindows: if window not in destroyedWindows: self.__enqueue(self.__grabHotkeysForWindow, window) if self.shutdown: break except ConnectionClosedError: # Autokey does not properly exit on logout. It causes an infinite exception loop, accumulating stack # traces along. This acts like a memory leak, filling the system RAM until it hits an OOM condition. # TODO: implement a proper exit mechanic that gracefully exits AutoKey in this case. # Maybe react to a dbus message that announces the session end, before the X server forcefully closes # the connection. # See https://github.com/autokey/autokey/issues/198 for details logger.exception("__flushEvents: Connection to the X server closed. Forcefully exiting Autokey now.") import os os._exit(1) except Exception: logger.exception("__flushEvents: Some exception occured:") pass logger.debug("__flushEvents: Left event loop.") def handle_keypress(self, keyCode): self.__enqueue(self.__handleKeyPress, keyCode) def __handleKeyPress(self, keyCode): focus = self.localDisplay.get_input_focus().focus modifier = self.__decodeModifier(keyCode) if modifier is not None: self.mediator.handle_modifier_down(modifier) else: window_info = self.get_window_info(focus) self.mediator.handle_keypress(keyCode, window_info) def handle_keyrelease(self, keyCode): self.__enqueue(self.__handleKeyrelease, keyCode) def __handleKeyrelease(self, keyCode): modifier = self.__decodeModifier(keyCode) if modifier is not None: self.mediator.handle_modifier_up(modifier) def handle_mouseclick(self, button, x, y): self.__enqueue(self.__handleMouseclick, button, x, y) def __handleMouseclick(self, button, x, y): # Sleep a bit to timing issues. A mouse click might change the active application. # If so, the switch happens asynchronously somewhere during the execution of the first two queries below, # causing the queried window title (and maybe the window class or even none of those) to be invalid. time.sleep(0.005) # TODO: may need some tweaking window_info = self.get_window_info() if x is None and y is None: ret = self.localDisplay.get_input_focus().focus.query_pointer() self.mediator.handle_mouse_click(ret.root_x, ret.root_y, ret.win_x, ret.win_y, button, window_info) else: focus = self.localDisplay.get_input_focus().focus try: rel = focus.translate_coords(self.rootWindow, x, y) self.mediator.handle_mouse_click(x, y, rel.x, rel.y, button, window_info) except: self.mediator.handle_mouse_click(x, y, 0, 0, button, window_info) def __decodeModifier(self, keyCode): """ Checks if the given keyCode is a modifier key. If it is, returns the modifier name constant as defined in the iomediator module. If not, returns C{None} """ keyName = self.lookup_string(keyCode, False, False, False) if keyName in MODIFIERS: return keyName return None def __sendKeyCode(self, keyCode, modifiers=0, theWindow=None): if cm.ConfigManager.SETTINGS[cm_constants.ENABLE_QT4_WORKAROUND] or self.__enableQT4Workaround: self.__doQT4Workaround(keyCode) self.__sendKeyPressEvent(keyCode, modifiers, theWindow) self.__sendKeyReleaseEvent(keyCode, modifiers, theWindow) def __checkWorkaroundNeeded(self): focus = self.localDisplay.get_input_focus().focus window_info = self.get_window_info(focus) w = self.app.configManager.workAroundApps if w.match(window_info.wm_title) or w.match(window_info.wm_class): self.__enableQT4Workaround = True else: self.__enableQT4Workaround = False def __doQT4Workaround(self, keyCode): if len(self.lastChars) > 0: if keyCode in self.lastChars: self.localDisplay.flush() time.sleep(0.0125) self.lastChars.append(keyCode) if len(self.lastChars) > 10: self.lastChars.pop(0) def __sendKeyPressEvent(self, keyCode, modifiers, theWindow=None): if theWindow is None: focus = self.localDisplay.get_input_focus().focus else: focus = theWindow keyEvent = event.KeyPress( detail=keyCode, time=X.CurrentTime, root=self.rootWindow, window=focus, child=X.NONE, root_x=1, root_y=1, event_x=1, event_y=1, state=modifiers, same_screen=1 ) focus.send_event(keyEvent) def __sendKeyReleaseEvent(self, keyCode, modifiers, theWindow=None): if theWindow is None: focus = self.localDisplay.get_input_focus().focus else: focus = theWindow keyEvent = event.KeyRelease( detail=keyCode, time=X.CurrentTime, root=self.rootWindow, window=focus, child=X.NONE, root_x=1, root_y=1, event_x=1, event_y=1, state=modifiers, same_screen=1 ) focus.send_event(keyEvent) def __lookupKeyCode(self, char: str) -> int: if char in AK_TO_XK_MAP: return self.localDisplay.keysym_to_keycode(AK_TO_XK_MAP[char]) elif char.startswith(" WindowInfo: try: if window is None: window = self.localDisplay.get_input_focus().focus return self._get_window_info(window, traverse) except error.BadWindow: logger.warning("Got BadWindow error while requesting window information.") return self._create_window_info(window, "", "") def _get_window_info(self, window, traverse: bool, wm_title: str=None, wm_class: str=None) -> WindowInfo: new_wm_title = self._try_get_window_title(window) new_wm_class = self._try_get_window_class(window) if not wm_title and new_wm_title: # Found title, update known information wm_title = new_wm_title if not wm_class and new_wm_class: # Found class, update known information wm_class = new_wm_class if traverse: # Recursive operation on the parent window if wm_title and wm_class: # Both known, abort walking the tree and return the data. return self._create_window_info(window, wm_title, wm_class) else: # At least one property is still not known. So walk the window tree up. parent = window.query_tree().parent # Stop traversal, if the parent is not a window. When querying the parent, at some point, an integer # is returned. Then just stop following the tree. if isinstance(parent, int): # At this point, wm_title or wm_class may still be None. The recursive call with traverse=False # will replace any None with an empty string. See below. return self._get_window_info(window, False, wm_title, wm_class) else: return self._get_window_info(parent, traverse, wm_title, wm_class) else: # No recursion, so fill unknown values with empty strings. if wm_title is None: wm_title = "" if wm_class is None: wm_class = "" return self._create_window_info(window, wm_title, wm_class) def _create_window_info(self, window, wm_title: str, wm_class: str): """ Creates a WindowInfo object from the window title and WM_CLASS. Also checks for the Java XFocusProxyWindow workaround and applies it if needed: Workaround for Java applications: Java AWT uses a XFocusProxyWindow class, so to get usable information, the parent window needs to be queried. Credits: https://github.com/mooz/xkeysnail/pull/32 https://github.com/JetBrains/jdk8u_jdk/blob/master/src/solaris/classes/sun/awt/X11/XFocusProxyWindow.java#L35 """ if "FocusProxy" in wm_class: parent = window.query_tree().parent # Discard both the already known wm_class and window title, because both are known to be wrong. return self._get_window_info(parent, False) else: return WindowInfo(wm_title=wm_title, wm_class=wm_class) def _try_get_window_title(self, window) -> typing.Optional[str]: atom = self._try_read_property(window, self.__VisibleNameAtom) if atom is None: atom = self._try_read_property(window, self.__NameAtom) if atom: value = atom.value # type: typing.Union[str, bytes] # based on python3-xlib version, atom.value may be a bytes object, then decoding is necessary. return value.decode("utf-8") if isinstance(value, bytes) else value else: return None @staticmethod def _try_read_property(window, property_name: str): """ Try to read the given property of the given window. Returns the atom, if successful, None otherwise. """ try: return window.get_property(property_name, 0, 0, 255) except error.BadAtom: return None @staticmethod def _try_get_window_class(window) -> typing.Optional[str]: wm_class = window.get_wm_class() if wm_class: return "{}.{}".format(wm_class[0], wm_class[1]) else: return None def get_window_title(self, window=None, traverse=True) -> str: return self.get_window_info(window, traverse).wm_title def get_window_class(self, window=None, traverse=True) -> str: return self.get_window_info(window, traverse).wm_class def cancel(self): logger.debug("XInterfaceBase: Try to exit event thread.") self.queue.put_nowait((None, None)) logger.debug("XInterfaceBase: Event thread exit marker enqueued.") self.shutdown = True logger.debug("XInterfaceBase: self.shutdown set to True. This should stop the listener thread.") self.listenerThread.join() self.eventThread.join() self.localDisplay.flush() self.localDisplay.close() self.join() class XRecordInterface(XInterfaceBase): def initialise(self): self.recordDisplay = display.Display() self.__locksChecked = False # Check for record extension if not self.recordDisplay.has_extension("RECORD"): raise Exception("Your X-Server does not have the RECORD extension available/enabled.") def run(self): # Create a recording context; we only want key and mouse events self.ctx = self.recordDisplay.record_create_context( 0, [record.AllClients], [{ 'core_requests': (0, 0), 'core_replies': (0, 0), 'ext_requests': (0, 0, 0, 0), 'ext_replies': (0, 0, 0, 0), 'delivered_events': (0, 0), 'device_events': (X.KeyPress, X.ButtonPress), #X.KeyRelease, 'errors': (0, 0), 'client_started': False, 'client_died': False, }]) # Enable the context; this only returns after a call to record_disable_context, # while calling the callback function in the meantime logger.info("XRecord interface thread starting") self.recordDisplay.record_enable_context(self.ctx, self.__processEvent) # Finally free the context self.recordDisplay.record_free_context(self.ctx) self.recordDisplay.close() def cancel(self): self.localDisplay.record_disable_context(self.ctx) XInterfaceBase.cancel(self) def __processEvent(self, reply): if reply.category != record.FromServer: return if reply.client_swapped: return if not len(reply.data) or str_or_bytes_to_bytes(reply.data)[0] < 2: # not an event return data = reply.data while len(data): event, data = rq.EventField(None).parse_binary_value(data, self.recordDisplay.display, None, None) if event.type == X.KeyPress: self.handle_keypress(event.detail) elif event.type == X.KeyRelease: self.handle_keyrelease(event.detail) elif event.type == X.ButtonPress: self.handle_mouseclick(event.detail, event.root_x, event.root_y) class AtSpiInterface(XInterfaceBase): def initialise(self): self.registry = pyatspi.Registry def start(self): logger.info("AT-SPI interface thread starting") self.registry.registerKeystrokeListener(self.__processKeyEvent, mask=pyatspi.allModifiers()) self.registry.registerEventListener(self.__processMouseEvent, 'mouse:button') def cancel(self): self.registry.deregisterKeystrokeListener(self.__processKeyEvent, mask=pyatspi.allModifiers()) self.registry.deregisterEventListener(self.__processMouseEvent, 'mouse:button') self.registry.stop() XInterfaceBase.cancel(self) def __processKeyEvent(self, event): if event.type == pyatspi.KEY_PRESSED_EVENT: self.handle_keypress(event.hw_code) else: self.handle_keyrelease(event.hw_code) def __processMouseEvent(self, event): if event.type[-1] == 'p': button = int(event.type[-2]) self.handle_mouseclick(button, event.detail1, event.detail2) def __pumpEvents(self): pyatspi.Registry.pumpQueuedEvents() return True from autokey.model.key import Key, MODIFIERS import autokey.configmanager.configmanager as cm XK.load_keysym_group('xkb') XK_TO_AK_MAP = { XK.XK_Shift_L: Key.SHIFT, XK.XK_Shift_R: Key.SHIFT, XK.XK_Caps_Lock: Key.CAPSLOCK, XK.XK_Control_L: Key.CONTROL, XK.XK_Control_R: Key.CONTROL, XK.XK_Alt_L: Key.ALT, XK.XK_Alt_R: Key.ALT, XK.XK_ISO_Level3_Shift: Key.ALT_GR, XK.XK_Super_L: Key.SUPER, XK.XK_Super_R: Key.SUPER, XK.XK_Hyper_L: Key.HYPER, XK.XK_Hyper_R: Key.HYPER, XK.XK_Meta_L: Key.META, XK.XK_Meta_R: Key.META, XK.XK_Num_Lock: Key.NUMLOCK, #SPACE: Key.SPACE, XK.XK_Tab: Key.TAB, XK.XK_Left: Key.LEFT, XK.XK_Right: Key.RIGHT, XK.XK_Up: Key.UP, XK.XK_Down: Key.DOWN, XK.XK_Return: Key.ENTER, XK.XK_BackSpace: Key.BACKSPACE, XK.XK_Scroll_Lock: Key.SCROLL_LOCK, XK.XK_Print: Key.PRINT_SCREEN, XK.XK_Pause: Key.PAUSE, XK.XK_Menu: Key.MENU, XK.XK_F1: Key.F1, XK.XK_F2: Key.F2, XK.XK_F3: Key.F3, XK.XK_F4: Key.F4, XK.XK_F5: Key.F5, XK.XK_F6: Key.F6, XK.XK_F7: Key.F7, XK.XK_F8: Key.F8, XK.XK_F9: Key.F9, XK.XK_F10: Key.F10, XK.XK_F11: Key.F11, XK.XK_F12: Key.F12, XK.XK_F13: Key.F13, XK.XK_F14: Key.F14, XK.XK_F15: Key.F15, XK.XK_F16: Key.F16, XK.XK_F17: Key.F17, XK.XK_F18: Key.F18, XK.XK_F19: Key.F19, XK.XK_F20: Key.F20, XK.XK_F21: Key.F21, XK.XK_F22: Key.F22, XK.XK_F23: Key.F23, XK.XK_F24: Key.F24, XK.XK_F25: Key.F25, XK.XK_F26: Key.F26, XK.XK_F27: Key.F27, XK.XK_F28: Key.F28, XK.XK_F29: Key.F29, XK.XK_F30: Key.F30, XK.XK_F31: Key.F31, XK.XK_F32: Key.F32, XK.XK_F33: Key.F33, XK.XK_F34: Key.F34, XK.XK_F35: Key.F35, XK.XK_Escape: Key.ESCAPE, XK.XK_Insert: Key.INSERT, XK.XK_Delete: Key.DELETE, XK.XK_Home: Key.HOME, XK.XK_End: Key.END, XK.XK_Page_Up: Key.PAGE_UP, XK.XK_Page_Down: Key.PAGE_DOWN, XK.XK_KP_Insert: Key.NP_INSERT, XK.XK_KP_Delete: Key.NP_DELETE, XK.XK_KP_End: Key.NP_END, XK.XK_KP_Down: Key.NP_DOWN, XK.XK_KP_Page_Down: Key.NP_PAGE_DOWN, XK.XK_KP_Left: Key.NP_LEFT, XK.XK_KP_Begin: Key.NP_5, XK.XK_KP_Right: Key.NP_RIGHT, XK.XK_KP_Home: Key.NP_HOME, XK.XK_KP_Up: Key.NP_UP, XK.XK_KP_Page_Up: Key.NP_PAGE_UP, XK.XK_KP_Divide: Key.NP_DIVIDE, XK.XK_KP_Multiply: Key.NP_MULTIPLY, XK.XK_KP_Add: Key.NP_ADD, XK.XK_KP_Subtract: Key.NP_SUBTRACT, XK.XK_KP_Enter: Key.ENTER, XK.XK_space: ' ' } AK_TO_XK_MAP = dict((v,k) for k, v in XK_TO_AK_MAP.items()) XK_TO_AK_NUMLOCKED = { XK.XK_KP_Insert: "0", XK.XK_KP_Delete: ".", XK.XK_KP_End: "1", XK.XK_KP_Down: "2", XK.XK_KP_Page_Down: "3", XK.XK_KP_Left: "4", XK.XK_KP_Begin: "5", XK.XK_KP_Right: "6", XK.XK_KP_Home: "7", XK.XK_KP_Up: "8", XK.XK_KP_Page_Up: "9", XK.XK_KP_Divide: "/", XK.XK_KP_Multiply: "*", XK.XK_KP_Add: "+", XK.XK_KP_Subtract: "-", XK.XK_KP_Enter: Key.ENTER } autokey-0.96.0/lib/autokey/iomediator/000077500000000000000000000000001427671440700176665ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/iomediator/__init__.py000066400000000000000000000000001427671440700217650ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/iomediator/constants.py000066400000000000000000000000371427671440700222540ustar00rootroot00000000000000X_RECORD_INTERFACE = "XRecord" autokey-0.96.0/lib/autokey/iomediator/iomediator.py000066400000000000000000000240771427671440700224060ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import threading import queue from autokey.configmanager.configmanager import ConfigManager from autokey.configmanager.configmanager_constants import INTERFACE_TYPE from autokey.interface import XRecordInterface, AtSpiInterface from autokey.model.phrase import SendMode from autokey.model.key import Key, KEY_SPLIT_RE, MODIFIERS, HELD_MODIFIERS from .constants import X_RECORD_INTERFACE from .waiter import Waiter CURRENT_INTERFACE = None logger = __import__("autokey.logger").logger.get_logger(__name__) class IoMediator(threading.Thread): """ The IoMediator is responsible for tracking the state of modifier keys and interfacing with the various Interface classes to obtain the correct characters to pass to the expansion service. This class must not store or maintain any configuration details. """ # List of targets interested in receiving keypress, hotkey and mouse events listeners = [] def __init__(self, service): threading.Thread.__init__(self, name="KeypressHandler-thread") self.queue = queue.Queue() self.listeners.append(service) self.interfaceType = ConfigManager.SETTINGS[INTERFACE_TYPE] self.waiter = Waiter # Modifier tracking self.modifiers = { Key.CONTROL: False, Key.ALT: False, Key.ALT_GR: False, Key.SHIFT: False, Key.SUPER: False, Key.HYPER: False, Key.META: False, Key.CAPSLOCK: False, Key.NUMLOCK: False } if self.interfaceType == X_RECORD_INTERFACE: self.interface = XRecordInterface(self, service.app) else: self.interface = AtSpiInterface(self, service.app) global CURRENT_INTERFACE CURRENT_INTERFACE = self.interface logger.info("Created IoMediator instance, current interface is: {}".format(CURRENT_INTERFACE)) def shutdown(self): logger.debug("IoMediator shutting down") self.interface.cancel() self.queue.put_nowait((None, None)) logger.debug("Waiting for IoMediator thread to end") self.join() logger.debug("IoMediator shutdown completed") # Callback methods for Interfaces ---- def set_modifier_state(self, modifier, state): logger.debug("Set modifier %s to %r", modifier, state) self.modifiers[modifier] = state def handle_modifier_down(self, modifier): """ Updates the state of the given modifier key to 'pressed' """ logger.debug("%s pressed", modifier) if modifier in (Key.CAPSLOCK, Key.NUMLOCK): if self.modifiers[modifier]: self.modifiers[modifier] = False else: self.modifiers[modifier] = True else: self.modifiers[modifier] = True def handle_modifier_up(self, modifier): """ Updates the state of the given modifier key to 'released'. """ logger.debug("%s released", modifier) # Caps and num lock are handled on key down only if modifier not in (Key.CAPSLOCK, Key.NUMLOCK): self.modifiers[modifier] = False def handle_keypress(self, key_code, window_info): """ Looks up the character for the given key code, applying any modifiers currently in effect, and passes it to the expansion service. """ self.queue.put_nowait((key_code, window_info)) def run(self): while True: key_code, window_info = self.queue.get() if key_code is None and window_info is None: break num_lock = self.modifiers[Key.NUMLOCK] modifiers = self._get_modifiers_on() shifted = self.modifiers[Key.CAPSLOCK] ^ self.modifiers[Key.SHIFT] key = self.interface.lookup_string(key_code, shifted, num_lock, self.modifiers[Key.ALT_GR]) raw_key = self.interface.lookup_string(key_code, False, False, False) # We make a copy here because the wait_for... functions modify the listeners, # and we want this processing cycle to complete before changing what happens for target in self.listeners.copy(): target.handle_keypress(raw_key, modifiers, key, window_info) self.queue.task_done() def handle_mouse_click(self, root_x, root_y, rel_x, rel_y, button, window_info): # We make a copy here because the wait_for... functions modify the listeners, # and we want this processing cycle to complete before changing what happens for target in self.listeners.copy(): target.handle_mouseclick(root_x, root_y, rel_x, rel_y, button, window_info) # Methods for expansion service ---- def send_string(self, string: str): """ Sends the given string for output. """ if not string: return string = string.replace('\n', "") string = string.replace('\t', "") logger.debug("Send via event interface") self._clear_modifiers() modifiers = [] for section in KEY_SPLIT_RE.split(string): if len(section) > 0: if Key.is_key(section[:-1]) and section[-1] == '+' and section[:-1] in MODIFIERS: # Section is a modifier application (modifier followed by '+') modifiers.append(section[:-1]) else: if len(modifiers) > 0: # Modifiers ready for application - send modified key if Key.is_key(section): self.interface.send_modified_key(section, modifiers) modifiers = [] else: self.interface.send_modified_key(section[0], modifiers) if len(section) > 1: self.interface.send_string(section[1:]) modifiers = [] else: # Normal string/key operation if Key.is_key(section): self.interface.send_key(section) else: self.interface.send_string(section) self._reapply_modifiers() def paste_string(self, string, paste_command: SendMode): if len(string) > 0: logger.debug("Send via clipboard") self.interface.send_string_clipboard(string, paste_command) def remove_string(self, string): backspaces = -1 # Start from -1 to discount the backspace already pressed by the user for section in KEY_SPLIT_RE.split(string): if Key.is_key(section): # TODO: Only a subset of keys defined in Key are printable, thus require a backspace. # Many keys are not printable, like the modifier keys or F-Keys. # If the current key is a modifier, it may affect the printability of the next character. # For example, if section == , and the next section begins with "+a", both the "+" and "a" are not # printable, because both belong to the keyboard combination "+a" backspaces += 1 else: backspaces += len(section) self.send_backspace(backspaces) def send_key(self, key_name): key_name = key_name.replace('\n', "") self.interface.send_key(key_name) def press_key(self, key_name): key_name = key_name.replace('\n', "") self.interface.fake_keydown(key_name) def release_key(self, key_name): key_name = key_name.replace('\n', "") self.interface.fake_keyup(key_name) def fake_keypress(self, key_name): key_name = key_name.replace('\n', "") self.interface.fake_keypress(key_name) def send_left(self, count): """ Sends the given number of left key presses. """ for i in range(count): self.interface.send_key(Key.LEFT) def send_right(self, count): for i in range(count): self.interface.send_key(Key.RIGHT) def send_up(self, count): """ Sends the given number of up key presses. """ for i in range(count): self.interface.send_key(Key.UP) def send_backspace(self, count): """ Sends the given number of backspace key presses. """ for i in range(count): self.interface.send_key(Key.BACKSPACE) def flush(self): self.interface.flush() # Utility methods ---- def _clear_modifiers(self): self.releasedModifiers = [] for modifier in list(self.modifiers.keys()): if self.modifiers[modifier] and modifier not in (Key.CAPSLOCK, Key.NUMLOCK): self.releasedModifiers.append(modifier) self.interface.release_key(modifier) def _reapply_modifiers(self): for modifier in self.releasedModifiers: self.interface.press_key(modifier) def _get_modifiers_on(self): modifiers = [] for modifier in HELD_MODIFIERS: if self.modifiers[modifier]: modifiers.append(modifier) modifiers.sort() return modifiers autokey-0.96.0/lib/autokey/iomediator/keygrabber.py000066400000000000000000000111761427671440700223630ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import datetime import time from .iomediator import IoMediator from autokey.model.key import Key, MODIFIERS from . import iomediator class KeyGrabber: """ Keygrabber used by the hotkey settings dialog to grab the key pressed """ def __init__(self, parent): self.target_parent = parent def start(self): # In QT version, sometimes the mouse click event arrives before we finish initialising # sleep slightly to prevent this time.sleep(0.1) IoMediator.listeners.append(self) iomediator.CURRENT_INTERFACE.grab_keyboard() def handle_keypress(self, raw_key, modifiers, key, *args): if raw_key not in MODIFIERS: IoMediator.listeners.remove(self) self.target_parent.set_key(raw_key, modifiers) iomediator.CURRENT_INTERFACE.ungrab_keyboard() def handle_mouseclick(self, root_x, root_y, rel_x, rel_y, button, window_info): IoMediator.listeners.remove(self) iomediator.CURRENT_INTERFACE.ungrab_keyboard() self.target_parent.cancel_grab() class Recorder(KeyGrabber): """ Recorder used by the record macro functionality """ def __init__(self, parent): KeyGrabber.__init__(self, parent) self.insideKeys = False self.start_time = .0 self.delay = .0 self.delay_finished = False self.record_keyboard = self.record_mouse = False def start(self, delay: float): time.sleep(0.1) IoMediator.listeners.append(self) self.target_parent.start_record() self.start_time = time.time() self.delay = delay self.delay_finished = False def start_withgrab(self): time.sleep(0.1) IoMediator.listeners.append(self) self.target_parent.start_record() self.start_time = time.time() self.delay = 0 self.delay_finished = True iomediator.CURRENT_INTERFACE.grab_keyboard() def stop(self): if self in IoMediator.listeners: IoMediator.listeners.remove(self) if self.insideKeys: self.target_parent.end_key_sequence() self.insideKeys = False def stop_withgrab(self): iomediator.CURRENT_INTERFACE.ungrab_keyboard() if self in IoMediator.listeners: IoMediator.listeners.remove(self) if self.insideKeys: self.target_parent.end_key_sequence() self.insideKeys = False def set_record_keyboard(self, record: bool): self.record_keyboard = record def set_record_mouse(self, record: bool): self.record_mouse = record def _delay_passed(self) -> bool: if not self.delay_finished: now = time.time() delta = datetime.datetime.utcfromtimestamp(now - self.start_time) self.delay_finished = (delta.second > self.delay) return self.delay_finished def handle_keypress(self, raw_key, modifiers, key, *args): if self.record_keyboard and self._delay_passed(): if not self.insideKeys: self.insideKeys = True self.target_parent.start_key_sequence() modifier_count = len(modifiers) # TODO: This check assumes that Key.SHIFT is the only case shifting modifier. What about ISO_Level3_Shift # or ISO_Level5_Lock? if modifier_count > 1 or (modifier_count == 1 and Key.SHIFT not in modifiers) or \ (Key.SHIFT in modifiers and len(raw_key) > 1): self.target_parent.append_hotkey(raw_key, modifiers) elif key not in MODIFIERS: self.target_parent.append_key(key) def handle_mouseclick(self, root_x, root_y, rel_x, rel_y, button, window_info): if self.record_mouse and self._delay_passed(): if self.insideKeys: self.insideKeys = False self.target_parent.end_key_sequence() self.target_parent.append_mouseclick(rel_x, rel_y, button, window_info[0]) autokey-0.96.0/lib/autokey/iomediator/waiter.py000066400000000000000000000034251427671440700215370ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import threading # from .iomediator import IoMediator from typing import Callable, Any class Waiter: """ Waits for a specified event to occur """ def __init__(self, raw_key, modifiers, button, check: Callable[[Any,str,list,str], bool], name: str, time_out): # IoMediator.listeners.append(self) self.raw_key = raw_key self.modifiers = modifiers self.button = button self.event = threading.Event() self.check = check self.name = name self.time_out = time_out self.result = '' if modifiers is not None: self.modifiers.sort() def wait(self): return self.event.wait(self.time_out) def handle_keypress(self, raw_key, modifiers, key, *args): if (raw_key == self.raw_key and modifiers == self.modifiers) or (self.check is not None and self.check(self, raw_key, modifiers, key, *args)): # IoMediator.listeners.remove(self) self.event.set() def handle_mouseclick(self, root_x, root_y, rel_x, rel_y, button, window_info): if button == self.button: self.event.set() autokey-0.96.0/lib/autokey/iomediator/windowgrabber.py000066400000000000000000000024241427671440700230760ustar00rootroot00000000000000# -*- coding: utf-8 -*- # Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import time import threading from .iomediator import IoMediator SEND_LOCK = threading.Lock() # TODO: This is never accessed anywhere. Does creating this lock do anything? class WindowGrabber: def __init__(self, dialog): self.dialog = dialog def start(self): time.sleep(0.1) IoMediator.listeners.append(self) def handle_keypress(self, raw_key, modifiers, key, *args): pass def handle_mouseclick(self, root_x, root_y, rel_x, rel_y, button, window_info): IoMediator.listeners.remove(self) self.dialog.receive_window_info(window_info) autokey-0.96.0/lib/autokey/logger.py000066400000000000000000000043651427671440700173730ustar00rootroot00000000000000# Copyright (C) 2018-2019 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import logging import logging.handlers import pathlib import sys from autokey.argument_parser import Namespace from autokey.common import APP_NAME, DATA_DIR root_logger = logging.getLogger(APP_NAME) MAX_LOG_SIZE = 5 * 2**20 # 5 megabytes MAX_LOG_COUNT = 3 LOG_FORMAT = "%(asctime)s %(levelname)s - %(name)s - %(message)s" LOG_FILE = pathlib.Path(DATA_DIR) / "autokey.log" def get_logger(full_module_path: str) -> logging.Logger: module_path = ".".join(full_module_path.split(".")[1:]) return root_logger.getChild(module_path) def configure_root_logger(args: Namespace): """Initialise logging system""" root_logger.setLevel(1) pathlib.Path(DATA_DIR).mkdir(parents=True, exist_ok=True) file_handler = logging.handlers.RotatingFileHandler( LOG_FILE, maxBytes=MAX_LOG_SIZE, backupCount=MAX_LOG_COUNT ) file_handler.setLevel(logging.INFO) stdout_stream_handler = logging.StreamHandler(sys.stdout) logging_level = logging.INFO if args.verbose: logging_level = logging.DEBUG if args.mouse_logging: logging_level = 9 stdout_stream_handler.setLevel(logging_level) stdout_stream_handler.setFormatter(logging.Formatter(LOG_FORMAT)) root_logger.addHandler(stdout_stream_handler) root_logger.addHandler(file_handler) if args.cutelog_integration: socket_handler = logging.handlers.SocketHandler("127.0.0.1", 19996) # default listening address root_logger.addHandler(socket_handler) root_logger.info(f"""Connected logger "{root_logger.name}" to local log server.""") autokey-0.96.0/lib/autokey/macro.py000066400000000000000000000171661427671440700172200ustar00rootroot00000000000000import datetime from abc import abstractmethod import shlex from autokey.model.key import Key, KEY_SPLIT_RE from autokey import common if common.USING_QT: from PyQt5.QtWidgets import QAction def _(text: str, args: tuple=None): """localisation function, currently returns the identity. If args are given, those are used to format text using the old-style % formatting.""" if args: text = text % args return text class MacroAction(QAction): def __init__(self, menu, macro, callback): super(MacroAction, self).__init__(macro.TITLE, menu) self.macro = macro self.callback = callback self.triggered.connect(self.on_triggered) def on_triggered(self): self.callback(self.macro) else: from gi.repository import Gtk # Escape any escaped angle brackets def encode_escaped_brackets(s): # If you need a literal '\' at the end of the macro args... IDK. Add a # space before the >? # If you need a literal \>, just add an extra \. # s.replace("\\\\", chr(27)) # ASCII Escape # Use arbitrary nonprinting ascii to represent escaped char. # Easier than having to parse escape chars. s = s.replace("\\<", chr(0x1e)) # Record seperator s = s.replace("\\>", chr(0x1f)) # unit seperator # s.replace(chr(27), "\\") return s def decode_escaped_brackets(s): s = s.replace(chr(0x1e), '<') # Record seperator s = s.replace(chr(0x1f), '>') # unit seperator return s def sections_decode_escaped_brackets(sections): for i, s in enumerate(sections): sections[i] = decode_escaped_brackets(s) # This must be passed a string containing only one macro. def extract_tag(s): if not isinstance(s, str): raise TypeError extracted = [p.split('>')[0] for p in s.split('<') if '>' in p] if len(extracted) == 0: return s else: return ''.join(extracted) def split_key_val(s): # Split as if a shell argument. # Splits at spaces, but preserves spaces within quotes. pairs = shlex.split(s) return dict(pair.split('=', 1) for pair in pairs) class MacroManager: def __init__(self, engine): self.macros = [] self.macros.append(ScriptMacro(engine)) self.macros.append(DateMacro()) self.macros.append(FileContentsMacro()) self.macros.append(CursorMacro()) self.macros.append(SystemMacro(engine)) def get_menu(self, callback, menu=None): if common.USING_QT: for macro in self.macros: menu.addAction(MacroAction(menu, macro, callback)) else: menu = Gtk.Menu() for macro in self.macros: menuItem = Gtk.MenuItem(macro.TITLE) menuItem.connect("activate", callback, macro) menu.append(menuItem) menu.show_all() return menu # Split expansion.string, expand and process its macros, then # replace with the results. def process_expansion_macros(self, content): # Split into sections with <> macros in them. # Using the Key split regex works for now. content = encode_escaped_brackets(content) content_sections = KEY_SPLIT_RE.split(content) for macroClass in self.macros: content_sections = macroClass.process(content_sections) return ''.join(content_sections) class AbstractMacro: @property @abstractmethod def ID(self): pass @property @abstractmethod def TITLE(self): pass @property @abstractmethod def ARGS(self): pass def get_token(self): ret = "<%s" % self.ID # TODO: v not used in initial implementation? This results in something like "<%s a= b= c=>" ret += "".join((" " + k + "=" for k, v in self.ARGS)) ret += ">" return ret def _get_args(self, macro): args = split_key_val(macro) expected_args = [arg[0] for arg in self.ARGS] expected_argnum = len(self.ARGS) for arg in expected_args: if arg not in args: raise ValueError("Missing mandatory argument '{}' for macro '{}'".format(arg, self.ID)) for arg in args: if arg not in expected_args: raise ValueError("Unexpected argument '{}' for macro '{}'".format(arg, self.ID)) return args def _extract_macro(self, section): content = extract_tag(section) content = decode_escaped_brackets(content) # type is space-separated from rest of macro. # Cursor macros have no space. if ' ' in content: macro_type, macro = content.split(' ', 1) else: macro_type, macro = (content, '') return macro_type, macro def process(self, sections): for i, section in enumerate(sections): # if MACRO_SPLIT_RE.match(section): if KEY_SPLIT_RE.match(section): macro_type, macro = self._extract_macro(sections[i]) if macro_type == self.ID: # parts and i are required for cursor macros. sections = self.do_process(sections, i) return sections @abstractmethod def do_process(self, sections, i): """ Returns updated sections """ # parts and i are required for cursor macros. return sections class CursorMacro(AbstractMacro): ID = "cursor" TITLE = _("Position cursor") ARGS = [] def do_process(self, sections, i): try: lefts = len(''.join(sections[i+1:])) sections.append(Key.LEFT * lefts) sections[i] = '' except IndexError: pass return sections class ScriptMacro(AbstractMacro): ID = "script" TITLE = _("Run script") ARGS = [("name", _("Name")), ("args", _("Arguments (comma separated)"))] def __init__(self, engine): self.engine = engine def do_process(self, sections, i): macro_type, macro = self._extract_macro(sections[i]) args = self._get_args(macro) self.engine.run_script_from_macro(args) sections[i] = self.engine._get_return_value() return sections class SystemMacro(AbstractMacro): ID = "system" TITLE = _("Run system command") ARGS = [("command", _("Command to be executed (including any arguments) - e.g. 'ls -l'")),] # ("getOutput", _("True or False, whether or not to set the return # value to the script's stdout (blocks until script finishes). If # false, "))] def __init__(self, engine): self.engine = engine def do_process(self, sections, i): macro_type, macro = self._extract_macro(sections[i]) args = self._get_args(macro) self.engine.run_system_command_from_macro(args) sections[i] = self.engine._get_return_value() return sections class DateMacro(AbstractMacro): ID = "date" TITLE = _("Insert date") ARGS = [("format", _("Format"))] def do_process(self, sections, i): macro_type, macro = self._extract_macro(sections[i]) format_ = self._get_args(macro)["format"] date = datetime.datetime.now() date = date.strftime(format_) sections[i] = date return sections class FileContentsMacro(AbstractMacro): ID = "file" TITLE = _("Insert file contents") ARGS = [("name", _("File name"))] def do_process(self, sections, i): macro_type, macro = self._extract_macro(sections[i]) name = self._get_args(macro)["name"] with open(name, "r") as inputFile: sections[i] = inputFile.read() return sections autokey-0.96.0/lib/autokey/model/000077500000000000000000000000001427671440700166325ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/model/__init__.py000066400000000000000000000000001427671440700207310ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/model/abstract_abbreviation.py000066400000000000000000000172541427671440700235450ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2019-2020 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import re import typing from autokey.model.helpers import DEFAULT_WORDCHAR_REGEX, TriggerMode class AbstractAbbreviation: """ Abstract class encapsulating the common functionality of an abbreviation list """ def __init__(self): self.abbreviations = [] # type: typing.List[str] self.backspace = True self.ignoreCase = False self.immediate = False self.triggerInside = False self.set_word_chars(DEFAULT_WORDCHAR_REGEX) def get_serializable(self): d = { "abbreviations": self.abbreviations, "backspace": self.backspace, "ignoreCase": self.ignoreCase, "immediate": self.immediate, "triggerInside": self.triggerInside, "wordChars": self.get_word_chars() } return d def load_from_serialized(self, data: dict): if "abbreviations" not in data: # check for pre v0.80.4 self.abbreviations = [data["abbreviation"]] # type: typing.List[str] else: self.abbreviations = data["abbreviations"] # type: typing.List[str] self.backspace = data["backspace"] self.ignoreCase = data["ignoreCase"] self.immediate = data["immediate"] self.triggerInside = data["triggerInside"] self.set_word_chars(data["wordChars"]) def copy_abbreviation(self, abbr): self.abbreviations = abbr.abbreviations self.backspace = abbr.backspace self.ignoreCase = abbr.ignoreCase self.immediate = abbr.immediate self.triggerInside = abbr.triggerInside self.set_word_chars(abbr.get_word_chars()) def set_word_chars(self, regex): self.wordChars = re.compile(regex, re.UNICODE) def get_word_chars(self): return self.wordChars.pattern def add_abbreviation(self, abbr): if not isinstance(abbr, str): raise ValueError("Abbreviations must be strings. Cannot add abbreviation '{}', having type {}.".format( abbr, type(abbr) )) self.abbreviations.append(abbr) if TriggerMode.ABBREVIATION not in self.modes: self.modes.append(TriggerMode.ABBREVIATION) def add_abbreviations(self, abbreviation_list: typing.Iterable[str]): if not isinstance(abbreviation_list, list): abbreviation_list = list(abbreviation_list) if not all(isinstance(abbr, str) for abbr in abbreviation_list): raise ValueError("All added Abbreviations must be strings.") self.abbreviations += abbreviation_list if TriggerMode.ABBREVIATION not in self.modes: self.modes.append(TriggerMode.ABBREVIATION) def clear_abbreviations(self): self.abbreviations = [] def get_abbreviations(self): if TriggerMode.ABBREVIATION not in self.modes: return "" elif len(self.abbreviations) == 1: return self.abbreviations[0] else: return "[" + ",".join(self.abbreviations) + "]" def _should_trigger_abbreviation(self, buffer): """ Checks whether, based on the settings for the abbreviation and the given input, the abbreviation should trigger. @param buffer Input buffer to be checked (as string) """ return any(self.__checkInput(buffer, abbr) for abbr in self.abbreviations) def _get_trigger_abbreviation(self, buffer): for abbr in self.abbreviations: if self.__checkInput(buffer, abbr): return abbr return None def __checkInput(self, buffer, abbr): stringBefore, typedAbbr, stringAfter = self._partition_input(buffer, abbr) if len(typedAbbr) > 0: # Check trigger character condition if not self.immediate: # If not immediate expansion, check last character if len(stringAfter) == 1: # Have a character after abbr if self.wordChars.match(stringAfter): # last character(s) is a word char, can't send expansion return False elif len(stringAfter) > 1: # Abbr not at/near end of buffer any more, can't send return False else: # Nothing after abbr yet, can't expand yet return False else: # immediate option enabled, check abbr is at end of buffer if len(stringAfter) > 0: return False # Check chars ahead of abbr # length of stringBefore should always be > 0 if len(stringBefore) > 0 and not re.match('(^\s)', stringBefore[-1]) and not self.triggerInside: # check if last char before the typed abbreviation is a word char # if triggerInside is not set, can't trigger when inside a word return False return True return False def _partition_input(self, current_string: str, abbr: typing.Optional[str]) -> typing.Tuple[str, str, str]: """ Partition the input into text before, typed abbreviation (if it exists), and text after """ if abbr: if self.ignoreCase: string_before, typed_abbreviation, string_after = self._case_insensitive_rpartition( current_string, abbr ) abbr_start_index = len(string_before) abbr_end_index = abbr_start_index + len(typed_abbreviation) typed_abbreviation = current_string[abbr_start_index:abbr_end_index] else: string_before, typed_abbreviation, string_after = current_string.rpartition(abbr) return string_before, typed_abbreviation, string_after else: # abbr is None. This happens if the phrase was typed/pasted using a hotkey and is about to be un-done. # In this case, there is no trigger character (thus empty before and after text). The complete string # should be undone. return "", current_string, "" @staticmethod def _case_insensitive_rpartition(input_string: str, separator: str) -> typing.Tuple[str, str, str]: """Same as str.rpartition(), except that the partitioning is done case insensitive.""" lowered_input_string = input_string.lower() lowered_separator = separator.lower() try: split_index = lowered_input_string.rindex(lowered_separator) except ValueError: # Did not find the separator in the input_string. # Follow https://docs.python.org/3/library/stdtypes.html#text-sequence-type-str # str.rpartition documentation and return the tuple ("", "", unmodified_input) in this case return "", "", input_string else: split_index_2 = split_index+len(separator) return input_string[:split_index], input_string[split_index: split_index_2], input_string[split_index_2:] autokey-0.96.0/lib/autokey/model/abstract_hotkey.py000066400000000000000000000051241427671440700223740ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2019-2020 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import typing from autokey.model.helpers import TriggerMode from autokey.model.abstract_window_filter import AbstractWindowFilter from autokey.model.key import Key class AbstractHotkey(AbstractWindowFilter): def __init__(self): self.modifiers = [] # type: typing.List[Key] self.hotKey = None # type: typing.Optional[str] def get_serializable(self): d = { "modifiers": self.modifiers, "hotKey": self.hotKey } return d def load_from_serialized(self, data): self.set_hotkey(data["modifiers"], data["hotKey"]) def copy_hotkey(self, theHotkey): [self.modifiers.append(modifier) for modifier in theHotkey.modifiers] self.hotKey = theHotkey.hotKey def set_hotkey(self, modifiers, key): modifiers.sort() self.modifiers = modifiers self.hotKey = key if key is not None and TriggerMode.HOTKEY not in self.modes: self.modes.append(TriggerMode.HOTKEY) def unset_hotkey(self): self.modifiers = [] self.hotKey = None if TriggerMode.HOTKEY in self.modes: self.modes.remove(TriggerMode.HOTKEY) def check_hotkey(self, modifiers, key, windowTitle): if self.hotKey is not None and self._should_trigger_window_title(windowTitle): return (self.modifiers == modifiers) and (self.hotKey == key) else: return False def get_hotkey_string(self, key=None, modifiers=None): if key is None and modifiers is None: if TriggerMode.HOTKEY not in self.modes: return "" key = self.hotKey modifiers = self.modifiers ret = "" for modifier in modifiers: ret += modifier ret += "+" if key == ' ': ret += "" else: ret += key return ret autokey-0.96.0/lib/autokey/model/abstract_window_filter.py000066400000000000000000000076521427671440700237550ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2019-2020 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import re import typing class AbstractWindowFilter: def __init__(self): self.windowInfoRegex = None self.isRecursive = False def get_serializable(self): if self.windowInfoRegex is not None: return {"regex": self.windowInfoRegex.pattern, "isRecursive": self.isRecursive} else: return {"regex": None, "isRecursive": False} def load_from_serialized(self, data): try: if isinstance(data, dict): # check needed for data from versions < 0.80.4 self.set_window_titles(data["regex"]) self.isRecursive = data["isRecursive"] else: self.set_window_titles(data) except re.error as e: raise e def copy_window_filter(self, window_filter): self.windowInfoRegex = window_filter.windowInfoRegex self.isRecursive = window_filter.isRecursive def set_window_titles(self, regex): if regex is not None: try: self.windowInfoRegex = re.compile(regex, re.UNICODE) except re.error as e: raise e else: self.windowInfoRegex = regex def set_filter_recursive(self, recurse): self.isRecursive = recurse def has_filter(self) -> bool: return self.windowInfoRegex is not None def inherits_filter(self) -> bool: if self.parent is not None: return self.parent.get_applicable_regex(True) is not None return False def get_child_filter(self): if self.isRecursive and self.windowInfoRegex is not None: return self.get_filter_regex() elif self.parent is not None: return self.parent.get_child_filter() else: return "" def get_filter_regex(self): """ Used by the GUI to obtain human-readable version of the filter """ if self.windowInfoRegex is not None: if self.isRecursive: return self.windowInfoRegex.pattern else: return self.windowInfoRegex.pattern elif self.parent is not None: return self.parent.get_child_filter() else: return "" def filter_matches(self, otherFilter): # XXX Should this be and? if otherFilter is None or self.get_applicable_regex() is None: return True return otherFilter == self.get_applicable_regex().pattern def same_filter_as_item(self, otherItem): if not isinstance(otherItem, AbstractWindowFilter): return False return self.filter_matches(otherItem.get_applicable_regex) def get_applicable_regex(self, forChild=False): if self.windowInfoRegex is not None: if (forChild and self.isRecursive) or not forChild: return self.windowInfoRegex elif self.parent is not None: return self.parent.get_applicable_regex(True) return None def _should_trigger_window_title(self, window_info): r = self.get_applicable_regex() # type: typing.Pattern if r is not None: return bool(r.match(window_info.wm_title)) or bool(r.match(window_info.wm_class)) else: return True autokey-0.96.0/lib/autokey/model/button.py000066400000000000000000000003751427671440700205240ustar00rootroot00000000000000# Key codes enumeration import enum @enum.unique class Button(int, enum.Enum): LEFT = 1 MIDDLE = 2 RIGHT = 3 SCROLL_UP = 4 SCROLL_DOWN = 5 SCROLL_LEFT = 6 SCROLL_RIGHT = 7 BACKWARD = 8 FORWARD = 9 BUTTON10 = 10 autokey-0.96.0/lib/autokey/model/folder.py000066400000000000000000000223521427671440700204630ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2019-2020 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import errno import glob import json import os import typing from autokey.configmanager import configmanager_constants as cm_constants from autokey.model.phrase import Phrase from autokey.model.script import Script from autokey.model.helpers import get_safe_path, TriggerMode from autokey.model.abstract_abbreviation import AbstractAbbreviation from autokey.model.abstract_window_filter import AbstractWindowFilter from autokey.model.abstract_hotkey import AbstractHotkey logger = __import__("autokey.logger").logger.get_logger(__name__) class Folder(AbstractAbbreviation, AbstractHotkey, AbstractWindowFilter): """ Manages a collection of subfolders/phrases/scripts, which may be associated with an abbreviation or hotkey. """ def __init__(self, title: str, show_in_tray_menu: bool=False, path: str=None): AbstractAbbreviation.__init__(self) AbstractHotkey.__init__(self) AbstractWindowFilter.__init__(self) self.title = title self.folders = [] self.items = [] self.modes = [] # type: typing.List[TriggerMode] self.usageCount = 0 self.show_in_tray_menu = show_in_tray_menu self.parent = None # type: typing.Optional[Folder] self.path = path self.temporary = False def build_path(self, base_name=None): if base_name is None: base_name = self.title if self.parent is not None: self.path = get_safe_path(self.parent.path, base_name) else: self.path = get_safe_path(cm_constants.CONFIG_DEFAULT_FOLDER, base_name) def persist(self): if self.path is None: self.build_path() if not os.path.exists(self.path): os.mkdir(self.path) with open(self.path + "/folder.json", 'w') as outFile: json.dump(self.get_serializable(), outFile, indent=4) def get_serializable(self): d = { "type": "folder", "title": self.title, "modes": [mode.value for mode in self.modes], # Store the enum value for compatibility with old user data. "usageCount": self.usageCount, "showInTrayMenu": self.show_in_tray_menu, "abbreviation": AbstractAbbreviation.get_serializable(self), "hotkey": AbstractHotkey.get_serializable(self), "filter": AbstractWindowFilter.get_serializable(self), } return d def load(self, parent=None): self.parent = parent if os.path.exists(self.get_json_path()): self.load_from_serialized() else: self.title = os.path.basename(self.path) self.load_children() def load_children(self): entries = glob.glob(self.path + "/*") self.folders = [] self.items = [] for entryPath in entries: #entryPath = self.path + '/' + entry if os.path.isdir(entryPath): f = Folder("", path=entryPath) f.load(self) self.folders.append(f) if os.path.isfile(entryPath): i = None if entryPath.endswith(".txt"): i = Phrase("", "", path=entryPath) elif entryPath.endswith(".py"): i = Script("", "", path=entryPath) if i is not None: i.load(self) self.items.append(i) def load_from_serialized(self): try: with open(self.path + "/folder.json", 'r') as inFile: data = json.load(inFile) self.inject_json_data(data) except Exception: logger.exception("Error while loading json data for " + self.title) logger.error("JSON data not loaded (or loaded incomplete)") def inject_json_data(self, data): self.title = data["title"] self.modes = [TriggerMode(item) for item in data["modes"]] self.usageCount = data["usageCount"] self.show_in_tray_menu = data["showInTrayMenu"] AbstractAbbreviation.load_from_serialized(self, data["abbreviation"]) AbstractHotkey.load_from_serialized(self, data["hotkey"]) AbstractWindowFilter.load_from_serialized(self, data["filter"]) def rebuild_path(self): if self.path is not None: oldName = self.path self.path = get_safe_path(os.path.split(oldName)[0], self.title) self.update_children() os.rename(oldName, self.path) else: self.build_path() def update_children(self): for childFolder in self.folders: childFolder.build_path(os.path.basename(childFolder.path)) childFolder.update_children() for childItem in self.items: childItem.build_path(os.path.basename(childItem.path)) def get_child_folders(self): out = [] for folder in self.folders: out.append(folder) out.extend(folder.get_child_folders()) return out def remove_data(self): if self.path is not None: for child in self.items: child.remove_data() for child in self.folders: child.remove_data() try: # The json file must be removed first. Otherwise the rmdir will fail. if os.path.exists(self.get_json_path()): os.remove(self.get_json_path()) os.rmdir(self.path) except OSError as err: # There may be user data in the removed directory. Only swallow the error, if it is caused by # residing user data. Other errors should propagate. if err.errno != errno.ENOTEMPTY: raise def get_json_path(self): return self.path + "/folder.json" def get_tuple(self): return "folder", self.title, self.get_abbreviations(), self.get_hotkey_string(), self def set_modes(self, modes: typing.List[TriggerMode]): self.modes = modes def add_folder(self, folder): folder.parent = self #self.folders[folder.title] = folder self.folders.append(folder) def remove_folder(self, folder): #del self.folders[folder.title] self.folders.remove(folder) def add_item(self, item): """ Add a new script or phrase to the folder. """ item.parent = self #self.phrases[phrase.description] = phrase self.items.append(item) def remove_item(self, item): """ Removes the given phrase or script from the folder. """ #del self.phrases[phrase.description] self.items.remove(item) def check_input(self, buffer, window_info): if TriggerMode.ABBREVIATION in self.modes: return self._should_trigger_abbreviation(buffer) and self._should_trigger_window_title(window_info) else: return False def increment_usage_count(self): self.usageCount += 1 if self.parent is not None: self.parent.increment_usage_count() def get_backspace_count(self, buffer): """ Given the input buffer, calculate how many backspaces are needed to erase the text that triggered this folder. """ if TriggerMode.ABBREVIATION in self.modes and self.backspace: if self._should_trigger_abbreviation(buffer): abbr = self._get_trigger_abbreviation(buffer) stringBefore, typedAbbr, stringAfter = self._partition_input(buffer, abbr) return len(abbr) + len(stringAfter) if self.parent is not None: return self.parent.get_backspace_count(buffer) return 0 def calculate_input(self, buffer): """ Calculate how many keystrokes were used in triggering this folder (if applicable). """ if TriggerMode.ABBREVIATION in self.modes and self.backspace: if self._should_trigger_abbreviation(buffer): if self.immediate: return len(self._get_trigger_abbreviation(buffer)) else: return len(self._get_trigger_abbreviation(buffer)) + 1 if self.parent is not None: return self.parent.calculate_input(buffer) return 0 """def __cmp__(self, other): if self.usageCount != other.usageCount: return cmp(self.usageCount, other.usageCount) else: return cmp(other.title, self.title)""" def __str__(self): return "folder '{}'".format(self.title) def __repr__(self): return str(self) autokey-0.96.0/lib/autokey/model/helpers.py000066400000000000000000000037501427671440700206530ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2019-2020 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import enum import os import re DEFAULT_WORDCHAR_REGEX = '[\w]' JSON_FILE_PATTERN = "{}/{}.json" SPACES_RE = re.compile(r"^ | $") def make_wordchar_re(word_chars: str): return "[^{word_chars}]".format(word_chars=word_chars) def extract_wordchars(regex): return regex[2:-1] def get_safe_path(base_path, name, ext=""): name = SPACES_RE.sub('_', name) safe_name = ''.join((char for char in name if char.isalnum() or char in "_ -.")) if safe_name == '': path = base_path + '/1' + ext jsonPath = base_path + "/1.json" n = 2 else: path = base_path + '/' + safe_name + ext jsonPath = base_path + '/' + safe_name + ".json" n = 1 while os.path.exists(path) or os.path.exists(jsonPath): path = base_path + '/' + safe_name + str(n) + ext jsonPath = base_path + '/' + safe_name + str(n) + ".json" n += 1 return path @enum.unique class TriggerMode(enum.Enum): """ Enumeration class for phrase match modes. NONE: Don't trigger this phrase (phrase will only be shown in its folder). ABBREVIATION: Trigger this phrase using an abbreviation. PREDICTIVE: Trigger this phrase using predictive mode. """ NONE = 0 ABBREVIATION = 1 PREDICTIVE = 2 HOTKEY = 3 autokey-0.96.0/lib/autokey/model/key.py000066400000000000000000000064471427671440700200070ustar00rootroot00000000000000# Key codes enumeration import enum import re # Matches the special syntax, like for non-printable or unknown keys. _code_point_re = re.compile(r"", re.UNICODE) @enum.unique class Key(str, enum.Enum): LEFT = "" RIGHT = "" UP = "" DOWN = "" BACKSPACE = "" TAB = "" ENTER = "" SCROLL_LOCK = "" PRINT_SCREEN = "" PAUSE = "" MENU = "" # Modifier keys CONTROL = "" ALT = "" ALT_GR = "" SHIFT = "" SUPER = "" HYPER = "" CAPSLOCK = "" NUMLOCK = "" META = "" F1 = "" F2 = "" F3 = "" F4 = "" F5 = "" F6 = "" F7 = "" F8 = "" F9 = "" F10 = "" F11 = "" F12 = "" F13 = "" F14 = "" F15 = "" F16 = "" F17 = "" F18 = "" F19 = "" F20 = "" F21 = "" F22 = "" F23 = "" F24 = "" F25 = "" F26 = "" F27 = "" F28 = "" F29 = "" F30 = "" F31 = "" F32 = "" F33 = "" F34 = "" F35 = "" # Other ESCAPE = "" INSERT = "" DELETE = "" HOME = "" END = "" PAGE_UP = "" PAGE_DOWN = "" # Numpad NP_INSERT = "" NP_DELETE = "" NP_HOME = "" NP_END = "" NP_PAGE_UP = "" NP_PAGE_DOWN = "" NP_LEFT = "" NP_RIGHT = "" NP_UP = "" NP_DOWN = "" NP_DIVIDE = "" NP_MULTIPLY = "" NP_ADD = "" NP_SUBTRACT = "" NP_5 = "" @classmethod def is_key(cls, key_string: str) -> bool: """ Returns if a string represents a key. """ # Key strings must be treated as case insensitive - always convert to lowercase # before doing any comparisons lowered_key_string = key_string.lower() try: cls(lowered_key_string) except ValueError: return _code_point_re.fullmatch(lowered_key_string) is not None else: return True NAVIGATION_KEYS = [Key.LEFT, Key.RIGHT, Key.UP, Key.DOWN, Key.BACKSPACE, Key.HOME, Key.END, Key.PAGE_UP, Key.PAGE_DOWN] # All known modifier keys. This is used to determine if a key is a modifier. Used by the Configuration manager # to verify that only modifier keys are placed in the disabled modifiers list. _ALL_MODIFIERS_ = ( Key.CONTROL, Key.ALT, Key.ALT_GR, Key.SHIFT, Key.SUPER, Key.HYPER, Key.CAPSLOCK, Key.NUMLOCK, Key.META ) # Used to identify special keys in texts. Also include literals as defined in the _code_point_re. KEY_FIND_RE = re.compile("|".join(("|".join(Key), _code_point_re.pattern)), re.UNICODE) KEY_SPLIT_RE = re.compile("(<[^<>]+>\+?)") MODIFIERS = [Key.CONTROL, Key.ALT, Key.ALT_GR, Key.SHIFT, Key.SUPER, Key.HYPER, Key.META, Key.CAPSLOCK, Key.NUMLOCK] HELD_MODIFIERS = [Key.CONTROL, Key.ALT_GR, Key.ALT, Key.SUPER, Key.SHIFT, Key.HYPER, Key.META ]autokey-0.96.0/lib/autokey/model/modelTypes.py000066400000000000000000000003041427671440700213260ustar00rootroot00000000000000# This almost always causes a circular import if used. import typing from .folder import Folder from .phrase import Phrase from .script import Script Item = typing.Union[Folder, Phrase, Script] autokey-0.96.0/lib/autokey/model/phrase.py000066400000000000000000000311211427671440700204640ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2019-2020 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import enum import json import os import typing from autokey.model.key import NAVIGATION_KEYS, Key, KEY_SPLIT_RE from autokey.model.helpers import JSON_FILE_PATTERN, get_safe_path, TriggerMode from autokey.model.abstract_abbreviation import AbstractAbbreviation from autokey.model.abstract_window_filter import AbstractWindowFilter from autokey.model.abstract_hotkey import AbstractHotkey logger = __import__("autokey.logger").logger.get_logger(__name__) class Phrase(AbstractAbbreviation, AbstractHotkey, AbstractWindowFilter): """ Encapsulates all data and behaviour for a phrase. """ def __init__(self, description, phrase, path=None): AbstractAbbreviation.__init__(self) AbstractHotkey.__init__(self) AbstractWindowFilter.__init__(self) self.description = description self.phrase = phrase self.modes = [] # type: typing.List[TriggerMode] self.usageCount = 0 self.prompt = False self.temporary = False self.omitTrigger = False self.matchCase = False self.parent = None self.show_in_tray_menu = False self.sendMode = SendMode.CB_CTRL_V self.path = path def build_path(self, base_name=None): if base_name is None: base_name = self.description else: base_name = base_name[:-4] self.path = get_safe_path(self.parent.path, base_name, ".txt") def get_json_path(self): directory, base_name = os.path.split(self.path[:-4]) return JSON_FILE_PATTERN.format(directory, base_name) def persist(self): if self.path is None: self.build_path() with open(self.get_json_path(), 'w') as json_file: json.dump(self.get_serializable(), json_file, indent=4) with open(self.path, "w") as out_file: out_file.write(self.phrase) def get_serializable(self): d = { "type": "phrase", "description": self.description, "modes": [mode.value for mode in self.modes], # Store the enum value for compatibility with old user data. "usageCount": self.usageCount, "prompt": self.prompt, "omitTrigger": self.omitTrigger, "matchCase": self.matchCase, "showInTrayMenu": self.show_in_tray_menu, "abbreviation": AbstractAbbreviation.get_serializable(self), "hotkey": AbstractHotkey.get_serializable(self), "filter": AbstractWindowFilter.get_serializable(self), "sendMode": self.sendMode.value } return d def load(self, parent): self.parent = parent with open(self.path, "r") as inFile: self.phrase = inFile.read() if os.path.exists(self.get_json_path()): self.load_from_serialized() else: self.description = os.path.basename(self.path)[:-4] def load_from_serialized(self): try: with open(self.get_json_path(), "r") as json_file: data = json.load(json_file) self.inject_json_data(data) except Exception: logger.exception("Error while loading json data for " + self.description) logger.error("JSON data not loaded (or loaded incomplete)") def inject_json_data(self, data: dict): self.description = data["description"] self.modes = [TriggerMode(item) for item in data["modes"]] self.usageCount = data["usageCount"] self.prompt = data["prompt"] self.omitTrigger = data["omitTrigger"] self.matchCase = data["matchCase"] self.show_in_tray_menu = data["showInTrayMenu"] self.sendMode = SendMode(data.get("sendMode", SendMode.KEYBOARD)) AbstractAbbreviation.load_from_serialized(self, data["abbreviation"]) AbstractHotkey.load_from_serialized(self, data["hotkey"]) AbstractWindowFilter.load_from_serialized(self, data["filter"]) def rebuild_path(self): if self.path is not None: old_name = self.path old_json = self.get_json_path() self.build_path() os.rename(old_name, self.path) os.rename(old_json, self.get_json_path()) else: self.build_path() def remove_data(self): if self.path is not None: if os.path.exists(self.path): os.remove(self.path) if os.path.exists(self.get_json_path()): os.remove(self.get_json_path()) def copy(self, source_phrase): self.description = source_phrase.description self.phrase = source_phrase.phrase # TODO - re-enable me if restoring predictive functionality #if TriggerMode.PREDICTIVE in source_phrase.modes: # self.modes.append(TriggerMode.PREDICTIVE) self.prompt = source_phrase.prompt self.omitTrigger = source_phrase.omitTrigger self.matchCase = source_phrase.matchCase self.parent = source_phrase.parent self.show_in_tray_menu = source_phrase.show_in_tray_menu self.copy_abbreviation(source_phrase) self.copy_hotkey(source_phrase) self.copy_window_filter(source_phrase) def get_tuple(self): return "text-plain", self.description, self.get_abbreviations(), self.get_hotkey_string(), self def set_modes(self, modes: typing.List[TriggerMode]): self.modes = modes def check_input(self, buffer, window_info): if TriggerMode.ABBREVIATION in self.modes: return self._should_trigger_abbreviation(buffer) and self._should_trigger_window_title(window_info) else: return False def build_phrase(self, buffer): self.usageCount += 1 self.parent.increment_usage_count() expansion = Expansion(self.phrase) trigger_found = False if TriggerMode.ABBREVIATION in self.modes: if self._should_trigger_abbreviation(buffer): abbr = self._get_trigger_abbreviation(buffer) stringBefore, typedAbbr, stringAfter = self._partition_input(buffer, abbr) trigger_found = True if self.backspace: # determine how many backspaces to send expansion.backspaces = len(abbr) + len(stringAfter) else: expansion.backspaces = len(stringAfter) if not self.omitTrigger: expansion.string += stringAfter if self.matchCase: if typedAbbr.istitle(): expansion.string = expansion.string.capitalize() elif typedAbbr.isupper(): expansion.string = expansion.string.upper() elif typedAbbr.islower(): expansion.string = expansion.string.lower() # TODO - re-enable me if restoring predictive functionality #if TriggerMode.PREDICTIVE in self.modes: # if self._should_trigger_predictive(buffer): # expansion.string = expansion.string[ConfigManager.SETTINGS[PREDICTIVE_LENGTH]:] # trigger_found = True if not trigger_found: # Phrase could have been triggered from menu - check parents for backspace count expansion.backspaces = self.parent.get_backspace_count(buffer) #self.__parsePositionTokens(expansion) return expansion def calculate_input(self, buffer): """ Calculate how many keystrokes were used in triggering this phrase. """ # TODO: This function is unused? if TriggerMode.ABBREVIATION in self.modes: if self._should_trigger_abbreviation(buffer): if self.immediate: return len(self._get_trigger_abbreviation(buffer)) else: return len(self._get_trigger_abbreviation(buffer)) + 1 # TODO - re-enable me if restoring predictive functionality #if TriggerMode.PREDICTIVE in self.modes: # if self._should_trigger_predictive(buffer): # return ConfigManager.SETTINGS[PREDICTIVE_LENGTH] if TriggerMode.HOTKEY in self.modes: if buffer == '': return len(self.modifiers) + 1 return self.parent.calculate_input(buffer) def get_trigger_chars(self, buffer): abbr = self._get_trigger_abbreviation(buffer) stringBefore, typedAbbr, stringAfter = self._partition_input(buffer, abbr) return typedAbbr + stringAfter def should_prompt(self, buffer): """ Get a value indicating whether the user should be prompted to select the phrase. Always returns true if the phrase has been triggered using predictive mode. """ # TODO - re-enable me if restoring predictive functionality #if TriggerMode.PREDICTIVE in self.modes: # if self._should_trigger_predictive(buffer): # return True return self.prompt def get_description(self, buffer): # TODO - re-enable me if restoring predictive functionality #if self._should_trigger_predictive(buffer): # length = ConfigManager.SETTINGS[PREDICTIVE_LENGTH] # endPoint = length + 30 # if len(self.phrase) > endPoint: # description = "... " + self.phrase[length:endPoint] + "..." # else: # description = "... " + self.phrase[length:] # description = description.replace('\n', ' ') # return description #else: return self.description # TODO - re-enable me if restoring predictive functionality """def _should_trigger_predictive(self, buffer): if len(buffer) >= ConfigManager.SETTINGS[PREDICTIVE_LENGTH]: typed = buffer[-ConfigManager.SETTINGS[PREDICTIVE_LENGTH]:] return self.phrase.startswith(typed) else: return False""" def parsePositionTokens(self, expansion): # Check the string for cursor positioning token and apply lefts and ups as appropriate # TODO make this a constant elsewhere, and check what it should # actually be defined as. This is a guess, since this func is # currently unused. CURSOR_POSITION_TOKEN = "|" if CURSOR_POSITION_TOKEN in expansion.string: firstpart, secondpart = expansion.string.split(CURSOR_POSITION_TOKEN) foundNavigationKey = False for key in NAVIGATION_KEYS: if key in expansion.string: expansion.lefts = 0 foundNavigationKey = True break if not foundNavigationKey: for section in KEY_SPLIT_RE.split(secondpart): if not Key.is_key(section) or section in [' ', '\n']: expansion.lefts += len(section) expansion.string = firstpart + secondpart def __str__(self): return "phrase '{}'".format(self.description) def __repr__(self): return "Phrase('" + self.description + "')" class Expansion: def __init__(self, string): self.string = string self.lefts = 0 self.backspaces = 0 class SendMode(enum.Enum): """ Enumeration class for phrase send modes KEYBOARD: Send using key events CB_CTRL_V: Send via clipboard and paste with Ctrl+v CB_CTRL_SHIFT_V: Send via clipboard and paste with Ctrl+Shift+v SELECTION: Send via X selection and paste with middle mouse button """ KEYBOARD = "kb" CB_CTRL_V = Key.CONTROL + "+v" CB_CTRL_SHIFT_V = Key.CONTROL + "+" + Key.SHIFT + "+v" CB_SHIFT_INSERT = Key.SHIFT + "+" + Key.INSERT SELECTION = None SEND_MODES = { "Keyboard": SendMode.KEYBOARD, "Clipboard (Ctrl+V)": SendMode.CB_CTRL_V, "Clipboard (Ctrl+Shift+V)": SendMode.CB_CTRL_SHIFT_V, "Clipboard (Shift+Insert)": SendMode.CB_SHIFT_INSERT, "Mouse Selection": SendMode.SELECTION } # type: typing.Dict[str, SendMode] autokey-0.96.0/lib/autokey/model/script.py000066400000000000000000000234551427671440700205210ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2019-2020 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import datetime import json import os import typing from pathlib import Path from autokey.model.store import Store from autokey.model.helpers import JSON_FILE_PATTERN, get_safe_path, TriggerMode from autokey.model.abstract_abbreviation import AbstractAbbreviation from autokey.model.abstract_window_filter import AbstractWindowFilter from autokey.model.abstract_hotkey import AbstractHotkey logger = __import__("autokey.logger").logger.get_logger(__name__) class Script(AbstractAbbreviation, AbstractHotkey, AbstractWindowFilter): """ Encapsulates all data and behaviour for a script. """ def __init__(self, description: str, source_code: str, path=None): AbstractAbbreviation.__init__(self) AbstractHotkey.__init__(self) AbstractWindowFilter.__init__(self) self.description = description self.code = source_code self.store = Store() self.modes = [] # type: typing.List[TriggerMode] self.usageCount = 0 self.prompt = False self.omitTrigger = False self.parent = None self.show_in_tray_menu = False self.path = path def build_path(self, base_name=None): if base_name is None: base_name = self.description else: base_name = base_name[:-3] self.path = get_safe_path(self.parent.path, base_name, ".py") def get_json_path(self): directory, base_name = os.path.split(self.path[:-3]) return JSON_FILE_PATTERN.format(directory, base_name) def persist(self): if self.path is None: self.build_path() self._persist_metadata() with open(self.path, "w") as out_file: out_file.write(self.code) def get_serializable(self): d = { "type": "script", "description": self.description, "store": self.store, "modes": [mode.value for mode in self.modes], # Store the enum value for compatibility with old user data. "usageCount": self.usageCount, "prompt": self.prompt, "omitTrigger": self.omitTrigger, "showInTrayMenu": self.show_in_tray_menu, "abbreviation": AbstractAbbreviation.get_serializable(self), "hotkey": AbstractHotkey.get_serializable(self), "filter": AbstractWindowFilter.get_serializable(self) } return d def _persist_metadata(self): """ Write all script meta-data, including the persistent script Store. The Store instance might contain arbitrary user data, like function objects, OpenCL contexts, or whatever other non-serializable objects, both as keys or values. Try to serialize the data, and if it fails, fall back to checking the store and removing all non-serializable data. """ serializable_data = self.get_serializable() try: self._try_persist_metadata(serializable_data) except TypeError: # The user added non-serializable data to the store, so skip all non-serializable keys or values. cleaned_data = Script._remove_non_serializable_store_entries(serializable_data["store"]) self._try_persist_metadata(cleaned_data) def _try_persist_metadata(self, serializable_data: dict): with open(self.get_json_path(), "w") as json_file: json.dump(serializable_data, json_file, indent=4) @staticmethod def _remove_non_serializable_store_entries(store: Store) -> dict: """ Copy all serializable data into a new dict, and skip the rest. This makes sure to keep the items during runtime, even if the user edits and saves the script. """ cleaned_store_data = {} for key, value in store.items(): if Script._is_serializable(key) and Script._is_serializable(value): cleaned_store_data[key] = value else: logger.info("Skip non-serializable item in the local script store. Key: '{}', Value: '{}'. " "This item cannot be saved and therefore will be lost when autokey quits.".format( key, value )) return cleaned_store_data @staticmethod def _is_serializable(data): try: json.dumps(data) except (TypeError, ValueError): # TypeError occurs with non-serializable types (type, function, etc.) # ValueError occurs when circular references are found. Example: `l=[]; l.append(l)` return False else: return True def load(self, parent): self.parent = parent with open(self.path, "r", encoding="UTF-8") as in_file: self.code = in_file.read() if os.path.exists(self.get_json_path()): self.load_from_serialized() else: self.description = os.path.basename(self.path)[:-3] def load_from_serialized(self, **kwargs): try: with open(self.get_json_path(), "r") as jsonFile: data = json.load(jsonFile) self.inject_json_data(data) except Exception: logger.exception("Error while loading json data for " + self.description) logger.error("JSON data not loaded (or loaded incomplete)") def inject_json_data(self, data: dict): self.description = data["description"] self.store = Store(data["store"]) self.modes = [TriggerMode(item) for item in data["modes"]] self.usageCount = data["usageCount"] self.prompt = data["prompt"] self.omitTrigger = data["omitTrigger"] self.show_in_tray_menu = data["showInTrayMenu"] AbstractAbbreviation.load_from_serialized(self, data["abbreviation"]) AbstractHotkey.load_from_serialized(self, data["hotkey"]) AbstractWindowFilter.load_from_serialized(self, data["filter"]) def rebuild_path(self): if self.path is not None: oldName = self.path oldJson = self.get_json_path() self.build_path() os.rename(oldName, self.path) os.rename(oldJson, self.get_json_path()) else: self.build_path() def remove_data(self): if self.path is not None: if os.path.exists(self.path): os.remove(self.path) if os.path.exists(self.get_json_path()): os.remove(self.get_json_path()) def copy(self, source_script): self.description = source_script.description self.code = source_script.code self.prompt = source_script.prompt self.omitTrigger = source_script.omitTrigger self.parent = source_script.parent self.show_in_tray_menu = source_script.show_in_tray_menu self.copy_abbreviation(source_script) self.copy_hotkey(source_script) self.copy_window_filter(source_script) def get_tuple(self): return "text-x-python", self.description, self.get_abbreviations(), self.get_hotkey_string(), self def set_modes(self, modes: typing.List[TriggerMode]): self.modes = modes def check_input(self, buffer, window_info): if TriggerMode.ABBREVIATION in self.modes: return self._should_trigger_abbreviation(buffer) and self._should_trigger_window_title(window_info) else: return False def process_buffer(self, buffer): self.usageCount += 1 self.parent.increment_usage_count() trigger_found = False backspaces = 0 string = "" if TriggerMode.ABBREVIATION in self.modes: if self._should_trigger_abbreviation(buffer): abbr = self._get_trigger_abbreviation(buffer) stringBefore, typedAbbr, stringAfter = self._partition_input(buffer, abbr) trigger_found = True if self.backspace: # determine how many backspaces to send backspaces = len(abbr) + len(stringAfter) else: backspaces = len(stringAfter) if not self.omitTrigger: string += stringAfter if not trigger_found: # Phrase could have been triggered from menu - check parents for backspace count backspaces = self.parent.get_backspace_count(buffer) return backspaces, string def should_prompt(self, buffer): return self.prompt def get_description(self, buffer): return self.description def __str__(self): return "script '{}'".format(self.description) def __repr__(self): return "Script('" + self.description + "')" class ScriptErrorRecord: """ This class holds a record of an error that caused a user Script to abort and additional meta-data . """ def __init__(self, script: typing.Union[Script, Path], error_traceback: str, start_time: datetime.time, error_time: datetime.time): self.script_name = script.description if isinstance(script, Script) else str(script) self.error_traceback = error_traceback self.start_time = start_time self.error_time = error_time autokey-0.96.0/lib/autokey/model/store.py000066400000000000000000000037711427671440700203500ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2019-2020 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . class Store(dict): """ Allows persistent storage of values between invocations of the script. """ GLOBALS = {} def set_value(self, key, value): """ Store a value Usage: C{store.set_value(key, value)} """ self[key] = value def get_value(self, key): """ Get a value Usage: C{store.get_value(key)} """ return self.get(key, None) def remove_value(self, key): """ Remove a value Usage: C{store.remove_value(key)} """ del self[key] def set_global_value(self, key, value): """ Store a global value Usage: C{store.set_global_value(key, value)} The value stored with this method will be available to all scripts. """ Store.GLOBALS[key] = value def get_global_value(self, key): """ Get a global value Usage: C{store.get_global_value(key)} """ return self.GLOBALS.get(key, None) def remove_global_value(self, key): """ Remove a global value Usage: C{store.remove_global_value(key)} """ del self.GLOBALS[key] def has_key(self, key): """ python 2 compatibility """ return key in self autokey-0.96.0/lib/autokey/monitor.py000066400000000000000000000104361427671440700175770ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import threading import os.path import time from pyinotify import WatchManager, Notifier, EventsCodes, ProcessEvent logger = __import__("autokey.logger").logger.get_logger(__name__) m = EventsCodes.OP_FLAGS MASK = m["IN_CREATE"]|m["IN_MODIFY"]|m["IN_DELETE"]|m["IN_MOVED_TO"]|m["IN_MOVED_FROM"] class Processor(ProcessEvent): def __init__(self, monitor, listener): ProcessEvent.__init__(self) self.listener = listener self.monitor = monitor def __getEventPath(self, event): if event.name != '': path = os.path.join(event.path, event.name) else: path = event.path logger.debug("Reporting %s event at %s", event.maskname, path) return path def process_IN_MOVED_TO(self, event): path = self.__getEventPath(event) if not self.monitor.is_suspended(): self.listener.path_created_or_modified(path) def process_IN_CREATE(self, event): path = self.__getEventPath(event) if not self.monitor.is_suspended(): self.listener.path_created_or_modified(path) def process_IN_MODIFY(self, event): path = self.__getEventPath(event) if not self.monitor.is_suspended(): self.listener.path_created_or_modified(path) def process_IN_DELETE(self, event): path = self.__getEventPath(event) if not self.monitor.is_suspended(): self.listener.path_removed(path) def process_IN_MOVED_FROM(self, event): path = self.__getEventPath(event) if not self.monitor.is_suspended(): self.listener.path_removed(path) class FileMonitor(threading.Thread): def __init__(self, listener): threading.Thread.__init__(self) self.__p = Processor(self, listener) self.manager = WatchManager() self.notifier = Notifier(self.manager, self.__p) self.event = threading.Event() self.setDaemon(True) self.watches = [] self.__isSuspended = False def suspend(self): self.__isSuspended = True def unsuspend(self): t = threading.Thread(target=self.__unsuspend) t.start() def __unsuspend(self): time.sleep(1.5) self.__isSuspended = False for watch in self.watches: if not os.path.exists(watch): logger.debug("Removed stale watch on %s", watch) self.watches.remove(watch) def is_suspended(self): return self.__isSuspended def has_watch(self, path): return path in self.watches def add_watch(self, path): logger.debug("Adding watch for %s", path) self.manager.add_watch(path, MASK, self.__p) self.watches.append(path) def remove_watch(self, path): logger.debug("Removing watch for %s", path) wd = self.manager.get_wd(path) self.manager.rm_watch(wd, True) self.watches.remove(path) for i in range(len(self.watches)): try: if self.watches[i].startswith(path): self.watches.remove(self.watches[i]) except IndexError: break def run(self): while not self.event.isSet(): self.notifier.process_events() if self.notifier.check_events(1000): self.notifier.read_events() logger.info("Shutting down file monitor") self.notifier.stop() def stop(self): self.event.set() self.join() autokey-0.96.0/lib/autokey/qtapp.py000066400000000000000000000265531427671440700172440ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys import os.path import queue import time import dbus from typing import NamedTuple, Iterable from PyQt5.QtCore import QObject, QEvent, Qt, pyqtSignal from PyQt5.QtGui import QCursor, QIcon from PyQt5.QtWidgets import QMessageBox, QApplication import autokey.model.script from autokey import common common.USING_QT = True from autokey import service, monitor import autokey.argument_parser import autokey.configmanager.configmanager as cm import autokey.configmanager.configmanager_constants as cm_constants from autokey.qtui import common as ui_common from autokey.qtui.notifier import Notifier from autokey.qtui.popupmenu import PopupMenu from autokey.qtui.configwindow import ConfigWindow from autokey.qtui.dbus_service import AppService from autokey.logger import get_logger, configure_root_logger from autokey.UI_common_functions import checkRequirements, checkOptionalPrograms, create_storage_directories import autokey.UI_common_functions as UI_common logger = get_logger(__name__) del get_logger AuthorData = NamedTuple("AuthorData", (("name", str), ("role", str), ("email", str))) AboutData = NamedTuple("AboutData", ( ("program_name", str), ("version", str), ("program_description", str), ("license_text", str), ("copyright_notice", str), ("homepage_url", str), ("bug_report_email", str), ("author_list", Iterable[AuthorData]) )) COPYRIGHT = """(c) 2009-2012 Chris Dekter (c) 2014 GuoCi (c) 2017, 2018 Thomas Hess """ author_data = ( AuthorData("Thomas Hess", "PyKDE4 to PyQt5 port", "thomas.hess@udo.edu"), AuthorData("GuoCi", "Python 3 port maintainer", "guociz@gmail.com"), AuthorData("Chris Dekter", "Developer", "cdekter@gmail.com"), AuthorData("Sam Peterson", "Original developer", "peabodyenator@gmail.com") ) about_data = AboutData( program_name="AutoKey", version=common.VERSION, program_description="Desktop automation utility", license_text="GPL v3", # TODO: load actual license text from disk somewhere copyright_notice=COPYRIGHT, homepage_url=common.HOMEPAGE, bug_report_email=common.BUG_EMAIL, author_list=author_data ) class Application(QApplication): """ Main application class; starting and stopping of the application is controlled from here, together with some interactions from the tray icon. """ monitoring_disabled = pyqtSignal(bool, name="monitoring_disabled") show_configure_signal = pyqtSignal() def __init__(self, argv: list=sys.argv): super().__init__(argv) self.handler = CallbackEventHandler() self.args = autokey.argument_parser.parse_args() try: create_storage_directories() configure_root_logger(self.args) except Exception as e: logger.exception("Fatal error starting AutoKey: " + str(e)) self.show_error_dialog("Fatal error starting AutoKey.", str(e)) sys.exit(1) checkOptionalPrograms() missing_reqs = checkRequirements() if len(missing_reqs)>0: self.show_error_dialog("AutoKey Requires the following programs or python modules to be installed to function properly\n\n"+missing_reqs) sys.exit("Missing required programs and/or python modules, exiting") logger.info("Initialising application") self.setWindowIcon(QIcon.fromTheme(common.ICON_FILE, ui_common.load_icon(ui_common.AutoKeyIcon.AUTOKEY))) try: if self._verify_not_running(): UI_common.create_lock_file() self.monitor = monitor.FileMonitor(self) self.configManager = cm.create_config_manager_instance(self) self.service = service.Service(self) self.serviceDisabled = False self._try_start_service() self.notifier = Notifier(self) self.configWindow = ConfigWindow(self) # Connect the mutual connections between the tray icon and the main window self.configWindow.action_show_last_script_errors.triggered.connect(self.notifier.reset_tray_icon) self.notifier.action_view_script_error.triggered.connect( self.configWindow.show_script_errors_dialog.update_and_show) self.monitor.start() # Initialise user code dir if self.configManager.userCodeDir is not None: sys.path.append(self.configManager.userCodeDir) logger.debug("Creating DBus service") self.dbus_service = AppService(self) logger.debug("Service created") self.show_configure_signal.connect(self.show_configure, Qt.QueuedConnection) if cm.ConfigManager.SETTINGS[cm_constants.IS_FIRST_RUN]: cm.ConfigManager.SETTINGS[cm_constants.IS_FIRST_RUN] = False self.args.show_config_window = True if self.args.show_config_window: self.show_configure() self.installEventFilter(KeyboardChangeFilter(self.service.mediator.interface)) except Exception as e: logger.exception("Fatal error starting AutoKey: " + str(e)) self.show_error_dialog("Fatal error starting AutoKey.", str(e)) sys.exit(1) else: sys.exit(self.exec_()) def _try_start_service(self): try: self.service.start() except Exception as e: logger.exception("Error starting interface: " + str(e)) self.serviceDisabled = True self.show_error_dialog("Error starting interface. Keyboard monitoring will be disabled.\n" + "Check your system/configuration.", str(e)) @staticmethod def _create_lock_file(): with open(common.LOCK_FILE, "w") as lock_file: lock_file.write(str(os.getpid())) def _verify_not_running(self): if UI_common.is_existing_running_autokey(): UI_common.test_Dbus_response(self) return True def init_global_hotkeys(self, configManager): logger.info("Initialise global hotkeys") configManager.toggleServiceHotkey.set_closure(self.toggle_service) configManager.configHotkey.set_closure(self.show_configure_signal.emit) def config_altered(self, persistGlobal): self.configManager.config_altered(persistGlobal) self.notifier.create_assign_context_menu() def hotkey_created(self, item): UI_common.hotkey_created(self.service, item) def hotkey_removed(self, item): UI_common.hotkey_removed(self.service, item) def path_created_or_modified(self, path): UI_common.path_created_or_modified(self.configManager, self.configWindow, path) def path_removed(self, path): UI_common.path_removed(self.configManager, self.configWindow, path) def unpause_service(self): """ Unpause the expansion service (start responding to keyboard and mouse events). """ self.service.unpause() def pause_service(self): """ Pause the expansion service (stop responding to keyboard and mouse events). """ self.service.pause() def toggle_service(self): """ Convenience method for toggling the expansion service on or off. This is called by the global hotkey. """ self.monitoring_disabled.emit(not self.service.is_running()) if self.service.is_running(): self.pause_service() else: self.unpause_service() def shutdown(self): """ Shut down the entire application. """ logger.info("Shutting down") self.closeAllWindows() self.notifier.hide() self.service.shutdown() self.monitor.stop() self.quit() os.remove(common.LOCK_FILE) # TODO: maybe use atexit to remove the lock/pid file? logger.debug("All shutdown tasks complete... quitting") def notify_error(self, error: autokey.model.script.ScriptErrorRecord): """ Show an error notification popup. @param error: The error that occurred in a Script """ message = "The script '{}' encountered an error".format(error.script_name) self.exec_in_main(self.notifier.notify_error, message) self.configWindow.script_errors_available.emit(True) def update_notifier_visibility(self): self.notifier.update_visible_status() def show_configure(self): """ Show the configuration window, or deiconify (un-minimise) it if it's already open. """ logger.info("Displaying configuration window") self.configWindow.show() self.configWindow.showNormal() self.configWindow.activateWindow() @staticmethod def show_error_dialog(message: str, details: str=None): """ Convenience method for showing an error dialog. """ # TODO: i18n logger.debug("Displaying Error Dialog") message_box = QMessageBox( QMessageBox.Critical, "Error", message, QMessageBox.Ok, None ) if details: message_box.setDetailedText(details) message_box.exec_() def show_popup_menu(self, folders: list=None, items: list=None, onDesktop=True, title=None): if items is None: items = [] if folders is None: folders = [] self.exec_in_main(self.__createMenu, folders, items, onDesktop, title) def hide_menu(self): self.exec_in_main(self.menu.hide) def __createMenu(self, folders, items, onDesktop, title): self.menu = PopupMenu(self.service, folders, items, onDesktop, title) self.menu.popup(QCursor.pos()) self.menu.setFocus() def exec_in_main(self, callback, *args): self.handler.postEventWithCallback(callback, *args) class CallbackEventHandler(QObject): def __init__(self): QObject.__init__(self) self.queue = queue.Queue() def customEvent(self, event): while True: try: callback, args = self.queue.get_nowait() except queue.Empty: break try: callback(*args) except Exception: logger.exception("callback event failed: %r %r", callback, args, exc_info=True) def postEventWithCallback(self, callback, *args): self.queue.put((callback, args)) app = QApplication.instance() app.postEvent(self, QEvent(QEvent.User)) class KeyboardChangeFilter(QObject): def __init__(self, interface): QObject.__init__(self) self.interface = interface def eventFilter(self, obj, event): if event.type() == QEvent.KeyboardLayoutChange: self.interface.on_keys_changed() return QObject.eventFilter(obj, event) autokey-0.96.0/lib/autokey/qtui/000077500000000000000000000000001427671440700165145ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/qtui/__init__.py000066400000000000000000000000001427671440700206130ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/qtui/__main__.py000066400000000000000000000025551427671440700206150ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os import faulthandler faulthandler.enable() from PyQt5 import QtCore from autokey.qtapp import Application QtCore.QCoreApplication.setAttribute(QtCore.Qt.AA_X11InitThreads) # remove WINDOWID environment variable so that zenity is not tied to the window from which it was launched. try: del os.environ['WINDOWID'] except KeyError: pass if __name__ == '__main__': # When invoked by the setup.py generated launcher, __name__ is set to "autokey.qtui.__main__", so # this is only executed if invoked directly from the source directory as "python3 -m [lib.]autokey.qtui" # The setup.py launcher directly calls Application() after importing Application() autokey-0.96.0/lib/autokey/qtui/autokey_treewidget.py000066400000000000000000000165351427671440700230040ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . from typing import Union, List, Optional from PyQt5.QtCore import Qt, QEvent, QModelIndex from PyQt5.QtGui import QKeySequence, QIcon, QKeyEvent, QMouseEvent, QDragMoveEvent, QDropEvent from PyQt5.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView import autokey.model.folder import autokey.model.phrase import autokey.model.script class AkTreeWidget(QTreeWidget): def edit(self, index: QModelIndex, trigger: QAbstractItemView.EditTrigger, event: QEvent): if index.column() == 0: super(QTreeWidget, self).edit(index, trigger, event) return False def keyPressEvent(self, event: QKeyEvent): if self.window().is_dirty() \ and (event.matches(QKeySequence.MoveToNextLine) or event.matches(QKeySequence.MoveToPreviousLine)): veto = self.window().central_widget.promptToSave() if not veto: QTreeWidget.keyPressEvent(self, event) else: event.ignore() else: QTreeWidget.keyPressEvent(self, event) def mousePressEvent(self, event: QMouseEvent): if self.window().is_dirty(): veto = self.window().central_widget.promptToSave() if not veto: QTreeWidget.mousePressEvent(self, event) QTreeWidget.mouseReleaseEvent(self, event) else: event.ignore() else: QTreeWidget.mousePressEvent(self, event) def dragMoveEvent(self, event: QDragMoveEvent): target = self.itemAt(event.pos()) if isinstance(target, FolderWidgetItem): QTreeWidget.dragMoveEvent(self, event) else: event.ignore() def dropEvent(self, event: QDropEvent): target = self.itemAt(event.pos()) sources = self.selectedItems() self.window().central_widget.move_items(sources, target) class FolderWidgetItem(QTreeWidgetItem): def __init__(self, parent: Optional[QTreeWidgetItem], folder: autokey.model.folder.Folder): QTreeWidgetItem.__init__(self) self.folder = folder self.setIcon(0, QIcon.fromTheme("folder")) self.setText(0, folder.title) self.setText(1, folder.get_abbreviations()) self.setText(2, folder.get_hotkey_string()) self.setData(3, Qt.UserRole, folder) if parent is not None: parent.addChild(self) self.setFlags(self.flags() | Qt.ItemIsEditable) def update(self): self.setText(0, self.folder.title) self.setText(1, self.folder.get_abbreviations()) self.setText(2, self.folder.get_hotkey_string()) def __ge__(self, other): if isinstance(other, ScriptWidgetItem): return QTreeWidgetItem.__ge__(self, other) else: return False def __lt__(self, other): if isinstance(other, FolderWidgetItem): return QTreeWidgetItem.__lt__(self, other) else: return True class PhraseWidgetItem(QTreeWidgetItem): def __init__(self, parent: Optional[FolderWidgetItem], phrase: autokey.model.phrase.Phrase): QTreeWidgetItem.__init__(self) self.phrase = phrase self.setIcon(0, QIcon.fromTheme("text-x-generic")) self.setText(0, phrase.description) self.setText(1, phrase.get_abbreviations()) self.setText(2, phrase.get_hotkey_string()) self.setData(3, Qt.UserRole, phrase) if parent is not None: # TODO: Phrase without parent allowed? This is should be an error. parent.addChild(self) self.setFlags(self.flags() | Qt.ItemIsEditable) def update(self): self.setText(0, self.phrase.description) self.setText(1, self.phrase.get_abbreviations()) self.setText(2, self.phrase.get_hotkey_string()) def __ge__(self, other): if isinstance(other, ScriptWidgetItem): return QTreeWidgetItem.__ge__(self, other) else: return True def __lt__(self, other): if isinstance(other, PhraseWidgetItem): return QTreeWidgetItem.__lt__(self, other) else: return False class ScriptWidgetItem(QTreeWidgetItem): def __init__(self, parent: Optional[FolderWidgetItem], script: autokey.model.script.Script): QTreeWidgetItem.__init__(self) self.script = script self.setIcon(0, QIcon.fromTheme("text-x-python")) self.setText(0, script.description) self.setText(1, script.get_abbreviations()) self.setText(2, script.get_hotkey_string()) self.setData(3, Qt.UserRole, script) if parent is not None: # TODO: Script without parent allowed? This is should be an error. parent.addChild(self) self.setFlags(self.flags() | Qt.ItemIsEditable) def update(self): self.setText(0, self.script.description) self.setText(1, self.script.get_abbreviations()) self.setText(2, self.script.get_hotkey_string()) def __ge__(self, other): if isinstance(other, ScriptWidgetItem): return QTreeWidgetItem.__ge__(self, other) else: return True def __lt__(self, other): if isinstance(other, ScriptWidgetItem): return QTreeWidgetItem.__lt__(self, other) else: return False ItemType = Union[autokey.model.folder.Folder, autokey.model.phrase.Phrase, autokey.model.script.Script] ItemWidgetType = Union[FolderWidgetItem, PhraseWidgetItem, ScriptWidgetItem] class WidgetItemFactory: def __init__(self, root_folders: List[autokey.model.folder.Folder]): self.folders = root_folders def get_root_folder_list(self): root_items = [] for folder in self.folders: item = WidgetItemFactory._build_item(None, folder) root_items.append(item) WidgetItemFactory.process_folder(item, folder) return root_items @staticmethod def process_folder(parent_item: ItemWidgetType, parent_folder: autokey.model.folder.Folder): for folder in parent_folder.folders: item = WidgetItemFactory._build_item(parent_item, folder) WidgetItemFactory.process_folder(item, folder) for childModelItem in parent_folder.items: WidgetItemFactory._build_item(parent_item, childModelItem) @staticmethod def _build_item(parent: Optional[FolderWidgetItem], item: ItemType) -> ItemWidgetType: if isinstance(item, autokey.model.folder.Folder): return FolderWidgetItem(parent, item) elif isinstance(item, autokey.model.phrase.Phrase): return PhraseWidgetItem(parent, item) elif isinstance(item, autokey.model.script.Script): return ScriptWidgetItem(parent, item) autokey-0.96.0/lib/autokey/qtui/centralwidget.py000066400000000000000000000610661427671440700217330ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018, 2020 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import logging import pathlib import typing from PyQt5.QtCore import Qt from PyQt5.QtGui import QIcon, QCursor, QBrush from PyQt5.QtWidgets import QHeaderView, QMessageBox, QFileDialog, QAction, QWidget, QMenu from PyQt5.QtWidgets import QListWidget, QListWidgetItem import autokey.model.folder import autokey.model.helpers import autokey.model.phrase import autokey.model.script import autokey.iomediator.keygrabber import autokey.configmanager.configmanager as cm import autokey.configmanager.configmanager_constants as cm_constants from autokey.qtui import common as ui_common from autokey.qtui import autokey_treewidget as ak_tree from autokey.logger import get_logger, root_logger logger = get_logger(__name__) del get_logger class CentralWidget(*ui_common.inherits_from_ui_file_with_name("centralwidget")): def __init__(self, parent): super(CentralWidget, self).__init__(parent) logger.debug("CentralWidget instance created.") self.setupUi(self) self.dirty = False self.configManager = None self.recorder = autokey.iomediator.keygrabber.Recorder(self.scriptPage) self.cutCopiedItems = [] for column_index in range(3): self.treeWidget.setColumnWidth( column_index, cm.ConfigManager.SETTINGS[cm_constants.COLUMN_WIDTHS][column_index] ) h_view = self.treeWidget.header() h_view.setSectionResizeMode(QHeaderView.ResizeMode(QHeaderView.Interactive | QHeaderView.ResizeToContents)) self.logHandler = None self.listWidget.hide() self.factory = None # type: ak_tree.WidgetItemFactory self.context_menu = None # type: QMenu self.action_clear_log = self._create_action("edit-clear-history", "Clear Log", None, self.on_clear_log) self.listWidget.addAction(self.action_clear_log) self.action_save_log = self._create_action("edit-clear-history", "Save Log As…", None, self.on_save_log) self.listWidget.addAction(self.action_save_log) @staticmethod def _create_action(icon_name: str, text: str, parent: QWidget=None, to_be_called_slot_function=None) -> QAction: icon = QIcon.fromTheme(icon_name) action = QAction(icon, text, parent) action.triggered.connect(to_be_called_slot_function) return action def init(self, app): self.configManager = app.configManager self.logHandler = ListWidgetHandler(self.listWidget, app) # Create and connect the custom context menu self.context_menu = self._create_treewidget_context_menu() self.treeWidget.customContextMenuRequested.connect(lambda position: self.context_menu.popup(QCursor.pos())) def _create_treewidget_context_menu(self) -> QMenu: main_window = self.window() context_menu = QMenu() context_menu.addAction(main_window.action_create) context_menu.addAction(main_window.action_rename_item) context_menu.addAction(main_window.action_clone_item) context_menu.addAction(main_window.action_cut_item) context_menu.addAction(main_window.action_copy_item) context_menu.addAction(main_window.action_paste_item) context_menu.addSeparator() context_menu.addAction(main_window.action_delete_item) context_menu.addSeparator() context_menu.addAction(main_window.action_run_script) return context_menu def populate_tree(self, config): self.factory = ak_tree.WidgetItemFactory(config.folders) root_folders = self.factory.get_root_folder_list() for item in root_folders: self.treeWidget.addTopLevelItem(item) self.treeWidget.sortItems(0, Qt.AscendingOrder) self.treeWidget.setCurrentItem(self.treeWidget.topLevelItem(0)) self.on_treeWidget_itemSelectionChanged() def set_splitter(self, window_size): pos = cm.ConfigManager.SETTINGS[cm_constants.HPANE_POSITION] self.splitter.setSizes([pos, window_size.width() - pos]) def set_dirty(self, dirty: bool): self.dirty = dirty def promptToSave(self): if cm.ConfigManager.SETTINGS[cm_constants.PROMPT_TO_SAVE]: # TODO: i18n result = QMessageBox.question( self.window(), "Save changes?", "There are unsaved changes. Would you like to save them?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel ) if result == QMessageBox.Yes: return self.on_save() elif result == QMessageBox.Cancel: return True else: return False else: # don't prompt, just save return self.on_save() # ---- Signal handlers def on_treeWidget_itemChanged(self, item, column): if item is self._get_current_treewidget_item() and column == 0: newText = str(item.text(0)) if ui_common.validate( not ui_common.EMPTY_FIELD_REGEX.match(newText), "The name can't be empty.", None, self.window()): self.window().app.monitor.suspend() self.stack.currentWidget().set_item_title(newText) self.stack.currentWidget().rebuild_item_path() persistGlobal = self.stack.currentWidget().save() self.window().app.monitor.unsuspend() self.window().app.config_altered(persistGlobal) self.treeWidget.sortItems(0, Qt.AscendingOrder) else: item.update() def on_treeWidget_itemSelectionChanged(self): model_items = self.__getSelection() if len(model_items) == 1: model_item = model_items[0] if isinstance(model_item, autokey.model.folder.Folder): self.stack.setCurrentIndex(0) self.folderPage.load(model_item) elif isinstance(model_item, autokey.model.phrase.Phrase): self.stack.setCurrentIndex(1) self.phrasePage.load(model_item) elif isinstance(model_item, autokey.model.script.Script): self.stack.setCurrentIndex(2) self.scriptPage.load(model_item) self.window().update_actions(model_items, True) self.set_dirty(False) self.window().cancel_record() else: self.window().update_actions(model_items, False) def on_new_topfolder(self): logger.info("User initiates top-level folder creation") message_box = QMessageBox( QMessageBox.Question, "Create Folder", "Create folder in the default location?", QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel, self.window() ) message_box.button(QMessageBox.No).setText("Create elsewhere") # TODO: i18n result = message_box.exec_() self.window().app.monitor.suspend() if result == QMessageBox.Yes: logger.debug("User creates a new top-level folder.") self.__createFolder(None) elif result == QMessageBox.No: logger.debug("User creates a new folder and chose to create it elsewhere") QMessageBox.warning( self.window(), "Beware", "AutoKey will take the full ownership of the directory you are about to select or create. " "It is advisable to only choose empty directories or directories that contain data created by AutoKey " "previously.\n\nIf you delete or move the directory from within AutoKey " "(for example by using drag and drop), all files unknown to AutoKey will be deleted.", QMessageBox.Ok) path = QFileDialog.getExistingDirectory( self.window(), "Where should the folder be created?" ) if path != "": path = pathlib.Path(path) if list(path.glob("*")): result = QMessageBox.warning( self.window(), "The chosen directory already contains files", "The selected directory already contains files. " "If you continue, AutoKey will take the ownership.\n\n" "You may lose all files in '{}' that are not related to AutoKey if you select this directory.\n" "Continue?".format(path), QMessageBox.Yes|QMessageBox.No) == QMessageBox.Yes else: result = True if result: folder = autokey.model.folder.Folder(path.name, path=str(path)) new_item = ak_tree.FolderWidgetItem(None, folder) self.treeWidget.addTopLevelItem(new_item) self.configManager.folders.append(folder) self.window().app.config_altered(True) self.window().app.monitor.unsuspend() else: logger.debug("User canceled top-level folder creation.") self.window().app.monitor.unsuspend() def on_new_folder(self): parent_item = self._get_current_treewidget_item() self.__createFolder(parent_item) def __createFolder(self, parent_item): folder = autokey.model.folder.Folder("New Folder") new_item = ak_tree.FolderWidgetItem(parent_item, folder) self.window().app.monitor.suspend() if parent_item is not None: parentFolder = self.__extractData(parent_item) parentFolder.add_folder(folder) else: self.treeWidget.addTopLevelItem(new_item) self.configManager.folders.append(folder) folder.persist() self.window().app.monitor.unsuspend() self.treeWidget.sortItems(0, Qt.AscendingOrder) self.treeWidget.setCurrentItem(new_item) self.on_treeWidget_itemSelectionChanged() self.on_rename() def on_new_phrase(self): self.window().app.monitor.suspend() tree_widget = self.treeWidget # type: ak_tree.AkTreeWidget parent_item = tree_widget.selectedItems()[0] # type: ak_tree.ItemWidgetType parent = self.__extractData(parent_item) phrase = autokey.model.phrase.Phrase("New Phrase", "Enter phrase contents") new_item = ak_tree.PhraseWidgetItem(parent_item, phrase) parent.add_item(phrase) phrase.persist() self.window().app.monitor.unsuspend() tree_widget.sortItems(0, Qt.AscendingOrder) tree_widget.setCurrentItem(new_item) parent_item.setSelected(False) self.on_treeWidget_itemSelectionChanged() self.on_rename() def on_new_script(self): self.window().app.monitor.suspend() tree_widget = self.treeWidget # type: ak_tree.AkTreeWidget parent_item = tree_widget.selectedItems()[0] # type: ak_tree.ItemWidgetType parent = self.__extractData(parent_item) script = autokey.model.script.Script("New Script", "#Enter script code") new_item = ak_tree.ScriptWidgetItem(parent_item, script) parent.add_item(script) script.persist() self.window().app.monitor.unsuspend() tree_widget.sortItems(0, Qt.AscendingOrder) tree_widget.setCurrentItem(new_item) parent_item.setSelected(False) self.on_treeWidget_itemSelectionChanged() self.on_rename() def on_undo(self): self.stack.currentWidget().undo() def on_redo(self): self.stack.currentWidget().redo() def on_copy(self): source_objects = self.__getSelection() for source in source_objects: if isinstance(source, autokey.model.phrase.Phrase): new_obj = autokey.model.phrase.Phrase('', '') else: new_obj = autokey.model.script.Script('', '') new_obj.copy(source) self.cutCopiedItems.append(new_obj) def on_clone(self): source_object = self.__getSelection()[0] tree_widget = self.treeWidget # type: ak_tree.AkTreeWidget parent_item = tree_widget.selectedItems()[0].parent() # type: ak_tree.ItemWidgetType parent = self.__extractData(parent_item) if isinstance(source_object, autokey.model.phrase.Phrase): new_obj = autokey.model.phrase.Phrase('', '') new_obj.copy(source_object) new_item = ak_tree.PhraseWidgetItem(parent_item, new_obj) else: new_obj = autokey.model.script.Script('', '') new_obj.copy(source_object) new_item = ak_tree.ScriptWidgetItem(parent_item, new_obj) parent.add_item(new_obj) self.window().app.monitor.suspend() new_obj.persist() self.window().app.monitor.unsuspend() tree_widget.sortItems(0, Qt.AscendingOrder) tree_widget.setCurrentItem(new_item) parent_item.setSelected(False) self.on_treeWidget_itemSelectionChanged() self.window().app.config_altered(False) def on_cut(self): self.cutCopiedItems = self.__getSelection() self.window().app.monitor.suspend() source_items = self.treeWidget.selectedItems() result = [f for f in source_items if f.parent() not in source_items] for item in result: self.__removeItem(item) self.window().app.monitor.unsuspend() self.window().app.config_altered(False) def on_paste(self): parent_item = self._get_current_treewidget_item() parent = self.__extractData(parent_item) self.window().app.monitor.suspend() new_items = [] for item in self.cutCopiedItems: if isinstance(item, autokey.model.folder.Folder): new_item = ak_tree.FolderWidgetItem(parent_item, item) ak_tree.WidgetItemFactory.process_folder(new_item, item) parent.add_folder(item) elif isinstance(item, autokey.model.phrase.Phrase): new_item = ak_tree.PhraseWidgetItem(parent_item, item) parent.add_item(item) else: new_item = ak_tree.ScriptWidgetItem(parent_item, item) parent.add_item(item) item.persist() new_items.append(new_item) self.treeWidget.sortItems(0, Qt.AscendingOrder) self.treeWidget.setCurrentItem(new_items[-1]) self.on_treeWidget_itemSelectionChanged() self.cutCopiedItems = [] for item in new_items: item.setSelected(True) self.window().app.monitor.unsuspend() self.window().app.config_altered(False) def on_delete(self): widget_items = self.treeWidget.selectedItems() self.window().app.monitor.suspend() if len(widget_items) == 1: widget_item = widget_items[0] data = self.__extractData(widget_item) if isinstance(data, autokey.model.folder.Folder): header = "Delete Folder?" msg = "Are you sure you want to delete the '{deleted_folder}' folder and all the items in it?".format( deleted_folder=data.title) else: entity_type = "Script" if isinstance(data, autokey.model.script.Script) else "Phrase" header = "Delete {}?".format(entity_type) msg = "Are you sure you want to delete '{element}'?".format(element=data.description) else: item_count = len(widget_items) header = "Delete {item_count} selected items?".format(item_count=item_count) msg = "Are you sure you want to delete the {item_count} selected folders/items?".format( item_count=item_count) result = QMessageBox.question(self.window(), header, msg, QMessageBox.Yes | QMessageBox.No) if result == QMessageBox.Yes: for widget_item in widget_items: self.__removeItem(widget_item) self.window().app.monitor.unsuspend() if result == QMessageBox.Yes: self.window().app.config_altered(False) def on_rename(self): widget_item = self._get_current_treewidget_item() self.treeWidget.editItem(widget_item, 0) def on_save(self): logger.info("User requested file save.") if self.stack.currentWidget().validate(): self.window().app.monitor.suspend() persist_global = self.stack.currentWidget().save() self.window().save_completed(persist_global) self.set_dirty(False) item = self._get_current_treewidget_item() item.update() self.treeWidget.update() self.treeWidget.sortItems(0, Qt.AscendingOrder) self.window().app.monitor.unsuspend() return False return True def on_reset(self): self.stack.currentWidget().reset() self.set_dirty(False) self.window().cancel_record() def on_save_log(self): file_name, _ = QFileDialog.getSaveFileName( # second return value contains the used file type filter. self.window(), "Save log file", "", "" # TODO: File type filter. Maybe "*.log"? ) del _ # We are only interested in the selected file name if file_name: list_widget = self.listWidget # type: QListWidget item_texts = (list_widget.item(row).text() for row in range(list_widget.count())) log_text = "\n".join(item_texts) + "\n" try: with open(file_name, "w") as log_file: log_file.write(log_text) except IOError: logger.exception("Error saving log file") else: self.on_clear_log() # Error log saved, so clear the previously saved entries def on_clear_log(self): self.listWidget.clear() def move_items(self, sourceItems, target): target_model_item = self.__extractData(target) # Filter out any child objects that belong to a parent already in the list result = [f for f in sourceItems if f.parent() not in sourceItems] self.window().app.monitor.suspend() for source in result: self.__removeItem(source) source_model_item = self.__extractData(source) if isinstance(source_model_item, autokey.model.folder.Folder): target_model_item.add_folder(source_model_item) self.__moveRecurseUpdate(source_model_item) else: target_model_item.add_item(source_model_item) source_model_item.path = None source_model_item.persist() target.addChild(source) self.window().app.monitor.unsuspend() self.treeWidget.sortItems(0, Qt.AscendingOrder) self.window().app.config_altered(True) def __moveRecurseUpdate(self, folder): folder.path = None folder.persist() for subfolder in folder.folders: self.__moveRecurseUpdate(subfolder) for child in folder.items: child.path = None child.persist() # ---- Private methods def _get_current_treewidget_item(self) -> ak_tree.ItemWidgetType: """ This method gets the TreeItem instance of the currently opened Item. Normally, this is just the selected item, but the user can deselect it by clicking in the whitespace below the tree. Some functions require the TreeItem of the currently opened Item. For example when renaming it, the name in the tree has to be updated. This function makes sure to always retrieve the required TreeItem instance. """ selected_items = self.treeWidget.selectedItems() # type: typing.List[ak_tree.ItemWidgetType] if selected_items: return selected_items[0] else: # The user deselected the item, so fall back to scan the whole tree for the desired item currently_edited_item = self.stack.currentWidget().get_current_item() if currently_edited_item is None: raise RuntimeError("Tried to perform an action on an item, while none is opened.") tree = self.treeWidget # type: ak_tree.AkTreeWidget item_widgets = [ tree.topLevelItem(top_level_index) for top_level_index in range(tree.topLevelItemCount()) ] # type: typing.List[ak_tree.ItemWidgetType] # Use a queue to iterate through the whole tree. while item_widgets: item_widget = item_widgets.pop(0) # The actual model data is stored in column 3 found_item = item_widget.data(3, Qt.UserRole) # type: ak_tree.ItemType if found_item is currently_edited_item: # Use identity to identify the right model instance. return item_widget if isinstance(item_widget, ak_tree.FolderWidgetItem): for child_index in range(item_widget.childCount()): item_widgets.append(item_widget.child(child_index)) raise RuntimeError("Expected item {} not found in the tree!".format(currently_edited_item)) def get_selected_item(self): return self.__getSelection() def __getSelection(self): items = self.treeWidget.selectedItems() ret = [self.__extractData(item) for item in items] # Filter out any child objects that belong to a parent already in the list result = [f for f in ret if f.parent not in ret] return result @staticmethod def __extractData(item): variant = item.data(3, Qt.UserRole) return variant def __removeItem(self, widgetItem): parent = widgetItem.parent() item = self.__extractData(widgetItem) self.__deleteHotkeys(item) if parent is None: removed_index = self.treeWidget.indexOfTopLevelItem(widgetItem) self.treeWidget.takeTopLevelItem(removed_index) self.configManager.folders.remove(item) else: removed_index = parent.indexOfChild(widgetItem) parent.removeChild(widgetItem) if isinstance(item, autokey.model.folder.Folder): item.parent.remove_folder(item) else: item.parent.remove_item(item) item.remove_data() self.treeWidget.sortItems(0, Qt.AscendingOrder) if parent is not None: if parent.childCount() > 0: new_index = min((removed_index, parent.childCount() - 1)) self.treeWidget.setCurrentItem(parent.child(new_index)) else: self.treeWidget.setCurrentItem(parent) else: new_index = min((removed_index, self.treeWidget.topLevelItemCount() - 1)) self.treeWidget.setCurrentItem(self.treeWidget.topLevelItem(new_index)) def __deleteHotkeys(self, removed_item): self.configManager.delete_hotkeys(removed_item) class ListWidgetHandler(logging.Handler): def __init__(self, list_widget: QListWidget, app): logging.Handler.__init__(self) self.widget = list_widget self.app = app self.level = logging.DEBUG log_format = "%(message)s" root_logger.addHandler(self) self.setFormatter(logging.Formatter(log_format)) def flush(self): pass def emit(self, record): try: item = QListWidgetItem(self.format(record)) if record.levelno > logging.INFO: item.setIcon(QIcon.fromTheme("dialog-warning")) item.setForeground(QBrush(Qt.red)) else: item.setIcon(QIcon.fromTheme("dialog-information")) self.app.exec_in_main(self._add_item, item) except (KeyboardInterrupt, SystemExit): raise except: self.handleError(record) def _add_item(self, item): self.widget.addItem(item) if self.widget.count() > 50: delItem = self.widget.takeItem(0) del delItem self.widget.scrollToBottom() autokey-0.96.0/lib/autokey/qtui/common.py000066400000000000000000000137641427671440700203710ustar00rootroot00000000000000# Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import re import os.path import pathlib import enum import functools from PyQt5.QtCore import QFile, QSize from PyQt5.QtGui import QFont, QIcon, QPixmap, QPainter, QColor, QFontDatabase from PyQt5.QtWidgets import QMessageBox, QLabel from PyQt5 import uic from PyQt5.QtSvg import QSvgRenderer import autokey.configmanager.configmanager_constants as cm_constants from autokey.logger import get_logger try: import autokey.qtui.compiled_resources except ModuleNotFoundError: import warnings # No compiled resource module found. Load bare files from disk instead. warn_msg = "Compiled Qt resources file not found. If autokey is launched directly from the source directory, " \ "this is expected and harmless. If not, this indicates a failure in the resource compilation." warnings.warn(warn_msg) RESOURCE_PATH_PREFIX = str(pathlib.Path(__file__).resolve().parent / "resources") local_path = pathlib.Path(__file__).resolve().parent.parent.parent.parent / "config" if local_path.exists(): # This is running from the source directory, thus icons are in /config ICON_PATH_PREFIX = str(local_path) else: # This is an installation. Icons reside in autokey/qtui/resources/icons, where they were copied by setup.py ICON_PATH_PREFIX = str(pathlib.Path(__file__).resolve().parent / "resources" / "icons") del local_path else: import atexit # Compiled resources found, so use it. RESOURCE_PATH_PREFIX = ":" ICON_PATH_PREFIX = ":/icons" atexit.register(autokey.qtui.compiled_resources.qCleanupResources) logger = get_logger(__name__) del get_logger EMPTY_FIELD_REGEX = re.compile(r"^ *$", re.UNICODE) def monospace_font() -> QFont: """ Returns a monospace font used in the code editor widgets. :return: QFont instance having a monospace font. """ font = QFontDatabase.systemFont(QFontDatabase.FixedFont) # font = QFont("monospace") # font.setStyleHint(QFont.Monospace) return font def set_url_label(label: QLabel, path: str): # In both cases, only replace the first occurence. if path.startswith(cm_constants.CONFIG_DEFAULT_FOLDER): text = path.replace(cm_constants.CONFIG_DEFAULT_FOLDER, "(Default folder)", 1) else: # if bob has added a path '/home/bob/some/folder/home/bobbie/foo/' to autokey, the desired replacement text # is '~/some/folder/home/bobbie/foo/' and NOT '~/some/folder~bie/foo/' text = path.replace(os.path.expanduser("~"), "~", 1) url = "file://" + path if not label.openExternalLinks(): # The openExternalLinks property is not set in the UI file, so fail fast instead of doing workarounds. raise ValueError("QLabel with disabled openExternalLinks property used to display an external URL. " "This won’t work, so fail now. Label: {}, Text: {}".format(label, label.text())) # TODO elide text? label.setText("""{text}""".format(url=url, text=text)) def validate(expression, message, widget, parent): if not expression: QMessageBox.critical(parent, message, message) if widget is not None: widget.setFocus() return expression class AutoKeyIcon(enum.Enum): AUTOKEY = "autokey.png" AUTOKEY_SCALABLE = "autokey.svg" SYSTEM_TRAY = "autokey-status.svg" SYSTEM_TRAY_DARK = "autokey-status-dark.svg" SYSTEM_TRAY_ERROR = "autokey-status-error.svg" @functools.lru_cache() def load_icon(name: AutoKeyIcon) -> QIcon: file_path = ICON_PATH_PREFIX + "/" + name.value icon = QIcon(file_path) if not icon.availableSizes() and file_path.endswith(".svg"): # FIXME: Work around Qt Bug: https://bugreports.qt.io/browse/QTBUG-63187 # Manually render the SVG to some common icon sizes. icon = QIcon() # Discard the bugged QIcon renderer = QSvgRenderer(file_path) for size in (16, 22, 24, 32, 64, 128): pixmap = QPixmap(QSize(size, size)) pixmap.fill(QColor(255, 255, 255, 0)) renderer.render(QPainter(pixmap)) icon.addPixmap(pixmap) return icon def _get_ui_qfile(name: str): """ Returns an opened, read-only QFile for the given QtDesigner UI file name. Expects a plain name like "centralwidget". The file ending and resource path is added automatically. Raises FileNotFoundError, if the given ui file does not exist. :param name: :return: """ file_path = RESOURCE_PATH_PREFIX + "/ui/{ui_file_name}.ui".format(ui_file_name=name) file = QFile(file_path) if not file.exists(): raise FileNotFoundError("UI file not found: " + file_path) file.open(QFile.ReadOnly) return file def load_ui_from_file(name: str): """ Returns a tuple from uic.loadUiType(), loading the ui file with the given name. :param name: :return: """ ui_file = _get_ui_qfile(name) try: base_type = uic.loadUiType(ui_file, from_imports=True) finally: ui_file.close() return base_type """ This renamed function is supposed to be used during class definition to make the intention clear. Usage example: class SomeWidget(*inherits_from_ui_file_with_name("SomeWidgetUiFileName")): def __init__(self, parent): super(SomeWidget, self).__init__(parent) self.setupUi(self) """ inherits_from_ui_file_with_name = load_ui_from_file autokey-0.96.0/lib/autokey/qtui/compiled_resources.pyi000066400000000000000000000017051427671440700231300ustar00rootroot00000000000000# Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import typing qt_version = ... # type: typing.List[str] rcc_version = ... # type: int qt_resource_data = ... # type: bytes qt_resource_name = ... # type: bytes qt_resource_struct = ... # type: bytes def qCleanupResources() -> None: ... def qInitResources() -> None: ... autokey-0.96.0/lib/autokey/qtui/configwindow.py000066400000000000000000000350031427671440700215640ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import webbrowser from PyQt5.QtCore import pyqtSignal, QTimer from PyQt5.QtGui import QIcon, QKeySequence, QCloseEvent from PyQt5.QtWidgets import QApplication, QAction, QMenu import autokey.common import autokey.model.folder import autokey.model.phrase import autokey.model.script import autokey.qtui.common import autokey.configmanager.configmanager as cm import autokey.configmanager.configmanager_constants as cm_constants from .settings import SettingsDialog from . import dialogs logger = __import__("autokey.logger").logger.get_logger(__name__) PROBLEM_MSG_PRIMARY = "Some problems were found" PROBLEM_MSG_SECONDARY = "%1\n\nYour changes have not been saved." class ConfigWindow(*autokey.qtui.common.inherits_from_ui_file_with_name("mainwindow")): script_errors_available = pyqtSignal(bool, name="script_errors_available") def __init__(self, app: QApplication): super().__init__() self.setupUi(self) self.about_dialog = dialogs.AboutAutokeyDialog(self) self.show_script_errors_dialog = self._create_show_recent_script_errors_dialog() self.app = app self.action_create = self._create_action_create() self.toolbar.insertAction(self.action_save, self.action_create) # Insert before action_save, i.e. at index 0 self._connect_all_file_menu_signals() self._connect_all_edit_menu_signals() self._connect_all_tools_menu_signals() self._connect_all_settings_menu_signals() self._connect_all_help_menu_signals() self._initialise_action_states() self._set_platform_specific_keyboard_shortcuts() self.central_widget.init(app) self.central_widget.populate_tree(self.app.configManager) def _create_action_create(self) -> QAction: """ The action_create action contains a menu with all four "new" actions. It is inserted into the main window tool bar and lets the user create new items in the file tree. QtCreator currently does not support defining such actions that open a menu with choices, so do it in code. """ icon = QIcon.fromTheme("document-new") action_create = QAction(icon, "New…", self) create_menu = QMenu(self) create_menu.insertActions(None, ( # "Insert before None", so append all items to the (empty) action list self.action_new_top_folder, self.action_new_sub_folder, self.action_new_phrase, self.action_new_script )) action_create.setMenu(create_menu) return action_create def _create_show_recent_script_errors_dialog(self) -> dialogs.ShowRecentScriptErrorsDialog: show_script_errors_dialog = dialogs.ShowRecentScriptErrorsDialog(self) # Forward the signal from the dialog instance to the own signal show_script_errors_dialog.script_errors_available.connect(self.script_errors_available) return show_script_errors_dialog def _connect_all_file_menu_signals(self): # Show the action_create popup menu regardless where the user places the click. # The Action is displayed as "[]v". Clicking on the downwards arrow opens the popup menu as # expected, but clicking on the larger icon does nothing by default, because no action is associated. # The intention is to show the popup regardless of where the user places the click, so call the containing # button’s showMenu when the action itself is pressed. # # Unlike other methods using action_create.menu().exec_() or .popup(position), this way is 100% UI consistent. self.action_create.triggered.connect(self.toolbar.widgetForAction(self.action_create).showMenu) self.action_new_top_folder.triggered.connect(self.central_widget.on_new_topfolder) self.action_new_sub_folder.triggered.connect(self.central_widget.on_new_folder) self.action_new_phrase.triggered.connect(self.central_widget.on_new_phrase) self.action_new_script.triggered.connect(self.central_widget.on_new_script) self.action_save.triggered.connect(self.central_widget.on_save) self.action_close_window.triggered.connect(self.on_close) self.action_quit.triggered.connect(self.on_quit) def _connect_all_edit_menu_signals(self): self.action_undo.triggered.connect(self.central_widget.on_undo) self.action_redo.triggered.connect(self.central_widget.on_redo) self.action_cut_item.triggered.connect(self.central_widget.on_cut) self.action_copy_item.triggered.connect(self.central_widget.on_copy) self.action_paste_item.triggered.connect(self.central_widget.on_paste) self.action_clone_item.triggered.connect(self.central_widget.on_clone) self.action_delete_item.triggered.connect(self.central_widget.on_delete) self.action_rename_item.triggered.connect(self.central_widget.on_rename) def _connect_all_tools_menu_signals(self): self.action_show_last_script_errors.triggered.connect(self.show_script_errors_dialog.update_and_show) # Only enable action_show_last_script_errors if script errors are recorded. # Automatically disable the action if no errors are viewable to prevent the user from seeing a dialogue window # in some undefined state. self.script_errors_available.connect(self.action_show_last_script_errors.setEnabled) self.action_record_script.triggered.connect(self.on_record) self.action_run_script.triggered.connect(self.on_run_script) # Add all defined macros to the »Insert Macros« menu self.app.service.phraseRunner.macroManager.get_menu(self.on_insert_macro, self.menu_insert_macros) def _connect_all_settings_menu_signals(self): # TODO: Connect and implement unconnected actions app = QApplication.instance() # Sync the action_enable_monitoring checkbox with the global state. Prevents a desync when the global hotkey # is used app.monitoring_disabled.connect(self.action_enable_monitoring.setChecked) self.action_enable_monitoring.triggered.connect(app.toggle_service) self.action_show_log_view.triggered.connect(self.on_show_log) self.action_configure_shortcuts.triggered.connect(self._none_action) # Currently not shown in any menu self.action_configure_toolbars.triggered.connect(self._none_action) # Currently not shown in any menu # Both actions above were part of the KXMLGUI window functionality and allowed to customize keyboard shortcuts # and toolbar items self.action_configure_autokey.triggered.connect(self.on_advanced_settings) def _connect_all_help_menu_signals(self): self.action_show_online_manual.triggered.connect(lambda: self.open_external_url(autokey.common.HELP_URL)) self.action_show_faq.triggered.connect(lambda: self.open_external_url(autokey.common.FAQ_URL)) self.action_show_api.triggered.connect(lambda: self.open_external_url(autokey.common.API_URL)) self.action_report_bug.triggered.connect(lambda: self.open_external_url(autokey.common.BUG_URL)) self.action_about_autokey.triggered.connect(self.about_dialog.show) self.action_about_qt.triggered.connect(QApplication.aboutQt) def _initialise_action_states(self): """ Some menu actions have on/off states that have to be initialised. Perform all non-trivial action state initialisations. Trivial ones (i.e. setting to some constant) are done in the Qt UI file, so only perform those that require some run-time state or configuration value here. """ self.action_enable_monitoring.setChecked(self.app.service.is_running()) self.action_enable_monitoring.setEnabled(not self.app.serviceDisabled) def _set_platform_specific_keyboard_shortcuts(self): """ QtDesigner does not support QKeySequence::StandardKey enum based default keyboard shortcuts. This means that all default key combinations ("Save", "Quit", etc) have to be defined in code. """ self.action_new_phrase.setShortcuts(QKeySequence.New) self.action_save.setShortcuts(QKeySequence.Save) self.action_close_window.setShortcuts(QKeySequence.Close) self.action_quit.setShortcuts(QKeySequence.Quit) self.action_undo.setShortcuts(QKeySequence.Undo) self.action_redo.setShortcuts(QKeySequence.Redo) self.action_cut_item.setShortcuts(QKeySequence.Cut) self.action_copy_item.setShortcuts(QKeySequence.Copy) self.action_paste_item.setShortcuts(QKeySequence.Paste) self.action_delete_item.setShortcuts(QKeySequence.Delete) self.action_configure_autokey.setShortcuts(QKeySequence.Preferences) def _none_action(self): import warnings warnings.warn("Unconnected menu item clicked! Nothing happens…", UserWarning) def set_dirty(self): self.central_widget.set_dirty(True) self.action_save.setEnabled(True) def closeEvent(self, event: QCloseEvent): """ This function is automatically called when the window is closed using the close [X] button in the window decorations or by right clicking in the system window list and using the close action, or similar ways to close the window. Just ignore this event and simulate that the user used the action_close_window instead. To quote the Qt5 QCloseEvent documentation: If you do not want your widget to be hidden, or want some special handling, you should reimplement the event handler and ignore() the event. """ event.ignore() # Be safe and emit this signal, because it might be connected to multiple slots. self.action_close_window.triggered.emit(True) def config_modified(self): pass def is_dirty(self): return self.central_widget.dirty def update_actions(self, items, changed): if len(items) > 0: can_create = isinstance(items[0], autokey.model.folder.Folder) and len(items) == 1 can_copy = True for item in items: if isinstance(item, autokey.model.folder.Folder): can_copy = False break self.action_new_top_folder.setEnabled(True) self.action_new_sub_folder.setEnabled(can_create) self.action_new_phrase.setEnabled(can_create) self.action_new_script.setEnabled(can_create) self.action_copy_item.setEnabled(can_copy) self.action_clone_item.setEnabled(can_copy) self.action_paste_item.setEnabled(can_create and len(self.central_widget.cutCopiedItems) > 0) self.action_record_script.setEnabled(isinstance(items[0], autokey.model.script.Script) and len(items) == 1) self.action_run_script.setEnabled(isinstance(items[0], autokey.model.script.Script) and len(items) == 1) self.menu_insert_macros.setEnabled(isinstance(items[0], autokey.model.phrase.Phrase) and len(items) == 1) if changed: self.action_save.setEnabled(False) self.action_undo.setEnabled(False) self.action_redo.setEnabled(False) def set_undo_available(self, state): self.action_undo.setEnabled(state) def set_redo_available(self, state): self.action_redo.setEnabled(state) def save_completed(self, persist_global): logger.debug("Saving completed. persist_global: {}".format(persist_global)) self.action_save.setEnabled(False) self.app.config_altered(persist_global) def cancel_record(self): if self.action_record_script.isChecked(): self.action_record_script.setChecked(False) self.central_widget.recorder.stop() # ---- Signal handlers ---- def queryClose(self): cm.ConfigManager.SETTINGS[cm_constants.HPANE_POSITION] = self.central_widget.splitter.sizes()[0] + 4 cm.ConfigManager.SETTINGS[cm_constants.COLUMN_WIDTHS] = [ self.central_widget.treeWidget.columnWidth(column_index) for column_index in range(3) ] if self.is_dirty(): if self.central_widget.promptToSave(): return False self.hide() return True # File Menu def on_close(self): self.cancel_record() self.queryClose() def on_quit(self): if self.queryClose(): self.app.shutdown() # Edit Menu def on_insert_macro(self, macro): token = macro.get_token() self.central_widget.phrasePage.insert_token(token) def on_record(self): if self.action_record_script.isChecked(): dlg = dialogs.RecordDialog(self, self._do_record) dlg.show() else: self.central_widget.recorder.stop() def _do_record(self, ok: bool, record_keyboard: bool, record_mouse: bool, delay: float): if ok: self.central_widget.recorder.set_record_keyboard(record_keyboard) self.central_widget.recorder.set_record_mouse(record_mouse) self.central_widget.recorder.start(delay) else: self.action_record_script.setChecked(False) def on_run_script(self): script = self.central_widget.get_selected_item()[0] QTimer.singleShot( 2000, # Fix the GUI tooltip for action_run_script when changing this! (lambda: self.app.service.scriptRunner.execute_script( script )) ) # Settings Menu def on_advanced_settings(self): s = SettingsDialog(self) s.show() def on_show_log(self): self.central_widget.listWidget.setVisible(self.action_show_log_view.isChecked()) # Help Menu @staticmethod def open_external_url(url: str): webbrowser.open(url, False, True) autokey-0.96.0/lib/autokey/qtui/data/000077500000000000000000000000001427671440700174255ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/qtui/data/api.txt000066400000000000000000000113371427671440700207440ustar00rootroot00000000000000engine.create_abbreviation(folder, description, abbr, contents) DEPRECATED! Use create_phrase() instead, create a Phrase with an abbreviation engine.create_hotkey(folder, description, modifiers, key, contents) DEPRECATED! Use create_phrase() instead, create a Phrase with a hotkey engine.create_phrase(folder, description, contents) Create a Phrase in the given Folder, Fully configure it using the optional keyword parameters engine.get_folder(title) Retrieve a folder by its title engine.get_macro_arguments() Get the arguments supplied to the current script via its macro engine.run_script(description) Run an existing script using its description to look it up engine.set_return_value(val) Store a return value to be used by a phrase macro engine.get_triggered_abbreviation() Provide the typed abbreviation and trigger character, or (None, None) if not triggered by an abbreviation keyboard.fake_keypress(key, repeat=1) Fake a keypress keyboard.press_key(key) Send a key down event keyboard.release_key(key) Send a key up event keyboard.send_key(key, repeat=1) Send a keyboard event keyboard.send_keys(key_string, send_mode=keyboard.SendMode.KEYBOARD) Send a sequence of keys via keyboard events (default) or via clipboard pasting keyboard.wait_for_keypress(self, key, modifiers=[], timeOut=10.0) Wait for a keypress or key combination keyboard.SendMode Contains all valid options for the send_mode parameter keyboard.SendMode.KEYBOARD Type using the keyboard keyboard.SendMode.CB_CTRL_V Use the clipboard and paste using +V keyboard.SendMode.CB_CTRL_SHIFT_V Use the clipboard and paste using ++V keyboard.SendMode.CB_SHIFT_INSERT Use the clipboard and paste using + keyboard.SendMode.SELECTION Use the X11 PRIMARY buffer / mouse selection and Paste using the middle mouse button mouse.click_absolute(x, y, button) Send a mouse click relative to the screen (absolute) mouse.click_relative(x, y, button) Send a mouse click relative to the active window mouse.click_relative_self(x, y, button) Send a mouse click relative to the current mouse position mouse.wait_for_click(self, button, timeOut=10.0) Wait for a mouse click clipboard.fill_clipboard(contents) Copy text into the clipboard clipboard.fill_selection(contents) Copy text into the X selection clipboard.get_clipboard() Read text from the clipboard clipboard.get_selection() Read text from the X selection dialog.info_dialog(title="Information", message="") Show an informational dialog. dialog.choose_colour(title="Select Colour") Show a Colour Chooser dialog dialog.choose_directory(title="Select Directory", initialDir="~", rememberAs=None, **kwargs) Show a Directory Chooser dialog dialog.combo_menu(options, title="Choose an option", message="Choose an option", **kwargs) Show a combobox menu dialog.input_dialog(title="Enter a value", message="Enter a value", default="", **kwargs) Show an input dialog dialog.list_menu(options, title="Choose a value", message="Choose a value", default=None, **kwargs) Show a single-selection list menu dialog.list_menu_multi(options, title="Choose one or more values", message="Choose one or more values", defaults=[], **kwargs) Show a multiple-selection list menu dialog.open_file(title="Open File", initialDir="~", fileTypes="*|All Files", rememberAs=None, **kwargs) Show an Open File dialog dialog.password_dialog(title="Enter password", message="Enter password", **kwargs) Show a password input dialog dialog.save_file(title="Save As", initialDir="~", fileTypes="*|All Files", rememberAs=None, **kwargs) Show a Save As dialog store.get_value(key) Get a value store.remove_value(key) Remove a value store.set_value(key, value) Store a value system.create_file(fileName, contents="") Create a file with contents system.exec_command(command, getOutput=True) Execute a shell command window.activate(title, switchDesktop=False, matchClass=False) Activate the specified window, giving it input focus window.close(title, matchClass=False) Close the specified window gracefully window.get_active_class() Get the class of the currently active window window.get_active_geometry() Get the geometry of the currently active window window.get_active_title() Get the visible title of the currently active window window.move_to_desktop(title, deskNum, matchClass=False) Move the specified window to the given desktop window.close(title, xOrigin=-1, yOrigin=-1, width=-1, height=-1, matchClass=False) Resize and/or move the specified window window.set_property(title, action, prop, matchClass=False) Set a property on the given window using the specified action window.switch_desktop(deskNum) Switch to the specified desktop window.wait_for_exist(title, timeOut=5) Wait for window with the given title to be created window.wait_for_focus(title, timeOut=5) Wait for window with the given title to have focus autokey-0.96.0/lib/autokey/qtui/dbus_service.py000066400000000000000000000042111427671440700215410ustar00rootroot00000000000000# Copyright (C) 2018 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from PyQt5.QtCore import Q_CLASSINFO, pyqtSlot from PyQt5.QtDBus import QDBusAbstractAdaptor, QDBusConnection class AppService(QDBusAbstractAdaptor): Q_CLASSINFO("D-Bus Interface", 'org.autokey.Service') Q_CLASSINFO( "D-Bus Introspection", ' \n' ' \n' ' \n' ' \n' ' \n' ' \n' ' \n' ' \n' ' \n' ' \n' ' \n' ' \n' ) def __init__(self, parent): super(AppService, self).__init__(parent) self.connection = QDBusConnection.sessionBus() path = '/AppService' service = 'org.autokey.Service' self.connection.registerObject(path, parent) self.connection.registerService(service) self.setAutoRelaySignals(True) @pyqtSlot() def show_configure(self): self.parent().show_configure() @pyqtSlot(str) def run_script(self, name): self.parent().service.run_script(name) @pyqtSlot(str) def run_phrase(self, name): self.parent().service.run_phrase(name) @pyqtSlot(str) def run_folder(self, name): self.parent().service.run_folder(name) autokey-0.96.0/lib/autokey/qtui/dialogs/000077500000000000000000000000001427671440700201365ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/qtui/dialogs/__init__.py000066400000000000000000000027251427671440700222550ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ This package contains all user dialogs. All these dialogs subclass QDialog. They perform various input tasks. """ __all__ = [ "validate", "EMPTY_FIELD_REGEX", "AbbrSettingsDialog", "AboutAutokeyDialog", "HotkeySettingsDialog", "GlobalHotkeyDialog", "WindowFilterSettingsDialog", "RecordDialog", "ShowRecentScriptErrorsDialog" ] from autokey.qtui.common import EMPTY_FIELD_REGEX, validate from .abbrsettings import AbbrSettingsDialog from .hotkeysettings import HotkeySettingsDialog, GlobalHotkeyDialog from .windowfiltersettings import WindowFilterSettingsDialog from .recorddialog import RecordDialog from .about_autokey_dialog import AboutAutokeyDialog from .show_recent_script_errors import ShowRecentScriptErrorsDialog autokey-0.96.0/lib/autokey/qtui/dialogs/abbrsettings.py000066400000000000000000000215251427671440700232040ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ This module contains the abbreviation settings dialog and used components. This dialog allows the user to set and configure abbreviations to trigger scripts and phrases. """ from PyQt5 import QtCore from PyQt5.QtWidgets import QListWidgetItem, QDialogButtonBox import autokey.model.folder import autokey.model.helpers import autokey.model.phrase from autokey.qtui import common as ui_common logger = __import__("autokey.logger").logger.get_logger(__name__) WORD_CHAR_OPTIONS = { "All non-word": autokey.model.helpers.DEFAULT_WORDCHAR_REGEX, "Space and Enter": r"[^ \n]", "Tab": r"[^\t]" } WORD_CHAR_OPTIONS_ORDERED = tuple(sorted(WORD_CHAR_OPTIONS.keys())) class AbbrListItem(QListWidgetItem): """ This is a list item used in the abbreviation QListWidget list. It simply holds a string value i.e. the user defined abbreviation string. """ def __init__(self, text): super(AbbrListItem, self).__init__(text) self.setFlags(self.flags() | QtCore.Qt.ItemFlags(QtCore.Qt.ItemIsEditable)) def setData(self, role, value): if value == "": self.listWidget().itemChanged.emit(self) else: QListWidgetItem.setData(self, role, value) class AbbrSettingsDialog(*ui_common.inherits_from_ui_file_with_name("abbrsettings")): def __init__(self, parent): super().__init__(parent) self.setupUi() self._reset_word_char_combobox() def setupUi(self): self.setObjectName("Form") # TODO: needed? Maybe use a better name than 'Form' super().setupUi(self) def on_addButton_pressed(self): logger.info("New abbreviation added.") item = AbbrListItem("") self.abbrListWidget.addItem(item) self.abbrListWidget.editItem(item) self.removeButton.setEnabled(True) def on_removeButton_pressed(self): item = self.abbrListWidget.takeItem(self.abbrListWidget.currentRow()) if item is not None: logger.info("User deletes abbreviation with text: {}".format(item.text())) if self.abbrListWidget.count() == 0: logger.debug("Last abbreviation deleted, disabling delete and OK buttons.") self.removeButton.setEnabled(False) # The user can only accept the dialog if the content is valid self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False) def on_abbrListWidget_itemChanged(self, item): if ui_common.EMPTY_FIELD_REGEX.match(item.text()): row = self.abbrListWidget.row(item) self.abbrListWidget.takeItem(row) logger.debug("User deleted abbreviation content. Deleted empty list element.") del item else: # The item is non-empty. Therefore there is at least one element in the list, thus input is valid. # Allow the user to accept his edits. self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(True) if self.abbrListWidget.count() == 0: logger.debug("Last abbreviation deleted, disabling delete and OK buttons.") self.removeButton.setEnabled(False) # The user can only accept the dialog if the content is valid self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False) def on_abbrListWidget_itemDoubleClicked(self, item): self.abbrListWidget.editItem(item) def on_ignoreCaseCheckbox_stateChanged(self, state): if not state: self.matchCaseCheckbox.setChecked(False) def on_matchCaseCheckbox_stateChanged(self, state): if state: self.ignoreCaseCheckbox.setChecked(True) def on_immediateCheckbox_stateChanged(self, state): if state: self.omitTriggerCheckbox.setChecked(False) self.omitTriggerCheckbox.setEnabled(False) self.wordCharCombo.setEnabled(False) else: self.omitTriggerCheckbox.setEnabled(True) self.wordCharCombo.setEnabled(True) def load(self, item): self.targetItem = item self.abbrListWidget.clear() if autokey.model.helpers.TriggerMode.ABBREVIATION in item.modes: for abbr in item.abbreviations: self.abbrListWidget.addItem(AbbrListItem(abbr)) self.removeButton.setEnabled(True) self.abbrListWidget.setCurrentRow(0) else: self.removeButton.setEnabled(False) self.removeTypedCheckbox.setChecked(item.backspace) self._reset_word_char_combobox() wordCharRegex = item.get_word_chars() if wordCharRegex in list(WORD_CHAR_OPTIONS.values()): # Default wordchar regex used for desc, regex in WORD_CHAR_OPTIONS.items(): if item.get_word_chars() == regex: self.wordCharCombo.setCurrentIndex(WORD_CHAR_OPTIONS_ORDERED.index(desc)) break else: # Custom wordchar regex used self.wordCharCombo.addItem(autokey.model.helpers.extract_wordchars(wordCharRegex)) self.wordCharCombo.setCurrentIndex(len(WORD_CHAR_OPTIONS)) if isinstance(item, autokey.model.folder.Folder): self.omitTriggerCheckbox.setVisible(False) else: self.omitTriggerCheckbox.setVisible(True) self.omitTriggerCheckbox.setChecked(item.omitTrigger) if isinstance(item, autokey.model.phrase.Phrase): self.matchCaseCheckbox.setVisible(True) self.matchCaseCheckbox.setChecked(item.matchCase) else: self.matchCaseCheckbox.setVisible(False) self.ignoreCaseCheckbox.setChecked(item.ignoreCase) self.triggerInsideCheckbox.setChecked(item.triggerInside) self.immediateCheckbox.setChecked(item.immediate) # Enable the OK button only if there are abbreviations in the loaded list. Otherwise only cancel is available # to the user until they add a non-empty abbreviation. self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(bool(self.get_abbrs())) def save(self, item): item.modes.append(autokey.model.helpers.TriggerMode.ABBREVIATION) item.clear_abbreviations() item.abbreviations = self.get_abbrs() item.backspace = self.removeTypedCheckbox.isChecked() option = str(self.wordCharCombo.currentText()) if option in WORD_CHAR_OPTIONS: item.set_word_chars(WORD_CHAR_OPTIONS[option]) else: item.set_word_chars(autokey.model.helpers.make_wordchar_re(option)) if not isinstance(item, autokey.model.folder.Folder): item.omitTrigger = self.omitTriggerCheckbox.isChecked() if isinstance(item, autokey.model.phrase.Phrase): item.matchCase = self.matchCaseCheckbox.isChecked() item.ignoreCase = self.ignoreCaseCheckbox.isChecked() item.triggerInside = self.triggerInsideCheckbox.isChecked() item.immediate = self.immediateCheckbox.isChecked() def reset(self): self.removeButton.setEnabled(False) self.abbrListWidget.clear() self._reset_word_char_combobox() self.omitTriggerCheckbox.setChecked(False) self.removeTypedCheckbox.setChecked(True) self.matchCaseCheckbox.setChecked(False) self.ignoreCaseCheckbox.setChecked(False) self.triggerInsideCheckbox.setChecked(False) self.immediateCheckbox.setChecked(False) def _reset_word_char_combobox(self): self.wordCharCombo.clear() for item in WORD_CHAR_OPTIONS_ORDERED: self.wordCharCombo.addItem(item) self.wordCharCombo.setCurrentIndex(0) def get_abbrs(self): ret = [] for i in range(self.abbrListWidget.count()): text = self.abbrListWidget.item(i).text() ret.append(str(text)) return ret def get_abbrs_readable(self): abbrs = self.get_abbrs() if len(abbrs) == 1: return abbrs[0] else: return "[%s]" % ','.join(abbrs) def reset_focus(self): self.addButton.setFocus() def accept(self): super().accept() def reject(self): self.load(self.targetItem) super().reject() autokey-0.96.0/lib/autokey/qtui/dialogs/about_autokey_dialog.py000066400000000000000000000025431427671440700247060ustar00rootroot00000000000000# Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys from PyQt5.QtWidgets import QWidget from PyQt5.QtCore import QSize import autokey.common from autokey.qtui import common as ui_common class AboutAutokeyDialog(*ui_common.inherits_from_ui_file_with_name("about_autokey_dialog")): def __init__(self, parent: QWidget = None): super(AboutAutokeyDialog, self).__init__(parent) self.setupUi(self) icon = ui_common.load_icon(ui_common.AutoKeyIcon.AUTOKEY) pixmap = icon.pixmap(icon.actualSize(QSize(1024, 1024))) self.autokey_icon.setPixmap(pixmap) self.autokey_version_label.setText(autokey.common.VERSION) self.python_version_label.setText(sys.version.replace("\n", " ")) autokey-0.96.0/lib/autokey/qtui/dialogs/detectdialog.py000066400000000000000000000041641427671440700231450ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . from typing import Tuple from PyQt5.QtWidgets import QWidget from autokey.qtui import common as ui_common logger = ui_common.logger.getChild("DetectDialog") class DetectDialog(*ui_common.inherits_from_ui_file_with_name("detectdialog")): """ The DetectDialog lets the user select window properties of a chosen window. The dialog shows the window title and window class of the chosen window and lets the user select one of those two options. """ def __init__(self, parent: QWidget): super(DetectDialog, self).__init__(parent) self.setupUi(self) self.window_title = "" self.window_class = "" def populate(self, window_info: Tuple[str, str]): self.window_title, self.window_class = window_info self.detected_title.setText(self.window_title) self.detected_class.setText(self.window_class) logger.info( "Detected window with properties title: {}, window class: {}".format(self.window_title, self.window_class) ) def get_choice(self) -> str: # This relies on autoExclusive being set to true in the ui file. if self.classButton.isChecked(): logger.debug("User has chosen the window class: {}".format(self.window_class)) return self.window_class else: logger.debug("User has chosen the window title: {}".format(self.window_title)) return self.window_title autokey-0.96.0/lib/autokey/qtui/dialogs/hotkeysettings.py000066400000000000000000000134521427671440700236010ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import typing from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QDialogButtonBox import autokey.model.folder import autokey.model.helpers import autokey.model.phrase import autokey.model.script from autokey.qtui import common as qtui_common from autokey import UI_common_functions as UI_common from autokey import iomediator import autokey.configmanager.configmanager as cm from autokey.model.key import Key logger = __import__("autokey.logger").logger.get_logger(__name__) Item = typing.Union[autokey.model.folder.Folder, autokey.model.script.Script, autokey.model.phrase.Phrase] class HotkeySettingsDialog(*qtui_common.inherits_from_ui_file_with_name("hotkeysettings")): KEY_MAP = { ' ': "", } REVERSE_KEY_MAP = {value: key for key, value in KEY_MAP.items()} DEFAULT_RECORDED_KEY_LABEL_CONTENT = "(None)" """ This signal is emitted whenever the key is assigned/deleted. This happens when the user records a key or cancels a key recording. """ key_assigned = pyqtSignal(bool, name="key_assigned") recording_finished = pyqtSignal(bool, name="recording_finished") def __init__(self, parent): super(HotkeySettingsDialog, self).__init__(parent) self.setupUi(self) # Enable the Ok button iff a correct key combination is assigned. This guides the user and obsoletes an error # message that was shown when the user did something invalid. self.key_assigned.connect(self.buttonBox.button(QDialogButtonBox.Ok).setEnabled) self.recording_finished.connect(self.record_combination_button.setEnabled) self.key = "" self._update_key(None) # Use _update_key to emit the key_assigned signal and disable the Ok button. self.target_item = None # type: Item self.grabber = None # type: iomediator.KeyGrabber self.MODIFIER_BUTTONS = { self.mod_control_button: Key.CONTROL, self.mod_alt_button: Key.ALT, # self.mod_altgr_button: Key.ALT_GR, self.mod_shift_button: Key.SHIFT, self.mod_super_button: Key.SUPER, self.mod_hyper_button: Key.HYPER, self.mod_meta_button: Key.META, } def _update_key(self, key): self.key = key if key is None: self.recorded_key_label.setText("Key: {}".format(self.DEFAULT_RECORDED_KEY_LABEL_CONTENT)) # TODO: i18n self.key_assigned.emit(False) else: self.recorded_key_label.setText("Key: {}".format(key)) # TODO: i18n self.key_assigned.emit(True) def on_record_combination_button_pressed(self): """ Start recording a key combination when the user clicks on the record_combination_button. The button itself is automatically disabled during the recording process. """ self.recorded_key_label.setText("Press a key or combination...") # TODO: i18n logger.debug("User starts to record a key combination.") self.grabber = iomediator.keygrabber.KeyGrabber(self) self.grabber.start() def load(self, item: Item): self.target_item = item UI_common.load_hotkey_settings_dialog(self, item) def populate_hotkey_details(self, item): self.activate_modifier_buttons(item.modifiers) key = item.hotKey key_text = UI_common.get_hotkey_text(self, key) self._update_key(key_text) logger.debug("Loaded item {}, key: {}, modifiers: {}".format(item, key_text, item.modifiers)) def activate_modifier_buttons(self, modifiers): for button, key in self.MODIFIER_BUTTONS.items(): button.setChecked(key in modifiers) def save(self, item): UI_common.save_hotkey_settings_dialog(self, item) def reset(self): for button in self.MODIFIER_BUTTONS: button.setChecked(False) self._update_key(None) def set_key(self, key, modifiers: typing.List[Key] = None): """This is called when the user successfully finishes recording a key combination.""" if modifiers is None: modifiers = [] if key in self.KEY_MAP: key = self.KEY_MAP[key] self._update_key(key) self.activate_modifier_buttons(modifiers) self.recording_finished.emit(True) def cancel_grab(self): """ This is called when the user cancels a recording. Canceling is done by clicking with the left mouse button. """ logger.debug("User canceled hotkey recording.") self.recording_finished.emit(True) def build_modifiers(self): modifiers = [] for button, key in self.MODIFIER_BUTTONS.items(): if button.isChecked(): modifiers.append(key) modifiers.sort() return modifiers def reject(self): self.load(self.target_item) super().reject() class GlobalHotkeyDialog(HotkeySettingsDialog): def load(self, item: cm.GlobalHotkey): self.target_item = item UI_common.load_global_hotkey_dialog(self, item) def save(self, item: cm.GlobalHotkey): UI_common.save_hotkey_settings_dialog(self, item) autokey-0.96.0/lib/autokey/qtui/dialogs/recorddialog.py000066400000000000000000000037331427671440700231540ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . from autokey.qtui import common as ui_common logger = __import__("autokey.logger").logger.get_logger(__name__) class RecordDialog(*ui_common.inherits_from_ui_file_with_name("record_dialog")): def __init__(self, parent, closure): super().__init__(parent) self.setupUi(self) self.closure = closure def get_record_keyboard(self): return self.record_keyboard_button.isChecked() def get_record_mouse(self): return self.record_mouse_button.isChecked() def get_delay(self): return self.delay_recording_start_seconds_spin_box.value() def accept(self): super().accept() logger.info("Dialog accepted: Record keyboard: {}, record mouse: {}, delay: {} s".format( self.get_record_keyboard(), self.get_record_mouse(), self.get_delay() )) self.closure(True, self.get_record_keyboard(), self.get_record_mouse(), self.get_delay()) def reject(self): super().reject() logger.info("Dialog closed (rejected/aborted): Record keyboard: {}, record mouse: {}, delay: {} s".format( self.get_record_keyboard(), self.get_record_mouse(), self.get_delay() )) self.closure(False, self.get_record_keyboard(), self.get_record_mouse(), self.get_delay()) autokey-0.96.0/lib/autokey/qtui/dialogs/show_recent_script_errors.py000066400000000000000000000225221427671440700260130ustar00rootroot00000000000000# Copyright (C) 2020 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import typing from PyQt5.QtWidgets import QApplication, QAbstractButton, QDialogButtonBox from PyQt5.QtCore import pyqtSlot, pyqtSignal from autokey.model.script import ScriptErrorRecord from autokey.qtui import common as ui_common logger = __import__("autokey.logger").logger.get_logger(__name__) class ShowRecentScriptErrorsDialog(*ui_common.inherits_from_ui_file_with_name("show_recent_script_errors_dialog")): """ This dialogue is used to show errors that caused user Scripts to abort. The ScriptRunner class holds a list with errors, which can be viewed and cleared using this dialogue window. """ # TODO: When the minimal python version is raised to >= 3.6, add millisecond display to both the script start # timestamp and the error timestamp. # When switching errors, emit a boolean indicating if previous errors are available. The Previous button reacts on # this and enables/disables itself based on the boolean value. The connection is defined in the .ui file. has_previous_error = pyqtSignal(bool, name="has_previous_error") # When switching errors, emit a boolean indicating if further errors are available. The Next button reacts on # this and enables/disables itself based on the boolean value. The connection is defined in the .ui file. has_next_error = pyqtSignal(bool, name="has_next_error") script_errors_available = pyqtSignal(bool, name="script_errors_available") def __init__(self, parent): super(ShowRecentScriptErrorsDialog, self).__init__(parent) self.setupUi(self) # This can’t be done in the UI editor, which can only set specific fonts. # Just use the system default mono-space font. This aids Python’s error location indicator in case of # SyntaxErrors in user scripts. self.stack_trace_text_browser.setFontFamily("monospace") # The suffix is stored in the UI file. Retrieve it self.currently_shown_error_number_spin_box_suffix = self.currently_shown_error_number_spin_box.suffix() self.recent_script_errors = QApplication.instance().\ service.scriptRunner.error_records # type: typing.List[ScriptErrorRecord] self.currently_viewed_error_index = 0 @property def total_error_count(self): return len(self.recent_script_errors) def _emit_has_next_error(self): has_next_error = self.currently_viewed_error_index < self.total_error_count - 1 self.has_next_error.emit(has_next_error) def _emit_has_previous_error(self): has_previous_error = self.currently_viewed_error_index > 0 self.has_previous_error.emit(has_previous_error) def _emit_script_errors_available(self): script_errors_available = bool(self.recent_script_errors) self.script_errors_available.emit(script_errors_available) def hide(self): self._emit_script_errors_available() super(ShowRecentScriptErrorsDialog, self).hide() @pyqtSlot(QAbstractButton) def handle_button_box_button_clicks(self, clicked_button: QAbstractButton): """ Used to connect the button presses with logic for 'Reset error list' and 'Discard current error' buttons in the button box at the bottom of the dialogue window. """ button_role = self.buttonBox.buttonRole(clicked_button) # The Close button is handled internally and simply hides the dialogue, therefore nothing is implemented here. if button_role == QDialogButtonBox.DestructiveRole: # Discard current error self.remove_currently_shown_error_from_error_list() elif button_role == QDialogButtonBox.ResetRole: # Clear the error list self.clear_error_list_and_hide() @pyqtSlot() def update_and_show(self): error_count = self.total_error_count if error_count: if self.currently_viewed_error_index >= error_count: self.currently_viewed_error_index = error_count-1 self._emit_has_next_error() self._emit_has_previous_error() logger.info("User views the last script errors. There are {} errors to review.".format(error_count)) self._show_currently_viewed_error() self.show() else: logger.error( "User is able to view the script error dialogue, even if no errors are available. " "This should be impossible. Do not show the dialogue window.") @pyqtSlot() def show_next_error(self): """ Switch to the next error in the error list. The connection from the Next button to this slot function is defined in the .ui file. """ # Out of bounds handling is not needed, as the Next button gets disables when the last error is reached. # See has_next_error Signal. self.currently_viewed_error_index += 1 self._emit_has_next_error() self._show_currently_viewed_error() @pyqtSlot() def show_previous_error(self): """ Switch to the previous error in the error list. The connection from the Previous button to this slot function is defined in the .ui file. """ # Out of bounds handling is not needed, as the Previous button gets disables when the first error is reached. # See has_previous_error Signal. self.currently_viewed_error_index -= 1 self._emit_has_previous_error() self._show_currently_viewed_error() @pyqtSlot() def show_last_error(self): """ Switch to the last error in the error list. The connection from the Last button to this slot function is defined in the .ui file. """ self.currently_viewed_error_index = self.total_error_count - 1 self._emit_has_next_error() self._show_currently_viewed_error() @pyqtSlot() def show_first_error(self): """ Switch to the first error in the error list. The connection from the First button to this slot function is defined in the .ui file. """ self.currently_viewed_error_index = 0 self._emit_has_previous_error() self._show_currently_viewed_error() @pyqtSlot(int) def show_error_at_index(self, error_index: int): """ Switch to a specific error in the error list. Subtract one, because the GUI uses a 1-based index. The connection from the error number spin box to this slot function is defined in the .ui file. """ self.currently_viewed_error_index = error_index - 1 self._emit_has_next_error() self._emit_has_previous_error() self._show_currently_viewed_error() def remove_currently_shown_error_from_error_list(self): """ Delete the currently shown error from the error list. Shows the next error, if available. Otherwise, show the previous error, if available. Or clear the list and hide the window, if the deleted error was the only one in the list. """ if self.total_error_count == 1: self.clear_error_list_and_hide() else: del self.recent_script_errors[self.currently_viewed_error_index] if self.currently_viewed_error_index == self.total_error_count - 1: self.show_previous_error() else: self._emit_has_next_error() self._show_currently_viewed_error() def clear_error_list_and_hide(self): """Clears all errors in the error list and hides the dialogue window.""" self.currently_viewed_error_index = 0 QApplication.instance().service.scriptRunner.clear_error_records() self.hide() def _show_currently_viewed_error(self): """Update the GUI to show the error at the current list index.""" script_error = self.recent_script_errors[self.currently_viewed_error_index] error_count = self.total_error_count logger.debug("User views error {} / {}.".format(self.currently_viewed_error_index+1, error_count)) self.currently_shown_error_number_spin_box.setMaximum(error_count) self.currently_shown_error_number_spin_box.setValue(self.currently_viewed_error_index+1) # Update the total count on each show. This updates the GUI, if new errors occur while the # dialogue window is shown and the user clicks on a previous or next button. self.currently_shown_error_number_spin_box.setSuffix( self.currently_shown_error_number_spin_box_suffix.format(error_count) ) self.script_start_time_edit.setTime(script_error.start_time) self.script_error_time_edit.setTime(script_error.error_time) self.script_name_view.setText(script_error.script_name) self.stack_trace_text_browser.setText(script_error.error_traceback) autokey-0.96.0/lib/autokey/qtui/dialogs/windowfiltersettings.py000066400000000000000000000063151427671440700250130ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import re from PyQt5.QtWidgets import QDialog import autokey.iomediator.windowgrabber import autokey.model.folder import autokey.model.modelTypes from autokey.qtui import common as qtui_common from autokey import UI_common_functions as UI_common from .detectdialog import DetectDialog from autokey import iomediator logger = __import__("autokey.logger").logger.get_logger(__name__) class WindowFilterSettingsDialog(*qtui_common.inherits_from_ui_file_with_name("window_filter_settings_dialog")): def __init__(self, parent): super(WindowFilterSettingsDialog, self).__init__(parent) self.setupUi(self) self.target_item = None self.grabber = None # type: autokey.iomediator._windowgrabber.WindowGrabber def load(self, item: autokey.model.modelTypes.Item): self.target_item = item if not isinstance(item, autokey.model.folder.Folder): self.apply_recursive_check_box.hide() else: self.apply_recursive_check_box.show() if not item.has_filter(): self.reset() else: self.trigger_regex_line_edit.setText(item.get_filter_regex()) self.apply_recursive_check_box.setChecked(item.isRecursive) def save(self, item): UI_common.save_item_filter(self, item) def get_is_recursive(self): return self.apply_recursive_check_box.isChecked() def reset(self): self.trigger_regex_line_edit.clear() self.apply_recursive_check_box.setChecked(False) def reset_focus(self): self.trigger_regex_line_edit.setFocus() def get_filter_text(self): return str(self.trigger_regex_line_edit.text()) def receive_window_info(self, info): self.parentWidget().window().app.exec_in_main(self._receiveWindowInfo, info) def _receiveWindowInfo(self, info): dlg = DetectDialog(self) dlg.populate(info) dlg.exec_() if dlg.result() == QDialog.Accepted: self.trigger_regex_line_edit.setText(dlg.get_choice()) self.detect_window_properties_button.setEnabled(True) # --- Signal handlers --- def on_detect_window_properties_button_pressed(self): self.detect_window_properties_button.setEnabled(False) self.grabber = iomediator.windowgrabber.WindowGrabber(self) self.grabber.start() # --- event handlers --- def slotButtonClicked(self, button): if button == QDialog.Cancel: self.load(self.targetItem) QDialog.slotButtonClicked(self, button) autokey-0.96.0/lib/autokey/qtui/folderpage.py000066400000000000000000000060751427671440700212060ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import subprocess from PyQt5.QtWidgets import QMessageBox import autokey.configmanager.configmanager_constants as cm_constants import autokey.qtui.common as ui_common from autokey.model.folder import Folder logger = __import__("autokey.logger").logger.get_logger(__name__) PROBLEM_MSG_PRIMARY = "Some problems were found" PROBLEM_MSG_SECONDARY = "{}\n\nYour changes have not been saved." class FolderPage(*ui_common.inherits_from_ui_file_with_name("folderpage")): def __init__(self): super(FolderPage, self).__init__() self.setupUi(self) self.current_folder = None # type: Folder def load(self, folder: Folder): self.current_folder = folder self.showInTrayCheckbox.setChecked(folder.show_in_tray_menu) self.settingsWidget.load(folder) if self.is_new_item(): self.urlLabel.setEnabled(False) self.urlLabel.setText("(Unsaved)") # TODO: i18n else: ui_common.set_url_label(self.urlLabel, self.current_folder.path) def save(self): self.current_folder.show_in_tray_menu = self.showInTrayCheckbox.isChecked() self.settingsWidget.save() self.current_folder.persist() ui_common.set_url_label(self.urlLabel, self.current_folder.path) return not self.current_folder.path.startswith(cm_constants.CONFIG_DEFAULT_FOLDER) def get_current_item(self): """Returns the currently held item.""" return self.current_folder def set_item_title(self, title: str): self.current_folder.title = title def rebuild_item_path(self): self.current_folder.rebuild_path() def is_new_item(self): return self.current_folder.path is None def reset(self): self.load(self.current_folder) def validate(self): # Check settings errors = self.settingsWidget.validate() if errors: msg = PROBLEM_MSG_SECONDARY.format('\n'.join([str(e) for e in errors])) QMessageBox.critical(self.window(), PROBLEM_MSG_PRIMARY, msg) return not bool(errors) def set_dirty(self): self.window().set_dirty() # --- Signal handlers def on_showInTrayCheckbox_stateChanged(self, state: bool): self.set_dirty() @staticmethod def on_urlLabel_leftClickedUrl(url: str=None): if url: subprocess.Popen(["/usr/bin/xdg-open", url]) autokey-0.96.0/lib/autokey/qtui/notifier.py000066400000000000000000000221231427671440700207050ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from typing import Optional, Callable, TYPE_CHECKING from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QSystemTrayIcon, QAction, QMenu from autokey.qtui import popupmenu import autokey.qtui.common as ui_common import autokey.configmanager.configmanager as cm import autokey.configmanager.configmanager_constants as cm_constants if TYPE_CHECKING: from autokey.qtapp import Application logger = __import__("autokey.logger").logger.get_logger(__name__) TOOLTIP_RUNNING = "AutoKey - running" TOOLTIP_PAUSED = "AutoKey - paused" class Notifier(QSystemTrayIcon): def __init__(self, app): logger.debug("Creating system tray icon notifier.") icon = self._load_default_icon() super(Notifier, self).__init__(icon, app) # Actions self.action_view_script_error = None # type: QAction self.action_hide_icon = None # type: QAction self.action_show_config_window = None # type: QAction self.action_quit = None # type: QAction self.action_enable_monitoring = None # type: QAction self.app = app # type: Application self.config_manager = self.app.configManager self.activated.connect(self.on_activate) self._create_static_actions() self.create_assign_context_menu() self.update_tool_tip(cm.ConfigManager.SETTINGS[cm_constants.SERVICE_RUNNING]) self.app.monitoring_disabled.connect(self.update_tool_tip) if cm.ConfigManager.SETTINGS[cm_constants.SHOW_TRAY_ICON]: logger.debug("About to show the tray icon.") self.show() logger.info("System tray icon notifier created.") def create_assign_context_menu(self): """ Create a context menu, then set the created QMenu as the context menu. This builds the menu with all required actions and signal-slot connections. """ menu = QMenu("AutoKey") self._build_menu(menu) self.setContextMenu(menu) def update_tool_tip(self, service_running: bool): """Slot function that updates the tooltip when the user activates or deactivates the expansion service.""" if service_running: self.setToolTip(TOOLTIP_RUNNING) else: self.setToolTip(TOOLTIP_PAUSED) @staticmethod def _load_default_icon() -> QIcon: return QIcon.fromTheme( cm.ConfigManager.SETTINGS[cm.NOTIFICATION_ICON], ui_common.load_icon(ui_common.AutoKeyIcon.SYSTEM_TRAY) ) @staticmethod def _load_error_state_icon() -> QIcon: return QIcon.fromTheme( "autokey-status-error", ui_common.load_icon(ui_common.AutoKeyIcon.SYSTEM_TRAY_ERROR) ) def _create_action( self, icon_name: Optional[str], title: str, slot_function: Callable[[None], None], tool_tip: Optional[str]=None)-> QAction: """ QAction factory. All items created belong to the calling instance, i.e. created QAction parent is self. """ action = QAction(title, self) if icon_name: action.setIcon(QIcon.fromTheme(icon_name)) action.triggered.connect(slot_function) if tool_tip: action.setToolTip(tool_tip) return action def _create_static_actions(self): """ Create all static menu actions. The created actions will be placed in the tray icon context menu. """ logger.info("Creating static context menu actions.") self.action_view_script_error = self._create_action( None, "&View script error", self.reset_tray_icon, "View the last script error." ) # The action should disable itself self.action_view_script_error.setDisabled(True) self.action_view_script_error.triggered.connect(self.action_view_script_error.setEnabled) self.action_hide_icon = self._create_action( "edit-clear", "Temporarily &Hide Icon", self.hide, "Temporarily hide the system tray icon.\nUse the settings to hide it permanently." ) self.action_show_config_window = self._create_action( "configure", "&Show Main Window", self.app.show_configure, "Show the main AutoKey window. This does the same as left clicking the tray icon." ) self.action_quit = self._create_action("application-exit", "Exit AutoKey", self.app.shutdown) # TODO: maybe import this from configwindow.py ? The exact same Action is defined in the main window. self.action_enable_monitoring = self._create_action( None, "&Enable Monitoring", self.app.toggle_service, "Pause the phrase expansion and script execution, both by abbreviations and hotkeys.\n" "The global hotkeys to show the main window and to toggle this setting, as defined in the AutoKey " "settings, are not affected and will work regardless." ) self.action_enable_monitoring.setCheckable(True) self.action_enable_monitoring.setChecked(self.app.service.is_running()) self.action_enable_monitoring.setDisabled(self.app.serviceDisabled) # Sync action state with internal service state self.app.monitoring_disabled.connect(self.action_enable_monitoring.setChecked) def _fill_context_menu_with_model_item_actions(self, context_menu: QMenu): """ Find all model items that should be available in the context menu and create QActions for each, by using the available logic in popupmenu.PopupMenu. """ # Get phrase folders to add to main menu logger.info("Rebuilding model item actions, adding all items marked for access through the tray icon.") folders = [folder for folder in self.config_manager.allFolders if folder.show_in_tray_menu] items = [item for item in self.config_manager.allItems if item.show_in_tray_menu] # Only extract the QActions, but discard the PopupMenu instance. # This is done, because the PopupMenu class is not directly usable as a context menu here. menu = popupmenu.PopupMenu(self.app.service, folders, items, False, "AutoKey") new_item_actions = menu.actions() context_menu.addActions(new_item_actions) for action in new_item_actions: # type: QAction # QMenu does not take the ownership when adding QActions, so manually re-parent all actions. # This causes the QActions to be destroyed when the context menu is cleared or re-created. action.setParent(context_menu) if not context_menu.isEmpty(): # Avoid a stray separator line, if no items are marked for display in the context menu. context_menu.addSeparator() def _build_menu(self, context_menu: QMenu): """Build the context menu.""" logger.debug("Show tray icon enabled in settings: {}".format( cm.ConfigManager.SETTINGS[cm_constants.SHOW_TRAY_ICON]) ) # Items selected for display are shown on top self._fill_context_menu_with_model_item_actions(context_menu) # The static actions are added at the bottom context_menu.addAction(self.action_view_script_error) context_menu.addAction(self.action_enable_monitoring) context_menu.addAction(self.action_hide_icon) context_menu.addAction(self.action_show_config_window) context_menu.addAction(self.action_quit) def update_visible_status(self): visible = cm.ConfigManager.SETTINGS[cm_constants.SHOW_TRAY_ICON] if visible: self.create_assign_context_menu() self.setVisible(visible) logger.info("Updated tray icon visibility. Is icon shown: {}".format(visible)) def notify_error(self, message: str): self.setIcon(self._load_error_state_icon()) self.action_view_script_error.setEnabled(True) self.showMessage("AutoKey Error", message) def reset_tray_icon(self): """ Slot function that resets the icon to the default, as configured in the settings. Used when the user switches the icon theme in the settings and when a script error condition is cleared. """ self.setIcon(self._load_default_icon()) def on_activate(self, reason: QSystemTrayIcon.ActivationReason): logger.debug("Triggered system tray icon with reason: {}".format(reason)) if reason == QSystemTrayIcon.ActivationReason(QSystemTrayIcon.Trigger): self.app.show_configure() autokey-0.96.0/lib/autokey/qtui/phrasepage.py000066400000000000000000000121621427671440700212070ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018, 2019 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import subprocess from PyQt5.QtWidgets import QMessageBox import autokey.model.phrase from autokey.qtui import common as ui_common PROBLEM_MSG_PRIMARY = "Some problems were found" PROBLEM_MSG_SECONDARY = "{}\n\nYour changes have not been saved." # TODO: Once the port to Qt5 is done, set the editor placeholder text in the UI file to "Enter your phrase here." # TODO: Pure Qt4 QTextEdit does not support placeholder texts, so this functionality is currently unavailable. class PhrasePage(*ui_common.inherits_from_ui_file_with_name("phrasepage")): def __init__(self): super(PhrasePage, self).__init__() self.setupUi(self) self.initialising = True self.current_phrase = None # type: autokey.model.phrase.Phrase for val in sorted(autokey.model.phrase.SEND_MODES.keys()): self.sendModeCombo.addItem(val) self.initialising = False def load(self, phrase: autokey.model.phrase.Phrase): self.current_phrase = phrase self.phraseText.setPlainText(phrase.phrase) self.showInTrayCheckbox.setChecked(phrase.show_in_tray_menu) for k, v in autokey.model.phrase.SEND_MODES.items(): if v == phrase.sendMode: self.sendModeCombo.setCurrentIndex(self.sendModeCombo.findText(k)) break if self.is_new_item(): self.urlLabel.setEnabled(False) self.urlLabel.setText("(Unsaved)") # TODO: i18n else: ui_common.set_url_label(self.urlLabel, self.current_phrase.path) # TODO - re-enable me if restoring predictive functionality #self.predictCheckbox.setChecked(model.TriggerMode.PREDICTIVE in phrase.modes) self.promptCheckbox.setChecked(phrase.prompt) self.settingsWidget.load(phrase) def save(self): self.settingsWidget.save() self.current_phrase.phrase = str(self.phraseText.toPlainText()) self.current_phrase.show_in_tray_menu = self.showInTrayCheckbox.isChecked() self.current_phrase.sendMode = autokey.model.phrase.SEND_MODES[str(self.sendModeCombo.currentText())] # TODO - re-enable me if restoring predictive functionality #if self.predictCheckbox.isChecked(): # self.currentPhrase.modes.append(model.TriggerMode.PREDICTIVE) self.current_phrase.prompt = self.promptCheckbox.isChecked() self.current_phrase.persist() ui_common.set_url_label(self.urlLabel, self.current_phrase.path) return False def get_current_item(self): """Returns the currently held item.""" return self.current_phrase def set_item_title(self, title): self.current_phrase.description = title def rebuild_item_path(self): self.current_phrase.rebuild_path() def is_new_item(self): return self.current_phrase.path is None def reset(self): self.load(self.current_phrase) def validate(self): errors = [] # Check phrase content phrase = str(self.phraseText.toPlainText()) if ui_common.EMPTY_FIELD_REGEX.match(phrase): errors.append("The phrase content can't be empty") # TODO: i18n # Check settings errors += self.settingsWidget.validate() if errors: msg = PROBLEM_MSG_SECONDARY.format('\n'.join([str(e) for e in errors])) QMessageBox.critical(self.window(), PROBLEM_MSG_PRIMARY, msg) return not bool(errors) def set_dirty(self): self.window().set_dirty() def undo(self): self.phraseText.undo() def redo(self): self.phraseText.redo() def insert_token(self, token): self.phraseText.insertPlainText(token) # --- Signal handlers def on_phraseText_textChanged(self): self.set_dirty() def on_phraseText_undoAvailable(self, state): self.window().set_undo_available(state) def on_phraseText_redoAvailable(self, state): self.window().set_redo_available(state) def on_predictCheckbox_stateChanged(self, state): self.set_dirty() def on_promptCheckbox_stateChanged(self, state): self.set_dirty() def on_showInTrayCheckbox_stateChanged(self, state): self.set_dirty() def on_sendModeCombo_currentIndexChanged(self, index): if not self.initialising: self.set_dirty() def on_urlLabel_leftClickedUrl(self, url=None): if url: subprocess.Popen(["/usr/bin/xdg-open", url]) autokey-0.96.0/lib/autokey/qtui/popupmenu.py000066400000000000000000000145361427671440700211270ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from typing import List, Union from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QMenu, QAction, QWidget import autokey.configmanager.configmanager as cm import autokey.configmanager.configmanager_constants as cm_constants import autokey.model import autokey.model.abstract_hotkey import autokey.model.folder import autokey.model.phrase import autokey.model.script import autokey.service logger = __import__("autokey.logger").logger.get_logger(__name__) FolderList = List[autokey.model.folder.Folder] Item = Union[autokey.model.script.Script, autokey.model.phrase.Phrase] class PopupMenu(QMenu): def __init__(self, service: autokey.service.Service, folders: FolderList=None, items: List[Item]=None, on_desktop: bool=True, title: str=None, parent=None): super(PopupMenu, self).__init__(parent) if items is None: items = [] if folders is None: folders = [] self.setFocusPolicy(Qt.StrongFocus) self.service = service self._on_desktop = on_desktop if title is not None: self.setTitle(title) if cm.ConfigManager.SETTINGS[cm_constants.SORT_BY_USAGE_COUNT]: logger.debug("Sorting phrase menu by usage count") folders.sort(key=lambda obj: obj.usageCount, reverse=True) items.sort(key=lambda obj: obj.usageCount, reverse=True) else: logger.debug("Sorting phrase menu by item name/title") folders.sort(key=lambda obj: str(obj)) items.sort(key=lambda obj: str(obj)) if len(folders) == 1 and len(items) == 0 and on_desktop: # Only one folder - create menu with just its folders and items self.setTitle(folders[0].title) for folder in folders[0].folders: sub_menu_item = SubMenu( self._getMnemonic(folder.title), self, service, folder.folders, folder.items, False ) self.addAction(sub_menu_item) if folders[0].folders: self.addSeparator() self._add_items_to_self(folders[0].items, on_desktop) else: # Create folder section for folder in folders: sub_menu_item = SubMenu( self._getMnemonic(folder.title), self, service, folder.folders, folder.items, False ) self.addAction(sub_menu_item) if folders: self.addSeparator() self._add_items_to_self(items, on_desktop) def _add_item(self, description, item): action = ItemAction(self, self._getMnemonic(description), item, self.service.item_selected) self.addAction(action) def _add_items_to_self(self, items, on_desktop): # Create item (script/phrase) section if cm.ConfigManager.SETTINGS[cm_constants.SORT_BY_USAGE_COUNT]: items.sort(key=lambda obj: obj.usageCount, reverse=True) else: items.sort(key=lambda obj: str(obj)) for item in items: if on_desktop: self._add_item(item.get_description(self.service.lastStackState), item) else: self._add_item(item.description, item) def _getMnemonic(self, desc): #if 1 < 10 and '&' not in desc and self._onDesktop: # ret = "&%d - %s" % (self.__i, desc) # self.__i += 1 # return ret #else: # FIXME - menu does not get keyboard focus, so mnemonic is useless return desc class SubMenu(QAction): """ This QAction is used to create submenu in the popup menu. It gets used when a folder with a sub-folder has a hotkey assigned, to recursively show subfolder contents. """ def __init__(self, title: str, parent: PopupMenu, service, folders: FolderList=None, items: List[Item]=None, on_desktop: bool=True): icon = QIcon.fromTheme("folder") super(SubMenu, self).__init__(icon, title, parent) self.setMenu(PopupMenu(service, folders, items, on_desktop, title, parent)) def setParent(self, parent: QWidget=None): super(SubMenu, self).setParent(parent) self.menu().setParent(parent) class ItemAction(QAction): action_sig = pyqtSignal([autokey.model.abstract_hotkey.AbstractHotkey], name="action_sig") def __init__(self, parent: QWidget, description: str, item: Item, target): icon = ItemAction._icon_for_item(item) super(ItemAction, self).__init__(icon, description, parent) self.item = item self.triggered.connect(lambda: self.action_sig.emit(self.item)) self.action_sig.connect(target) @staticmethod def _icon_for_item(item: Item) -> QIcon: if isinstance(item, autokey.model.script.Script): return QIcon.fromTheme("text-x-python") elif isinstance(item, autokey.model.phrase.Phrase): return QIcon.fromTheme("text-x-generic") else: error_msg = "ItemAction got unknown item. Expected Union[autokey.model.script.Script, autokey.model.phrase.Phrase], " \ "got '{}'".format(str(type(item))) logger.error(error_msg) raise ValueError(error_msg) autokey-0.96.0/lib/autokey/qtui/resources/000077500000000000000000000000001427671440700205265ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/qtui/resources/resources.qrc000066400000000000000000000020361427671440700232500ustar00rootroot00000000000000 ui/abbrsettings.ui ui/hotkeysettings.ui ui/detectdialog.ui ui/window_filter_settings_dialog.ui ui/record_dialog.ui ui/settingswidget.ui ui/scriptpage.ui ui/phrasepage.ui ui/folderpage.ui ui/centralwidget.ui ui/mainwindow.ui ui/enginesettings.ui ui/generalsettings.ui ui/specialhotkeysettings.ui ui/settingsdialog.ui ui/about_autokey_dialog.ui ui/show_recent_script_errors_dialog.ui icons/autokey.png icons/autokey.svg icons/autokey-status.svg icons/autokey-status-dark.svg icons/autokey-status-error.svg autokey-0.96.0/lib/autokey/qtui/resources/ui/000077500000000000000000000000001427671440700211435ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/qtui/resources/ui/abbrsettings.ui000066400000000000000000000140231427671440700241710ustar00rootroot00000000000000 Dialog Qt::NonModal 0 0 566 467 Set Abbreviations These abbreviations are already assigned. After editing, press <Enter> or select another element to save. true true If checked, the typed abbreviation text is deleted before phrase expansion or script execution. If unchecked, the abbreviation text is kept. Remove typed abbreviation Trigger when typed as part of a word Add a new abbreviation. .. false Ignore case of typed abbreviation true Match phrase case to typed abbreviation Qt::Horizontal 360 29 Trigger on: Omit trigger character Trigger immediately (don't require a trigger character) Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok Delete the selected abbreviation. .. Qt::Vertical 20 40 Add, delete or edit abbreviations. Existing abbreviations in the list on the left can be edited. After typing a new abbreviation or editing an existing one, press <Enter> or click somewhere in the left list to submit it. addButton abbrListWidget wordCharCombo removeTypedCheckbox omitTriggerCheckbox matchCaseCheckbox ignoreCaseCheckbox triggerInsideCheckbox immediateCheckbox removeButton buttonBox accepted() Dialog accept() 224 368 157 274 buttonBox rejected() Dialog reject() 292 374 286 274 autokey-0.96.0/lib/autokey/qtui/resources/ui/about_autokey_dialog.ui000066400000000000000000002052331427671440700257010ustar00rootroot00000000000000 Dialog Qt::NonModal 0 0 719 361 About AutoKey true Qt::Horizontal QDialogButtonBox::Close 0 About <html><head/><body><p><span style=" font-size:xx-large; font-weight:600;">AutoKey (Qt)</span></p><p><br/></p></body></html> Qt::NoTextInteraction Qt::Vertical 20 40 Qt::Horizontal 40 20 <html><head/><body><p>AutoKey is a desktop automation utility for Linux and X11.</p><p>Website: <a href="https://github.com/autokey/autokey"><span style=" text-decoration: underline; color:#2980b9;">AutoKey on GitHub</span></a></p></body></html> true Version: Qt::NoTextInteraction Qt::PlainText Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter Qt::TextSelectableByMouse Python Version: TextLabel Qt::TextSelectableByMouse License Qt::ImhNone <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> <html><head><meta name="qrichtext" content="1" /><title>GNU General Public License v3.0 - GNU Project - Free Software Foundation (FSF)</title><style type="text/css"> p, li { white-space: pre-wrap; } </style></head><body style=" font-family:'Ubuntu'; font-size:10pt; font-weight:400; font-style:normal;"> <p align="center" style=" margin-top:14px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-size:large; font-weight:600;">GNU GENERAL PUBLIC LICENSE</span></p> <p align="center" style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Version 3, 29 June 2007 </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Copyright © 2007 Free Software Foundation, Inc. &lt;<a href="https://fsf.org/"><span style=" text-decoration: underline; color:#2980b9;">https://fsf.org/</span></a>&gt;</p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. </p> <p style=" margin-top:14px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="preamble"></a><span style=" font-size:large; font-weight:600;">P</span><span style=" font-size:large; font-weight:600;">reamble</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The GNU General Public License is a free, copyleft license for software and other kinds of works. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The precise terms and conditions for copying, distribution and modification follow. </p> <p style=" margin-top:14px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="terms"></a><span style=" font-size:large; font-weight:600;">T</span><span style=" font-size:large; font-weight:600;">ERMS AND CONDITIONS</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section0"></a><span style=" font-size:medium; font-weight:600;">0</span><span style=" font-size:medium; font-weight:600;">. Definitions.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">“This License” refers to version 3 of the GNU General Public License. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">“Copyright” also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">“The Program” refers to any copyrightable work licensed under this License. Each licensee is addressed as “you”. “Licensees” and “recipients” may be individuals or organizations. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">To “modify” a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a “modified version” of the earlier work or a work “based on” the earlier work. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A “covered work” means either the unmodified Program or a work based on the Program. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">To “propagate” a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">To “convey” a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">An interactive user interface displays “Appropriate Legal Notices” to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section1"></a><span style=" font-size:medium; font-weight:600;">1</span><span style=" font-size:medium; font-weight:600;">. Source Code.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The “source code” for a work means the preferred form of the work for making modifications to it. “Object code” means any non-source form of a work. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A “Standard Interface” means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The “System Libraries” of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A “Major Component”, in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The “Corresponding Source” for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The Corresponding Source for a work in source code form is that same work. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section2"></a><span style=" font-size:medium; font-weight:600;">2</span><span style=" font-size:medium; font-weight:600;">. Basic Permissions.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section3"></a><span style=" font-size:medium; font-weight:600;">3</span><span style=" font-size:medium; font-weight:600;">. Protecting Users' Legal Rights From Anti-Circumvention Law.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section4"></a><span style=" font-size:medium; font-weight:600;">4</span><span style=" font-size:medium; font-weight:600;">. Conveying Verbatim Copies.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section5"></a><span style=" font-size:medium; font-weight:600;">5</span><span style=" font-size:medium; font-weight:600;">. Conveying Modified Source Versions.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: </p> <ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">a) The work must carry prominent notices stating that you modified it, and giving a relevant date. </li> <li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to “keep intact all notices”. </li> <li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. </li> <li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. </li></ul> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an “aggregate” if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section6"></a><span style=" font-size:medium; font-weight:600;">6</span><span style=" font-size:medium; font-weight:600;">. Conveying Non-Source Forms.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: </p> <ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. </li> <li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. </li> <li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. </li> <li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. </li> <li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. </li></ul> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A “User Product” is either (1) a “consumer product”, which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, “normally used” refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">“Installation Information” for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section7"></a><span style=" font-size:medium; font-weight:600;">7</span><span style=" font-size:medium; font-weight:600;">. Additional Terms.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">“Additional permissions” are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: </p> <ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or </li> <li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or </li> <li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or </li> <li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">d) Limiting the use for publicity purposes of names of licensors or authors of the material; or </li> <li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or </li> <li style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. </li></ul> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">All other non-permissive additional terms are considered “further restrictions” within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section8"></a><span style=" font-size:medium; font-weight:600;">8</span><span style=" font-size:medium; font-weight:600;">. Termination.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section9"></a><span style=" font-size:medium; font-weight:600;">9</span><span style=" font-size:medium; font-weight:600;">. Acceptance Not Required for Having Copies.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section10"></a><span style=" font-size:medium; font-weight:600;">1</span><span style=" font-size:medium; font-weight:600;">0. Automatic Licensing of Downstream Recipients.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">An “entity transaction” is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section11"></a><span style=" font-size:medium; font-weight:600;">1</span><span style=" font-size:medium; font-weight:600;">1. Patents.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A “contributor” is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's “contributor version”. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A contributor's “essential patent claims” are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, “control” includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">In the following three paragraphs, a “patent license” is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To “grant” such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. “Knowingly relying” means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">A patent license is “discriminatory” if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section12"></a><span style=" font-size:medium; font-weight:600;">1</span><span style=" font-size:medium; font-weight:600;">2. No Surrender of Others' Freedom.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section13"></a><span style=" font-size:medium; font-weight:600;">1</span><span style=" font-size:medium; font-weight:600;">3. Use with the GNU Affero General Public License.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section14"></a><span style=" font-size:medium; font-weight:600;">1</span><span style=" font-size:medium; font-weight:600;">4. Revised Versions of this License.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License “or any later version” applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section15"></a><span style=" font-size:medium; font-weight:600;">1</span><span style=" font-size:medium; font-weight:600;">5. Disclaimer of Warranty.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section16"></a><span style=" font-size:medium; font-weight:600;">1</span><span style=" font-size:medium; font-weight:600;">6. Limitation of Liability.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="section17"></a><span style=" font-size:medium; font-weight:600;">1</span><span style=" font-size:medium; font-weight:600;">7. Interpretation of Sections 15 and 16.</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">END OF TERMS AND CONDITIONS </p> <p style=" margin-top:14px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><a name="howto"></a><span style=" font-size:large; font-weight:600;">H</span><span style=" font-size:large; font-weight:600;">ow to Apply These Terms to Your New Programs</span> </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the “copyright” line and a pointer to where the full notice is found. </p> <p style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> &lt;one line to give the program's name and a brief idea of what it does.&gt;</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> Copyright (C) &lt;year&gt; &lt;name of author&gt;</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Ubuntu Mono';"><br /></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> This program is free software: you can redistribute it and/or modify</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> it under the terms of the GNU General Public License as published by</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> the Free Software Foundation, either version 3 of the License, or</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> (at your option) any later version.</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Ubuntu Mono';"><br /></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> This program is distributed in the hope that it will be useful,</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> but WITHOUT ANY WARRANTY; without even the implied warranty of</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> GNU General Public License for more details.</span></p> <p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'Ubuntu Mono';"><br /></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> You should have received a copy of the GNU General Public License</span></p> <p style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> along with this program. If not, see &lt;https://www.gnu.org/licenses/&gt;. </span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Also add information on how to contact you by electronic and paper mail. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: </p> <p style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> &lt;program&gt; Copyright (C) &lt;year&gt; &lt;name of author&gt;</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.</span></p> <p style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> This is free software, and you are welcome to redistribute it</span></p> <p style=" margin-top:0px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;"><span style=" font-family:'Ubuntu Mono';"> under certain conditions; type `show c' for details. </span></p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an “about box”. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">You should also get your employer (if you work as a programmer) or school, if any, to sign a “copyright disclaimer” for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see &lt;<a href="https://www.gnu.org/licenses/"><span style=" text-decoration: underline; color:#2980b9;">https://www.gnu.org/licenses/</span></a>&gt;. </p> <p style=" margin-top:12px; margin-bottom:12px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read &lt;<a href="https://www.gnu.org/licenses/why-not-lgpl.html"><span style=" text-decoration: underline; color:#2980b9;">https://www.gnu.org/licenses/why-not-lgpl.html</span></a>&gt;. </p></body></html> true buttonBox accepted() Dialog accept() 248 254 157 274 buttonBox rejected() Dialog reject() 316 260 286 274 autokey-0.96.0/lib/autokey/qtui/resources/ui/centralwidget.ui000066400000000000000000000071601427671440700243420ustar00rootroot00000000000000 CentralWidget 0 0 832 590 Form Qt::Horizontal 0 0 Qt::CustomContextMenu true QAbstractItemView::InternalMove QAbstractItemView::ExtendedSelection true false 3 true Name Abbr. Hotkey 1 0 0 Qt::ActionsContextMenu false true QAbstractItemView::NoSelection true FolderPage QWidget
autokey.qtui.folderpage
1
PhrasePage QWidget
autokey.qtui.phrasepage
1
ScriptPage QWidget
autokey.qtui.scriptpage
1
AkTreeWidget QTreeWidget
autokey.qtui.autokey_treewidget
autokey-0.96.0/lib/autokey/qtui/resources/ui/detectdialog.ui000066400000000000000000000107161427671440700241370ustar00rootroot00000000000000 Dialog 0 0 400 300 Window Information Window property selection Window &class (entire application) true Window &title Qt::Vertical 20 40 Qt::LeftToRight Window information of selected window 0 0 Window title: Qt::Vertical 20 40 TextLabel 0 0 Window class: TextLabel Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok buttonBox accepted() Dialog accept() 248 254 157 274 buttonBox rejected() Dialog reject() 316 260 286 274 autokey-0.96.0/lib/autokey/qtui/resources/ui/enginesettings.ui000066400000000000000000000071401427671440700245320ustar00rootroot00000000000000 engine_settings 0 0 325 200 325 200 Form User Module Folder Currently selected: Browse .. Clear selection Qt::Horizontal 40 20 <html><head/><body><p><span style=" font-style:italic;">Technical information</span>: This places the selected directory on the Python module search path.</p></body></html> Any Python modules and packages placed in this folder will be available for import by scripts. true 0 0 None selected header_label browse_button folder_header_label clear_button horizontalSpacer folder_label Qt::Vertical 20 40 autokey-0.96.0/lib/autokey/qtui/resources/ui/folderpage.ui000066400000000000000000000044231427671440700236150ustar00rootroot00000000000000 FolderPage 0 0 568 530 FolderPage Open the folder in the default file manager Qt::RichText Qt::AlignCenter true Qt::TextBrowserInteraction Folder Settings Show in notification icon menu Qt::Horizontal Qt::Vertical 20 40 SettingsWidget QWidget
autokey.qtui.settingswidget
1
autokey-0.96.0/lib/autokey/qtui/resources/ui/generalsettings.ui000066400000000000000000000203621427671440700247030ustar00rootroot00000000000000 general_settings 0 0 557 557 Form Application If checked, automatically save changes to the current Script or Phrase without asking for confirmation. If unchecked, ask if changes should be saved or not. Automatically save changes without confirmation true System tray icon Qt::Horizontal 40 20 Choose between a light and a dark icon theme. Light .. Dark .. Used system tray icon theme: Enabling this places an icon in the desktop system tray or notification area. The icon can be used to show the main window. It also provides access to scripts and phrases marked for display in the tray icon context menu. Show a notification icon in the system tray or notification area Enable to automatically start AutoKey after logging in. <html><head/><body><p>Enabling this option places a <span style=" font-weight:600;">autokey.desktop</span> file into the autostart folder inside the user folder. It can be found in <a href="file://$XDG_CONFIG_HOME/autostart"><span style=" text-decoration: underline; color:#2980b9;">$XDG_CONFIG_HOME/autostart</span></a>. </p></body></html> A&utomatically start AutoKey after login true false Qt::Horizontal 40 20 If checked, start in the foreground by showing the main window. If unchecked, start in the background. Show main window when starting Start using this interface: Choose which GUI should be starting. Only available if both the Qt and GTK+ GUIs are installed. Popup Menu true Allow keyboard navigation of popup menu Sort menu items with most frequently used first Phrase expansions Enable undoing Phrase expansions using the backspace key Undo only works if the Phrase does not contain any special keys. AutoKey will refuse to perform an undo of a Phrase containing any such keys, regardless of this setting. Enable undo by pressing backspace Modifier Keys Enable this, if you remapped the Capslock key to be something else, like Ctrl. This will prevent any special handling for this key. Disable handling of the Capslock key. Qt::Vertical 20 40 autokey-0.96.0/lib/autokey/qtui/resources/ui/hotkeysettings.ui000066400000000000000000000117211427671440700245700ustar00rootroot00000000000000 Dialog Qt::ApplicationModal 0 0 546 150 Set Hotkey Alt true Hyper true Meta true Qt::Vertical 81 27 Shift true Control true Super true Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok 0 0 Key: (None) Record a key combination .. Modifiers: mod_control_button mod_alt_button mod_shift_button mod_super_button mod_hyper_button mod_meta_button buttonBox accepted() Dialog accept() 230 143 160 132 buttonBox rejected() Dialog reject() 298 143 286 124 record_combination_button clicked(bool) record_combination_button setEnabled(bool) 538 90 538 98 autokey-0.96.0/lib/autokey/qtui/resources/ui/mainwindow.ui000066400000000000000000000421251427671440700236620ustar00rootroot00000000000000 MainWindow 0 0 878 574 AutoKey 0 0 878 30 Fi&le &New ../../../../../../../.designer/backup../../../../../../../.designer/backup Edi&t T&ools &Insert Macro Setti&ngs Hel&p toolBar Qt::ToolButtonFollowStyle TopToolBarArea false ../../../../../../../.designer/backup../../../../../../../.designer/backup &Folder Create a new top level Folder anywhere in the file system. ../../../../../../../.designer/backup../../../../../../../.designer/backup &Sub-folder Create a new sub-folder in the currently selected folder. ../../../../../../../.designer/backup../../../../../../../.designer/backup S&cript Create a new Script in the currently selected folder. Ctrl+Shift+N ../../../../../../../.designer/backup../../../../../../../.designer/backup &Phrase Create a new Phrase in the currently selected folder. Ctrl+N false ../../../../../../../.designer/backup../../../../../../../.designer/backup &Save Save changes. ../../../../../../../.designer/backup../../../../../../../.designer/backup &Close Window Close the AutoKey window QAction::QuitRole ../../../../../../../.designer/backup../../../../../../../.designer/backup &Quit Quit AutoKey QAction::QuitRole false ../../../../../../../.designer/backup../../../../../../../.designer/backup &Undo Undo the last change. Ctrl+Z false ../../../../../../../.designer/backup../../../../../../../.designer/backup &Redo Redo the last undone change. Ctrl+Shift+Z ../../../../../../../.designer/backup../../../../../../../.designer/backup &Cut Item Cut the selected item(s). ../../../../../../../.designer/backup../../../../../../../.designer/backup Copy &Item Copy the selected item(s). ../../../../../../../.designer/backup../../../../../../../.designer/backup &Paste Item Paste items from the clipboard into the currently selected folder. ../../../../../../../.designer/backup../../../../../../../.designer/backup C&lone Item Duplicate the selected item(s). Ctrl+Shift+C ../../../../../../../.designer/backup../../../../../../../.designer/backup &Delete Delete the selected items. Deleting folders also deletes the content. Ctrl+D ../../../../../../../.designer/backup../../../../../../../.designer/backup R&ename Rename the first selected item. F2 false ../../../../../../../.designer/backup../../../../../../../.designer/backup &Show recorded errors from Scripts Show recorded errors that occured in recently-run Scripts. true true &Enable Monitoring true true &Show Toolbar true Show &log view &Configure Shortcuts… QAction::PreferencesRole C&onfigure Toolbars… QAction::PreferencesRole ../../../../../../../.designer/backup../../../../../../../.designer/backup Configure &AutoKey QAction::PreferencesRole ../../../../../../../.designer/backup../../../../../../../.designer/backup &Online Manual ../../../../../../../.designer/backup../../../../../../../.designer/backup &F. A. Q. Show the online FAQ. ../../../../../../../.designer/backup../../../../../../../.designer/backup &Scripting Help Show the online scripting Help and API documentation. ../../../../../../../.designer/backup../../../../../../../.designer/backup &Report a Bug Report a Bug on the GitHub issue tracker. ../../../../../../../.designer/backup../../../../../../../.designer/backup &About AutoKey QAction::AboutRole ../../../../../../../.designer/backup../../../../../../../.designer/backup About &Qt QAction::AboutQtRole ../../../../../../../.designer/backup../../../../../../../.designer/backup &Record Script ../../../../../../../.designer/backup../../../../../../../.designer/backup R&un Script Run the currently opened Script. Execution start is delayed by two seconds. CentralWidget QWidget
autokey.qtui.centralwidget
1
action_show_toolbar toggled(bool) toolbar setVisible(bool) -1 -1 391 48
autokey-0.96.0/lib/autokey/qtui/resources/ui/phrasepage.ui000066400000000000000000000063051427671440700236250ustar00rootroot00000000000000 PhrasePage 0 0 540 421 Form Open the phrase in the default text editor. Qt::RichText Qt::AlignCenter true Qt::TextBrowserInteraction QTextEdit::NoWrap false Phrase Settings Always prompt before pasting this phrase Paste using Qt::Horizontal 40 20 Show in notification icon menu Qt::Horizontal SettingsWidget QWidget
autokey.qtui.settingswidget
1
autokey-0.96.0/lib/autokey/qtui/resources/ui/record_dialog.ui000066400000000000000000000064071427671440700243060ustar00rootroot00000000000000 Dialog Qt::ApplicationModal 0 0 412 213 Record Script Record mouse events (experimental) Record a keyboard/mouse macro Start recording after seconds Record keyboard events Qt::Vertical 20 40 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok 0 0 5 button_box accepted() Dialog accept() 248 254 157 274 button_box rejected() Dialog reject() 316 260 286 274 autokey-0.96.0/lib/autokey/qtui/resources/ui/scriptpage.ui000066400000000000000000000046571427671440700236570ustar00rootroot00000000000000 ScriptPage 0 0 587 581 ScriptPage Open the script in the default text editor Qt::RichText Qt::AlignCenter true Qt::TextBrowserInteraction Script Settings Always prompt before running this script Show in notification icon menu Qt::Horizontal SettingsWidget QWidget
autokey.qtui.settingswidget
1
QsciScintilla QFrame
Qsci/qsciscintilla.h
1
autokey-0.96.0/lib/autokey/qtui/resources/ui/settingsdialog.ui000066400000000000000000000116521427671440700245270ustar00rootroot00000000000000 Settings 0 0 656 595 Settings true QFrame::StyledPanel QFrame::Raised General .. true true true true Special Hotkeys .. true true true Script Engine .. true true true Qt::Vertical 20 40 0 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Save GeneralSettings QWidget
autokey.qtui.settings.general
1
SpecialHotkeySettings QWidget
autokey.qtui.settings.specialhotkeys
1
EngineSettings QWidget
autokey.qtui.settings.engine
1
buttonBox accepted() Settings accept() 224 569 157 274 buttonBox rejected() Settings reject() 292 575 286 274
autokey-0.96.0/lib/autokey/qtui/resources/ui/settingswidget.ui000066400000000000000000000100251427671440700245440ustar00rootroot00000000000000 SettingsWidget 0 0 353 132 Form Abbreviations: $abbr Hotkey: $hotkey Window Filter: $filter Qt::Horizontal 40 20 Add abbreviations and edit already assigned abbreviations Set& Clear all assigned abbreviations Clear& .. Assign a new hotkey Set& Clear the assigned hotkey Clear& .. Set a regular expression based window filter. If set, this item will only trigger, if the currently active window matches this filter. Set& Clear the current window filter Clear& .. autokey-0.96.0/lib/autokey/qtui/resources/ui/show_recent_script_errors_dialog.ui000066400000000000000000000362551427671440700303340ustar00rootroot00000000000000 Dialog 0 0 800 600 Show recorded errors from Scripts Crashed Script name: script_name_view Script was triggered by: Timestamp at which the error occured Qt::ImhPreferNumbers|Qt::ImhTime true QAbstractSpinBox::NoButtons HH:mm:ss Currently showing error / {} 1 1 Qt::Horizontal 40 20 Show the oldest recorded script error. First .. Show the previous recorded script error. Previous .. Show the next recorded script error. Next .. Show the newest recorded script error. Last .. Qt::Horizontal QDialogButtonBox::Close|QDialogButtonBox::Discard|QDialogButtonBox::Reset Error occured at: script_error_time_edit false The error message, as returned by Python. QTextEdit::NoWrap false The failing script’s stack trace is shown here. false Timestamp at which the Script execution started Qt::ImhPreferNumbers|Qt::ImhTime true QAbstractSpinBox::NoButtons HH:mm:ss Shown below are the recorded errors that occurred in Scripts run by AutoKey during this session. The name of the failed Script true The failing Script’s name is shown here. Qt::Horizontal Script start time: script_start_time_edit The Python Stacktrace: stack_trace_text_browser currently_shown_error_number_spin_box show_first_error_button show_previous_error_button show_next_error_button show_last_error_button script_name_view script_start_time_edit script_error_time_edit stack_trace_text_browser buttonBox accepted() Dialog accept() 260 560 157 274 buttonBox rejected() Dialog reject() 328 560 286 274 show_next_error_button clicked() Dialog show_next_error() 628 64 380 282 show_previous_error_button clicked() Dialog show_previous_error() 533 64 380 282 show_first_error_button clicked() Dialog show_first_error() 443 64 380 282 show_last_error_button clicked() Dialog show_last_error() 718 47 380 282 Dialog has_next_error(bool) show_next_error_button setEnabled(bool) 622 47 628 64 Dialog has_previous_error(bool) show_previous_error_button setEnabled(bool) 661 47 533 64 Dialog has_next_error(bool) show_last_error_button setEnabled(bool) 622 47 718 64 Dialog has_previous_error(bool) show_first_error_button setEnabled(bool) 661 47 443 64 show_next_error_button clicked(bool) show_previous_error_button setDisabled(bool) 628 64 533 64 show_next_error_button clicked(bool) show_first_error_button setDisabled(bool) 628 64 443 64 show_previous_error_button clicked(bool) show_next_error_button setDisabled(bool) 533 64 628 64 show_previous_error_button clicked(bool) show_last_error_button setDisabled(bool) 533 64 718 64 show_first_error_button clicked(bool) show_next_error_button setDisabled(bool) 443 64 628 64 show_first_error_button clicked(bool) show_last_error_button setDisabled(bool) 443 64 718 64 show_last_error_button clicked(bool) show_previous_error_button setDisabled(bool) 718 47 533 64 show_last_error_button clicked(bool) show_first_error_button setDisabled(bool) 718 47 443 64 currently_shown_error_number_spin_box valueChanged(int) Dialog show_error_at_index(int) 185 47 354 258 buttonBox clicked(QAbstractButton*) Dialog handle_button_box_button_clicks(QAbstractButton*) 354 560 354 258 autokey-0.96.0/lib/autokey/qtui/resources/ui/specialhotkeysettings.ui000066400000000000000000000075401427671440700261350ustar00rootroot00000000000000 special_hotkey_settings 0 0 531 397 Form Toggle monitoring using a hotkey Hotkey: $hotkey Qt::Horizontal 269 20 Set Clear Show configuration window using a hotkey Hotkey: $hotkey Qt::Horizontal 269 20 Set Clear Qt::Vertical 20 40 autokey-0.96.0/lib/autokey/qtui/resources/ui/window_filter_settings_dialog.ui000066400000000000000000000065511427671440700276240ustar00rootroot00000000000000 Dialog 0 0 554 163 Set Window Filter Apply recursively to subfolders and items Regular expression to match: Qt::Horizontal 182 31 Detect Window Properties Qt::Vertical 20 40 Qt::Horizontal QDialogButtonBox::Cancel|QDialogButtonBox::Ok Window title or class Enter a regular expression that matches the title of windows or the window class (WM_CLASS attribute) in which you want this item to trigger. true trigger_regex_line_edit apply_recursive_check_box detect_window_properties_button button_box accepted() Dialog accept() 388 159 157 157 button_box rejected() Dialog reject() 388 159 286 157 autokey-0.96.0/lib/autokey/qtui/scriptpage.py000066400000000000000000000134231427671440700212320ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import os.path import subprocess from PyQt5 import Qsci from PyQt5.QtWidgets import QMessageBox import autokey.model.script from autokey.qtui import common as ui_common API_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data/api.txt") PROBLEM_MSG_PRIMARY = "Some problems were found" PROBLEM_MSG_SECONDARY = "{}\n\nYour changes have not been saved." class ScriptPage(*ui_common.inherits_from_ui_file_with_name("scriptpage")): def __init__(self): super(ScriptPage, self).__init__() self.setupUi(self) self.scriptCodeEditor.setUtf8(1) lex = Qsci.QsciLexerPython(self) api = Qsci.QsciAPIs(lex) api.load(API_FILE) api.prepare() self.current_script = None # type: autokey.model.script.Script self.scriptCodeEditor.setLexer(lex) self.scriptCodeEditor.setBraceMatching(Qsci.QsciScintilla.SloppyBraceMatch) self.scriptCodeEditor.setAutoIndent(True) self.scriptCodeEditor.setBackspaceUnindents(True) self.scriptCodeEditor.setIndentationWidth(4) self.scriptCodeEditor.setIndentationGuides(True) self.scriptCodeEditor.setIndentationsUseTabs(False) self.scriptCodeEditor.setAutoCompletionThreshold(3) self.scriptCodeEditor.setAutoCompletionSource(Qsci.QsciScintilla.AcsAll) self.scriptCodeEditor.setCallTipsStyle(Qsci.QsciScintilla.CallTipsNoContext) lex.setFont(ui_common.monospace_font()) def load(self, script: autokey.model.script.Script): self.current_script = script self.scriptCodeEditor.clear() self.scriptCodeEditor.append(script.code) self.showInTrayCheckbox.setChecked(script.show_in_tray_menu) self.promptCheckbox.setChecked(script.prompt) self.settingsWidget.load(script) self.window().set_undo_available(False) self.window().set_redo_available(False) if self.is_new_item(): self.urlLabel.setEnabled(False) self.urlLabel.setText("(Unsaved)") # TODO: i18n else: ui_common.set_url_label(self.urlLabel, self.current_script.path) def save(self): self.settingsWidget.save() self.current_script.code = str(self.scriptCodeEditor.text()) self.current_script.show_in_tray_menu = self.showInTrayCheckbox.isChecked() self.current_script.prompt = self.promptCheckbox.isChecked() self.current_script.persist() ui_common.set_url_label(self.urlLabel, self.current_script.path) return False def get_current_item(self): """Returns the currently held item.""" return self.current_script def set_item_title(self, title): self.current_script.description = title def rebuild_item_path(self): self.current_script.rebuild_path() def is_new_item(self): return self.current_script.path is None def reset(self): self.load(self.current_script) self.window().set_undo_available(False) self.window().set_redo_available(False) def set_dirty(self): self.window().set_dirty() def start_record(self): self.scriptCodeEditor.append("\n") def start_key_sequence(self): self.scriptCodeEditor.append("keyboard.send_keys(\"") def end_key_sequence(self): self.scriptCodeEditor.append("\")\n") def append_key(self, key): self.scriptCodeEditor.append(key) def append_hotkey(self, key, modifiers): keyString = self.current_script.get_hotkey_string(key, modifiers) self.scriptCodeEditor.append(keyString) def append_mouseclick(self, xCoord, yCoord, button, windowTitle): self.scriptCodeEditor.append("mouse.click_relative(%d, %d, %d) # %s\n" % (xCoord, yCoord, int(button), windowTitle)) def undo(self): self.scriptCodeEditor.undo() self.window().set_undo_available(self.scriptCodeEditor.isUndoAvailable()) def redo(self): self.scriptCodeEditor.redo() self.window().set_redo_available(self.scriptCodeEditor.isRedoAvailable()) def validate(self): errors = [] # Check script code code = str(self.scriptCodeEditor.text()) if ui_common.EMPTY_FIELD_REGEX.match(code): errors.append("The script code can't be empty") # TODO: i18n # Check settings errors += self.settingsWidget.validate() if errors: msg = PROBLEM_MSG_SECONDARY.format('\n'.join([str(e) for e in errors])) header = PROBLEM_MSG_PRIMARY QMessageBox.critical(self.window(), header, msg) return not bool(errors) # --- Signal handlers def on_scriptCodeEditor_textChanged(self): self.set_dirty() self.window().set_undo_available(self.scriptCodeEditor.isUndoAvailable()) self.window().set_redo_available(self.scriptCodeEditor.isRedoAvailable()) def on_promptCheckbox_stateChanged(self, state): self.set_dirty() def on_showInTrayCheckbox_stateChanged(self, state): self.set_dirty() def on_urlLabel_leftClickedUrl(self, url=None): if url: subprocess.Popen(["/usr/bin/xdg-open", url]) autokey-0.96.0/lib/autokey/qtui/settings/000077500000000000000000000000001427671440700203545ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/qtui/settings/__init__.py000066400000000000000000000016351427671440700224720ustar00rootroot00000000000000# Copyright (C) 2018 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ This package contains the Settings dialog window and all widgets used inside. The SettingsDialog implements a multi-page dialog that allows the user to change all AutoKey settings. """ from .settingsdialog import SettingsDialog autokey-0.96.0/lib/autokey/qtui/settings/engine.py000066400000000000000000000076701427671440700222050ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import sys from PyQt5.QtCore import pyqtSlot from PyQt5.QtWidgets import QFileDialog, QWidget, QApplication from autokey.qtui import common logger = __import__("autokey.logger").logger.get_logger(__name__) class EngineSettings(*common.inherits_from_ui_file_with_name("enginesettings")): """ The EngineSettings class is used inside the AutoKey configuration dialog. It allows the user to select and add a custom Python module search path entry. """ def __init__(self, parent: QWidget=None): super(EngineSettings, self).__init__(parent) self.setupUi(self) # Save the path label text stored in the Qt UI file. # It is used to reset the label to this value if a custom module path is currently set and the user deletes it. # Do not hard-code it to prevent possible inconsistencies. self.initial_folder_label_text = self.folder_label.text() self.config_manager = QApplication.instance().configManager self.path = self.config_manager.userCodeDir self.clear_button.setEnabled(self.path is not None) if self.config_manager.userCodeDir is not None: self.folder_label.setText(self.config_manager.userCodeDir) logger.debug("EngineSettings widget initialised, custom module search path is set to: {}".format(self.path)) def save(self): """This function is called by the parent dialog window when the user selects to save the settings.""" if self.path is None: # Delete requested, so remove the current path from sys.path, if present if self.config_manager.userCodeDir is not None: sys.path.remove(self.config_manager.userCodeDir) self.config_manager.userCodeDir = None logger.info("Removed custom module search path from configuration and sys.path.") else: if self.path != self.config_manager.userCodeDir: if self.config_manager.userCodeDir is not None: sys.path.remove(self.config_manager.userCodeDir) sys.path.append(self.path) self.config_manager.userCodeDir = self.path logger.info("Saved custom module search path and added it to sys.path: {}".format(self.path)) @pyqtSlot() def on_browse_button_pressed(self): """ PyQt slot called when the user hits the "Browse" button. Display a directory selection dialog and store the returned path. """ path = QFileDialog.getExistingDirectory(self.parentWidget(), "Choose a directory containing Python modules") if path: # Non-empty means the user chose a path and clicked on OK self.path = path self.clear_button.setEnabled(True) self.folder_label.setText(path) logger.debug("User selects a custom module search path: {}".format(self.path)) @pyqtSlot() def on_clear_button_pressed(self): """ PyQt slot called when the user hits the "Clear" button. Removes any set custom module search path. """ self.path = None self.clear_button.setEnabled(False) self.folder_label.setText(self.initial_folder_label_text) logger.debug("User selects to clear the custom module search path.") autokey-0.96.0/lib/autokey/qtui/settings/general.py000066400000000000000000000165771427671440700223630ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QWidget, QComboBox import autokey.configmanager.autostart import autokey.configmanager.configmanager as cm import autokey.configmanager.configmanager_constants as cm_constants from autokey.model.key import Key import autokey.qtui.common as ui_common import autokey.common as common logger = __import__("autokey.logger").logger.get_logger(__name__) class GeneralSettings(*ui_common.inherits_from_ui_file_with_name("generalsettings")): """This widget implements the "general settings" widget and is used in the settings dialog.""" GUI_TABLE = ( ("autokey-qt.desktop", "Qt5"), ("autokey-gtk.desktop", "GTK+") ) ICON_TABLE = ( (0, common.ICON_FILE_NOTIFICATION), (1, common.ICON_FILE_NOTIFICATION_DARK) ) def __init__(self, parent: QWidget=None): super(GeneralSettings, self).__init__(parent) self.setupUi(self) self.autosave_checkbox.setChecked(not cm.ConfigManager.SETTINGS[cm_constants.PROMPT_TO_SAVE]) self.show_tray_checkbox.setChecked(cm.ConfigManager.SETTINGS[cm_constants.SHOW_TRAY_ICON]) # self.allow_kb_nav_checkbox.setChecked(cm.ConfigManager.SETTINGS[cm.MENU_TAKES_FOCUS]) self.allow_kb_nav_checkbox.setVisible(False) self.sort_by_usage_checkbox.setChecked(cm.ConfigManager.SETTINGS[cm_constants.SORT_BY_USAGE_COUNT]) self.enable_undo_checkbox.setChecked(cm.ConfigManager.SETTINGS[cm_constants.UNDO_USING_BACKSPACE]) self.disable_capslock_checkbox.setChecked(cm.ConfigManager.is_modifier_disabled(Key.CAPSLOCK)) self._fill_notification_icon_combobox_user_data() self._load_system_tray_icon_theme() self._fill_autostart_gui_selection_combobox() self.autostart_settings = autokey.configmanager.autostart.get_autostart() self._load_autostart_settings() logger.debug("Created widget and loaded current settings: " + self._settings_str()) def save(self): """Called by the parent settings dialog when the user clicks on the Save button. Stores the current settings in the ConfigManager.""" logger.debug("User requested to save settings. New settings: " + self._settings_str()) cm.ConfigManager.SETTINGS[cm_constants.PROMPT_TO_SAVE] = not self.autosave_checkbox.isChecked() cm.ConfigManager.SETTINGS[cm_constants.SHOW_TRAY_ICON] = self.show_tray_checkbox.isChecked() # cm.ConfigManager.SETTINGS[cm_constants.MENU_TAKES_FOCUS] = self.allow_kb_nav_checkbox.isChecked() cm.ConfigManager.SETTINGS[cm_constants.SORT_BY_USAGE_COUNT] = self.sort_by_usage_checkbox.isChecked() cm.ConfigManager.SETTINGS[cm_constants.UNDO_USING_BACKSPACE] = self.enable_undo_checkbox.isChecked() cm.ConfigManager.SETTINGS[cm_constants.NOTIFICATION_ICON] = \ self.system_tray_icon_theme_combobox.currentData(Qt.UserRole) self._save_disable_capslock_setting() self._save_autostart_settings() # TODO: After saving the notification icon, apply it to the currently running instance. def _save_disable_capslock_setting(self): # Only update the modifier key handling if the value changed. if self.disable_capslock_checkbox.isChecked() and not cm.ConfigManager.is_modifier_disabled(Key.CAPSLOCK): cm.ConfigManager.disable_modifier(Key.CAPSLOCK) elif not self.disable_capslock_checkbox.isChecked() and cm.ConfigManager.is_modifier_disabled(Key.CAPSLOCK): cm.ConfigManager.enable_modifier(Key.CAPSLOCK) def _settings_str(self): """Returns a human readable settings representation for logging purposes.""" settings = "Automatically save changes: {}, " \ "Show tray icon: {}, " \ "Allow keyboard navigation: {}, " \ "Sort by usage count: {}, " \ "Enable undo using backspace: {}, " \ "Tray icon theme: {}, " \ "Disable Capslock: {}".format( self.autosave_checkbox.isChecked(), self.show_tray_checkbox.isChecked(), self.allow_kb_nav_checkbox.isChecked(), self.sort_by_usage_checkbox.isChecked(), self.enable_undo_checkbox.isChecked(), self.system_tray_icon_theme_combobox.currentData(Qt.UserRole), self.disable_capslock_checkbox.isChecked() ) return settings def _fill_autostart_gui_selection_combobox(self): combobox = self.autostart_interface_choice_combobox # type: QComboBox for desktop_file, name in GeneralSettings.GUI_TABLE: try: autokey.configmanager.autostart.get_source_desktop_file(desktop_file) except FileNotFoundError: # Skip unavailable GUIs pass else: combobox.addItem(name, desktop_file) def _fill_notification_icon_combobox_user_data(self): combo_box = self.system_tray_icon_theme_combobox # type: QComboBox for index, icon_name in GeneralSettings.ICON_TABLE: combo_box.setItemData(index, icon_name, Qt.UserRole) def _load_system_tray_icon_theme(self): combo_box = self.system_tray_icon_theme_combobox # type: QComboBox data = cm.ConfigManager.SETTINGS[cm_constants.NOTIFICATION_ICON] combo_box_index = combo_box.findData(data, Qt.UserRole) if combo_box_index == -1: # Invalid data in user configuration. TODO: should this be a warning or error? # Just revert to theme at index 0 (light) combo_box_index = 0 combo_box.setCurrentIndex(combo_box_index) def _load_autostart_settings(self): combobox = self.autostart_interface_choice_combobox # type: QComboBox self.autostart_groupbox.setChecked(self.autostart_settings.desktop_file_name is not None) if self.autostart_settings.desktop_file_name is not None: combobox.setCurrentIndex(combobox.findData(self.autostart_settings.desktop_file_name)) self.autostart_show_main_window_checkbox.setChecked(self.autostart_settings.switch_show_configure) def _save_autostart_settings(self): combobox = self.autostart_interface_choice_combobox # type: QComboBox desktop_entry = None if not self.autostart_groupbox.isChecked() else combobox.currentData(Qt.UserRole) show_main_window = self.autostart_show_main_window_checkbox.isChecked() new_settings = autokey.configmanager.autostart.AutostartSettings(desktop_entry, show_main_window) if new_settings != self.autostart_settings: # Only write if settings changed to preserve eventual user-made modifications. autokey.configmanager.autostart.set_autostart_entry(new_settings) autokey-0.96.0/lib/autokey/qtui/settings/settingsdialog.py000066400000000000000000000050721427671440700237520ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . from typing import TYPE_CHECKING from PyQt5.QtCore import pyqtSlot from PyQt5.QtWidgets import QApplication, QWidget from autokey.qtui import common if TYPE_CHECKING: from autokey.qtapp import Application logger = __import__("autokey.logger").logger.get_logger(__name__) class SettingsDialog(*common.inherits_from_ui_file_with_name("settingsdialog")): def __init__(self, parent: QWidget=None): super(SettingsDialog, self).__init__(parent) self.setupUi(self) logger.info("Settings dialog window created.") @pyqtSlot() # Avoid the slot being called twice, by both signals clicked() and clicked(bool). def on_show_general_settings_button_clicked(self): logger.debug("User views general settings") self.settings_pages.setCurrentWidget(self.general_settings_page) @pyqtSlot() # Avoid the slot being called twice, by both signals clicked() and clicked(bool). def on_show_special_hotkeys_button_clicked(self): logger.debug("User views special hotkeys settings") self.settings_pages.setCurrentWidget(self.special_hotkeys_page) @pyqtSlot() # Avoid the slot being called twice, by both signals clicked() and clicked(bool). def on_show_script_engine_button_clicked(self): logger.debug("User views script engine settings") self.settings_pages.setCurrentWidget(self.script_engine_page) def accept(self): logger.info("User requested to save the settings.") app = QApplication.instance() # type: Application self.general_settings_page.save() self.special_hotkeys_page.save() self.script_engine_page.save() app.configManager.config_altered(True) app.update_notifier_visibility() app.notifier.reset_tray_icon() super(SettingsDialog, self).accept() logger.debug("Save completed, dialog window hidden.") autokey-0.96.0/lib/autokey/qtui/settings/specialhotkeys.py000066400000000000000000000117251427671440700237630ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . import typing from PyQt5.QtWidgets import QDialog, QWidget, QApplication, QLabel, QPushButton from autokey.qtui.dialogs import GlobalHotkeyDialog import autokey.qtui.common as ui_common if typing.TYPE_CHECKING: from autokey.qtapp import Application logger = __import__("autokey.logger").logger.get_logger(__name__) class SpecialHotkeySettings(*ui_common.inherits_from_ui_file_with_name("specialhotkeysettings")): """ The SpecialHotkeySettings class is used inside the AutoKey configuration dialog. It allows the user to select or clear global hotkeys. Currently has two hotkeys: - use_service enables/disables the autokey background service - use_config shows the autokey config/main window, if hidden. """ KEY_MAP = GlobalHotkeyDialog.KEY_MAP REVERSE_KEY_MAP = GlobalHotkeyDialog.REVERSE_KEY_MAP def __init__(self, parent: QWidget=None): super(SpecialHotkeySettings, self).__init__(parent) self.setupUi(self) self.show_config_dlg = GlobalHotkeyDialog(parent) self.toggle_monitor_dlg = GlobalHotkeyDialog(parent) self.use_config_hotkey = False self.use_service_hotkey = False app = QApplication.instance() # type: Application self.config_manager = app.configManager self.use_config_hotkey = self._load_hotkey(self.config_manager.configHotkey, self.config_key_label, self.show_config_dlg, self.clear_config_button) self.use_service_hotkey = self._load_hotkey(self.config_manager.toggleServiceHotkey, self.monitor_key_label, self.toggle_monitor_dlg, self.clear_monitor_button) @staticmethod def _load_hotkey(item, label: QLabel, dialog: GlobalHotkeyDialog, clear_button: QPushButton): dialog.load(item) if item.enabled: key = item.hotKey label.setText(item.get_hotkey_string(key, item.modifiers)) clear_button.setEnabled(True) return True else: label.setText("(None configured)") clear_button.setEnabled(False) return False def save(self): config_hotkey = self.config_manager.configHotkey toggle_hotkey = self.config_manager.toggleServiceHotkey app = QApplication.instance() # type: Application if config_hotkey.enabled: app.hotkey_removed(config_hotkey) config_hotkey.enabled = self.use_config_hotkey if self.use_config_hotkey: self.show_config_dlg.save(config_hotkey) app.hotkey_created(config_hotkey) if toggle_hotkey.enabled: app.hotkey_removed(toggle_hotkey) toggle_hotkey.enabled = self.use_service_hotkey if self.use_service_hotkey: self.toggle_monitor_dlg.save(toggle_hotkey) app.hotkey_created(toggle_hotkey) # ---- Signal handlers def on_set_config_button_pressed(self): self.show_config_dlg.exec_() if self.show_config_dlg.result() == QDialog.Accepted: self.use_config_hotkey = True key = self.show_config_dlg.key modifiers = self.show_config_dlg.build_modifiers() self.config_key_label.setText(self.show_config_dlg.target_item.get_hotkey_string(key, modifiers)) self.clear_config_button.setEnabled(True) def on_clear_config_button_pressed(self): self.use_config_hotkey = False self.clear_config_button.setEnabled(False) self.config_key_label.setText("(None configured)") self.show_config_dlg.reset() def on_set_monitor_button_pressed(self): self.toggle_monitor_dlg.exec_() if self.toggle_monitor_dlg.result() == QDialog.Accepted: self.use_service_hotkey = True key = self.toggle_monitor_dlg.key modifiers = self.toggle_monitor_dlg.build_modifiers() self.monitor_key_label.setText(self.toggle_monitor_dlg.target_item.get_hotkey_string(key, modifiers)) self.clear_monitor_button.setEnabled(True) def on_clear_monitor_button_pressed(self): self.use_service_hotkey = False self.clear_monitor_button.setEnabled(False) self.monitor_key_label.setText("(None configured)") self.toggle_monitor_dlg.reset() autokey-0.96.0/lib/autokey/qtui/settingswidget.py000066400000000000000000000234101427671440700221320ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . from PyQt5.QtWidgets import QDialog import autokey.model.helpers import autokey.model.modelTypes from autokey.qtui.common import inherits_from_ui_file_with_name from autokey.qtui.dialogs import HotkeySettingsDialog, AbbrSettingsDialog, WindowFilterSettingsDialog class SettingsWidget(*inherits_from_ui_file_with_name("settingswidget")): """ The SettingsWidget is used to configure model items. It allows display, assigning and clearing of abbreviations, hotkeys and window filters. """ KEY_MAP = HotkeySettingsDialog.KEY_MAP REVERSE_KEY_MAP = HotkeySettingsDialog.REVERSE_KEY_MAP def __init__(self, parent): super(SettingsWidget, self).__init__(parent) self.setupUi(self) self.abbr_settings_dialog = AbbrSettingsDialog(self) self.hotkey_settings_dialog = HotkeySettingsDialog(self) self.window_filter_dialog = WindowFilterSettingsDialog(self) self.current_item = None # type: autokey.model.modelTypes.Item self.abbreviation_enabled = False self.hotkey_enabled = False self.window_filter_enabled = False def load(self, item: autokey.model.modelTypes.Item): self.current_item = item self._load_abbreviation_data(item) self._load_hotkey_data(item) self._load_window_filter_data(item) def _load_abbreviation_data(self, item: autokey.model.modelTypes.Item): self.abbr_settings_dialog.load(item) item_has_abbreviation = autokey.model.helpers.TriggerMode.ABBREVIATION in item.modes self.abbreviation_label.setText(item.get_abbreviations() if item_has_abbreviation else "(None configured)") self.clear_abbreviation_button.setEnabled(item_has_abbreviation) self.abbreviation_enabled = item_has_abbreviation def _load_hotkey_data(self, item: autokey.model.modelTypes.Item): self.hotkey_settings_dialog.load(item) item_has_hotkey = autokey.model.helpers.TriggerMode.HOTKEY in item.modes self.hotkey_label.setText(item.get_hotkey_string() if item_has_hotkey else "(None configured)") self.clear_hotkey_button.setEnabled(item_has_hotkey) self.hotkey_enabled = item_has_hotkey def _load_window_filter_data(self, item: autokey.model.modelTypes.Item): self.window_filter_dialog.load(item) item_has_window_filter = item.has_filter() or item.inherits_filter() self.window_filter_label.setText(item.get_filter_regex() if item_has_window_filter else "(None configured)") self.window_filter_enabled = item_has_window_filter self.clear_window_filter_button.setEnabled(item_has_window_filter) if item.inherits_filter(): # Inherited window filters can’t be deleted on specific items. self.clear_window_filter_button.setEnabled(False) self.window_filter_enabled = False def save(self): # Perform hotkey ungrab if autokey.model.helpers.TriggerMode.HOTKEY in self.current_item.modes: self.window().app.hotkey_removed(self.current_item) self.current_item.set_modes([]) if self.abbreviation_enabled: self.abbr_settings_dialog.save(self.current_item) if self.hotkey_enabled: self.hotkey_settings_dialog.save(self.current_item) else: self.current_item.unset_hotkey() if self.window_filter_enabled: self.window_filter_dialog.save(self.current_item) else: self.current_item.set_window_titles(None) if self.hotkey_enabled: self.window().app.hotkey_created(self.current_item) def set_dirty(self): self.window().set_dirty() def validate(self): # Start by getting all applicable information if self.abbreviation_enabled: abbreviations = self.abbr_settings_dialog.get_abbrs() else: abbreviations = [] if self.hotkey_enabled: modifiers = self.hotkey_settings_dialog.build_modifiers() key = self.hotkey_settings_dialog.key else: modifiers = [] key = None filter_expression = None if self.window_filter_enabled: filter_expression = self.window_filter_dialog.get_filter_text() elif self.current_item.parent is not None: r = self.current_item.parent.get_applicable_regex(True) if r is not None: filter_expression = r.pattern # Validate ret = [] config_manager = self.window().app.configManager for abbr in abbreviations: unique, conflicting = config_manager.check_abbreviation_unique(abbr, filter_expression, self.current_item) if not unique: f = conflicting.get_applicable_regex() # TODO: i18n if f is None: msg = "The abbreviation {abbreviation} is already in use by the {conflicting_item}.".format( abbreviation=abbr, conflicting_item=str(conflicting) ) else: msg = "The abbreviation {abbreviation} is already in use by the {conflicting_item} " \ "for windows matching '{matching_pattern}'.".format( abbreviation=abbr, conflicting_item=str(conflicting), matching_pattern=f.pattern ) ret.append(msg) unique, conflicting = config_manager.check_hotkey_unique(modifiers, key, filter_expression, self.current_item) if not unique: f = conflicting.get_applicable_regex() # TODO: i18n if f is None: msg = "The hotkey '{hotkey}' is already in use by the {conflicting_item}.".format( hotkey=conflicting.get_hotkey_string(), conflicting_item=str(conflicting) ) else: msg = "The hotkey '{hotkey}' is already in use by the {conflicting_item} " \ "for windows matching '{matching_pattern}.".format( hotkey=conflicting.get_hotkey_string(), conflicting_item=str(conflicting), matching_pattern=f.pattern ) ret.append(msg) return ret # ---- Signal handlers def on_set_abbreviation_button_pressed(self): self.abbr_settings_dialog.exec_() if self.abbr_settings_dialog.result() == QDialog.Accepted: self.set_dirty() self.abbreviation_enabled = True self.abbreviation_label.setText(self.abbr_settings_dialog.get_abbrs_readable()) self.clear_abbreviation_button.setEnabled(True) def on_clear_abbreviation_button_pressed(self): self.set_dirty() self.abbreviation_enabled = False self.clear_abbreviation_button.setEnabled(False) self.abbreviation_label.setText("(None configured)") # TODO: i18n self.abbr_settings_dialog.reset() def on_set_hotkey_button_pressed(self): self.hotkey_settings_dialog.exec_() if self.hotkey_settings_dialog.result() == QDialog.Accepted: self.set_dirty() self.hotkey_enabled = True key = self.hotkey_settings_dialog.key modifiers = self.hotkey_settings_dialog.build_modifiers() self.hotkey_label.setText(self.current_item.get_hotkey_string(key, modifiers)) self.clear_hotkey_button.setEnabled(True) def on_clear_hotkey_button_pressed(self): self.set_dirty() self.hotkey_enabled = False self.clear_hotkey_button.setEnabled(False) self.hotkey_label.setText("(None configured)") # TODO: i18n self.hotkey_settings_dialog.reset() def on_set_window_filter_button_pressed(self): self.window_filter_dialog.exec_() if self.window_filter_dialog.result() == QDialog.Accepted: self.set_dirty() filter_text = self.window_filter_dialog.get_filter_text() if filter_text: self.window_filter_enabled = True self.clear_window_filter_button.setEnabled(True) self.window_filter_label.setText(filter_text) else: self.window_filter_enabled = False self.clear_window_filter_button.setEnabled(False) if self.current_item.inherits_filter(): text = self.current_item.parent.get_child_filter() else: text = "(None configured)" # TODO: i18n self.window_filter_label.setText(text) def on_clear_window_filter_button_pressed(self): self.set_dirty() self.window_filter_enabled = False self.clear_window_filter_button.setEnabled(False) if self.current_item.inherits_filter(): text = self.current_item.parent.get_child_filter() else: text = "(None configured)" # TODO: i18n self.window_filter_label.setText(text) self.window_filter_dialog.reset() autokey-0.96.0/lib/autokey/scripting/000077500000000000000000000000001427671440700175345ustar00rootroot00000000000000autokey-0.96.0/lib/autokey/scripting/__init__.py000066400000000000000000000024711427671440700216510ustar00rootroot00000000000000# Copyright (C) 2018 Thomas Hess # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ This package contains the scripting API. This file centralises the public API classes for easier importing. """ import autokey.common from . import highlevel from .common import ColourData, DialogData from .engine import Engine from .keyboard import Keyboard from .mouse import Mouse from autokey.model.store import Store from .system import System from .window import Window # Platform abstraction if autokey.common.USING_QT: from .clipboard_qt import QtClipboard as Clipboard from .dialog_qt import QtDialog as Dialog else: from .clipboard_gtk import GtkClipboard as Clipboard from .dialog_gtk import GtkDialog as Dialog autokey-0.96.0/lib/autokey/scripting/clipboard_gtk.py000066400000000000000000000102651427671440700227160ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ GtkClipboard Functions """ from gi.repository import Gtk, Gdk from pathlib import Path class GtkClipboard: """ Read/write access to the X selection and clipboard - GTK version """ def __init__(self, app): """ Initialize the Gtk version of the Clipboard Usage: Called when GtkClipboard is imported @param app: refers to the application instance """ self.clipBoard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) """ Refers to the data contained in the Gtk Clipboard (conventional clipboard) """ self.selection = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY) """ Refers to the "selection" of the clipboard or the highlighted text """ self.app = app """ Refers to the application instance """ def fill_selection(self, contents): """ Copy text into the X selection Usage: C{clipboard.fill_selection(contents)} @param contents: string to be placed in the selection """ #self.__execAsync(self.__fillSelection, contents) self.__fillSelection(contents) def __fillSelection(self, string): """ Backend for the C{fill_selection} method Sets the selection text to the C{string} value @param string: Value to change the selection to """ Gdk.threads_enter() self.selection.set_text(string, -1) Gdk.threads_leave() #self.sem.release() def get_selection(self): """ Read text from the X selection The X selection refers to the currently highlighted text. Usage: C{clipboard.get_selection()} @return: text contents of the mouse selection @rtype: C{str} @raise Exception: if no text was found in the selection """ Gdk.threads_enter() text = self.selection.wait_for_text() Gdk.threads_leave() if text is not None: return text else: raise Exception("No text found in X selection") def fill_clipboard(self, contents): """ Copy text into the clipboard Usage: C{clipboard.fill_clipboard(contents)} @param contents: string to be placed in the selection """ Gdk.threads_enter() if Gtk.get_major_version() >= 3: self.clipBoard.set_text(contents, -1) else: self.clipBoard.set_text(contents) Gdk.threads_leave() def get_clipboard(self): """ Read text from the clipboard Usage: C{clipboard.get_clipboard()} @return: text contents of the clipboard @rtype: C{str} @raise Exception: if no text was found on the clipboard """ Gdk.threads_enter() text = self.clipBoard.wait_for_text() Gdk.threads_leave() if text is not None: return text else: raise Exception("No text found on clipboard") def set_clipboard_image(self, path): """ Set clipboard to image Usage: C{clipboard.set_clipboard_image(path)} @param path: Path to image file @raise OSError: If path does not exist """ image_path = Path(path).expanduser() if image_path.exists(): Gdk.threads_enter() copied_image = Gtk.Image.new_from_file(str(image_path)) self.clipBoard.set_image(copied_image.get_pixbuf()) Gdk.threads_leave() else: raise OSError("Image file not found") autokey-0.96.0/lib/autokey/scripting/clipboard_qt.py000066400000000000000000000072051427671440700225550ustar00rootroot00000000000000""" QtClipboard Functions """ import threading from PyQt5.QtGui import QClipboard, QImage from PyQt5.QtWidgets import QApplication from pathlib import Path class QtClipboard: """ Read/write access to the X selection and clipboard - QT version """ def __init__(self, app): """ Initialize the Qt version of the Clipboard Usage: Called when QtClipboard is imported. @param app: refers to the application instance """ self.clipBoard = QApplication.clipboard() """ Refers to the Qt clipboard object """ self.app = app """ Refers to the application instance """ self.text = None """ Used to temporarily store the value of the selection or clipboard """ self.sem = None """ Qt semaphore object used for asynchronous method execution """ def fill_selection(self, contents): """ Copy text into the X selection Usage: C{clipboard.fill_selection(contents)} @param contents: string to be placed in the selection """ self.__execAsync(self.__fillSelection, contents) def __fillSelection(self, string): """ Backend for the C{fill_selection} method Sets the selection text to the C{string} value @param string: Value to change the selection to """ self.clipBoard.setText(string, QClipboard.Selection) self.sem.release() def get_selection(self): """ Read text from the X selection Usage: C{clipboard.get_selection()} @return: text contents of the mouse selection @rtype: C{str} """ self.__execAsync(self.__getSelection) return str(self.text) def __getSelection(self): self.text = self.clipBoard.text(QClipboard.Selection) self.sem.release() def fill_clipboard(self, contents): """ Copy text into the clipboard Usage: C{clipboard.fill_clipboard(contents)} @param contents: string to be placed in the selection """ self.__execAsync(self.__fillClipboard, contents) def set_clipboard_image(self, path): """ Set clipboard to image Usage: C{clipboard.set_clipboard_image(path)} @param path: Path to image file @raise OSError: If path does not exist """ self.__execAsync(self.__set_clipboard_image, path) def __set_clipboard_image(self, path): image_path = Path(path).expanduser() if image_path.exists(): copied_image = QImage() copied_image.load(str(image_path)) self.clipBoard.setImage(copied_image) else: raise OSError def __fillClipboard(self, string): self.clipBoard.setText(string, QClipboard.Clipboard) self.sem.release() def get_clipboard(self): """ Read text from the clipboard Usage: C{clipboard.get_clipboard()} @return: text contents of the clipboard @rtype: C{str} """ self.__execAsync(self.__getClipboard) return str(self.text) def __getClipboard(self): """ Backend for the C{get_clipboard} method Stores the value of the clipboard into the C{self.text} variable """ self.text = self.clipBoard.text(QClipboard.Clipboard) self.sem.release() def __execAsync(self, callback, *args): """ Backend to execute methods asynchronously in Qt """ self.sem = threading.Semaphore(0) self.app.exec_in_main(callback, *args) self.sem.acquire() autokey-0.96.0/lib/autokey/scripting/common.py000066400000000000000000000061561427671440700214060ustar00rootroot00000000000000# Copyright (C) 2018 Thomas Hess # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Contains common functions for both Qt and Gtk versions """ from typing import NamedTuple, Union, List class ColourData(NamedTuple("ColourData", (("r", int), ("g", int), ("b", int)))): """Colour data type for colour chooser dialogs.""" @property def hex_code(self) -> str: """Returns rgb in hex format""" return "{0:02x}{1:02x}{2:02x}".format(self.r, self.g, self.b) @property def html_code(self) -> str: """Converts the ColourData into a HTML-style colour, equivalent to the KDialog output.""" return "#" + self.hex_code @property def zenity_tuple_str(self) -> str: """Converts the ColourData into a tuple-like string, equivalent to the Zenity output. ("rgb(R, G, B)")""" return "rgb({})".format(",".join(map(str,self))) @staticmethod def from_html(html_style_colour_str: str): """ Parser for KDialog output, which outputs a HTML style hex code like #55aa00 @param html_style_colour_str: HTML style hex string encoded colour. (#rrggbb) @return: ColourData instance @rtype: ColourData """ html_style_colour_str = html_style_colour_str.lstrip("#") components = list(map("".join, zip(*[iter(html_style_colour_str)]*2))) return ColourData(*(int(colour, 16) for colour in components)) @staticmethod def from_zenity_tuple_str(zenity_tuple_str: str): """ Parser for Zenity output, which outputs a named tuple-like string: "rgb(R, G, B)", where R, G, B are base10 integers. @param zenity_tuple_str: tuple-like string: "rgb(r, g, b), where r, g, b are base10 integers. @return: ColourData instance @rtype: ColourData """ components = zenity_tuple_str.strip("rgb()").split(",") return ColourData(*map(int, components)) class DialogData(NamedTuple("DialogData", (("return_code", int), ("data", Union[ColourData, str, List[str], None])))): """Dialog data type for return values from input dialogs""" @property def successful(self) -> bool: """ Returns True, if the dialog execution was successful, i.e. KDialog or Zenity exited with a zero return value. This includes: - Command line parameters are correct - Execution is otherwise successful (Can open X Display, load shared libraries, etc.) - The user clicked on OK or otherwise 'accepted' the dialog. """ return self.return_code == 0 autokey-0.96.0/lib/autokey/scripting/dialog_gtk.py000066400000000000000000000230251427671440700222140ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Class for creating Gtk Window dialogs""" import re import subprocess from autokey.scripting.common import DialogData, ColourData class GtkDialog: """ Provides a simple interface for the display of some basic dialogs to collect information from the user. This version uses Zenity to integrate well with GNOME. To pass additional arguments to Zenity that are not specifically handled, use keyword arguments. For example, to pass the --timeout argument to Zenity pass C{timeout="15"} as one of the parameters. All keyword arguments must be given as strings. @note: Exit codes: an exit code of 0 indicates that the user clicked OK. """ def _run_zenity(self, title, args, kwargs) -> DialogData: for k, v in kwargs.items(): args.append("--" + k) args.append(v) with subprocess.Popen( ["zenity", "--title", title] + args, stdout=subprocess.PIPE, universal_newlines=True) as p: output = p.communicate()[0][:-1] # type: str # Drop trailing newline return_code = p.returncode return DialogData(return_code, output) def info_dialog(self, title="Information", message="", **kwargs): """ Show an information dialog Usage: C{dialog.info_dialog(title="Information", message="", **kwargs)} @param title: window title for the dialog @param message: message displayed in the dialog @return: a tuple containing the exit code and user input @rtype: C{tuple(int, str)} """ return self._run_zenity(title, ["--info", "--text", message], kwargs) def input_dialog(self, title="Enter a value", message="Enter a value", default="", **kwargs): """ Show an input dialog Usage: C{dialog.input_dialog(title="Enter a value", message="Enter a value", default="", **kwargs)} @param title: window title for the dialog @param message: message displayed above the input box @param default: default value for the input box @return: a tuple containing the exit code and user input @rtype: C{DialogData(int, str)} """ return self._run_zenity(title, ["--entry", "--text", message, "--entry-text", default], kwargs) def password_dialog(self, title="Enter password", message="Enter password", **kwargs): """ Show a password input dialog Usage: C{dialog.password_dialog(title="Enter password", message="Enter password")} @param title: window title for the dialog @param message: message displayed above the password input box @return: a tuple containing the exit code and user input @rtype: C{DialogData(int, str)} """ return self._run_zenity(title, ["--entry", "--text", message, "--hide-text"], kwargs) #def combo_menu(self, options, title="Choose an option", message="Choose an option"): """ Show a combobox menu - not supported by zenity Usage: C{dialog.combo_menu(options, title="Choose an option", message="Choose an option")} @param options: list of options (strings) for the dialog @param title: window title for the dialog @param message: message displayed above the combobox """ #return self._run_zenity(title, ["--combobox", message] + options) def list_menu(self, options, title="Choose a value", message="Choose a value", default=None, **kwargs): """ Show a single-selection list menu Usage: C{dialog.list_menu(options, title="Choose a value", message="Choose a value", default=None, **kwargs)} @param options: list of options (strings) for the dialog @param title: window title for the dialog @param message: message displayed above the list @param default: default value to be selected @return: a tuple containing the exit code and user choice @rtype: C{DialogData(int, str)} """ choices = [] for option in options: if option == default: choices.append("TRUE") else: choices.append("FALSE") choices.append(option) return self._run_zenity( title, ["--list", "--radiolist", "--text", message, "--column", " ", "--column", "Options"] + choices, kwargs) def list_menu_multi(self, options, title="Choose one or more values", message="Choose one or more values", defaults: list=None, **kwargs): """ Show a multiple-selection list menu Usage: C{dialog.list_menu_multi(options, title="Choose one or more values", message="Choose one or more values", defaults=[], **kwargs)} @param options: list of options (strings) for the dialog @param title: window title for the dialog @param message: message displayed above the list @param defaults: list of default values to be selected @return: a tuple containing the exit code and user choice @rtype: C{DialogData(int, List[str])} """ if defaults is None: defaults = [] choices = [] for option in options: if option in defaults: choices.append("TRUE") else: choices.append("FALSE") choices.append(option) return_code, output = self._run_zenity( title, ["--list", "--checklist", "--text", message, "--column", " ", "--column", "Options"] + choices, kwargs) results = output.split('|') return DialogData(return_code, results) def open_file(self, title="Open File", **kwargs): """ Show an Open File dialog Usage: C{dialog.open_file(title="Open File", **kwargs)} @param title: window title for the dialog @return: a tuple containing the exit code and file path @rtype: C{DialogData(int, str)} """ #if rememberAs is not None: # return self._run_zenity(title, ["--getopenfilename", initialDir, fileTypes, ":" + rememberAs]) #else: return self._run_zenity(title, ["--file-selection"], kwargs) def save_file(self, title="Save As", **kwargs): """ Show a Save As dialog Usage: C{dialog.save_file(title="Save As", **kwargs)} @param title: window title for the dialog @return: a tuple containing the exit code and file path @rtype: C{DialogData(int, str)} """ #if rememberAs is not None: # return self._run_zenity(title, ["--getsavefilename", initialDir, fileTypes, ":" + rememberAs]) #else: return self._run_zenity(title, ["--file-selection", "--save"], kwargs) def choose_directory(self, title="Select Directory", initialDir="~", **kwargs): """ Show a Directory Chooser dialog Usage: C{dialog.choose_directory(title="Select Directory", **kwargs)} @param title: window title for the dialog @param initialDir: @return: a tuple containing the exit code and path @rtype: C{DialogData(int, str)} """ #if rememberAs is not None: # return self._run_zenity(title, ["--getexistingdirectory", initialDir, ":" + rememberAs]) #else: return self._run_zenity(title, ["--file-selection", "--directory"], kwargs) def choose_colour(self, title="Select Colour", **kwargs): """ Show a Colour Chooser dialog Usage: C{dialog.choose_colour(title="Select Colour")} @param title: window title for the dialog @return: @rtype: C{DialogData(int, Optional[ColourData])} """ return_data = self._run_zenity(title, ["--color-selection"], kwargs) if return_data.successful: converted_colour = ColourData.from_zenity_tuple_str(return_data.data) return DialogData(return_data.return_code, converted_colour) else: return DialogData(return_data.return_code, None) def calendar(self, title="Choose a date", format_str="%Y-%m-%d", date="today", **kwargs): """ Show a calendar dialog Usage: C{dialog.calendar_dialog(title="Choose a date", format="%Y-%m-%d", date="YYYY-MM-DD", **kwargs)} @param title: window title for the dialog @param format_str: format of date to be returned @param date: initial date as YYYY-MM-DD, otherwise today @return: a tuple containing the exit code and date @rtype: C{DialogData(int, str)} """ if re.match(r"[0-9]{4}-[0-9]{2}-[0-9]{2}", date): year = date[0:4] month = date[5:7] day = date[8:10] date_args = ["--year=" + year, "--month=" + month, "--day=" + day] else: date_args = [] return self._run_zenity(title, ["--calendar", "--date-format=" + format_str] + date_args, kwargs) autokey-0.96.0/lib/autokey/scripting/dialog_qt.py000066400000000000000000000247321427671440700220610ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Class for creating Qt dialogs""" import subprocess from autokey.scripting.common import DialogData, ColourData class QtDialog: """ Provides a simple interface for the display of some basic dialogs to collect information from the user. This version uses KDialog to integrate well with KDE. To pass additional arguments to KDialog that are not specifically handled, use keyword arguments. For example, to pass the --geometry argument to KDialog to specify the desired size of the dialog, pass C{geometry="700x400"} as one of the parameters. All keyword arguments must be given as strings. A note on exit codes: an exit code of 0 indicates that the user clicked OK. """ def _run_kdialog(self, title, args, kwargs) -> DialogData: for k, v in kwargs.items(): args.append("--" + k) args.append(v) with subprocess.Popen( ["kdialog", "--title", title] + args, stdout=subprocess.PIPE, universal_newlines=True) as p: output = p.communicate()[0][:-1] # type: str # Drop trailing newline return_code = p.returncode return DialogData(return_code, output) def info_dialog(self, title="Information", message="", **kwargs): """ Show an information dialog Usage: C{dialog.info_dialog(title="Information", message="", **kwargs)} @param title: window title for the dialog @param message: message displayed in the dialog @return: a tuple containing the exit code and user input @rtype: C{DialogData(int, str)} """ return self._run_kdialog(title, ["--msgbox", message], kwargs) def input_dialog(self, title="Enter a value", message="Enter a value", default="", **kwargs): """ Show an input dialog Usage: C{dialog.input_dialog(title="Enter a value", message="Enter a value", default="", **kwargs)} @param title: window title for the dialog @param message: message displayed above the input box @param default: default value for the input box @return: a tuple containing the exit code and user input @rtype: C{DialogData(int, str)} """ return self._run_kdialog(title, ["--inputbox", message, default], kwargs) def password_dialog(self, title="Enter password", message="Enter password", **kwargs): """ Show a password input dialog Usage: C{dialog.password_dialog(title="Enter password", message="Enter password", **kwargs)} @param title: window title for the dialog @param message: message displayed above the password input box @return: a tuple containing the exit code and user input @rtype: C{DialogData(int, str)} """ return self._run_kdialog(title, ["--password", message], kwargs) def combo_menu(self, options, title="Choose an option", message="Choose an option", **kwargs): """ Show a combobox menu Usage: C{dialog.combo_menu(options, title="Choose an option", message="Choose an option", **kwargs)} @param options: list of options (strings) for the dialog @param title: window title for the dialog @param message: message displayed above the combobox @return: a tuple containing the exit code and user choice @rtype: C{DialogData(int, str)} """ return self._run_kdialog(title, ["--combobox", message] + options, kwargs) def list_menu(self, options, title="Choose a value", message="Choose a value", default=None, **kwargs): """ Show a single-selection list menu Usage: C{dialog.list_menu(options, title="Choose a value", message="Choose a value", default=None, **kwargs)} @param options: list of options (strings) for the dialog @param title: window title for the dialog @param message: message displayed above the list @param default: default value to be selected @return: a tuple containing the exit code and user choice @rtype: C{DialogData(int, str)} """ choices = [] optionNum = 0 for option in options: choices.append(str(optionNum)) choices.append(option) if option == default: choices.append("on") else: choices.append("off") optionNum += 1 return_code, result = self._run_kdialog(title, ["--radiolist", message] + choices, kwargs) choice = options[int(result)] return DialogData(return_code, choice) def list_menu_multi(self, options, title="Choose one or more values", message="Choose one or more values", defaults: list=None, **kwargs): """ Show a multiple-selection list menu Usage: C{dialog.list_menu_multi(options, title="Choose one or more values", message="Choose one or more values", defaults=[], **kwargs)} @param options: list of options (strings) for the dialog @param title: window title for the dialog @param message: message displayed above the list @param defaults: list of default values to be selected @return: a tuple containing the exit code and user choice @rtype: C{DialogData(int, List[str])} """ if defaults is None: defaults = [] choices = [] optionNum = 0 for option in options: choices.append(str(optionNum)) choices.append(option) if option in defaults: choices.append("on") else: choices.append("off") optionNum += 1 return_code, output = self._run_kdialog(title, ["--separate-output", "--checklist", message] + choices, kwargs) results = output.split() choices = [options[int(choice_index)] for choice_index in results] return DialogData(return_code, choices) def open_file(self, title="Open File", initialDir="~", fileTypes="*|All Files", rememberAs=None, **kwargs): """ Show an Open File dialog Usage: C{dialog.open_file(title="Open File", initialDir="~", fileTypes="*|All Files", rememberAs=None, **kwargs)} @param title: window title for the dialog @param initialDir: starting directory for the file dialog @param fileTypes: file type filter expression @param rememberAs: gives an ID to this file dialog, allowing it to open at the last used path next time @return: a tuple containing the exit code and file path @rtype: C{DialogData(int, str)} """ if rememberAs is not None: return self._run_kdialog(title, ["--getopenfilename", initialDir, fileTypes, ":" + rememberAs], kwargs) else: return self._run_kdialog(title, ["--getopenfilename", initialDir, fileTypes], kwargs) def save_file(self, title="Save As", initialDir="~", fileTypes="*|All Files", rememberAs=None, **kwargs): """ Show a Save As dialog Usage: C{dialog.save_file(title="Save As", initialDir="~", fileTypes="*|All Files", rememberAs=None, **kwargs)} @param title: window title for the dialog @param initialDir: starting directory for the file dialog @param fileTypes: file type filter expression @param rememberAs: gives an ID to this file dialog, allowing it to open at the last used path next time @return: a tuple containing the exit code and file path @rtype: C{DialogData(int, str)} """ if rememberAs is not None: return self._run_kdialog(title, ["--getsavefilename", initialDir, fileTypes, ":" + rememberAs], kwargs) else: return self._run_kdialog(title, ["--getsavefilename", initialDir, fileTypes], kwargs) def choose_directory(self, title="Select Directory", initialDir="~", rememberAs=None, **kwargs): """ Show a Directory Chooser dialog Usage: C{dialog.choose_directory(title="Select Directory", initialDir="~", rememberAs=None, **kwargs)} @param title: window title for the dialog @param initialDir: starting directory for the directory chooser dialog @param rememberAs: gives an ID to this file dialog, allowing it to open at the last used path next time @return: a tuple containing the exit code and chosen path @rtype: C{DialogData(int, str)} """ if rememberAs is not None: return self._run_kdialog(title, ["--getexistingdirectory", initialDir, ":" + rememberAs], kwargs) else: return self._run_kdialog(title, ["--getexistingdirectory", initialDir], kwargs) def choose_colour(self, title="Select Colour", **kwargs): """ Show a Colour Chooser dialog Usage: C{dialog.choose_colour(title="Select Colour")} @param title: window title for the dialog @return: a tuple containing the exit code and colour @rtype: C{DialogData(int, str)} """ return_data = self._run_kdialog(title, ["--getcolor"], kwargs) if return_data.successful: return DialogData(return_data.return_code, ColourData.from_html(return_data.data)) else: return DialogData(return_data.return_code, None) def calendar(self, title="Choose a date", format_str="%Y-%m-%d", date="today", **kwargs): """ Show a calendar dialog Usage: C{dialog.calendar_dialog(title="Choose a date", format="%Y-%m-%d", date="YYYY-MM-DD", **kwargs)} Note: the format and date parameters are not currently used @param title: window title for the dialog @param format_str: format of date to be returned @param date: initial date as YYYY-MM-DD, otherwise today @return: a tuple containing the exit code and date @rtype: C{DialogData(int, str)} """ return self._run_kdialog(title, ["--calendar", title], kwargs) autokey-0.96.0/lib/autokey/scripting/engine.py000066400000000000000000000651211427671440700213600ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Engine backend for Autokey""" import pathlib from collections.abc import Iterable from typing import Tuple, Optional, List, Union import autokey.model.folder import autokey.model.helpers import autokey.model.phrase import autokey.model.script from autokey import configmanager from autokey.model.key import Key from autokey.scripting.system import System logger = __import__("autokey.logger").logger.get_logger(__name__) class Engine: """ Provides access to the internals of AutoKey. Note that any configuration changes made using this API while the configuration window is open will not appear until it is closed and re-opened. """ SendMode = autokey.model.phrase.SendMode Key = Key def __init__(self, config_manager, runner): """ """ self.configManager = config_manager self.runner = runner self.monitor = config_manager.app.monitor self._macro_args = [] self._script_args = [] self._script_kwargs = {} self._return_value = '' self._triggered_abbreviation = None # type: Optional[str] def get_folder(self, title: str): """ Retrieve a folder by its title Usage: C{engine.get_folder(title)} Note that if more than one folder has the same title, only the first match will be returned. """ validateType(title, "title", str) for folder in self.configManager.allFolders: if folder.title == title: return folder return None def create_folder(self, title: str, parent_folder=None, temporary=False): """ Create and return a new folder. Usage: C{engine.create_folder("new folder"), parent_folder=folder, temporary=True} Descriptions for the optional arguments: @param parentFolder: Folder to make this folder a subfolder of. If passed as a folder, it will be that folder within auotkey. If passed as pathlib.Path, it will be created or added at that path. Paths expand ~ to $HOME. @param temporary: Folders created with temporary=True are not persisted. Used for single-source rc-style scripts. Cannot be used if parent_folder is a Path. If a folder of that name already exists, this will return it unchanged. If the folder wasn't already added to autokey, it will be. The 'temporary' property is not touched to avoid deleting an existing folder. Note that if more than one folder has the same title, only the first match will be returned. """ validateType(title, "title", str) validateType(parent_folder, "parent_folder", [autokey.model.folder.Folder, pathlib.Path]) validateType(temporary, "temporary", bool) # XXX Doesn't check if a folder already exists at this path in autokey. if isinstance(parent_folder, pathlib.Path): if temporary: raise ValueError("Parameter 'temporary' is True, but a path \ was given as the parent folder. Temporary folders \ cannot use absolute paths.") path = parent_folder.expanduser() / title path.mkdir(parents=True, exist_ok=True) new_folder = autokey.model.folder.Folder(title, path=str(path.resolve())) self.configManager.allFolders.append(new_folder) return new_folder # TODO: Convert this to use get_folder, when we change to specifying # the exact folder by more than just title. if parent_folder is None: parent_folders = self.configManager.allFolders elif isinstance(parent_folder, autokey.model.folder.Folder): parent_folders = parent_folder.folders else: # Input is previously validated, must match one of the above. pass for folder in parent_folders: if folder.title == title: return folder else: new_folder = autokey.model.folder.Folder(title) if parent_folder is None: self.configManager.allFolders.append(new_folder) else: parent_folder.add_folder(new_folder) if not temporary and parent_folder.temporary: raise ValueError("Parameter 'temporary' is False, but parent folder is a temporary one. \ Folders created within temporary folders must themselves be set temporary") if not temporary: new_folder.persist() else: new_folder.temporary = True return new_folder def create_phrase(self, folder, name: str, contents: str, abbreviations: Union[str, List[str]]=None, hotkey: Tuple[List[Union[Key, str]], Union[Key, str]]=None, send_mode: autokey.model.phrase.SendMode = autokey.model.phrase.SendMode.CB_CTRL_V, window_filter: str=None, show_in_system_tray: bool=False, always_prompt: bool=False, temporary=False, replace_existing_hotkey=False): """ Create a new text phrase inside the given folder. Use C{engine.get_folder(folder_name)} to retrieve the folder you wish to create the Phrase in. If the folder is a temporary one, the phrase will be created as temporary. The first three arguments (folder, name and contents) are required. All further arguments are optional and considered to be keyword-argument only. Do not rely on the order of the optional arguments. The optional parameters can be used to configure the newly created Phrase. Usage (minimal example): C{engine.create_phrase(folder, name, contents)} Further concrete examples: C{ engine.create_phrase(folder, "My new Phrase", "This is the Phrase content", abbreviations=["abc", "def"], hotkey=([engine.Key.SHIFT], engine.Key.NP_DIVIDE), send_mode=engine.SendMode.CB_CTRL_SHIFT_V, window_filter="konsole\\.Konsole", show_in_system_tray=True) } Descriptions for the optional arguments: abbreviations may be a single string or a list of strings. Each given string is assigned as an abbreviation to the newly created phrase. hotkey parameter: The hotkey parameter accepts a 2-tuple, consisting of a list of modifier keys in the first element and an unshifted (lowercase) key as the second element. Modifier keys must be given as a list of strings (or Key enum instances), with the following values permitted: The key must be an unshifted character (i.e. lowercase) or a Key enum instance. Modifier keys from the list above are NOT allowed here. Example: (["", ""], "9") to assign "++9" as a hotkey. The Key enum contains objects representing various special keys and is available as an attribute of the "engine" object, named "Key". So to access a function key, you can use the string "" or engine.Key.F12 See the AutoKey Wiki for an overview of all available keys in the enumeration. send_mode: This parameter configures how AutoKey sends the phrase content, for example by typing or by pasting using the clipboard. It accepts items from the SendMode enumeration, which is also available from the engine object as engine.SendMode. The parameter defaults to engine.SendMode.KEYBOARD. Available send modes are: KEYBOARD CB_CTRL_V CB_CTRL_SHIFT_V CB_SHIFT_INSERT SELECTION To paste the Phrase using "+, set send_mode=engine.SendMode.CB_SHIFT_INSERT window_filter: Accepts a string which will be used as a regular expression to match window titles or applications using the WM_CLASS attribute. @param folder: folder to place the abbreviation in, retrieved using C{engine.get_folder()} @param name: Name/description for the phrase. @param contents: the expansion text @param abbreviations: Can be a single string or a list (or other iterable) of strings. Assigned to the Phrase @param hotkey: A tuple containing a keyboard combination that will be assigned as a hotkey. First element is a list of modifiers, second element is the key. @param send_mode: The pasting mode that will be used to expand the Phrase. Used to configure, how the Phrase is expanded. Defaults to typing using the "CTRL+V" method. @param window_filter: A string containing a regular expression that will be used as the window filter. @param show_in_system_tray: A boolean defaulting to False. If set to True, the new Phrase will be shown in the tray icon context menu. @param always_prompt: A boolean defaulting to False. If set to True, the Phrase expansion has to be manually confirmed, each time it is triggered. @param temporary: Hotkeys created with temporary=True are not persisted as .jsons, and are replaced if the description is not unique within the folder. Used for single-source rc-style scripts. @param replace_existing_hotkey: If true, instead of warning if the hotkey is already in use by another phrase or folder, it removes the hotkey from those clashes and keeps this phrase's hotkey. @raise ValueError: If a given abbreviation or hotkey is already in use or parameters are otherwise invalid @return The created Phrase object. This object is NOT considered part of the public API and exposes the raw internals of AutoKey. Ignore it, if you don’t need it or don’t know what to do with it. It can be used for _really_ advanced use cases, where further customizations are desired. Use at your own risk. No guarantees are made about the object’s structure. Read the AutoKey source code for details. """ validateArguments(folder, name, contents, abbreviations, hotkey, send_mode, window_filter, show_in_system_tray, always_prompt, temporary, replace_existing_hotkey) if abbreviations and isinstance(abbreviations, str): abbreviations = [abbreviations] check_abbreviation_unique(self.configManager, abbreviations, window_filter) if not replace_existing_hotkey: check_hotkey_unique(self.configManager, hotkey, window_filter) else: # XXX If something causes the phrase creation to fail after this, # this will unset the hotkey without replacing it. self.__clear_existing_hotkey(hotkey, window_filter) self.monitor.suspend() try: p = autokey.model.phrase.Phrase(name, contents) if send_mode in autokey.model.phrase.SendMode: p.sendMode = send_mode if abbreviations: p.add_abbreviations(abbreviations) if hotkey: p.set_hotkey(*hotkey) if window_filter: p.set_window_titles(window_filter) p.show_in_tray_menu = show_in_system_tray p.prompt = always_prompt p.temporary = temporary folder.add_item(p) # Don't save a json if it is a temporary hotkey. Won't persist across # reloads. if not temporary: p.persist() return p finally: self.monitor.unsuspend() self.configManager.config_altered(False) def __clear_existing_hotkey(self, hotkey, window_filter): existing_item = self.get_item_with_hotkey(hotkey) if existing_item and not isinstance(existing_item, configmanager.configmanager.GlobalHotkey): if existing_item.filter_matches(window_filter): existing_item.unset_hotkey() def create_abbreviation(self, folder, description, abbr, contents): """ DEPRECATED. Use engine.create_phrase() with appropriate keyword arguments instead. Create a new text phrase inside the given folder and assign the abbreviation given. Usage: C{engine.create_abbreviation(folder, description, abbr, contents)} When the given abbreviation is typed, it will be replaced with the given text. @param folder: folder to place the abbreviation in, retrieved using C{engine.get_folder()} @param description: description for the phrase @param abbr: the abbreviation that will trigger the expansion @param contents: the expansion text @raise Exception: if the specified abbreviation is not unique """ if not self.configManager.check_abbreviation_unique(abbr, None, None)[0]: raise Exception("The specified abbreviation is already in use") self.monitor.suspend() p = autokey.model.phrase.Phrase(description, contents) p.modes.append(autokey.model.helpers.TriggerMode.ABBREVIATION) p.abbreviations = [abbr] folder.add_item(p) p.persist() self.monitor.unsuspend() self.configManager.config_altered(False) def create_hotkey(self, folder, description, modifiers, key, contents): """ DEPRECATED. Use engine.create_phrase() with appropriate keyword arguments instead. Create a text hotkey Usage: C{engine.create_hotkey(folder, description, modifiers, key, contents)} When the given hotkey is pressed, it will be replaced with the given text. Modifiers must be given as a list of strings, with the following values permitted: The key must be an unshifted character (i.e. lowercase) @param folder: folder to place the abbreviation in, retrieved using C{engine.get_folder()} @param description: description for the phrase @param modifiers: modifiers to use with the hotkey (as a list) @param key: the hotkey @param contents: the expansion text @raise Exception: if the specified hotkey is not unique """ modifiers.sort() if not self.configManager.check_hotkey_unique(modifiers, key, None, None)[0]: raise Exception("The specified hotkey and modifier combination is already in use") self.monitor.suspend() p = autokey.model.phrase.Phrase(description, contents) p.modes.append(autokey.model.helpers.TriggerMode.HOTKEY) p.set_hotkey(modifiers, key) folder.add_item(p) p.persist() self.monitor.unsuspend() self.configManager.config_altered(False) def run_script(self, description, *args, **kwargs): """ Run an existing script using its description or path to look it up Usage: C{engine.run_script(description, 'foo', 'bar', foobar='foobar'})} @param description: description of the script to run. If parsable as an absolute path to an existing file, that will be run instead. @raise Exception: if the specified script does not exist """ self._script_args = args self._script_kwargs = kwargs path = pathlib.Path(description) path = path.expanduser() # Check if absolute path. if pathlib.PurePath(path).is_absolute() and path.exists(): self.runner.run_subscript(path) else: target_script = None for item in self.configManager.allItems: if item.description == description and isinstance(item, autokey.model.script.Script): target_script = item if target_script is not None: self.runner.run_subscript(target_script) else: raise Exception("No script with description '%s' found" % description) return self._return_value def run_script_from_macro(self, args): """ Used internally by AutoKey for phrase macros """ self._macro_args = args["args"].split(',') try: self.run_script(args["name"]) except Exception as e: # TODO: Log more information here, instead of setting the return # value. self.set_return_value("{ERROR: %s}" % str(e)) def run_system_command_from_macro(self, args): """ Used internally by AutoKey for system macros """ try: self._return_value = System.exec_command(args["command"], getOutput=True) except Exception as e: self.set_return_value("{ERROR: %s}" % str(e)) def get_script_arguments(self): """ Get the arguments supplied to the current script via the scripting api Usage: C{engine.get_script_arguments()} @return: the arguments @rtype: C{list[Any]} """ return self._script_args def get_script_keyword_arguments(self): """ Get the arguments supplied to the current script via the scripting api as keyword args. Usage: C{engine.get_script_keyword_arguments()} @return: the arguments @rtype: C{Dict[str, Any]} """ return self._script_kwargs def get_macro_arguments(self): """ Get the arguments supplied to the current script via its macro Usage: C{engine.get_macro_arguments()} @return: the arguments @rtype: C{list(str())} """ return self._macro_args def set_return_value(self, val): """ Store a return value to be used by a phrase macro Usage: C{engine.set_return_value(val)} @param val: value to be stored """ self._return_value = val def _get_return_value(self): """ Used internally by AutoKey for phrase macros """ ret = self._return_value self._return_value = '' return ret def _set_triggered_abbreviation(self, abbreviation: str, trigger_character: str): """ Used internally by AutoKey to provide the abbreviation and trigger that caused the script to execute. @param abbreviation: Abbreviation that caused the script to execute @param trigger_character: Possibly empty "trigger character". As defined in the abbreviation configuration. """ self._triggered_abbreviation = abbreviation self._triggered_character = trigger_character def get_triggered_abbreviation(self) -> Tuple[Optional[str], Optional[str]]: """ This function can be queried by a script to get the abbreviation text that triggered it’s execution. If a script is triggered by an abbreviation, this function returns a tuple containing two strings. First element is the abbreviation text. The second element is the trigger character that finally caused the execution. It is typically some whitespace character, like ' ', '\t' or a newline character. It is empty, if the abbreviation was configured to "trigger immediately". If the script execution was triggered by a hotkey, a call to the DBus interface, the tray icon, the "Run" button in the main window or any other means, this function returns a tuple containing two None values. Usage: C{abbreviation, trigger_character = engine.get_triggered_abbreviation()} You can determine if the script was triggered by an abbreviation by simply testing the truth value of the first returned value. @return: Abbreviation that triggered the script execution, if any. @rtype: C{Tuple[Optional[str], Optional[str]]} """ return self._triggered_abbreviation, self._triggered_character def remove_all_temporary(self, folder=None, in_temp_parent=False): """ Removes all temporary folders and phrases, as well as any within temporary folders. Useful for rc-style scripts that want to change a set of keys. """ self.configManager.remove_all_temporary(folder, in_temp_parent) def get_item_with_hotkey(self, hotkey): if not hotkey: return modifiers = sorted(hotkey[0]) return self.configManager.get_item_with_hotkey(modifiers, hotkey[1]) def validateAbbreviations(abbreviations): """ Checks if the given abbreviations are a list/iterable of strings @param abbreviations: Abbreviations list to be validated @raise ValueError: Raises C{ValueError} if C{abbreviations} is anything other than C{str} or C{Iterable} """ if abbreviations is None: return fail=False if not isinstance(abbreviations, str): fail=True if isinstance(abbreviations, Iterable): fail=False for item in abbreviations: if not isinstance(item, str): fail=True if fail: raise ValueError("Expected abbreviations to be a single string or a list/iterable of strings, not {}".format( type(abbreviations)) ) def check_abbreviation_unique(configmanager, abbreviations, window_filter): """ Checks if the given abbreviations are unique @param configmanager: ConfigManager Instance to check abbrevations @param abbreviations: List of abbreviations to be checked @param window_filter: Window filter that the abbreviation will apply to. @raise ValueError: Raises C{ValueError} if an abbreviation is already in use. """ if not abbreviations: return for abbr in abbreviations: if not configmanager.check_abbreviation_unique(abbr, window_filter, None)[0]: raise ValueError("The specified abbreviation '{}' is already in use.".format(abbr)) def check_hotkey_unique(configmanager, hotkey, window_filter): """ Checks if the given hotkey is unique @param configmanager: ConfigManager Instance used to check hotkey @param hotkey: hotkey to be check if unique @param window_filter: Window filter to be applied to the hotkey """ if not hotkey: return modifiers = sorted(hotkey[0]) if not configmanager.check_hotkey_unique(modifiers, hotkey[1], window_filter, None)[0]: raise ValueError("The specified hotkey and modifier combination is already in use: {}".format(hotkey)) def isValidHotkeyType(item): """ Checks if the hotkey is valid. @param item: Hotkey to be checked @return: Returns C{True} if hotkey is valid, C{False} otherwise """ fail=False if isinstance(item, Key): fail=False elif isinstance(item, str): if len(item) == 1: fail=False else: fail = not Key.is_key(item) else: fail=True return not fail def validateHotkey(hotkey): """ """ failmsg = "Expected hotkey to be a tuple of modifiers then keys, as lists of Key or str, not {}".format(type(hotkey)) if hotkey is None: return fail=False if not isinstance(hotkey, tuple): fail=True else: if len(hotkey) != 2: fail=True else: # First check modifiers is list of valid hotkeys. if isinstance(hotkey[0], list): for item in hotkey[0]: if not isValidHotkeyType(item): fail=True failmsg = "Hotkey is not a valid modifier: {}".format(item) else: fail=True failmsg = "Hotkey modifiers is not a list" # Then check second element is a key or str if not isValidHotkeyType(hotkey[1]): fail=True failmsg = "Hotkey is not a valid key: {}".format(hotkey[1]) if fail: raise ValueError(failmsg) def validateArguments(folder, name, contents, abbreviations, hotkey, send_mode, window_filter, show_in_system_tray, always_prompt, temporary, replace_existing_hotkey): if folder is None: raise ValueError("Parameter 'folder' is None. Check the folder is a valid autokey folder") validateType(folder, "folder", autokey.model.folder.Folder) # For when we allow pathlib.Path # validateType(folder, "folder", # [model.Folder, pathlib.Path]) validateType(name, "name", str) validateType(contents, "contents", str) validateAbbreviations(abbreviations) validateHotkey(hotkey) validateType(send_mode, "send_mode", autokey.model.phrase.SendMode) validateType(window_filter, "window_filter", str) validateType(show_in_system_tray, "show_in_system_tray", bool) validateType(always_prompt, "always_prompt", bool) validateType(temporary, "temporary", bool) validateType(replace_existing_hotkey, "replace_existing_hotkey", bool) # TODO: The validation should be done by some controller functions in the model base classes. if folder.temporary and not temporary: raise ValueError("Parameter 'temporary' is False, but parent folder is a temporary one. \ Phrases created within temporary folders must themselves be explicitly set temporary") def validateType(item, name, type_): """ type_ may be a list, in which case if item matches any type, no error is raised. """ if item is None: return if isinstance(type_, list): failed=True for type__ in type_: if isinstance(item, type__): failed=False if failed: raise ValueError("Expected {} to be one of {}, not {}".format( name, type_, type(item))) else: if not isinstance(item, type_): raise ValueError("Expected {} to be {}, not {}".format( name, type_, type(item))) autokey-0.96.0/lib/autokey/scripting/highlevel.py000066400000000000000000000134051427671440700220600ustar00rootroot00000000000000""" Highlevel scripting API, requires xautomation to be installed """ import time import os import subprocess import tempfile import imghdr import struct class PatternNotFound(Exception): """Exception raised by functions""" pass # numeric representation of the mouse buttons. For use in visgrep. LEFT = 1 """Left mouse button""" MIDDLE = 2 """Middle mouse button""" RIGHT = 3 """Right mouse button""" def visgrep(scr: str, pat: str, tolerance: int = 0) -> int: """ Usage: C{visgrep(scr: str, pat: str, tolerance: int = 0) -> int} Visual grep of scr for pattern pat. Requires xautomation (http://hoopajoo.net/projects/xautomation.html). Usage: C{visgrep("screen.png", "pat.png")} @param scr: path of PNG image to be grepped. @param pat: path of pattern image (PNG) to look for in scr. @param tolerance: An integer ≥ 0 to specify the level of tolerance for 'fuzzy' matches. @raise ValueError: Raised if tolerance is negative or not convertable to int @raise PatternNotFound: Raised if C{pat} not found. @raise FileNotFoundError: Raised if either file is not found @returns: Coordinates of the topleft point of the match, if any. Raises L{PatternNotFound} exception otherwise. """ tol = int(tolerance) if tol < 0: raise ValueError("tolerance must be ≥ 0.") with open(scr), open(pat): pass with tempfile.NamedTemporaryFile() as f: subprocess.call(['png2pat', pat], stdout=f) # don't use check_call, some versions (1.05) have a missing return statement in png2pat.c so the exit status ≠ 0 f.flush() os.fsync(f.fileno()) vg = subprocess.Popen(['visgrep', '-t' + str(tol), scr, f.name], stdout=subprocess.PIPE, stderr=subprocess.PIPE) out = vg.communicate() coord_str = out[0].decode().split(' ')[0].split(',') try: coord = [int(coord_str[0]), int(coord_str[1])] except (ValueError, IndexError) as e: raise PatternNotFound(str([x.decode() for x in out]) + '\n\t' + repr(e)) return coord def get_png_dim(filepath: str) -> int: """ Usage: C{get_png_dim(filepath:str) -> (int)} Finds the dimension of a PNG. @param filepath: file path of the PNG. @returns: (width, height). @raise Exception: Raised if the file is not a png """ if not imghdr.what(filepath) == 'png': raise Exception("not PNG") head = open(filepath, 'rb').read(24) return struct.unpack('!II', head[16:24]) def mouse_move(x: int, y: int, display: str=''): """ Moves the mouse using xte C{mousemove} from xautomation @param x: x location to move the mouse to @param y: y location to move the mouse to @param display: X display to pass to C{xte} """ subprocess.call(['xte', '-x', display, "mousemove {} {}".format(int(x), int(y))]) def mouse_rmove(x: int, y: int, display: str=''): """ Moves the mouse using xte C{mousermove} command from xautomation @param x: x location to move the mouse to @param y: y location to move the mouse to @param display: X display to pass to C{xte} """ subprocess.call(['xte', '-x', display, "mousermove {} {}".format(int(x), int(y))]) def mouse_click(button: int, display: str=''): """ Clicks the mouse in the current location using xte C{mouseclick} from xautomation @param button: Which button signal to send from the mouse @param display: X display to pass to C{xte} """ subprocess.call(['xte', '-x', display, "mouseclick {}".format(int(button))]) def mouse_pos(): """ Returns the current location of the mouse. @returns: Returns the mouse location in a C{list} """ tmp = subprocess.check_output("xmousepos").decode().split() return list(map(int, tmp))[:2] def click_on_pat(pat: str, mousebutton: int=1, offset: (float, float)=None, tolerance: int=0, restore_pos: bool=False) -> None: """ Requires C{imagemagick}, C{xautomation}, C{xwd}. Click on a pattern at a specified offset (x,y) in percent of the pattern dimension. x is the horizontal distance from the top left corner, y is the vertical distance from the top left corner. By default, the offset is (50,50), which means that the center of the pattern will be clicked at. @param pat: path of pattern image (PNG) to click on. @param mousebutton: mouse button number used for the click @param offset: offset from the top left point of the match. (float,float) @param tolerance: An integer ≥ 0 to specify the level of tolerance for 'fuzzy' matches. If negative or not convertible to int, raises ValueError. @param restore_pos: return to the initial mouse position after the click. @raises: L{PatternNotFound}: Raised when the pattern is not found on the screen """ x0, y0 = mouse_pos() move_to_pat(pat, offset, tolerance) mouse_click(mousebutton) if restore_pos: mouse_move(x0, y0) def move_to_pat(pat: str, offset: (float, float)=None, tolerance: int=0) -> None: """See L{click_on_pat}""" with tempfile.NamedTemporaryFile() as f: subprocess.call(''' xwd -root -silent -display :0 | convert xwd:- png:''' + f.name, shell=True) loc = visgrep(f.name, pat, tolerance) pat_size = get_png_dim(pat) if offset is None: x, y = [l + ps//2 for l, ps in zip(loc, pat_size)] else: x, y = [l + ps*(off/100) for off, l, ps in zip(offset, loc, pat_size)] mouse_move(x, y) def acknowledge_gnome_notification(): """ Moves mouse pointer to the bottom center of the screen and clicks on it. """ x0, y0 = mouse_pos() mouse_move(10000, 10000) # TODO: What if the screen is larger? Loop until mouse position does not change anymore? x, y = mouse_pos() mouse_rmove(-x/2, 0) mouse_click(LEFT) time.sleep(.2) mouse_move(x0, y0) autokey-0.96.0/lib/autokey/scripting/keyboard.py000066400000000000000000000200621427671440700217060ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Keyboard Functions""" import typing import autokey.model.phrase import autokey.iomediator.waiter from autokey import iomediator, model from typing import Callable class Keyboard: """ Provides access to the keyboard for event generation. """ SendMode = autokey.model.phrase.SendMode def __init__(self, mediator): """Initialize the Keyboard""" self.mediator = mediator # type: iomediator.IoMediator """See C{IoMediator} documentation""" def send_keys(self, key_string, send_mode: typing.Union[ autokey.model.phrase.SendMode, int] = autokey.model.phrase.SendMode.KEYBOARD): """ Send a sequence of keys via keyboard events as the default or via clipboard pasting. Because the clipboard can only contain printable characters, special keys and embedded key combinations can only be sent in keyboard mode. Trying to send special keys using a clipboard pasting method will paste the literal representation (e.g. "+") instead of the actual special key or key combination. Usage: C{keyboard.send_keys(keyString)} @param key_string: string of keys to send. Special keys are only possible in keyboard mode. @param send_mode: Determines how the string is sent. """ if not isinstance(key_string, str): raise TypeError("Only strings can be sent using this function") send_mode = _validate_send_mode(send_mode) self.mediator.interface.begin_send() try: if send_mode is autokey.model.phrase.SendMode.KEYBOARD: self.mediator.send_string(key_string) else: self.mediator.paste_string(key_string, send_mode) finally: self.mediator.interface.finish_send() def send_key(self, key, repeat=1): """ Send a keyboard event Usage: C{keyboard.send_key(key, repeat=1)} @param key: they key to be sent (e.g. "s" or "") @param repeat: number of times to repeat the key event """ for _ in range(repeat): self.mediator.send_key(key) self.mediator.flush() def press_key(self, key): """ Send a key down event Usage: C{keyboard.press_key(key)} The key will be treated as down until a matching release_key() is sent. @param key: they key to be pressed (e.g. "s" or "") """ self.mediator.press_key(key) def release_key(self, key): """ Send a key up event Usage: C{keyboard.release_key(key)} If the specified key was not made down using press_key(), the event will be ignored. @param key: they key to be released (e.g. "s" or "") """ self.mediator.release_key(key) def fake_keypress(self, key, repeat=1): """ Fake a keypress Usage: C{keyboard.fake_keypress(key, repeat=1)} Uses XTest to 'fake' a keypress. This is useful to send keypresses to some applications which won't respond to keyboard.send_key() @param key: they key to be sent (e.g. "s" or "") @param repeat: number of times to repeat the key event """ for _ in range(repeat): self.mediator.fake_keypress(key) def wait_for_keypress(self, key, modifiers: list=None, timeOut=10.0): """ Wait for a keypress or key combination Usage: C{keyboard.wait_for_keypress(self, key, modifiers=[], timeOut=10.0)} Note: this function cannot be used to wait for modifier keys on their own @param key: they key to wait for @param modifiers: list of modifiers that should be pressed with the key @param timeOut: maximum time, in seconds, to wait for the keypress to occur """ if modifiers is None: modifiers = [] w = self.mediator.waiter(key, modifiers, None, None, None, timeOut) self.mediator.listeners.append(w) rtn = w.wait() self.mediator.listeners.remove(w) return rtn def wait_for_keyevent(self, check: Callable[[any,str,list,str], bool], name: str = None, timeOut=10.0): """ Wait for a key event, potentially accumulating the intervening characters Usage: C{keyboard.wait_for_keypress(self, check, name=None, timeOut=10.0)} @param check: a function that returns True or False to signify we've finished waiting @param name: only one waiter can have this name. Used to prevent more threads waiting on this. @param timeOut: maximum time, in seconds, to wait for the keypress to occur Example: # Accumulate the traditional emacs C-u prefix arguments # See https://www.gnu.org/software/emacs/manual/html_node/elisp/Prefix-Command-Arguments.html def check(waiter,rawKey,modifiers,key,*args): isCtrlU = (key == 'u' and len(modifiers) == 1 and modifiers[0] == '') if isCtrlU: # If we get here, they've already pressed C-u at least 2x try: val = int(waiter.result) * 4 waiter.result = str(val) except ValueError: waiter.result = "16" return False elif any(m == "" or m == "" or m == "" or m == "" or m == "" for m in modifiers): # Some other control character is an indication we're done. if waiter.result is None or waiter.result == "": waiter.result = "4" store.set_global_value("emacs-prefix-arg", waiter.result) return True else: # accumulate as a string waiter.result = waiter.result + key return False keyboard.wait_for_keyevent(check, "emacs-prefix") """ if name is None or not any(elem.name == name for elem in self.mediator.listeners): w = self.mediator.waiter(None, None, None, check, name, timeOut) self.mediator.listeners.append(w) rtn = w.wait() self.mediator.listeners.remove(w) return rtn return False def _validate_send_mode(send_mode): permissible_values = "\n".join("keyboard.{}".format(mode) for mode in map(str, autokey.model.phrase.SendMode)) if isinstance(send_mode, int): if send_mode in range(len(autokey.model.phrase.SendMode)): send_mode = tuple(autokey.model.phrase.SendMode)[send_mode] # type: model.SendMode else: permissible_values = "\n".join( "{}: keyboard.{}".format( number, str(constant)) for number, constant in enumerate(autokey.model.phrase.SendMode) ) raise ValueError( "send_mode out of range for index-based access. " "Permissible values are:\n{}".format(permissible_values)) elif isinstance(send_mode, str): try: send_mode = autokey.model.phrase.SendMode(send_mode) except ValueError as v: raise ValueError("Permissible values are: " + permissible_values) from v elif send_mode is None: # Selection has value None send_mode = autokey.model.phrase.SendMode.SELECTION elif not isinstance(send_mode, autokey.model.phrase.SendMode): raise TypeError("Unsupported type for send_mode parameter: {} Use one of: {}".format(send_mode, permissible_values)) return send_mode autokey-0.96.0/lib/autokey/scripting/mouse.py000066400000000000000000000204631427671440700212430ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Mouse functions, see also L{highlevel} for mouse move and click functions using C{xte} from C{xautomation}""" import typing import time if typing.TYPE_CHECKING: import autokey.iomediator.iomediator from autokey.model.button import Button import autokey.iomediator.waiter class Mouse: """ Provides access to send mouse clicks """ Button = Button def __init__(self, mediator): self.mediator = mediator # type: autokey.iomediator.iomediator.IoMediator self.interface = self.mediator.interface def click_relative(self, x, y, button): """ Send a mouse click relative to the active window Usage: C{mouse.click_relative(x, y, button)} @param x: x-coordinate in pixels, relative to upper left corner of window @param y: y-coordinate in pixels, relative to upper left corner of window @param button: mouse button to simulate (left=1, middle=2, right=3) """ self.interface.send_mouse_click(x, y, button, True) def click_relative_self(self, x, y, button): """ Send a mouse click relative to the current mouse position Usage: C{mouse.click_relative_self(x, y, button)} @param x: x-offset in pixels, relative to current mouse position @param y: y-offset in pixels, relative to current mouse position @param button: mouse button to simulate (left=1, middle=2, right=3) """ self.interface.send_mouse_click_relative(x, y, button) def click_absolute(self, x, y, button): """ Send a mouse click relative to the screen (absolute) Usage: C{mouse.click_absolute(x, y, button)} @param x: x-coordinate in pixels, relative to upper left corner of window @param y: y-coordinate in pixels, relative to upper left corner of window @param button: mouse button to simulate (left=1, middle=2, right=3) """ self.interface.send_mouse_click(x, y, button, False) def wait_for_click(self, button, timeOut=10.0): """ Wait for a mouse click Usage: C{mouse.wait_for_click(self, button, timeOut=10.0)} @param button: they mouse button click to wait for as a button number, 1-9 @param timeOut: maximum time, in seconds, to wait for the keypress to occur """ button = int(button) w = autokey.iomediator.waiter.Waiter(None, None, button, None, None, timeOut) w.wait() def move_cursor(self, x,y): """ Move mouse cursor to xy location on screen without warping back to the start location Usage: C{mouse.move_cursor(x,y)} @param x: x-coordinate in pixels, relative to upper left corner of screen @param y: y-coordinate in pixels, relative to upper left corner of screen """ self.interface.move_cursor(x,y) def move_relative(self, x, y): """ Move cursor relative to xy location based on the top left hand corner of the window that has input focus Usage: C{mouse.move_relative(x,y)} @param x: x-coordinate in pixels, relative to upper left corner of window @param y: y-coordinate in pixels, relative to upper left corner of window """ self.interface.move_cursor(x,y,relative=True) def move_relative_self(self, x, y): """ Move cursor relative to the location of the mouse cursor Usage: C{mouse.move_relative_self(x,y)} @param x: x-coordinate in pixels, relative to current position of mouse cursor @param y: y-coordinate in pixels, relative to current position of mouse cursor """ self.interface.move_cursor(x, y, relative_self=True) def press_button(self, button): """ Send mouse button down signal at current location Usage: C{mouse.press_button(button)} @param button: the mouse button to press down """ x,y = self.interface.mouse_location() self.interface.mouse_press(x,y,button) def release_button(self, button): """ Send mouse button up signal at current location Usage: C{mouse.release_button(button)} @param button: the mouse button to press down """ x,y = self.interface.mouse_location() self.interface.mouse_release(x,y,button) def select_area(self, startx, starty, endx, endy, button, scrollNumber=0, down=True, warp=True): """ "Drag and Select" for an area with the top left corner at (startx, starty) and the bottom right corner at (endx, endy) and uses C{button} for the mouse button held down Usage: C{mouse.select_area(startx, starty, endx, endy, button)} @param startx: X coordinate of where to start the drag and select @param starty: Y coordinate of where to start the drag and select @param endx: X coordinate of where to end the drag and select @param endy: Y coordinate of where to end the drag and select @param button: What mouse button to press at the start coordinates and release at the end coordinates @param scrollNumber: Number of times to scroll, defaults to 0 @param down: Boolean to choose which direction to scroll, True for down, False for up., defaults to scrolling down. @param warp: If True method will return cursor to the position it was at at the start of execution """ #store mouse location x,y = self.interface.mouse_location() self.interface.move_cursor(startx, starty) self.interface.mouse_press(startx, starty, button) if down: self.interface.scroll_down(scrollNumber) else: self.interface.scroll_up(scrollNumber) self.interface.move_cursor(endx,endy) self.interface.mouse_release(endx, endy, button) #restore mouse location if warp: self.interface.move_cursor(x,y) def get_location(self): """ Returns the current location of the mouse. Incorporates a tiny delay in order to make sure AutoKey executes any queued commands before checking the location. C{mouse.move_cursor(0,0) x,y = mouse.get_location()} Usage: C{mouse.get_location()} @return: x,y location of the mouse @rtype: C{tuple(int,int)} """ #minimal delay added to make sure location is correct time.sleep(0.05) return self.interface.mouse_location() def get_relative_location(self): """ Returns the relative location of the mouse in the window that has input focus Incorporates a tiny delay in order to make sure AutoKey executes any queued commands Usage: C{mouse.get_relative_location()} @return: x,y location of the mouse relative to the top left hand corner of the window that has input focus @rtype: C{tuple(int, int)} """ time.sleep(0.05) return self.interface.relative_mouse_location() def scroll_down(self, number): """ Fires the mouse button 5 signal the specified number of times. Note that the behavior of these methods are effected (and untested) by programs like imwheel. Usage: C{mouse.scroll_down()} @param number: The number of times the scroll up signal will be fired. """ self.interface.scroll_down(number) def scroll_up(self, number): """ Fires the mouse button 4 signal the specified number of times. Note that the behavior of these methods are effected (and untested) by programs like imwheel. Usage: C{mouse.scroll_up()} @param number: The number of times the scroll up signal will be fired. """ self.interface.scroll_up(number)autokey-0.96.0/lib/autokey/scripting/system.py000066400000000000000000000047711427671440700214430ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Provides access to run system commands through C{subprocess} and basic file creation""" import subprocess class System: """ Simplified access to some system commands. """ @staticmethod def exec_command(command, getOutput=True): """ Execute a shell command Usage: C{system.exec_command(command, getOutput=True)} Set getOutput to False if the command does not exit and return immediately. Otherwise AutoKey will not respond to any hotkeys/abbreviations etc until the process started by the command exits. @param command: command to be executed (including any arguments) - e.g. "ls -l" @param getOutput: whether to capture the (stdout) output of the command @raise subprocess.CalledProcessError: if the command returns a non-zero exit code """ if getOutput: with subprocess.Popen( command, shell=True, bufsize=-1, stdout=subprocess.PIPE, universal_newlines=True) as p: output = p.communicate()[0] # Most shell output has a new line at the end, which we don't want. output = output.rstrip("\n") if p.returncode: raise subprocess.CalledProcessError(p.returncode, output) return output else: subprocess.Popen(command, shell=True, bufsize=-1) @staticmethod def create_file(file_name, contents=""): """ Create a file with contents Usage: C{system.create_file(fileName, contents="")} @param fileName: full path to the file to be created @param contents: contents to insert into the file """ with open(file_name, "w") as written_file: written_file.write(contents) autokey-0.96.0/lib/autokey/scripting/window.py000066400000000000000000000214511427671440700214200ustar00rootroot00000000000000# Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """Basic window management. Requies C{wmctrl} to be installed.""" import re import subprocess import time class Window: """ Basic window management using wmctrl Note: in all cases where a window title is required (with the exception of wait_for_focus()), two special values of window title are permitted: :ACTIVE: - select the currently active window :SELECT: - select the desired window by clicking on it """ def __init__(self, mediator): self.mediator = mediator def wait_for_focus(self, title, timeOut=5): """ Wait for window with the given title to have focus Usage: C{window.wait_for_focus(title, timeOut=5)} If the window becomes active, returns True. Otherwise, returns False if the window has not become active by the time the timeout has elapsed. @param title: title to match against (as a regular expression) @param timeOut: period (seconds) to wait before giving up @rtype: boolean """ regex = re.compile(title) waited = 0 while waited <= timeOut: if regex.match(self.mediator.interface.get_window_title()): return True if timeOut == 0: break # zero length timeout, if not matched go straight to end time.sleep(0.3) waited += 0.3 return False def wait_for_exist(self, title, timeOut=5): """ Wait for window with the given title to be created Usage: C{window.wait_for_exist(title, timeOut=5)} If the window is in existence, returns True. Otherwise, returns False if the window has not been created by the time the timeout has elapsed. @param title: title to match against (as a regular expression) @param timeOut: period (seconds) to wait before giving up @rtype: boolean """ regex = re.compile(title) waited = 0 while waited <= timeOut: retCode, output = self._run_wmctrl(["-l"]) for line in output.split('\n'): if regex.match(line[14:].split(' ', 1)[-1]): return True if timeOut == 0: break # zero length timeout, if not matched go straight to end time.sleep(0.3) waited += 0.3 return False def activate(self, title, switchDesktop=False, matchClass=False): """ Activate the specified window, giving it input focus Usage: C{window.activate(title, switchDesktop=False, matchClass=False)} If switchDesktop is False (default), the window will be moved to the current desktop and activated. Otherwise, switch to the window's current desktop and activate it there. @param title: window title to match against (as case-insensitive substring match) @param switchDesktop: whether or not to switch to the window's current desktop @param matchClass: if True, match on the window class instead of the title """ if switchDesktop: args = ["-a", title] else: args = ["-R", title] if matchClass: args += ["-x"] self._run_wmctrl(args) def close(self, title, matchClass=False): """ Close the specified window gracefully Usage: C{window.close(title, matchClass=False)} @param title: window title to match against (as case-insensitive substring match) @param matchClass: if True, match on the window class instead of the title """ if matchClass: self._run_wmctrl(["-c", title, "-x"]) else: self._run_wmctrl(["-c", title]) def resize_move(self, title, xOrigin=-1, yOrigin=-1, width=-1, height=-1, matchClass=False): """ Resize and/or move the specified window Usage: C{window.close(title, xOrigin=-1, yOrigin=-1, width=-1, height=-1, matchClass=False)} Leaving and of the position/dimension values as the default (-1) will cause that value to be left unmodified. @param title: window title to match against (as case-insensitive substring match) @param xOrigin: new x origin of the window (upper left corner) @param yOrigin: new y origin of the window (upper left corner) @param width: new width of the window @param height: new height of the window @param matchClass: if True, match on the window class instead of the title """ mvArgs = ["0", str(xOrigin), str(yOrigin), str(width), str(height)] if matchClass: xArgs = ["-x"] else: xArgs = [] self._run_wmctrl(["-r", title, "-e", ','.join(mvArgs)] + xArgs) def move_to_desktop(self, title, deskNum, matchClass=False): """ Move the specified window to the given desktop Usage: C{window.move_to_desktop(title, deskNum, matchClass=False)} @param title: window title to match against (as case-insensitive substring match) @param deskNum: desktop to move the window to (note: zero based) @param matchClass: if True, match on the window class instead of the title """ if matchClass: xArgs = ["-x"] else: xArgs = [] self._run_wmctrl(["-r", title, "-t", str(deskNum)] + xArgs) def switch_desktop(self, deskNum): """ Switch to the specified desktop Usage: C{window.switch_desktop(deskNum)} @param deskNum: desktop to switch to (note: zero based) """ self._run_wmctrl(["-s", str(deskNum)]) def set_property(self, title, action, prop, matchClass=False): """ Set a property on the given window using the specified action Usage: C{window.set_property(title, action, prop, matchClass=False)} Allowable actions: C{add, remove, toggle} Allowable properties: C{modal, sticky, maximized_vert, maximized_horz, shaded, skip_taskbar, skip_pager, hidden, fullscreen, above} @param title: window title to match against (as case-insensitive substring match) @param action: one of the actions listed above @param prop: one of the properties listed above @param matchClass: if True, match on the window class instead of the title """ if matchClass: xArgs = ["-x"] else: xArgs = [] self._run_wmctrl(["-r", title, "-b" + action + ',' + prop] + xArgs) def get_active_geometry(self): """ Get the geometry of the currently active window Usage: C{window.get_active_geometry()} @return: a 4-tuple containing the x-origin, y-origin, width and height of the window (in pixels) @rtype: C{tuple(int, int, int, int)} """ active = self.mediator.interface.get_window_title() result, output = self._run_wmctrl(["-l", "-G"]) matchingLine = None for line in output.split('\n'): if active in line[34:].split(' ', 1)[-1]: matchingLine = line if matchingLine is not None: output = matchingLine.split()[2:6] # return [int(x) for x in output] return list(map(int, output)) else: return None def get_active_title(self): """ Get the visible title of the currently active window Usage: C{window.get_active_title()} @return: the visible title of the currentle active window @rtype: C{str} """ return self.mediator.interface.get_window_title() def get_active_class(self): """ Get the class of the currently active window Usage: C{window.get_active_class()} @return: the class of the currentle active window @rtype: C{str} """ return self.mediator.interface.get_window_class() def _run_wmctrl(self, args): try: with subprocess.Popen(["wmctrl"] + args, stdout=subprocess.PIPE) as p: output = p.communicate()[0].decode()[:-1] # Drop trailing newline returncode = p.returncode except FileNotFoundError: return 1, 'ERROR: Please install wmctrl' return returncode, output autokey-0.96.0/lib/autokey/service.py000066400000000000000000000533141427671440700175520ustar00rootroot00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2011 Chris Dekter # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . import collections import datetime import pathlib import threading import time import traceback import typing import autokey.model import autokey.model.phrase import autokey.model.script import autokey.model.store from autokey.model.key import Key, KEY_FIND_RE from autokey.iomediator.iomediator import IoMediator from autokey.macro import MacroManager import autokey.scripting from autokey.configmanager.configmanager import ConfigManager, save_config import autokey.configmanager.configmanager_constants as cm_constants logger = __import__("autokey.logger").logger.get_logger(__name__) MAX_STACK_LENGTH = 150 def threaded(f): def wrapper(*args, **kwargs): t = threading.Thread(target=f, args=args, kwargs=kwargs, name="Phrase-thread") t.setDaemon(False) t.start() wrapper.__name__ = f.__name__ wrapper.__dict__ = f.__dict__ wrapper.__doc__ = f.__doc__ wrapper._original = f # Store the original function for unit testing purposes. return wrapper def synchronized(lock): """ Synchronization decorator. """ def wrap(f): def new_function(*args, **kw): lock.acquire() try: return f(*args, **kw) finally: lock.release() return new_function return wrap class Service: """ Handles general functionality and dispatching of results down to the correct execution service (phrase or script). """ def __init__(self, app): logger.info("Starting service") self.configManager = app.configManager ConfigManager.SETTINGS[cm_constants.SERVICE_RUNNING] = False self.mediator = None self.app = app self.inputStack = collections.deque(maxlen=MAX_STACK_LENGTH) self.lastStackState = '' self.lastMenu = None self.name = None def start(self): self.mediator = IoMediator(self) self.mediator.interface.initialise() self.mediator.interface.start() self.mediator.start() ConfigManager.SETTINGS[cm_constants.SERVICE_RUNNING] = True self.scriptRunner = ScriptRunner(self.mediator, self.app) self.phraseRunner = PhraseRunner(self) autokey.model.store.Store.GLOBALS.update(ConfigManager.SETTINGS[cm_constants.SCRIPT_GLOBALS]) logger.info("Service now marked as running") def unpause(self): ConfigManager.SETTINGS[cm_constants.SERVICE_RUNNING] = True logger.info("Unpausing - service now marked as running") def pause(self): ConfigManager.SETTINGS[cm_constants.SERVICE_RUNNING] = False logger.info("Pausing - service now marked as stopped") def is_running(self): return ConfigManager.SETTINGS[cm_constants.SERVICE_RUNNING] def shutdown(self, save=True): logger.info("Service shutting down") if self.mediator is not None: self.mediator.shutdown() if save: save_config(self.configManager) logger.debug("Service shutdown completed.") def handle_mouseclick(self, rootX, rootY, relX, relY, button, windowTitle): # logger.debug("Received mouse click - resetting buffer") self.inputStack.clear() logger.log(level=9, msg="Mouse click at root:("+str(rootX)+", "+str(rootY)+") Relative:("+str(relX)+","+str(relY)+") Button: "+str(button)+" In window: "+str(windowTitle)) # If we had a menu and receive a mouse click, means we already # hid the menu. Don't need to do it again self.lastMenu = None # Clear last to prevent undo of previous phrase in unexpected places self.phraseRunner.clear_last() def handle_keypress(self, rawKey, modifiers, key, window_info): logger.debug("Raw key: %r, modifiers: %r, Key: %s", rawKey, modifiers, key) logger.debug("Window visible title: %r, Window class: %r" % window_info) self.configManager.lock.acquire() # Always check global hotkeys for hotkey in self.configManager.globalHotkeys: hotkey.check_hotkey(modifiers, rawKey, window_info) if self.__shouldProcess(window_info): itemMatch = None menu = None for item in self.configManager.hotKeys: if item.check_hotkey(modifiers, rawKey, window_info): itemMatch = item break if itemMatch is not None: logger.info('Matched {} "{}" with hotkey and prompt={}'.format( itemMatch.__class__.__name__, itemMatch.description, itemMatch.prompt )) if itemMatch.prompt: menu = ([], [itemMatch]) else: for folder in self.configManager.hotKeyFolders: if folder.check_hotkey(modifiers, rawKey, window_info): #menu = PopupMenu(self, [folder], []) menu = ([folder], []) if menu is not None: logger.debug("Matched Folder with hotkey - showing menu") if self.lastMenu is not None: #self.lastMenu.remove_from_desktop() self.app.hide_menu() self.lastStackState = '' self.lastMenu = menu #self.lastMenu.show_on_desktop() self.app.show_popup_menu(*menu) if itemMatch is not None: self.__tryReleaseLock() self.__processItem(itemMatch) ### --- end of hotkey processing --- ### modifierCount = len(modifiers) if modifierCount > 1 or (modifierCount == 1 and Key.SHIFT not in modifiers): self.inputStack.clear() self.__tryReleaseLock() return ### --- end of processing if non-printing modifiers are on --- ### if self.__updateStack(key): currentInput = ''.join(self.inputStack) item, menu = self.__checkTextMatches([], self.configManager.abbreviations, currentInput, window_info, True) if not item or menu: item, menu = self.__checkTextMatches( self.configManager.allFolders, self.configManager.allItems, currentInput, window_info) # type: autokey.model.phrase.Phrase, list if item: self.__tryReleaseLock() logger.info('Matched {} "{}" having abbreviations "{}" against current input'.format( item.__class__.__name__, item.description, item.abbreviations)) self.__processItem(item, currentInput) elif menu: if self.lastMenu is not None: #self.lastMenu.remove_from_desktop() self.app.hide_menu() self.lastMenu = menu #self.lastMenu.show_on_desktop() self.app.show_popup_menu(*menu) logger.debug("Input queue at end of handle_keypress: %s", self.inputStack) self.__tryReleaseLock() def __tryReleaseLock(self): try: if self.configManager.lock.locked(): self.configManager.lock.release() except: logger.exception("Ignored locking error in handle_keypress") def run_folder(self, name): folder = None for f in self.configManager.allFolders: if f.title == name: folder = f if folder is None: raise Exception("No folder found with name '%s'" % name) self.app.show_popup_menu([folder]) def run_phrase(self, name): phrase = self.__findItem(name, autokey.model.phrase.Phrase, "phrase") self.phraseRunner.execute(phrase) def run_script(self, name): path = pathlib.Path(name) path = path.expanduser() # Check if absolute path. if pathlib.PurePath(path).is_absolute() and path.exists(): self.scriptRunner.execute_path(path) else: script = self.__findItem(name, autokey.model.script.Script, "script") self.scriptRunner.execute_script(script) def __findItem(self, name, objType, typeDescription): for item in self.configManager.allItems: if item.description == name and isinstance(item, objType): return item raise Exception("No %s found with name '%s'" % (typeDescription, name)) @threaded def item_selected(self, item): time.sleep(0.25) # wait for window to be active self.lastMenu = None # if an item has been selected, the menu has been hidden self.__processItem(item, self.lastStackState) def calculate_extra_keys(self, buffer): """ Determine extra keys pressed since the given buffer was built """ extraBs = len(self.inputStack) - len(buffer) if extraBs > 0: extraKeys = ''.join(self.inputStack[len(buffer)]) else: extraBs = 0 extraKeys = '' return extraBs, extraKeys def __updateStack(self, key): """ Update the input stack in non-hotkey mode, and determine if anything further is needed. @return: True if further action is needed """ #if self.lastMenu is not None: # if not ConfigManager.SETTINGS[MENU_TAKES_FOCUS]: # self.app.hide_menu() # # self.lastMenu = None if key == Key.ENTER: # Special case - map Enter to \n key = '\n' if key == Key.TAB: # Special case - map Tab to \t key = '\t' if key == Key.BACKSPACE: if ConfigManager.SETTINGS[cm_constants.UNDO_USING_BACKSPACE] and self.phraseRunner.can_undo(): self.phraseRunner.undo_expansion() else: # handle backspace by dropping the last saved character try: self.inputStack.pop() except IndexError: # in case self.inputStack is empty pass return False elif len(key) > 1: # non-simple key self.inputStack.clear() self.phraseRunner.clear_last() return False else: # Key is a character self.phraseRunner.clear_last() # if len(self.inputStack) == MAX_STACK_LENGTH, front items will removed for appending new items. self.inputStack.append(key) return True def __checkTextMatches(self, folders, items, buffer, windowInfo, immediate=False): """ Check for an abbreviation/predictive match among the given folder and items (scripts, phrases). @return: a tuple possibly containing an item to execute, or a menu to show """ itemMatches = [] folderMatches = [] for item in items: if item.check_input(buffer, windowInfo): if not item.prompt and immediate: return item, None else: itemMatches.append(item) for folder in folders: if folder.check_input(buffer, windowInfo): folderMatches.append(folder) break # There should never be more than one folder match anyway if self.__menuRequired(folderMatches, itemMatches, buffer): self.lastStackState = buffer #return (None, PopupMenu(self, folderMatches, itemMatches)) return None, (folderMatches, itemMatches) elif len(itemMatches) == 1: self.lastStackState = buffer return itemMatches[0], None else: return None, None def __shouldProcess(self, windowInfo): """ Return a boolean indicating whether we should take any action on the keypress """ return windowInfo[0] != "Set Abbreviations" and self.is_running() def __processItem(self, item, buffer=''): self.inputStack.clear() self.lastStackState = '' if isinstance(item, autokey.model.phrase.Phrase): self.phraseRunner.execute(item, buffer) else: self.scriptRunner.execute_script(item, buffer) def __haveMatch(self, data): folder_match, item_matches = data if folder_match is not None: return True if len(item_matches) > 0: return True return False def __menuRequired(self, folders, items, buffer): """ @return: a boolean indicating whether a menu is needed to allow the user to choose """ if len(folders) > 0: # Folders always need a menu return True if len(items) == 1: return items[0].should_prompt(buffer) elif len(items) > 1: # More than one 'item' (phrase/script) needs a menu return True return False class PhraseRunner: def __init__(self, service: Service): self.service = service self.macroManager = MacroManager(service.scriptRunner.engine) self.lastExpansion = None self.lastPhrase = None self.lastBuffer = None self.contains_special_keys = False @threaded #@synchronized(iomediator.SEND_LOCK) def execute(self, phrase: autokey.model.phrase.Phrase, buffer=''): mediator = self.service.mediator # type: IoMediator mediator.interface.begin_send() try: expansion = phrase.build_phrase(buffer) expansion.string = \ self.macroManager.process_expansion_macros(expansion.string) self.contains_special_keys = self.phrase_contains_special_keys(expansion) mediator.send_backspace(expansion.backspaces) if phrase.sendMode == autokey.model.phrase.SendMode.KEYBOARD: mediator.send_string(expansion.string) else: mediator.paste_string(expansion.string, phrase.sendMode) self.lastExpansion = expansion self.lastPhrase = phrase self.lastBuffer = buffer finally: mediator.interface.finish_send() def can_undo(self): can_undo = self.lastExpansion is not None and not self.phrase_contains_special_keys(self.lastExpansion) logger.debug("Undoing last phrase expansion requested. Can undo last expansion: {}".format(can_undo)) return can_undo @staticmethod def phrase_contains_special_keys(expansion: autokey.model.phrase.Expansion) -> bool: """ Determine if the expansion contains any special keys, including those resulting from any processed macros (