pax_global_header00006660000000000000000000000064136223235050014513gustar00rootroot0000000000000052 comment=c0c2e2939f2352a1d3d83c8edafc9007848d6a1e autokey-0.95.10/000077500000000000000000000000001362232350500133505ustar00rootroot00000000000000autokey-0.95.10/.github/000077500000000000000000000000001362232350500147105ustar00rootroot00000000000000autokey-0.95.10/.github/ISSUE_TEMPLATE.md000066400000000000000000000021231362232350500174130ustar00rootroot00000000000000## Classification: (Pick one of: Bug, Crash/Hang/Data Loss, Performance, UI/Usability, Feature (New), Enhancement) ## Reproducibility: (Pick one of: Always, Sometimes, Rarely, Unable, I Didn't Try) ## Version AutoKey version: Used GUI (Gtk, Qt, or both): If the problem is known to be present in more than one version, please list all of those. Installed via: (PPA, pip3, …). Linux Distribution: ## Summary Summary of the problem. ## Steps to Reproduce (if applicable) - I do this - I do that ## Expected Results - This should happen. ## Actual Results - Instead, this happens. :( If helpful, submit screenshots of the issue to help debug.\ Debugging output, obtained by launching autokey via `autokey-gtk --verbose` (or `autokey-qt --verbose`, if you use the Qt interface) is also useful.\ Please upload the log somewhere accessible or put the output into a code block (enclose in triple backticks). ``` Example code block. Replace this whith your log content. ``` ## Notes Describe any debugging steps you've taken yourself. If you've found a workaround, please provide it here. autokey-0.95.10/.gitignore000066400000000000000000000001071362232350500153360ustar00rootroot00000000000000*.html *.bak *.swp __pycache__ *.egg-info *.pyc # IDE's .idea .vscode autokey-0.95.10/ACKNOWLEDGMENTS000066400000000000000000000042041362232350500155200ustar00rootroot00000000000000Thanks 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.95.10/CHANGELOG.rst000066400000000000000000000511701362232350500153750ustar00rootroot00000000000000========= Changelog ========= .. contents:: Version 0.95.10 <2019-02-16> ============================ Bug fixes --------- - Mitigate crashes when entering invalid Python regular expressions in the window filter dialogue. Fixes issue #212 - Added option to disable the handling of the Capslock modifier key. Fixes issues when that key is remapped to something else, for example Ctrl. The new option can be found in the settings dialogue. Fixes issues #95, #291 - API function `system.exec_command()` now only trims the last character in the output, if it is actually a newline character. If the executed command does not output a newline at the end, the full output is returned. Fixes issue #354 - Fixed wrong optional argument in man page for `autokey-run`. Fixed by pull request #361 - Removed unnecessarily set executable bit from several AutoKey SVG icons. Fixed by pull request #363 Version 0.95.9 <2019-12-07> =========================== Bug fixes --------- - Prevent data losses when deleting or moving directories from within AutoKey. AutoKey will only delete data it knows and keep unknown user data. So adding $HOME and then removing it again will not purge everything below it. Affected were deleting directories and moving them via drag & drop. Fixes issues #171, #332 Version 0.95.8 <2019-11-07> =========================== Bug fixes --------- - Qt GUI: Fix issue with Python 3.7.4 and PyQt 5.11-5.13.0 that prevented AutoKey from starting on certain distributions shipping this configuration, notably Kubuntu 19.10. Fixes issues #313, #301 - Qt GUI: Fix crash when saving the currently edited item, after deselecting it in the tree view. Fixes issue #285 - Qt GUI: Disable Main window -> Tools -> Insert Macro when not editing a Phrase. Fixes issue #276 - Qt GUI: Add a warning that explains possible data loss when creating top level directories at used specified locations. See issue #171 - GTK GUI: Fix application hang when setting a custom value for "Trigger on" in the Abbreviation settings dialogue. Fixes issue #315 Version 0.95.7 <2019-04-29> =========================== Bug fixes --------- - GTK GUI: Fixed system tray icon context menu entry :code:`View script error`, which was non-functional, if the main window is closed. The entry now opens the main window first as a workaround, because a proper fix will require a major code overhaul. Fixes #222 - Qt GUI: Fixed the truncated GPLv3 license text shown in the About AutoKey dialogue. The dialogue window now shows the full license text. Fixes #258 - Hardened the logic to read application window titles. AutoKey now works, if applications do not set the :code:`_NET_WM_VISIBLE_NAME` property of their windows. Fixes #257 - Fixed Phrase expansion using the Keyboard method, which was broken if AutoKey was started for the first time. Fixes #274 Other fixes ----------- - Improved the debug logging output: Removed unnecessary output, clarified wordings, etc. See #230 - Qt GUI: Display the current Python version number in the About dialogue. Version 0.95.6 <2019-02-09> =========================== Bug fixes --------- - Fix the combination of phrase settings :code:`Match phrase case to typed abbreviation` and :code:`Trigger immediately` to cause Scripts and Phrases to trigger on each and every key press. Fixes issue #254 introduced in 0.95.5. Version 0.95.5 <2019-02-07> =========================== Bug fixes --------- - Fix window filter detection always returning Title: :code:`FocusProxy`, Class: `Focus-Proxy-Window.FocusProxy` on Java AWT applications. It now detects the proper window title and WM_CLASS attribute for Java AWT applications. Fixes issue #113 - GTK GUI: Fix the window filter detection dialogue. On clicking OK, it hung the whole application. Now the dialogue window works as intended. Fixes issue #229 - Fix abbreviation case folding (ignore case option) with abbreviations defined as UPPER CASE in the abbreviation dialogue. Options :code:`Ignore case` and :code:`Match case` now work with upper case abbreviations. Fixes issue #197 - Prevent the keyboard from staying grabbed by AutoKey if exceptions are thrown while AutoKey performs a clipboard pasting action. Fixes issues #72, #225 - Prevent writing :code:`None` to the clipboard. This prevents autokey-gtk from deadlocking, caused by an unreleased mutex. Fixes issue #226 - Restrict Phrase Undo functionality to phrases without special keys, because phrases containing special keys cannot be reliably undone. Fixes issue #196 - Clarified autosave option wording in the settings window. The option now explicitly states what it does. Fixes issue #194 - Force AutoKey to exit, if the X server connection closes, most probably at logout or session end. Fixes issue #198 Qt tray icon fixes and improvements +++++++++++++++++++++++++++++++++++ - Added »View script error« entry to the Tray icon context menu, like in the GTK GUI. Part of issue #158 - Tray icon turns red, when scripts raise an error, like in the GTK GUI. Part of issue #158 - If changing the tray icon theme in the settings (light or dark), instantly apply the new theme, without requiring an application restart. Part of issue #158 - The tray icon now works, after if it is disabled in the settings and then enabled again. Fixes issue #223 Other fixes ----------- - Enable :code:`setup.py` to be directly called from the system shell. Fixes issue #218 - Cleaned up some legacy leftovers in the autokey repository Version 0.95.4 <2018-10-14> =========================== Bug fixes --------- - Fix grabbed hotkeys being incorrectly received by other applications. - Fixed crashes when processing `` literals in strings. It is now possible to place `` and `` literals in Phrases. Additionally, such literals can be typed in scripts using the keyboard.send_keys function. - Increased the reliability of the window filter detection dialog in autokey-qt. The dialog allows sampling windows to aid writing window filters. Due to timing issues in certain cases, sometimes the window title of the previously active window was returned. Version 0.95.3 <2018-08-21> =========================== Features -------- - Phrase expansion can now always be undone using the backspace key, if the feature is enabled in the settings. Previously it was only be possible if the phrase was triggered by an abbreviation. Now it also works when using hotkeys or selecting phrases from menus. This also prevents crashes in `certain cases`_. - Qt GUI: Add support for automatically starting `autokey-qt` during login. It can be configured in the settings dialogue. The configuration option allows to choose which GUI is automatically started, if both `autokey-qt` and `autokey-gtk` are installed simultaneously, and whether the main window should be shown automatically on launch. - Qt GUI: Added the notification icon theme selection to the settings dialogue. The added section in the general settings allow to choose between the light and dark theme, like in the `autokey-gtk` settings dialogue. Changing this setting currently requires an application restart to take effect. Bug fixes --------- - Scripting API: The Python `__file__` global variable is now properly set for AutoKey scripts. It contains the full path to the Python script file currently running. Previously, it contained the full path to the `service.py` file of the currently running AutoKey instance. - Crash fix: Skip import of the AT-SPI interface, if importing of `pyatspi` fails with a SyntaxError. This may happen with certain versions of `pyatspi` on Python 3.7. For details see `#173`_ - Fix serializing the store during saving, if user stores recursive data structures. It now handles/skips lists that contain themselves or other circular referenced data structures. - GTK GUI: Fix autostart handling: Create the `$XDG_CONFIG_HOME/autostart` (`~/.config/autostart`) directory, if it is not already present. Fixes `#149`_ - Qt GUI: Create the user data directories before initializing the logger system. This prevents crashes when autokey-qt is used for the first time or when the user wiped all previous data. Fixes `#170`_ - Qt GUI: Fix saving the "Always prompt before running this script" checkbox content when editing scripts. This option now works as intended again. Packaging --------- - Stop shipping the `autokey.png` icon file inside a `scalable` icon theme directory. Moved to the appropriate raster image directory. - Corrected broken dependency package name in setup.py. The library is called `python-xlib` and not `python3-xlib` on PyPI. .. _certain cases: https://github.com/autokey/autokey/issues/164 .. _`#173`: https://github.com/autokey/autokey/issues/173 .. _`#149`: https://github.com/autokey/autokey/issues/149 .. _`#170`: https://github.com/autokey/autokey/issues/170 Version 0.95.2 <2018-07-16> =========================== - Fix broken imports in autokey-shell script - Skip non-json-serializable data in script storage (both script local and global) during saving. This allows putting non-serializable items (like function objects) into the store without crashing autokey during saving. - Qt GUI: Fix minor bug when creating new items. Created items are now properly selected for renaming directly after creation. - Minor code simplifications. Removed unnecessary functions that were obsoleted during prior changes. Version 0.95.1 <2018-06-30> =========================== This is a small bug fixing release. - Fix a long standing bug that errors occurring during phrase parsing or script execution can lock up the user keyboard. Make sure to always release the keyboard after grabbing it. See `#72`_, launchpad_1551054_ - Qt GUI: Fix saving the content of the log view to a file using the context menu entry. - Some small, internal code quality improvements. .. _`#72`: https://github.com/autokey/autokey/issues/72 .. _launchpad_1551054: https://bugs.launchpad.net/ubuntu/+source/autokey/+bug/1551054 Version 0.95.0 <2018-06-28> =========================== Rewritten the Qt GUI, ported to PyQt5 ------------------------------------- Resurrected, re-written and cleaned up the `autokey-qt` Qt GUI. `autokey-qt` is now a pure `PyQt5` application, only dependent on currently supported libraries. Added improvements ++++++++++++++++++ - The main window now keeps its complete state when closed and re-opened (excluding complete application restarts). This includes the currently selected item(s) in the tree view on the left of the main window, selected text and cursor position in the editor on the right if currently editing a script or phrase. - The entries in the popup menu, that is shown when a hotkey assigned to a folder is pressed, now show icons based on their type (folder, phrase or script). This also works when items are configured to be shown in the system tray icon context menu. - The *A* autokey application icons are now always displayed correctly, both in the main window and the system tray icon. - Various menu actions now have system dependent keyboard shortcuts, that should adjust to the expected default of the user’s current platform/desktop environment. - Added icons and descriptive tooltip texts to various buttons. - The `enable monitoring` checkboxes (both in the `Settings` menu and the tray icon context menu) now properly react to pressing the global hotkey for this action and thus stay in sync. (Even if the hotkey is used while the menu is shown.) Regressions +++++++++++ - Customizing the main window toolbar entries and keyboard shortcuts to trigger various UI actions is no longer possible. This feature was provided by the KDE4 libraries and is currently dropped. - The previous, KDE4-based About dialogue is replaced with a very minimalistic one. - The settings dialogue heavily used the KDE4 functionalities. During the port to Qt5, the dialogue lost some visual style, but all core functionality is kept. Runtime dependencies ++++++++++++++++++++ - Removed dependencies on deprecated and unmaintained PyQt4 and PyKDE4 libraries. - Removed dependency on `dbus.mainloop.qt`, instead use the DBus support built into Qt5. - Now depend on PyQt5, the Qt5 SVG module and the Qt5 QScintilla2 module. Build-time dependencies +++++++++++++++++++++++ Optionally depend on `pyrcc5` command line tool to compile Qt resources into a Python module. Qt UI files are no longer compiled using `pykdeuic4`, Removed the old compiler wrapper script in commit 6eeeb92f_. .. _6eeeb92f: https://github.com/autokey/autokey/commit/6eeeb92f14c694979c1367d51350c1e6509329b1 Known bugs ++++++++++ The system tray icon is shown, but non-functional, after enabling it in the settings dialogue. AutoKey Qt has to be restarted for the tray icon to start working. This should have no impact on the normal daily use. Changed features ++++++++++++++++ The `hide tray icon` entry in the tray icon context menu now hides the icon for the current session only. The entry does not permanently disable the tray icon any more without any confirmation. Now, the only way to permanently disable the tray icon is through using the appropriate setting in the settings dialogue. Fixed the broken `Clipboard` and `Mouse selection` phrase paste modes --------------------------------------------------------------------- - Pasting using both `Clipboard` and `Mouse selection` works in both the Qt and GTK GUI. See `#101`_ - Fixed restoring the clipboard after a paste is performed. Both GUIs now restore the previous clipboard content, after a phrase is pasted. .. _`#101`: https://github.com/autokey/autokey/issues/101 Scripting API Changes --------------------- Additions +++++++++ - Added a colour picker dialogue to the GTK dialog class, because the used `zenity` now supports it. - The picked colour is returned as three integers using the ColourData NamedTuple, providing both index based access and attribute access, using the channel names (`r`, `g`, `b`). Additionally, ColourData provides some conversion methods. Breaking changes ++++++++++++++++ - See Pull request `#148`_. The `dialog` classes for user input in scripts now return typed NamedTuple tuples instead of plain tuples. This change is safe as long as users do not perform needlessly restrictive type checks in their scripts (e.g. `if type(returned_data) == type(tuple()): ...`). User scripts doing so will break. - The KDialog based colour picker now also returns a ColourData instance instead of a HTML style hex string, thus making this portable between both GTK and Qt GUIs. AutoKey users previously using the old KDE GUI and using the colour picker dialogue have to port their scripts. A simple fix is using the `html_code` property of the returned ColourData instance. .. _`#148`: https://github.com/autokey/autokey/pull/148 Fixes +++++ - Re-introduce the newline trimming for system.exec_command() function. During the porting to Python 3, the newline trimming was removed, causing users various issues with unexpected newline characters at end of output. Now properly remove the _last_ newline at end of command output. (See issues `#75`_, `#92`_, `#145`_) - Applied various code style improvements to the scripting module. .. _`#75`: https://github.com/autokey/autokey/issues/75 .. _`#92`: https://github.com/autokey/autokey/issues/92 .. _`#145`: https://github.com/autokey/autokey/issues/145 Other fixes and improvements ---------------------------- - Fix the KDialog based colour picker provided in the scripting API. Newer versions of KDialog require an additional parameter, which is added now. - Fixed crashes related to mouse pasting when using the GTK GUI. - Both `autokey-gtk` and `autokey-qt` are now automatically generated setuptools entry-points. - `autokey-gtk` can now be launched directly from the autokey source tree. From the shell, `cd` into the `lib` directory, then use .. code-block:: sh /lib$ python3 -m autokey.gtkui [-l] [-c] # Or alternatively, to launch autokey-qt use: /lib$ python3 -m autokey.qtui [-l] [-c] - Various internal code style improvements at various locations, like added type hints, PEP8 style fixes, etc. Version 0.94.0 <2018-05-12> =========================== - Various README updates - Ported autokey-run from the legacy optparse module to the new Python 3 argparse module - Use $XDG_RUNTIME_DIR and $XDG_DATA_HOME directories for lock and log file - Added support for function keys F13 to F35 - Refactored the iomediator modules into a package. Applied various code cleanups and fixes. Version 0.93.10 <2017-02-17> ============================ - The scripting global storage now returns None if the requested key is not present. - Improved the error messages in autokey-run. It is now clear that autokey has to run in the background for autokey-run to work. - Added a LICENSE file containing the GPL v3 license terms. Version 0.93.9 <2017-01-11> =========================== Fixed a regression with setup.py install_requires keyword argument. Updated the GitHub issue template. Version 0.93.8 <2017-01-09> =========================== - Readme updates - Depend on Ubuntu appindicator - Leverage libappindicator completely, fix "View script error" Version 0.93.7 <2016-12-21> =========================== This release contains various bug/crash fixes - Renamed repository from autokey-py3 to autokey - Moved the AutoKey source code out of src folder one level up. - Removed donate button - autokey-gtk script is now a setuptools generated entry point - Require GTK 3.0 to fix autokey-gtk startup - Updated various web links around the codebase - New feature: Return the result of wait events in the iomediator module. Version 0.93.6 <2016-08-13> =========================== - Ensure Compatibility with official python-xlib - Fixed several GTK related warnings - GTK GUI: Add feature to trigger popupmenu items with letters, rather than numbers. - Add an AUR link Version 0.93.4 <2015-02-17> =========================== Bugfix: Prevent clipboard related crashes with GTK3. Version 0.93.3 <2015-02-20> =========================== Bugfix for defining abbreviations by `@kuhanalog`_ .. _@kuhanalog: https://github.com/kuhanalog Version 0.93.2 <2014-08-09> =========================== Read user scripts with UTF-8 encoding. Version 0.93.1 <2014-03-02> =========================== Internal changes: Changed the data structure of the input stack. Version 0.93.0 <2014-02-27 Thu> =============================== Added functions “acknowledge_gnome_notification” and “move_to_pat”, more details `here`_. .. _here: https://github.com/autokey/autokey/blob/master/new_features.rst Version 0.92.0 <2014-02-21 Fri> =============================== Added an interactive shell launcher, “autokey-shell”. “autokey-shell” allows you to run some AutoKey functions interactively. Read `this`_ for more details. Version 0.91.0 <2014-02-14 Fri> =============================== Added a new function “click_on_pat” for use in user scripts. See `this`_ for more details. .. _this: https://github.com/autokey/autokey/blob/master/new_features.rst First release <2014-01-31 Fri> ============================== This describes some of the changes to the original AutoKey source code. Python 3 related changes ------------------------ Python 3 is less tolerant of circular imports so some files were split into several files. Those pieces of the original have their file names prefixed with the original's. Bug fixes +++++++++ Eliminate possible deadlock. Changed .. code-block:: python p = subprocess.Popen([…], stdout=subprocess.PIPE) retCode = p.wait() output = p.stdout.read()[:-1] # Drop trailing newline to .. code:: python p = subprocess.Popen([…], stdout=subprocess.PIPE) output = p.communicate()[0].decode()[:-1] # Drop trailing newline retCode = p.returncode The former may cause a deadlock, for more information, see `Python docs`_. This pattern appears several times in the source codes. .. _Python docs: http://docs.python.org/3/library/subprocess.html#subprocess.Popen.wait For a “gi.repository.Notify.Notification” object, test if method “attach_to_status_icon” exists before calling. After this fix, errors in user scripts will trigger a notification. Respect XDG standard. Details `here`__. __ https://code.google.com/p/autokey/issues/detail?id=266 Corrected a typo in manpage of autokey-run. For the GTK GUI, after script error is viewed, tray icon is reverted back to original. Other changes +++++++++++++ In setup.py, the “/usr/” prefix to the directory names in the data_files argument were removed to allow for non-root install. Removed the “WINDOWID” environment variable so that zenity is not tied to the window from which it was launched. Modified the launcher and other files to allow for editable installs (“pip install -e”). Added an “about” dialog for the Python 3 port. Changed hyperlink for bug reports. autokey-0.95.10/INSTALL000066400000000000000000000010451362232350500144010ustar00rootroot00000000000000$Id$ The full application can be installed using the setup script: python3 setup.py install Alternatively, you can build Debian packages using the following command: dpkg-buildpackage -us -uc # The built packages are placed in the parent directory cd ../ Then to install the GTK version: sudo dpkg --install autokey-gtk_.deb autokey-common_.deb Or the Qt version: sudo dpkg --install autokey-qt_.deb autokey-common_.deb After dpkg finished, run this to install any missing dependencies: sudo apt install -f autokey-0.95.10/LICENSE000066400000000000000000001045051362232350500143620ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. 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. 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. 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. 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. 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. 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. 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. 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. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "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. 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. A "covered work" means either the unmodified Program or a work based on the Program. 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. 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. 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. 1. Source Code. 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. 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. 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. 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. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. 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. 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. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 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. 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. 4. Conveying Verbatim Copies. 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. 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. 5. Conveying Modified Source Versions. 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: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. 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". 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. 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. 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. 6. Conveying Non-Source Forms. 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: 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. 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. 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. 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. 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. 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. 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. "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. 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). 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. 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. 7. Additional Terms. "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. 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. 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: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or 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 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 d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or 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. 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. 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. 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. 8. Termination. 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). 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. 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. 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. 9. Acceptance Not Required for Having Copies. 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. 10. Automatic Licensing of Downstream Recipients. 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. 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. 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. 11. Patents. 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". 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. 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. 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. 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. 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. 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. 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. 12. No Surrender of Others' Freedom. 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. 13. Use with the GNU Affero General Public License. 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. 14. Revised Versions of this License. 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. 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. 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. 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. 15. Disclaimer of Warranty. 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. 16. Limitation of Liability. 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. 17. Interpretation of Sections 15 and 16. 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. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs 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. 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. {one line to give the program's name and a brief idea of what it does.} Copyright (C) {year} {name of author} 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 . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: {project} Copyright (C) {year} {fullname} This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. 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". 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 . 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 . autokey-0.95.10/MANIFEST.in000066400000000000000000000000171362232350500151040ustar00rootroot00000000000000include INSTALLautokey-0.95.10/PKG-INFO000066400000000000000000000010721362232350500144450ustar00rootroot00000000000000Metadata-Version: 1.0 Name: autokey Version: 0.90.4 Summary: Desktop automation utility Home-page: http://autokey.googlecode.com/ Author: Chris Dekter Author-email: cdekter@gmail.com License: GPL v3 Description: AutoKey is a desktop automation utility for Linux and X11. It allows the automation of virtually any task by responding to typed abbreviations and hotkeys. It offers a full-featured GUI that makes it highly accessible for novices, as well as a scripting interface offering the full flexibility and power of the Python language. Platform: UNKNOWN autokey-0.95.10/README.rst000066400000000000000000000077411362232350500150500ustar00rootroot00000000000000======= AutoKey ======= .. image:: https://img.shields.io/badge/IRC-%23autokey%20on%20freenode-blue.svg :target: https://webchat.freenode.net/?channels=autokey .. image:: https://badges.gitter.im/autokey/autokey.svg :alt: Join the chat at https://gitter.im/autokey/autokey :target: https://gitter.im/autokey/autokey .. image:: http://img.shields.io/badge/stackoverflow-autokey-blue.svg :alt: Ask and answer questions on StackOverflow :target: https://stackoverflow.com/questions/tagged/autokey .. contents:: About ===== `AutoKey`_, a desktop automation utility for Linux and X11, formerly hosted at `OldAutoKey`_. Updated to run on Python 3. **Important**: This is an X11 application, and as such will not function 100% on distributions that default to using Wayland instead of Xorg. .. _AutoKey: https://github.com/autokey/autokey .. _OldAutoKey: https://code.google.com/archive/p/autokey/ Installation ============ **Please remove previous installations of both AutoKey and AutoKey-py3 fully before installing!** For detailed installation instructions, please visit the `Installation`_ page. in our wiki. .. _Installation: https://github.com/autokey/autokey/wiki/Installing Zero-installation Method ++++++++++++++++++++++++ AutoKey can also be used directly from the cloned repository. This is useful, e.g., for trying out a new version without removing a current installation. 1. Start the Autokey daemon .. code:: sh cd lib python3 -m autokey.gtkui # or for KDE python3 -m autokey.qtui 2. Start the Autokey UI (if desired) by appending the --configure or -c command line switch to the end of the command. The commands accept CLI switches just like the regular installation, so :code:`python3 -m autokey.qtui -lc` works as expected. Documentation ============= Documentation for `new features`_. For older features, please refer to the original AutoKey's `scripting API`_, `wiki`_, and `Stack Overflow`_. Examples of AutoKey scripts can be found by `searching GitHub`_ and reading AutoKey's `wiki`_. .. _scripting API: https://autokey.github.io/index.html .. _searching GitHub: https://github.com/search?l=Python&q=autokey&ref=cmdform&type=Repositories .. _wiki: https://github.com/autokey/autokey/wiki .. _Stack Overflow: https://stackoverflow.com/questions/tagged/autokey .. _new features: https://github.com/autokey/autokey/blob/master/new_features.rst Support ======= Please do not request support on the issue tracker. Instead, head over to the autokey-users `Google Groups`_ forum, `StackOverflow`_, on `IRC`_ (#autokey on Freenode), or `Gitter`_ web-based chat. We'd appreciate it if you take a look at `Problem reporting guide`_ before posting. By providing as much information as you can, you'll have a much better chance of getting a good answer in less time. .. _Google Groups: https://groups.google.com/forum/#!forum/autokey-users .. _StackOverflow: https://stackoverflow.com/questions/tagged/autokey .. _IRC: irc://irc.freenode.net/#autokey .. _Gitter: https://gitter.im/autokey/autokey .. _Problem reporting guide: https://github.com/autokey/autokey/wiki/Problem-Reporting-Guide Bug reports and Pull Requests ============================= Bug reports and PRs are welcome. Please use the `GitHub Issue Tracker`_ for bug reports. When reporting a suspected bug, please test against latest ``git HEAD`` and make sure to include as much information as possible to expedite troubleshooting and resolution. For example, * **required:** How to reproduce the issue you are experiencing * Python tracebacks, if any * Verbose logging information obtained by starting the frontend (``autokey-gtk`` or ``autokey-qt``) from terminal with the ``--verbose`` option. .. _GitHub Issue Tracker: https://github.com/autokey/autokey/issues Changelog ========= Here__. __ https://github.com/autokey/autokey/blob/master/CHANGELOG.rst License ======= `GNU GPL v3.`_ See the LICENSE file alongside this README for a plain text copy of the license text. .. _GNU GPL v3.: https://www.gnu.org/licenses/gpl.html autokey-0.95.10/autokey-run000066400000000000000000000055601362232350500155640ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # -*- mode: python -*- # Copyright (C) 2011 Chris Dekter # Copyright (C) 2017 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 import dbus import argparse def generate_argument_parser(): description = "Run an AutoKey function by sending a command to a running AutoKey instance over dbus." argument_parser = argparse.ArgumentParser(description=description) mode_group = argument_parser.add_mutually_exclusive_group(required=True) mode_group.add_argument("-s", "--script", action="store_const", const="script", dest="mode", help="Run a script") mode_group.add_argument("-p", "--phrase", action="store_const", const="phrase", dest="mode", help="Paste a phrase") mode_group.add_argument("-f", "--folder", action="store_const", const="folder", dest="mode", help="Show a folder popup menu") argument_parser.add_argument("name", help="The name of the script, phrase or folder.") return argument_parser if __name__ == "__main__": parser = generate_argument_parser() arguments = parser.parse_args() bus = dbus.SessionBus() try: dbus_service = bus.get_object("org.autokey.Service", "/AppService") except dbus.DBusException as e: print("AutoKey must be running to use autokey-run.\n", file=sys.stderr) print(str(e), file=sys.stderr) sys.exit(1) if arguments.mode == "script": dbus_function = dbus_service.run_script elif arguments.mode == "phrase": dbus_function = dbus_service.run_phrase elif arguments.mode == "folder": dbus_function = dbus_service.run_folder else: print("BUG: Unknown run mode encountered: {}".format(arguments.mode), file=sys.stderr) sys.exit(1) try: dbus_function(arguments.name, dbus_interface="org.autokey.Service") except dbus.DBusException as e: print(e.get_dbus_message(), file=sys.stderr) sys.exit(1) autokey-0.95.10/autokey-shell000077500000000000000000000020201362232350500160560ustar00rootroot00000000000000#! /usr/bin/env python3 # -*- coding: utf-8 -*- # -*- mode: python -*- import distutils.spawn import subprocess import sys start_script = ''' import autokey.iomediator import autokey.configmanager import autokey.scripting import autokey.scripting_highlevel as hl system = autokey.scripting.System() print(""" AutoKey functions from “autokey.scripting_highlevel” (under name “hl”) and “autokey.scripting.System()” (under name “system”) are imported. You can try the functions on the interpreter. For example: help(hl.click_on_pat) print(system.exec_command('ls')) """) ''' ipython3_cmd = distutils.spawn.find_executable('ipython3') if ipython3_cmd is not None: retcode = subprocess.call([ipython3_cmd, '-ic', start_script]) raise SystemExit(retcode) python3_path = distutils.spawn.find_executable('python3') if python3_path is not None: retcode = subprocess.call([python3_path, '-ic', start_script]) raise SystemExit(retcode) print('\033[91m' + 'Error! No Python 3 shell found.' + '\033[0m') sys.exit(1) autokey-0.95.10/autokey.spec000066400000000000000000000114641362232350500157130ustar00rootroot00000000000000# # This file and all modifications and additions to the pristine # package are under the same license as the package itself. # #define python macros for openSUSE < 112 %{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")} %{!?python_sitearch: %global python_sitearch %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")} Name: autokey Version: 0.94.1 Release: 1 License: GPLv3 Summary: Desktop automation utility Url: http://github.com/autokey/autokey Group: System/X11/Utilities Source: %{name}_%{version}.tar.gz BuildRequires: python-base BuildRequires: update-desktop-files BuildRoot: %{_tmppath}/%{name}-%{version}-build %if 0%{?suse_version} > 1110 # This works on newer version # on older version it dies misserably if used BuildArch: noarch %endif %description AutoKey is a desktop automation utility for Linux and X11. It allows the automation of virtually any task by responding to typed abbreviations and hotkeys. It offers a full-featured GUI that makes it highly accessible for novices, as well as a scripting interface offering the full flexibility and power of the Python language. %package common License: GPLv3 Summary: Desktop automation utility -- Common Files Group: System/X11/Utilities Requires: python-simplejson Requires: python-pyinotify Requires: python-xlib Requires: wmctrl Recommends: %{name}-gtk %py_requires %description common AutoKey is a desktop automation utility for Linux and X11. It allows the automation of virtually any task by responding to typed abbreviations and hotkeys. It offers a full-featured GUI that makes it highly accessible for novices, as well as a scripting interface offering the full flexibility and power of the Python language. %package gtk License: GPLv3 Summary: Desktop automation utility -- GTK+ Interface Group: System/X11/Utilities Requires: %{name}-common = %{version} Requires: python-gtk Requires: python-gtksourceview Requires: python-notify Requires: zenity %py_requires %description gtk AutoKey is a desktop automation utility for Linux and X11. It allows the automation of virtually any task by responding to typed abbreviations and hotkeys. It offers a full-featured GUI that makes it highly accessible for novices, as well as a scripting interface offering the full flexibility and power of the Python language. %package qt License: GPLv3 Summary: Desktop automation utility -- Qt Interface Group: System/X11/Utilities Requires: %{name}-common = %{version} Requires: python-qt4 Requires: python-qscintilla2 Requires: python-notify %py_requires %description qt AutoKey is a desktop automation utility for Linux and X11. It allows the automation of virtually any task by responding to typed abbreviations and hotkeys. It offers a full-featured GUI that makes it highly accessible for novices, as well as a scripting interface offering the full flexibility and power of the Python language. %prep %setup -n %{name}_%{version} find src/lib -name "*.py" -exec sed -i '/^#!\/usr\/bin\/env python$/d' {} ";" %build %{__python} setup.py build %install %{__python} setup.py install --prefix=%{_prefix} --root=%{buildroot} %suse_update_desktop_file autokey-gtk DesktopSettings %files common %defattr(-,root,root) %doc ACKNOWLEDGMENTS README %dir %{python_sitelib}/autokey %{python_sitelib}/autokey/*.py* %exclude %{python_sitelib}/autokey/gtkapp.py* %{python_sitelib}/%{name}-%{version}-py%{py_ver}.egg-info %{_datadir}/icons/hicolor/scalable/apps/autokey-status*.svg %{_datadir}/icons/hicolor/scalable/apps/autokey.png %{_datadir}/icons/Humanity/scalable/apps/*.svg %{_datadir}/icons/ubuntu-mono-dark/scalable/apps/*.svg %{_datadir}/icons/ubuntu-mono-light/scalable/apps/*.svg %{_bindir}/autokey-run %{_mandir}/man1/autokey-run.1* %files gtk %defattr(-,root,root) %{_bindir}/autokey-gtk %{python_sitelib}/autokey/gtkapp.py* %{python_sitelib}/autokey/gtkui/ %{_datadir}/applications/autokey-gtk.desktop %{_datadir}/icons/hicolor/scalable/apps/autokey.svg %{_mandir}/man1/autokey-gtk.1* %files qt %defattr(-,root,root) %{python_sitelib}/autokey/qtapp.py* %{python_sitelib}/autokey/qtui/ %{_datadir}/applications/autokey-qt.desktop %{_datadir}/icons/hicolor/scalable/apps/autokey.png %{_mandir}/man1/autokey-qt.1* %post common /bin/touch --no-create %{_datadir}/icons/hicolor &>/dev/null || : %postun common if [ $1 -eq 0 ] ; then /bin/touch --no-create %{_datadir}/icons/hicolor &>/dev/null /usr/bin/gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || : fi %posttrans common /usr/bin/gtk-update-icon-cache %{_datadir}/icons/hicolor &>/dev/null || : %changelog autokey-0.95.10/config/000077500000000000000000000000001362232350500146155ustar00rootroot00000000000000autokey-0.95.10/config/Humanity/000077500000000000000000000000001362232350500164135ustar00rootroot00000000000000autokey-0.95.10/config/Humanity/autokey-status-error.svg000066400000000000000000000051701362232350500232700ustar00rootroot00000000000000 image/svg+xml autokey-0.95.10/config/Humanity/autokey-status.svg000066400000000000000000000051701362232350500221410ustar00rootroot00000000000000 image/svg+xml autokey-0.95.10/config/autokey-gtk.desktop000066400000000000000000000003741362232350500204600ustar00rootroot00000000000000# $Id: autokey-gtk.desktop 22 2008-01-29 11:29:13Z peabody $ [Desktop Entry] Name=AutoKey GenericName=Keyboard Automation Comment=Program keyboard shortcuts Exec=autokey-gtk -c Terminal=false Type=Application Icon=autokey Categories=GNOME;GTK;Utility; autokey-0.95.10/config/autokey-qt.desktop000066400000000000000000000003651362232350500203170ustar00rootroot00000000000000# $Id: autokey-qt.desktop 22 2008-01-29 11:29:13Z peabody $ [Desktop Entry] Name=AutoKey (Qt) GenericName=Keyboard Automation Comment=Program keyboard shortcuts Exec=autokey-qt Terminal=false Type=Application Icon=autokey Categories=Qt;Utility; autokey-0.95.10/config/autokey-status-dark.svg000066400000000000000000000110571362232350500212630ustar00rootroot00000000000000 image/svg+xml autokey-0.95.10/config/autokey-status-error.svg000066400000000000000000000111131362232350500214640ustar00rootroot00000000000000 image/svg+xml autokey-0.95.10/config/autokey-status.svg000066400000000000000000000072741362232350500203520ustar00rootroot00000000000000 image/svg+xml autokey-0.95.10/config/autokey.png000066400000000000000000000173031362232350500170100ustar00rootroot00000000000000PNG  IHDR\[sBIT|d pHYs B(xtEXtSoftwarewww.inkscape.org<@IDATx][\Y>37*E%YeK۹*K(*.x@E[2JBBŐPeّ"˱ɒ%v^ffFz̙3t>=(&"LW*NJJl;@'.=bMx }OFījw+kW_x/~'lȈh @[ ;X85 '잏nfr ӛ֮~?ԟ< fnȈ_ѩuXm\lB~`dffp}g? *uPɀ>|⓳S,nama}gpL!o+;rlcmf'G>OU| xo0w훺%f?IUy=1U+n ?q x׀ѱNBq(--NWͮi}1V`R04<̮oWzyu_>~-\lfoK(螂uP-JHo.'t{dp{f&PRmk1:Y-< j򡯣 Z;B\JjA)v?oZHY~]*Q@Ғ2!L9zFNnV^=uD t ެƶ% Gj:wǻT; B!R(/w<7iEAw5/?*Dkҫh k6 2 `=P${$H d\$`G}XzJcaMF9W/-Wb."Wx57nl9:(@L151&.WQe(R+Nt; )ƀF@5x72 ɷ 52X]LKɉT<%ֲI&C{J $W%\YBkd#aLNZfiSPe[%i*^Ep '{VPwOIjH% gJNO>[3 &65ɀH[+ ԉ:]W`WUU|$kO1g J-rkxYŞl[nn()kӍ4ϼ˫ b0zYs E)pY*I䬦ˡJ)-˃-Y :`~S׾Lg& @h=)QGp(Z>'h ˔c 8j3Xj3yK_@Txc6Np[Nfӣ7a\-2 eo9bZW)e'.5qIԶ 84"D)^S^3&/9/_<=E`kksp 3֥AحM|Z Tdb(:>)XWY4FL>KumMBk VsNrM֚stO;F:[.M20X%$bkq%ڏ N Wz˓ 'MPBJ;ZW!oqw5=ٝpeu+z\$#D0Z8c hk4Fx Vi7y%Q,%ԓ~rC˝s^9-G}[`S?7 ބlvlv 7q feMp5_R@ܛsryV";tje2vs8m><<}贷ioöp6n`FNdqS,lKORu*_oc,&3m`ADDrYs0IjY6Q>\ YX IJO]p^EX6>rdKM ~ibl g25Y ; v*H ^+ZeJ),6H+u@'Ǹd988<w cJɧNI>LOb@S(;b[ 'T/cT:RlSsꤧ[9N_s8z]*666{ga pc Vel]RM9l1*7x1تsȲ&<}\r[c{S; f, rKW̃Ŧ>n& .:߹,w`E5|=r{-kkO77zh40s6-)1[I'2תN3я%yvR,$9;x+cܵoFex6Xig=}0/GIƁx'̩,z/T-(b4s|YwommSa,]ޣ>MO!3h$;Cd)IQYEWeY N RJ\uhptZV-pO<,p58j״{3VӤG3đ\s,ebT>xLe3:pkAt:x籱 Gξ}|)ybT$zmx\uebEyX9\JuhokUpglql׎̩o?>˯/vCCCm\|L|JA%rb+tDY sΜuݓq+oқWzSc]:0&AJClFDRJA:ޥu]bddgϽ}J([eJ-9ES Rp.T He[:tPJ\"<8Ãs ZmxiO=w33=s,˴gWkVB%ysJ{jdd(I>/|Ѕk@.4礩2t|4{byR ˯/|؊I|=zqaޕm(\ڭ"=8<|vڌB}LBOSUeb;x׾ W O?w/r +4@Yg{ׯ|ttUzEihQꋯ%[5fyL/SGB!Ժ1LE";S ]t? $ضpu,03=tzJEx^W4Ica 71orxIk}dPWN>sOySX1錩^m05>8nF#؇r_LHz5N( { eAT:]ڳ\ q9oIh%sZvN#3:8%c tC]uN,0wdjУYT*;;WU¢Nf#sSNI XPM>U*C<Գ#Aj dv ,|ppz$ sm /)B4Օ7# ]b!m;p߽-lSzzTQ13.^ $6ZxQŒouj:b[{r4rtL(^-~RfO:}s8HN+Jbia{bixN &+Ɲ|S£ ,k(yҨB84k;ᎃS8u, {МMek,8gw0oa$bd09 j1T"p* vᮚ |\K pҚi;njݧ}_G[vj.])WWXǁ9'B?<簨E*[=Y` pl.q_FYIH[/SakkCC'1og_)C&v>2ZGeG"Zpr<bwGܾub|}?v13cp"g kϣ ԭ{НK&x?8vWnf\z3`!<Ùv%WTDuA9'؄un9uv95VILs$CwW r}Js"pBN;T͝8mV{?;V?|Hwcaf"3c=88K![<+;ͨû-'!x:b=h6kq}o)0' B\fC*[ڜrN6']v3?oa6(4F '_֝Ǥ"U-qFq H:U`lna3,ĸ=s7?=9^P(J.(;oH\0H7:bdc9;k vtkSŃ:ҵ:=;ɱQXg9NT p8MRz dn Տ[{zX}#xʚ>70VMc~ "k5\sbYܱ7t['QJ_f,*,<94Ij38&s?r+FGwtg{]Q8Hx -"DLFq5ƑCp⹗%g}'yr#LbVϧ{!NOi!5_ލ2E|Cl?!CjˁY0Y71vap+ 8l:ŎOE,E0,~zǞ|Ϝ})։=MFCiq&d ›K+v}dE<} Et!p2%^1z󥹅oNMyw 1]=8;0L9'eI$Y,&zI7Y淕vbe: "<X[[XOg0l`裴/8CQQ/$uTT*!ry3ص:3xg -#I~P4ǂ#oP]^o{8 ߽p5WϰEP)b<~a kR8iZ/)>XݷNcݭƍ {:j"so;.MK0Nw$rf869~CcMoG ;cGOi8Y;&d ޓcs2!q)gdX1M&>:eJ^=g 'ǼZ\ܠgZis3{.tؓN1$a~e֬d~HL@,6ťt,Jex)}0+97r4+Ti9Q&%/܁n9 M(vn&Fjr$\ 2nR8j*kQ+a[cE&凝JtũE&h$I Rze@I2AM;y=՗LC D)Pi)Zi}5UҊ& ŒMy&4P 8c+n9IkA0lP}3DPFt _x&S ^p02Qֶ,79j5_(HhDAbݩWOLr=)i9V@-IP:  `kcU3a|??^g2?g"'1eJLXKp2ꂵ1 `KKa BZeICA?bV Fx:bJ??SR8*}/} >~霻n]-jH+6:ă%rn#p ,T;-Q;`W?bEMxVm1MS#%<xUm9)E+0i RDC oD(j7ssRv k'.TL0 '0:4 S#(8T|kˇO~'P!,* |T9k h񎴬I*^<]ae-[@ t_;aczr|.+! *l\Yj8VeYwEG"Bybd 6ț6xaʬ=4^M'?^z'<%oXy b@ce~]ZanZl.!9n ASa}J9:+\/8=Ѻ%N.!yE(c})sRn=ƺjp[|o ſ=w|+?b `]16iv `^X?G11iQv ̈oIENDB`autokey-0.95.10/config/autokey.svg000066400000000000000000000322171362232350500170240ustar00rootroot00000000000000 image/svg+xml autokey-0.95.10/config/ubuntu-mono-dark/000077500000000000000000000000001362232350500200245ustar00rootroot00000000000000autokey-0.95.10/config/ubuntu-mono-dark/autokey-status-error.svg000066400000000000000000000067001362232350500247010ustar00rootroot00000000000000 image/svg+xml autokey-0.95.10/config/ubuntu-mono-dark/autokey-status.svg000066400000000000000000000067001362232350500235520ustar00rootroot00000000000000 image/svg+xml autokey-0.95.10/config/ubuntu-mono-light/000077500000000000000000000000001362232350500202125ustar00rootroot00000000000000autokey-0.95.10/config/ubuntu-mono-light/autokey-status-error.svg000066400000000000000000000067001362232350500250670ustar00rootroot00000000000000 image/svg+xml autokey-0.95.10/config/ubuntu-mono-light/autokey-status.svg000066400000000000000000000067001362232350500237400ustar00rootroot00000000000000 image/svg+xml autokey-0.95.10/debian/000077500000000000000000000000001362232350500145725ustar00rootroot00000000000000autokey-0.95.10/debian/autokey-common.install000066400000000000000000000017661362232350500211430ustar00rootroot00000000000000usr/bin/autokey-run usr/bin/autokey-shell usr/lib/python*/*-packages/autokey/common.py usr/lib/python*/*-packages/autokey/configmanager.py usr/lib/python*/*-packages/autokey/configmanager_constants.py usr/lib/python*/*-packages/autokey-*.egg-info usr/lib/python*/*-packages/autokey/__init__.py usr/lib/python*/*-packages/autokey/interface.py usr/lib/python*/*-packages/autokey/iomediator/*.py usr/lib/python*/*-packages/autokey/macro.py usr/lib/python*/*-packages/autokey/model.py usr/lib/python*/*-packages/autokey/monitor.py usr/lib/python*/*-packages/autokey/scripting.py usr/lib/python*/*-packages/autokey/scripting_highlevel.py usr/lib/python*/*-packages/autokey/scripting_Store.py usr/lib/python*/*-packages/autokey/service.py usr/share/icons/hicolor/scalable/apps/autokey-status*.svg usr/share/icons/hicolor/scalable/apps/autokey.svg usr/share/icons/Humanity/scalable/apps/*.svg usr/share/icons/ubuntu-mono-dark/apps/48/*.svg usr/share/icons/ubuntu-mono-light/apps/48/*.svg usr/share/man/man1/autokey-run.1 autokey-0.95.10/debian/autokey-gtk.install000066400000000000000000000002671362232350500204330ustar00rootroot00000000000000usr/bin/autokey-gtk usr/lib/python*/*-packages/autokey/gtkapp.py usr/lib/python*/*-packages/autokey/gtkui/ usr/share/applications/autokey-gtk.desktop usr/share/man/man1/autokey-gtk.1 autokey-0.95.10/debian/autokey-gtk.postinst000066400000000000000000000003271362232350500206450ustar00rootroot00000000000000#!/bin/sh set -e update-alternatives --install /usr/bin/autokey autokey \ /usr/bin/autokey-gtk 50 \ --slave /usr/share/man/man1/autokey.1.gz autokey.1.gz \ /usr/share/man/man1/autokey-gtk.1.gz #DEBHELPER# autokey-0.95.10/debian/autokey-gtk.prerm000066400000000000000000000002331362232350500201030ustar00rootroot00000000000000#!/bin/sh set -e case "$1" in remove|deconfigure) update-alternatives --remove autokey /usr/bin/autokey-gtk ;; esac #DEBHELPER# exit 0 autokey-0.95.10/debian/autokey-qt.install000066400000000000000000000003411362232350500202630ustar00rootroot00000000000000usr/bin/autokey-qt usr/lib/python*/*-packages/autokey/qtapp.py usr/lib/python*/*-packages/autokey/qtui/ usr/share/applications/autokey-qt.desktop usr/share/icons/hicolor/96x96/apps/autokey.png usr/share/man/man1/autokey-qt.1 autokey-0.95.10/debian/autokey-qt.postinst000066400000000000000000000003251362232350500205020ustar00rootroot00000000000000#!/bin/sh set -e update-alternatives --install /usr/bin/autokey autokey \ /usr/bin/autokey-qt 60 \ --slave /usr/share/man/man1/autokey.1.gz autokey.1.gz \ /usr/share/man/man1/autokey-qt.1.gz #DEBHELPER# autokey-0.95.10/debian/autokey-qt.prerm000066400000000000000000000002321362232350500177410ustar00rootroot00000000000000#!/bin/sh set -e case "$1" in remove|deconfigure) update-alternatives --remove autokey /usr/bin/autokey-qt ;; esac #DEBHELPER# exit 0 autokey-0.95.10/debian/changelog000066400000000000000000001255301362232350500164520ustar00rootroot00000000000000autokey (0.95.10-0) bionic; urgency=medium [Bug fixes] * Mitigate crashes when entering invalid Python regular expressions in the window filter dialogue. Fixes issue #212 * Added option to disable the handling of the Capslock modifier key. Fixes issues when that key is remapped to something else, for example Ctrl. The new option can be found in the settings dialogue. Fixes issues #95, #291 * API function `system.exec_command()` now only trims the last character in the output, if it is actually a newline character. If the executed command does not output a newline at the end, the full output is returned. Fixes issue #354 * Fixed wrong optional argument in man page for `autokey-run`. Fixed by pull request #361 * Removed unnecessarily set executable bit from several AutoKey SVG icons. Fixed by pull request #363 -- Thomas Hess Sun, 16 Feb 2019 21:30:00 +0100 autokey (0.95.9-0) bionic; urgency=medium [Bug fixes] * Prevent data losses when deleting or moving directories from within AutoKey. AutoKey will only delete data it knows and keep unknown user data. So adding $HOME and then removing it again will not purge everything below it. Affected were deleting directories and moving them via drag & drop. Fixes issues #171, #332 -- Thomas Hess Sat, 7 Dec 2019 20:33:00 +0100 autokey (0.95.8-0) bionic; urgency=medium [Bug fixes] * Qt GUI: Fix issue with Python 3.7.4 and PyQt 5.11-5.13.0 that prevented AutoKey from starting on certain distributions shipping this configuration, notably Kubuntu 19.10. Fixes issues #313, #301 * Qt GUI: Fix crash when saving the currently edited item, after deselecting it in the tree view. Fixes issue #285 * Qt GUI: Disable Main window -> Tools -> Insert Macro when not editing a Phrase. Fixes issue #276 * Qt GUI: Add a warning that explains possible data loss when creating top level directories at used specified locations. See issue #171 * GTK GUI: Fix application hang when setting a custom value for "Trigger on" in the Abbreviation settings dialogue. Fixes issue #315 -- Thomas Hess Sat, 7 Nov 2019 21:25:00 +0100 autokey (0.95.7-0) bionic; urgency=medium [Bug fixes] * GTK GUI: Fixed system tray icon context menu entry `View script error`, which was non-functional, if the main window is closed. The entry now opens the main window first as a workaround, because a proper fix will require a major code overhaul. Fixes issue #222 * Qt GUI: Fixed the truncated GPLv3 license text shown in the About AutoKey dialogue. The dialogue now shows the full license text. Fixes issue #258 * Hardened the logic to read application window titles. AutoKey now works, if applications do not set the `_NET_WM_VISIBLE_NAME` property of their windows. Fixes issue #257 * Fixed Phrase expansion using the Keyboard method, which was broken if AutoKey was started for the first time. Fixes issue #274 [Other fixes] * Improved the debug logging output: Removed unnecessary output, clarified wordings, etc. See issue #230 * Qt GUI: Display the current Python version number in the About dialogue. -- Thomas Hess Sat, 29 Apr 2019 14:40:00 +0200 autokey (0.95.6-0) bionic; urgency=medium [Bug fixes] * Fix the combination of phrase settings 'Match phrase case to typed abbreviation' and 'Trigger immediately' to cause Scripts and Phrases to trigger on each and every key press. Fixes issue #254 introduced in 0.95.5. -- Thomas Hess Sat, 09 Feb 2019 18:34:00 +0200 autokey (0.95.5-0) bionic; urgency=medium [Bug fixes] * Fix window filter detection always returning Title: 'FocusProxy', Class: 'Focus-Proxy-Window.FocusProxy' on Java AWT applications. It now detects the proper window title and WM_CLASS attribute for Java AWT applications. Fixes issue #113 * GTK GUI: Fix the window filter detection dialogue. On clicking OK, it hung the whole application. Now the dialogue window works as intended. Fixes issue #229 * Fix abbreviation case folding (ignore case option) with abbreviations defined as UPPER CASE in the abbreviation dialogue. Options `Ignore case` and `Match case` now work with upper case abbreviations. Fixes issue #197 * Prevent the keyboard from staying grabbed by AutoKey if exceptions are thrown while AutoKey performs a clipboard pasting action. Fixes issues #72, #225 * Prevent writing `None` to the clipboard. This prevents autokey-gtk from deadlocking, caused by an unreleased mutex. Fixes issue #226 * Restrict Phrase Undo functionality to phrases without special keys, because phrases containing special keys cannot be reliably undone. Fixes issue #196 * Clarified autosave option wording in the settings window. The option now explicitly states what it does. Fixes issue #194 * Force AutoKey to exit, if the X server connection closes, most probably at logout or session end. Fixes issue #198 [Qt tray icon bug fixes] * Added »View script error« entry to the Tray icon context menu, like in the GTK GUI. Part of issue #158 * Tray icon turns red, when scripts raise an error, like in the GTK GUI. Part of issue #158 * If changing the tray icon theme in the settings (light or dark), instantly apply the new theme, without requiring an application restart. Part of issue #158 * The tray icon now works, after if it is disabled in the settings and then enabled again. Fixes issue #223 -- Thomas Hess Tue, 07 Feb 2019 15:05:00 +0200 autokey (0.95.4-0) bionic; urgency=low [Bug fixes] * Fix grabbed hotkeys being incorrectly received by other applications. * Fixed crashes when processing literals in strings. It is now possible to place and literals in Phrases. Additionally, such literals can be typed in scripts using the keyboard.send_keys function. * Increased the reliability of the window filter detection dialog in autokey-qt. The dialog allows sampling windows to aid writing window filters. Due to timing issues in certain cases, sometimes the window title of the previously active window was returned. -- Thomas Hess Sun, 14 Oct 2018 17:15:00 +0200 autokey (0.95.3-0) bionic; urgency=medium [Features] * Phrase expansion can now always be undone using the backspace key, if the feature is enabled in the settings. Previously it was only be possible if the phrase was triggered by an abbreviation. Now it also works when using hotkeys or selecting phrases from menus. This also prevents crashes in certain cases. * Qt GUI: Add support for automatically starting `autokey-qt` during login. It can be configured in the settings dialogue. The configuration option allows to choose which GUI is automatically started, if both `autokey-qt` and `autokey-gtk` are installed simultaneously, and whether the main window should be shown automatically on launch. * Qt GUI: Added the notification icon theme selection to the settings dialogue. The added section in the general settings allow to choose between the light and dark theme, like in the `autokey-gtk` settings dialogue. Changing this setting currently requires an application restart to take effect. (Restriction lifted in 0.95.5) [Bug fixes] * Scripting API: The Python `__file__` global variable is now properly set for AutoKey scripts. It contains the full path to the Python script file currently running. Previously, it contained the full path to the `service.py` file of the currently running AutoKey instance. * Crash fix: Skip import of the AT-SPI interface, if importing of `pyatspi` fails with a SyntaxError. This may happen with certain versions of `pyatspi` on Python 3.7. For details see the GitHub issue #173. * Fix serializing the store during saving, if user stores recursive data structures. It now handles/skips lists that contain themselves or other circular referenced data structures. * GTK GUI: Fix autostart handling: Create the `$XDG_CONFIG_HOME/autostart` (`~/.config/autostart`) directory, if it is not already present. Fixes #149 * Qt GUI: Create the user data directories before initializing the logger system. This prevents crashes when autokey-qt is used for the first time or when the user wiped all previous data. Fixes #170 * Qt GUI: Fix saving the "Always prompt before running this script" checkbox content when editing scripts. This option now works as intended again. [Packaging] * Stop shipping the `autokey.png` icon file inside a `scalable` icon theme directory. Moved to the appropriate raster image directory. * Corrected broken dependency package name in setup.py. The library is called `python-xlib` and not `python3-xlib` on PyPI. -- Thomas Hess Tue, 21 Aug 2018 21:48:00 +0200 autokey (0.95.2-0) bionic; urgency=medium * Fix broken imports in autokey-shell script * Skip non-json-serializable data in script storage (both script local and global) during saving. This allows putting non-serializable items (like function objects) into the store without crashing autokey during saving. * [Qt] Fix minor bug when creating new items. Created items are now properly selected for renaming directly after creation. -- Thomas Hess Mon, 16 Jul 2018 15:13:03 +0200 autokey (0.95.1-0) bionic; urgency=medium * Fix a long standing bug that errors occurring during phrase parsing or script execution can lock up the user keyboard. Make sure to always release the keyboard after grabbing it. * [Qt] Fix saving the content of the log view to a file using the context menu entry. -- Thomas Hess Sat, 30 Jun 2018 23:02:19 +0200 autokey (0.95.0-0) bionic; urgency=medium * [Qt] Ported GUI from PyQt4/PyKDE4 to PyQt5 * Fixed clipboard pasting for Phrases * [Scripting API] Re-added newline trimming in system.exec_command() API call. * [Scripting API] Fixed broken KDialog based colour chooser dialogue. * [Scripting API] Added zenity based colour chooser dialogue. * [Scripting API] Both colour dialogs return parsed data in the form of a Python namedtuple. This API change breaks old scripts that used the KDialog based colour chooser. * See the CHANGELOG.rst for details -- Thomas Hess Thu, 28 Jun 2018 17:21:32 +0200 autokey (0.94.1-1ubuntu1~bionicppa1) bionic; urgency=medium * Rebuild for bionic -- Troy C. (Ubuntu PPA) Wed, 09 May 2018 17:05:29 -0500 autokey (0.94.0-1) artful; urgency=low * Python 3 version is official * Ported autokey-run from the legacy optparse module to the new Python 3 argparse module (luziferius) * Use XDG_RUNTIME_DIR and XDG_DATA_HOME for lock and log file (founderio) -- Troy C. (Ubuntu PPA) Sat, 30 Dec 2017 00:06:07 -0600 autokey (0.93.10-1) xenial; urgency=low * [QT] Package fixes * Scripting bug fix * Small clarification fixes -- Troy C. (Ubuntu PPA) Thu, 16 Feb 2017 22:03:47 -0600 autokey (0.93.9-1) xenial; urgency=low * Fix regression with setuptools install_requires -- Troy C. (Ubuntu PPA) Mon, 10 Jan 2017 13:40:11 -0600 autokey (0.93.8-1) xenial; urgency=low * [GTK] Fix traceback for "View script error" dialog * Update documentation -- Troy C. (Ubuntu PPA) Mon, 9 Jan 2017 10:52:59 -0600 autokey (0.93.7-1) xenial; urgency=low * [GTK] Add missing gi.require_version() calls to fix warnings * Update debian/ packaging files for Python 3 * Fix freeze when using clipboard.fill_selection() * Update documentation * Update setup.py to use setuptools -- Troy C. (Ubuntu PPA) Tue, 20 Dec 2016 23:48:12 -0600 autokey (0.90.4-0) unreleased; urgency=low * [GTK] Fix various issues with notifications * [GTK] Fix various issues with clipboard scripting * Apply patch from issue 199 - add support for Meta modifier -- Chris Dekter Tue, 15 May 2012 21:38:12 +1000 autokey (0.90.3-0) unreleased; urgency=low * Remove set_title from IndicatorNotifier, as it isn't supported in all versions of the AppIndicator API. -- Chris Dekter Fri, 4 May 2012 18:41:03 +1000 autokey (0.90.2-1) unreleased; urgency=low * Packaging change only: fix installation paths for ubuntu-mono-* icons -- Chris Dekter Wed, 2 May 2012 19:23:02 +1000 autokey (0.90.2-0) unreleased; urgency=low * [GTK] Run action should not be enabled for phrases * [GTK] Bring back support for Unity application indicator * [GTK] Fix a problem deleting items with older GTK3 versions * [GTK] Make showing the main window on startup the default behaviour when using the desktop file. * [GTK] Fix window filter not being applied after clicking OK in the filter settings dialog * [GTK] Only show one warning per session about externally modified config * Correctly handle scenario where a phrase and script with the same name exist * Add Unity to the list of environments messing with the Super key * [GTK] Enable toggling of tree row collapsed/expanded status -- Chris Dekter Tue, 1 May 2012 20:27:31 +1000 autokey (0.90.1-0) unreleased; urgency=low * [KDE] Fix two critical issues causing the application to not start -- Chris Dekter Sun, 22 Apr 2012 11:35:11 +1000 autokey (0.90.0-0) unreleased; urgency=low * [GTK] Port the GTK UI to GTK3 * [GTK] Add a help menu entry for the scripting API * [GTK] Get rid of the Abbreviation Selector as it's unmaintained and unused * [GTK] Add function to run the currently selected script * [GTK] Add a Tools menu, merge with View menu and add some new options * [GTK] Add the ability to enter custom trigger characters for abbreviations * [GTK] Obey desktop setting for monospace font in script editor * [GTK] Add a dialog for automatically configuring window filters * [GTK] Fix for issue 190 - special hotkeys won't save * [GTK] Save and restore the clipboard when using it to send text * [GTK] Allow manual sorting of the treeview on the name column * [GTK] Enable typeahead search in the treeview * [GTK] Add the ability to record keystrokes to phrases * [KDE] Add a help menu entry for scripting API, and enable standard "About KDE" entry * Add ability to store global variables in the script store * Completely remove the EvDev interface and daemon, and all related UIs * Use PNG icons for KDE and SVG icons for GNOME * Add cinnamon to the list of environments using Super key workaround -- Chris Dekter Fri, 20 Apr 2012 20:39:11 +1000 autokey (0.82.2-0) unreleased; urgency=low * [GTK] Fix critical bug for issue 185 - unable to save abbreviations * [KDE] Add a context menu for the log view * [KDE] Improve running of scripts from main window - handle exceptions and run in a worker thread -- Chris Dekter Wed, 7 Mar 2012 8:32:33 +1100 autokey (0.82.1-0) unreleased; urgency=low * Fix critical bug causing GTK version to no longer work * [KDE] Add a log view that can be shown/hidden using F4 * [KDE] Use the KDE global setting for monospace font instead of custom fonts -- Chris Dekter Mon, 5 Mar 2012 10:10:04 +1100 autokey (0.82.0-0) unreleased; urgency=low * Revive the KDE UI and bring it up to date with features developed since its deprecation * [KDE] Add a function for running a script from the GUI (2 second delay) * Implement enhancement issue 176 - add tab as a trigger character option * [KDE] Allow entry of custom trigger character ranges for abbreviations * Resolve issue 175 - wait for sub-scripts to complete before continuing * Resolve issue 177 - create a shallow copy of the namespace object for each script invocation * [KDE] Enable calltips and suggestions and update the API file * [KDE] Save and restore clipboard contents when using Send via Clipboard * Add RPM spec and update to build the KDE package as well * Add support for using the Hyper button in hotkeys * Fix incorrect logic in Mutter workaround causing it to always trigger -- Chris Dekter Sat, 3 Mar 2012 22:04:02 +1100 autokey (0.81.4-0) unreleased; urgency=low * Critical bugfix for mutter workaround method breaking on Python < 2.7 -- Chris Dekter Thu, 15 Dec 2011 10:40:02 +1100 autokey (0.81.3-0) unreleased; urgency=low * Handle exceptions during grab/ungrab of hotkeys * Prevent the X event loop from terminating if an exception occurs * Fix numpad hotkeys not working when numlock is on * Allow modifier keys to be auto-grabbed when setting a hotkey * Change icons for phrases and scripts to the standard ones for text files and python scripts * Refine the Gnome Shell workaround now that it's confirmed as a 'feature' * Improve clean shutdown again * Implement named, specified arguments for macros (work-in-progress) -- Chris Dekter Wed, 14 Dec 2011 17:22:12 +1100 autokey (0.81.2-0) unreleased; urgency=low * Improve compatibility with Gnome Shell by grabbing all hotkeys recursively * Set a prettier indicator icon name * Don't display the Interface tab in the preferences dialog if there is no need to change the setting * Fix engine.create_hotkey() and engine.create_abbreviation() -- Chris Dekter Sat, 3 Dec 2011 14:55:13 +1100 autokey (0.81.1-0) unreleased; urgency=low * Critical bug fix for error "unknown internal child: selection" on older versions of GTK. -- Chris Dekter Fri, 25 Nov 2011 09:08:23 +1100 autokey (0.81.0-0) unreleased; urgency=low * Implement issue 154 - external method for triggering scripts/phrases/folders * Add auto-mnemonic to the first 9 items in the popup menu * Allow window filters on folders to be applied recursively to children * Another attempt at improving grab/ungrab hotkey behaviour and performance * Fix bug where our own changes were sometimes detected by file monitor * Implement keyboard.wait_for_keypress() for scripts * Implement mouse.wait_for_click() for scripts * Allow multiple abbreviations to be assigned * Allow hotkeys and abbreviations to be re-used when the window filter is different * Rework validation framework, and do detailed validation at save time * Bring back a version of the macro system for phrases * Add a cursor-positioning macro * Add a macro that allows scripts to be run and the results used * Bring back the option to configure notification icon style * Add a dark-style notification icon * Bring back OSD notifications for script errors * Change notification icon to red on script errors * Tweak alignment and spacing of all windows and dialogs to conform to GNOME HIG * Add a "Report a Bug" help menu option * Disable the "Trigger On" combo box when trigger immediately is enabled * Drop the separate Save/Revert buttons and move Save into the toolbar * Make auto-saving the default mode * Allow creation without needing to select a parent folder first * Fix intermittent hanging during shutdown, and wait for all threads to terminate before quitting * Focus correct control on Set Abbreviations dialog * Fix bug where folders are always created at the top level * Enable window class matching features of wmctrl -- Chris Dekter Wed, 22 Nov 2011 16:20:55 +1100 autokey (0.80.3-0) unreleased; urgency=low * Fix a bug where Firefox/Thunderbird were not responding to keyboard events * Fix for issue 150 - window titles containing unicode characters were not handled correctly * Re-choose interface type each time a version upgrade occurs * Implement a better way of determining Xorg version * Make XRecord the default interface type if we can't determine Xorg version * Disable daemon and don't start it during postinst if Xorg supports XRecord -- Chris Dekter Wed, 12 Oct 2011 11:54:02 +1100 autokey (0.80.2-0) unreleased; urgency=low * Rewrite X interface to use a single-threaded queue model * Fix a bug where keyboard was not ungrabbed after creating a hotkey * Don't create file names with spaces at the start or end * Get detection of window creation working again and use CreateNotify instead of MapNotify * Grab/ungrab hotkeys using window title and class for the filter * Minor tweaks and improvements to EvDev and ATSPI interfaces * Fix a bug where keyboard ungrabbing stops working after keymap change * Correctly ungrab and regrab hotkeys when the keymap is changed * Make both parts of the window class available to match on -- Chris Dekter Sat, 8 Oct 2011 14:56:01 +1100 autokey (0.80.1-0) unreleased; urgency=low * Fix for issue 143 - deadlocks in python-xlib during high-speed events * Fix for issue 144 - handle phrase/script names that have no safe chars -- Chris Dekter Fri, 30 Sep 2011 09:33:21 +1100 autokey (0.80.0-0) unreleased; urgency=low * Rewrite persistence layer - use a real filesystem layout of folders and files to persist configuration and allow direct editing in other programs * Create conversion path from previous single configuration file * Add inotify-based file monitoring and reload configuration in real-time if modified externally * Implement reloading of the keyboard mapping when it is changed * Simplify and improve implementation of dynamic keyboard remapping * Better detection and use of ISO Level 3 shift (Alt-Grid) * Make AutoKey compatible with QT4 apps having the event rate bug * Add a configurable window title regex to allow specifying which apps should have the workaround performed * Implement a Dbus session service and use it to trigger display of the main window when a second instance of the program is started * Add ability for scripts to get the active window title and class * Add sample script that shows the active window information * Use application indicator instead of GTK notification icon if available * Use icon names rather than file names to allow icon themes to change our icons * Redesigned application icon * Add a standard notification area icon that is less intrusive * Make enabling/disabling the notification icon instant instead of requiring a restart * Give icons more sensible names * Deprecate and remove the KDE GUI * Stop building transitional 'autokey' package * Remove nogui code and documentation - it's been broken for a long time * Fix drag and drop in the tree view * Silence various GTK builder warnings * Add F2 as an accelerator for Rename * Completely rework item naming and renaming by adding a separate dialog to perform this function * Fix for issue 137 - add locks around display for grab/ungrab during phrase send * Implement issue 110 - add option to clone a phrase/script * Implement issue 133 - add scripting function to click mouse relative to current location * Make delete nicer by not prompting for every single selected item * Fix problem where having two identically-named top-level folders was impossible * Fix divergence between list of folders in configmanager and treemodel * Simulate press and release of modifier keys when sending modified symbols * Merge patch for issue 140 - don't use hardcoded errno value -- Chris Dekter Thu, 22 Sep 2011 14:26:08 +1000 autokey (0.71.3-1) unreleased; urgency=low * Documentation change only - license in README -- Chris Dekter Sat, 16 Jul 2011 10:45:13 +1000 autokey (0.71.3-0) unreleased; urgency=low * Fix for issue 108 - cannot upgrade a v0.5x config file any more * Fix for issue 107 - clipboard get functions are not using unicode * Implement enhancement 109 - add general keyword arguments to script dialogs * If unable to connect to the daemon on startup, retry a few times * If interface initialisation fails, still initialise necessary objects to allow user to modify interface configuration. -- Chris Dekter Fri, 1 Apr 2011 11:24:11 +1100 autokey (0.71.2-0) unreleased; urgency=low * Update all licenses to GPLv3 and add where missing * Update package license to GPLv3 * Add missing COPYING file * Fix engine.run_script() (issue 97) * Fix for issue 95, dropping next character after hotkey -- Chris Dekter Fri, 28 Jan 2011 18:24:11 +1100 autokey (0.71.1-1) unreleased; urgency=low * Packaging change only, add missing xml data file for GTK UI -- Chris Dekter Sun, 24 Oct 2010 09:59:23 +1100 autokey (0.71.1-0) unreleased; urgency=low * [gtk] Handle situation where no folders exist (issue 62) * Remove KeyboardInterrupt handling as it didn't work anyway (issue 3) * Fix window.get_active_geometry() (issue 77) * Don't use absolute path name to application icon * Add 'status' function to init script to meet Redhat sysv init specs -- Chris Dekter Fri, 22 Oct 2010 13:21:11 +1100 autokey (0.71.0-0) unreleased; urgency=low * Add an import/export facility (issue 58) * Add support for hotkeys with no modifiers (issue 53) * Fix shift unable to be used as a standalone modifier for a hotkey (issue 59) * Add ability to separately press and release keys using scripting API (issue 52) * Add some debug info around keyboard remap operation * Disable configuration of popup menu focus behaviour for now as it's causing too many hard-to-diagnose bugs * Don't grab keyboard when recording a macro * Don't save changes to a global hotkey if the hotkey is disabled * Add assertions to detect invalid hotkeys being created -- Chris Dekter Fri, 30 Jul 2010 12:33:10 +1000 autokey (0.70.5-1) unreleased; urgency=low * Packaging fix only - prevent errors with akicon-status.png by specifiying replaces and breaks in autokey-common -- Chris Dekter Mon, 19 Jul 2010 10:57:23 +1000 autokey (0.70.5-0) unreleased; urgency=low * Improve window name determination by using the FreeDesktop specified atom names * Fix handling of Tab key in service and iomediator * Disable detection of keymap changes, as this seems to cause weird interaction with dynamic key remapping * Fix misnamed argument in fake_keypress() * [gtk] Implement option to configure tray icon style (issue 51) * Fix naming convention of main window (too many uses of "Configure") * [kde] Make notification tooltip translatable -- Chris Dekter Wed, 14 Jul 2010 15:11:15 +1000 autokey (0.70.4-0) unreleased; urgency=low * Make sending text via keyboard dramatically faster by eliminating 2 RTT per character * Remove event replaying code as it is no longer needed and too unreliable * Make phrase execution sequential using a lock, to prevent phrases stomping on each other's output when multiple phrases are triggered one after the other * Only reapply modifiers when entire string has been typed * Remove locks from iomediator as they were made redundant by keyboard grabbing * Fix handling of space by adding it to the XK/AK map * Tweak get_window_title() courtesy of patch by Joseph Reagle * Change text on interface settings page to reflect Record being fixed in x.org v1.7.6 * Fix _chooseInterface() and update it to reflect Record being fixed in x.org v1.7.6 * Disable substring abbreviation crosscheck - too many spurious warnings * Set initial state of Caps and Numlock using the keyboard LED mask * Get rid of lock state setting in Record interface, wasn't working anyway * Initialise global hotkeys after creating fresh config on first run * Fix for issue 42: Set showPopupHotkey to be a no-op closure in KDE version -- Chris Dekter Sun, 25 Apr 2010 15:31:15 +1000 autokey (0.70.3-1) unreleased; urgency=low * Fix tiny oops in interface.py -- Chris Dekter Wed, 21 Apr 2010 21:28:02 +1000 autokey (0.70.3-0) unreleased; urgency=low * Automatically adjust keyboard mapping to allow sending of non-mapped characters in phrases * Remove untypable character validation as it's no longer needed * Catch BadWindow errors during initial hotkey grabs * Prevent abbreviations triggering in the abbreviation settings dialog * [kde] Enable auto-sizing of treeview columns * [gtk] Wrap clipboard calls in gtk.gdk.threads_enter() and leave() * [gtk] Raise a nicer exception if no text was returned from selection or clipboard * [gtk] Fix incorrect validation message in phrase page * Enable config file upgrade from 0.6x.x to to 0.70.0 * Add some logging around config upgrades -- Chris Dekter Wed, 21 Apr 2010 19:28:02 +1000 autokey (0.70.2-0) unreleased; urgency=low * Fix for issue 35 - Cleared hotkeys show in column * Remove send_unicode_char() as it no longer works * Handle missing modifier masks by warning (and not crashing) * Don't grab hotkey combinations for Caps and Num if they aren't mapped * [gtk] Autosize treeview columns * [kde] Remember column and splitter positions * [gtk] Fix hang when filling the clipboard/mouse selection * Exorcise all remaining traces of SourceForge site * Change left click action of notification icon to show the configuration window * When using XRecord interface, get initial state of lock keys * Distinguish between numpad and ordinary keys * Fix numpad key decoding when both numlock and shift are active * Grab the keyboard while grabbing the key for a hotkey in hotkey settings dialog * Implement configuration option to send phrase via different modes * Implement validation to check for untypable characters in a phrase * Improve documentation for scripting interface (issue 37) * Don't log list of phrases/scripts in debug mode as it can be a security risk * Disable abbreviation popup hotkey by default * Make hotkey and abbreviation validation messages more informative * Detect substring abbreviation conflicts when validating -- Chris Dekter Sat, 17 Apr 2010 10:23:02 +1000 autokey (0.70.1-0) unreleased; urgency=low * Add extra columns to treewidget to display abbreviation and/or hotkey * Always prompt before deleting anything * Much nicer determination of new selection after deleting an item * [kde] Improve treewidget behaviour when creating phrases/scripts * [kde] Get rid of unused help menu entries * Fix a number of bugs around prompting to save and autosaving * [gtk] Fix for issue 29 - Clicking "Yes" for saving a phrase doesn't save the phrase * Use shutil.copy2 to preserve config file's timestamp * Extensive rewrite of keyboard mapping code * Prevent spurious hotkey grabs on window create * Flush generated keyboard events before ungrabbing the keyboard * Add a fake_keypress() to scripting keyboard class to send events using xtest * Fix bug where word characters were not detected correctly in non-English locales * Add dependency and import handling for differing JSON libraries in Python 2.5 -- Chris Dekter Tue, 13 Apr 2010 20:26:12 +1000 autokey (0.70.0-0) unreleased; urgency=low * Persist configuration using json instead of pickle * Make hotkeys exclusive - prevent other applications from receiving them * Hotkeys are grabbed globally if they have no filter * Hotkeys are grabbed only in matching windows if they have a filter * Grab keyboard while sending strings to prevent user-typed input mixing with output * Fix problem where hotkeys with backspace, tab and enter would not work * Improve window name determination * Build a map of modifier masks * Use modifier masks to correctly emit modified keys instead of using Xtest -- Chris Dekter Fri, 9 Apr 2010 20:26:12 +1000 autokey (0.61.7-0) unreleased; urgency=low * Fix incorrect reference to instance variable in KDE version * Fix incorrect method signature in KeyGrabber class * ConfigManager uses version from common.py now -- Chris Dekter Tue, 30 Mar 2010 21:30:52 +1000 autokey (0.61.6-0) unreleased; urgency=low * Fix problem with autostart in GTK version - issue #27 * Allow system.exec_command to be used with long-running processes * Fix - hotkey dialog does not re-enable the "Press to set" button after cancelling - issue #23 * Slight improvement to installation instructions * Fix - special hotkeys - cannot assign previously cleared hotkey - issue #9 * Revert status icon for GTK version - it only looked good on one version of one distro * Patch holes in gettext support for GTK version (patch contribued by mail@paddy-net.com) -- Chris Dekter Thu, 25 Mar 2010 21:30:52 +1000 autokey (0.61.5-0) unreleased; urgency=high * SECURITY UPDATE: arbitrary file overwriting via symlinks (LP: #538471) - Store files for the EvDev daemon in FHS-specified locations - debian/autokey-common.init: Set pidfile path to "/var/run/autokey-daemon.pid" - src/lib/common.py: Set DOMAIN_SOCKET_PATH to "/var/run/autokey-daemon" - CVE-2010-0398 -- Chris Dekter Sat, 20 Mar 2010 21:30:52 +1000 autokey (0.61.4-0) unreleased; urgency=low [Chris Dekter] * Combine GTK and QT versions into single source tree [Luke Faraone] * Update package build to build autokey-gtk, autokey-qt and autokey-common packages -- Chris Dekter Sun, 28 Feb 2010 08:49:52 +1000 autokey (0.61.3-0) unreleased; urgency=low [ Luke Faraone ] * Handle invalid or empty pidfiles in src/daemon.py. * Lower build-depends requirements to minimum Python versions. [ Chris Dekter ] * Add --error-handler to debian/rules -- Chris Dekter Sat, 27 Feb 2010 08:49:52 +1000 autokey (0.61.2-0) unreleased; urgency=low * Bring back cut/copy/paste item menu options * Add 'engine' class to scripting framework to enable access to AutoKey internals * Add a configurable user module folder for import into scripts * Enable multiple selection mode in treeview and update all necessary interactions to work correctly * Enable inline renaming of items in treeview, get rid of title and description fields from the various pages -- Chris Dekter Wed, 6 Jan 2010 10:38:05 +1000 autokey (0.61.0b-0) unreleased; urgency=low [ Chris Dekter ] * Fix bug with sending newlines from scripts * Fix another crash in the EvDev daemon related to button conversions [ Luke Faraone ] * Fix l10n bug in daemon.py which caused breakage on package removal -- Chris Dekter Fri, 11 Dec 2009 10:38:05 +1000 autokey (0.61.0a-0) unreleased; urgency=low * Fix/improve mouse button conversion in EvDev daemon -- Chris Dekter Sun, 29 Nov 2009 10:38:05 +1000 autokey (0.61.0-0) unreleased; urgency=low * Add mouse click recording and playback capabilities * Rework record functionality to work as a full macro facility * Add ability to view tracebacks from script errors in GUI * Change sample scripts so they work on both GTK and KDE versions * Handle scenario where xlib record module is not available * Don't undo last expansion if the mouse has been clicked * Improvements to AT-SPI interface - now gives the real window title -- Chris Dekter Mon, 26 Oct 2009 10:38:05 +1000 autokey (0.60.7a-0) unreleased; urgency=low * Fix syntax error in configwindow.py * Fix a few issues with scripting window management library -- Chris Dekter Tue, 20 Oct 2009 10:38:05 +1000 autokey (0.60.7-0) unreleased; urgency=low * Add window management capabilities to scripting API * Minor GUI tweaks to bring into line with GTK version * Fix bug where keyboard was sometimes lost after resume from S3 * Flush event buffer after executing keyboard events from a script -- Chris Dekter Mon, 19 Oct 2009 12:38:05 +1000 autokey (0.60.6-0) unreleased; urgency=low * Fix bug where html phrases where not loaded as plain text -- Chris Dekter Wed, 30 Sep 2009 12:38:05 +1000 autokey (0.60.5-0) unreleased; urgency=low * Change to using pickle instead of cPickle due to a bug in the latter * Bring in changes to common modules needed for GTK version * Fix design flaw in pickle error trapping code * Fix not being sent at the end of unicode char entry * Fix service crashing when undoing an expansion with backspace * Use Phrase instead of Script in some nogui functions -- Chris Dekter Mon, 31 Aug 2009 12:38:05 +1000 autokey (0.60.4-0) unreleased; urgency=low * Revert to sending phrases via keyboard rather than X selection -- Chris Dekter Sun, 23 Aug 2009 12:38:05 +1000 autokey (0.60.3-0) unreleased; urgency=low * Initial version of nogui API * Fix some more unicode issues with the new GUI * Remove import of configobj from configmanager -- Chris Dekter Fri, 21 Aug 2009 12:38:05 +1000 autokey (0.60.2-0) unreleased; urgency=low * Fix unicode issues when saving and sending keys * Fix QT4 workaround current value not being loaded in the settings dialog * Remove relative imports as Python 2.5 can't handle them properly -- Chris Dekter Tue, 18 Aug 2009 17:38:05 +1000 autokey (0.60.1-0) unreleased; urgency=low [ Chris Dekter ] * Point Manual link in Help menu at Wiki index * Get rid of default phrases that no longer work * Add some default scripts * Remove reference to showPopupHotkey in ConfigManager [ Luke Faraone ] * Change over most of the URLs from sourceforge to the new project hosting at Google Code -- Chris Dekter Tue, 18 Aug 2009 17:38:05 +1000 autokey (0.60.0-0) unreleased; urgency=low * Implement scripting interface (ScriptRunner) * Rebuild UI using PyKDE/PyQT * Refactor ExpansionService, split into PhraseRunner and ScriptRunner * Implement libraries to provide script namespaces * Add option to undo abbreviation expansion using backspace -- Chris Dekter Thu, 9 Jul 2009 20:38:05 +1000 autokey (0.54.5-0) unreleased; urgency=low * debian/control: Changed Arch:Any to all since we don't build platform-specific binaries * debian/control: Bump standards version to 3.8.2 * Remove redundant encoding in .desktop file which is deprecated in latest Freedesktop.org spec -- Luke Faraone Sun, 12 Jul 2009 17:55:46 -0400 autokey (0.54.4-0) unreleased; urgency=low [ Chris Dekter ] * Add support for numpad keys, and a few other standard keys that were not handled * Add AT-SPI interface * Add configuration screen to choose interface type (requires restart) * Make tooltip display 'running' or 'paused' depending on service status * Fix hotkeys not working if Capslock or Numlock are on * Get rid of interface switching code * Create manpage * Fix bug where the system would sometimes lock up a while after displaying a phrase menu * Fix bug with EvDev interface where repeated keypress (key being held down) are ignored * Fix PhraseMenu sorting, and make it sort alphabetically if not by usage count * Disable mnemonics in PhraseMenu - or phrases with _ in title not displayed correctly * Minor tweak to fix for 2782632, as it broke macros in other ways. * Improve logging - log to file if run without -v * Add -c | --configure option to forcibly show the configure dialog on startup * Fix annoying bug where right-click of treeview doesn't always pick up the right node * Implement proper handling of Unicode/UTF-8 - all internal strings are now unicode instances * Implement yet another (rather hacky) fix for Alt-Gr issues. [ Luke Faraone ] * debian/control: Fixed improper line lengths * debian/copyright: Fix usage of (C) with © (the latter is not valid) * debian/copyright: Credit original author * debian/copyright: Remove dh_make template text * debian/autokey.init: Add required option force-reload * src/lib/autokey.py: Create configuration directory if it doesn't already exist -- Chris Dekter Wed, 8 Jul 2009 20:38:05 +1000 autokey (0.54.3-0) unreleased; urgency=low * Fix daemon sometimes dying when receiving events with integer code * Make daemon find devices using by-path entries instead of relying on HAL * Update TODO * Make Comment in .desktop entry nicer. Add GenericName -- Chris Dekter Tue, 7 Jul 2009 08:38:05 +1000 autokey (0.54.2-0) unreleased; urgency=low * Fix daemon dropping connection on mouse clicks * Add logging and command line option to set level to debug * Add LSB info to daemon script -- Chris Dekter Sun, 5 Jul 2009 18:38:05 +1000 autokey (0.54.1-0) unreleased; urgency=low * Refactor daemon so it starts successfully during boot * Fix problem where daemon exits if run on a PC without a touchpad * Fix initialisation of global hotkeys * Fix inability to configure any hotkeys -- Chris Dekter Sun, 5 Jul 2009 00:11:05 +1000 autokey (0.54.0-0) unreleased; urgency=low * Add new EvDev interface as an alternative where XRecord is not available. * Make EvDev the default (changing to XRecord will require hacking the code, for now). * Fix bug where Cut/Copy/Paste entries under Edit menu were not correctly disabled. * * Includes fix for: * 2782632 - Capitalization is applied to macros and specialkeys -- Chris Dekter Sat, 4 Jul 2009 12:11:05 +1000 autokey (0.53.1-2) unreleased; urgency=low * Alter cut/copy/paste to release standard key bindings to normal functions * Break sub plugin input at line endings, in addition to spaces * Add donate button as per ER 2789380 -- Chris Dekter Thu, 21 May 2009 19:13:37 +1000 autokey (0.53.0-2) unreleased; urgency=low * Alter build-depends to allow compatibility with hardy * Added a fully functioning distutils-based installer. * Added links to the Manual and FAQ in the Help menu. * Includes fixes for the following defects: * 2782607 - Issues with '<' sign * 2783421 - Multiple use of $(sub ) produces superfluous backspace * 2720487 - $(sub ) problem with parentheses, quotes etc. * 2783016 - Sub: '~' does not work as split character with DE keyboard * 2782526 - not working * 2710535 - Exception on repeated window switching * 2710118 - Multiple abbreviation windows opened * 2697670 - Exception when using predefined hotkeys. * 2697653 - Can't assign hotkey -- Chris Dekter Tue, 12 May 2009 19:35:37 +1000 autokey (0.31.1-0ubuntu3) gutsy; urgency=low * Cleaned up code so eventfile specification goes in [config] section -- Sam Peterson Thu, 21 Feb 2008 11:49:03 -0800 autokey (0.31-0ubuntu2) gutsy; urgency=low * Update so abbr.ini has config option for turning voice off -- Sam Peterson Thu, 21 Feb 2008 11:49:03 -0800 autokey (0.30-0ubuntu1) gutsy; urgency=low * Initial release -- Sam Peterson Mon, 18 Feb 2008 17:14:17 -0800 autokey-0.95.10/debian/clean000066400000000000000000000000701362232350500155740ustar00rootroot00000000000000lib/autokey/qtui/resources/icons/ lib/autokey.egg-info/ autokey-0.95.10/debian/control000066400000000000000000000054301362232350500161770ustar00rootroot00000000000000Source: autokey Maintainer: Python Applications Packaging Team Section: utils Priority: optional Build-Depends: debhelper-compat (= 12), dh-python, gir1.2-gtk-3.0, pyqt5-dev-tools, python3-all, python3-dbus, python3-gi, python3-pyqt5.qsci, python3-pyqt5.qtsvg, python3-setuptools, python3-xlib Standards-Version: 4.5.0 Vcs-Browser: https://salsa.debian.org/python-team/applications/autokey Vcs-Git: https://salsa.debian.org/python-team/applications/autokey.git Homepage: https://github.com/autokey/autokey Package: autokey-common Architecture: all Depends: python3-dbus, python3-pyinotify, python3-xlib, wmctrl, ${misc:Depends}, ${python3:Depends} Suggests: python3-pyatspi Description: desktop automation utility - common data AutoKey is a desktop automation utility for Linux and X11. It allows the automation of virtually any task by responding to typed abbreviations and hotkeys. It offers a full-featured GUI that makes it highly accessible for novices, as well as a scripting interface offering the full flexibility and power of the Python language. . This package contains the common data shared between the various frontends. Package: autokey-gtk Architecture: all Section: gnome Depends: autokey-common (= ${binary:Version}), gir1.2-appindicator3-0.1, gir1.2-glib-2.0, gir1.2-gtk-3.0, gir1.2-gtksource-3.0, gir1.2-notify-0.7, python3-dbus, python3-gi, zenity, ${misc:Depends}, ${python3:Depends} Description: desktop automation utility - GTK+ version AutoKey is a desktop automation utility for Linux and X11. It allows the automation of virtually any task by responding to typed abbreviations and hotkeys. It offers a full-featured GUI that makes it highly accessible for novices, as well as a scripting interface offering the full flexibility and power of the Python language. . This package contains the GTK+ frontend. Package: autokey-qt Architecture: all Section: kde Depends: autokey-common (= ${binary:Version}), python3-pyqt5, python3-pyqt5.qsci, python3-pyqt5.qtsvg, ${misc:Depends}, ${python3:Depends} Description: desktop automation utility - Qt version AutoKey is a desktop automation utility for Linux and X11. It allows the automation of virtually any task by responding to typed abbreviations and hotkeys. It offers a full-featured GUI that makes it highly accessible for novices, as well as a scripting interface offering the full flexibility and power of the Python language. . This package contains the Qt frontend. autokey-0.95.10/debian/copyright000066400000000000000000000044051362232350500165300ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: autokey Source: https://github.com/autokey/autokey Files: * Copyright: © 2008 Sam Peterson © 2008-2012 Chris Dekter © 2014-2016 GuoCi © 2016-2018 Troy C. © 2017-2020 Thomas Hess License: GPL-3+ This package 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 package 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 . . On Debian systems, the complete text of the GNU General Public License can be found in `/usr/share/common-licenses/GPL-3'. Files: debian/* Copyright: © 2008 Sam Peterson © 2009-2012 Chris Dekter © 2009-2012 Luke Faraone © 2016-2018 Troy C. © 2018-2020 Thomas Hess License: GPL-2+ 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 package; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA . On Debian systems, the full text of the GNU General Public License version 2 can be found in the file `/usr/share/common-licenses/GPL-2'. autokey-0.95.10/debian/docs000066400000000000000000000000621362232350500154430ustar00rootroot00000000000000ACKNOWLEDGMENTS PKG-INFO CHANGELOG.rst README.rst autokey-0.95.10/debian/py3dist-overrides000066400000000000000000000000621362232350500201120ustar00rootroot00000000000000dbus-python python3-dbus python-xlib python3-xlib autokey-0.95.10/debian/rules000077500000000000000000000001031362232350500156440ustar00rootroot00000000000000#!/usr/bin/make -f %: dh $@ --with python3 --buildsystem=pybuild autokey-0.95.10/debian/source/000077500000000000000000000000001362232350500160725ustar00rootroot00000000000000autokey-0.95.10/debian/source/format000066400000000000000000000000141362232350500173000ustar00rootroot000000000000003.0 (quilt) autokey-0.95.10/debian/watch000066400000000000000000000002551362232350500156250ustar00rootroot00000000000000version=4 opts="filenamemangle=s%(?:.*?)?v?(\d[\d.]*)\.tar\.gz%autokey-$1.tar.gz%" \ https://github.com/autokey/autokey/tags \ (?:.*?/)?v?(\d[\d.]*)\.tar\.gz debian autokey-0.95.10/doc/000077500000000000000000000000001362232350500141155ustar00rootroot00000000000000autokey-0.95.10/doc/man/000077500000000000000000000000001362232350500146705ustar00rootroot00000000000000autokey-0.95.10/doc/man/autokey-gtk.1000066400000000000000000000037401362232350500172220ustar00rootroot00000000000000.\" Hey, EMACS: -*- nroff -*- .\" First parameter, NAME, should be all caps .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection .\" other parameters are allowed: see man(7), man(1) .TH AUTOKEY-GTK "1" "August 19, 2009" .\" Please adjust this date whenever revising the manpage. .\" .\" Some roff macros, for reference: .\" .nh disable hyphenation .\" .hy enable hyphenation .\" .ad l left justify .\" .ad b justify to both left and right margins .\" .nf disable filling .\" .fi enable filling .\" .br insert line break .\" .sp insert n+1 empty lines .\" for manpage-specific macros, see man(7) .SH NAME autokey-gtk \- keyboard automation utility for GNOME and GTK .SH SYNOPSIS .B autokey-gtk .RI [ options ] .SH DESCRIPTION This manual page briefly documents the .B autokey-gtk command. .PP .\" TeX users may be more comfortable with the \fB\fP and .\" \fI\fP escape sequences to invode bold face and italics, .\" respectively. \fBautokey-gtk\fP AutoKey is a desktop automation utility for Linux and X11. It allows the automation of virtually any task by responding to typed abbreviations and hotkeys. It offers a full-featured GUI that makes it highly accessible for novices, as well as a scripting interface offering the full flexibility and power of the Python language. .br For more information refer to the online wiki at: https://github.com/autokey/autokey/wiki .SH OPTIONS This program follows the usual GNU command line syntax, with long options starting with two dashes (`-'). A summary of options is included below. .TP .B \-\-help Show summary of options. .TP .B \-l, \-\-verbose Enable verbose (debug) logging. .TP .B \-c, \-\-configure Show the configuration window on startup, even if this is not the first run. .SH AUTHOR AutoKey was written by Chris Dekter, loosely based on a script by Sam Peterson. .PP This manual page was written by Chris Dekter . autokey-0.95.10/doc/man/autokey-qt.1000066400000000000000000000037301362232350500170600ustar00rootroot00000000000000.\" Hey, EMACS: -*- nroff -*- .\" First parameter, NAME, should be all caps .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection .\" other parameters are allowed: see man(7), man(1) .TH AUTOKEY-QT "1" "August 19, 2009" .\" Please adjust this date whenever revising the manpage. .\" .\" Some roff macros, for reference: .\" .nh disable hyphenation .\" .hy enable hyphenation .\" .ad l left justify .\" .ad b justify to both left and right margins .\" .nf disable filling .\" .fi enable filling .\" .br insert line break .\" .sp insert n+1 empty lines .\" for manpage-specific macros, see man(7) .SH NAME autokey-qt \- keyboard automation utility for KDE and QT .SH SYNOPSIS .B autokey-qt .RI [ options ] .SH DESCRIPTION This manual page briefly documents the .B autokey-qt command. .PP .\" TeX users may be more comfortable with the \fB\fP and .\" \fI\fP escape sequences to invode bold face and italics, .\" respectively. \fBautokey-qt\fP AutoKey is a desktop automation utility for Linux and X11. It allows the automation of virtually any task by responding to typed abbreviations and hotkeys. It offers a full-featured GUI that makes it highly accessible for novices, as well as a scripting interface offering the full flexibility and power of the Python language. .br For more information refer to the online wiki at: https://github.com/autokey/autokey/wiki .SH OPTIONS This program follows the usual GNU command line syntax, with long options starting with two dashes (`-'). A summary of options is included below. .TP .B \-\-help Show summary of options. .TP .B \-l, \-\-verbose Enable verbose (debug) logging. .TP .B \-c, \-\-configure Show the configuration window on startup, even if this is not the first run. .SH AUTHOR AutoKey was written by Chris Dekter, loosely based on a script by Sam Peterson. .PP This manual page was written by Chris Dekter . autokey-0.95.10/doc/man/autokey-run.1000066400000000000000000000034401362232350500172360ustar00rootroot00000000000000.\" Hey, EMACS: -*- nroff -*- .\" First parameter, NAME, should be all caps .\" Second parameter, SECTION, should be 1-8, maybe w/ subsection .\" other parameters are allowed: see man(7), man(1) .TH AUTOKEY-GTK "1" "February 02, 2020" .\" Please adjust this date whenever revising the manpage. .\" .\" Some roff macros, for reference: .\" .nh disable hyphenation .\" .hy enable hyphenation .\" .ad l left justify .\" .ad b justify to both left and right margins .\" .nf disable filling .\" .fi enable filling .\" .br insert line break .\" .sp insert n+1 empty lines .\" for manpage-specific macros, see man(7) .SH NAME autokey-run \- command-line execution utility for AutoKey .SH SYNOPSIS .B autokey-run .RI -[s|p|f] [name] .SH DESCRIPTION This manual page briefly documents the .B autokey-run command. .PP .\" TeX users may be more comfortable with the \fB\fP and .\" \fI\fP escape sequences to invode bold face and italics, .\" respectively. \fBautokey-run\fP A command-line execution utility for AutoKey, autokey-run allows you to initiate execution of phrases, scripts or folders from the command line. .br For more information refer to the online wiki at: https://github.com/autokey/autokey/wiki .SH OPTIONS This program follows the usual GNU command line syntax, with long options starting with two dashes (`-'). A summary of options is included below. .TP .B \-\-help Show summary of options. .TP .B \-s, \-\-script [name] Run a script with the specified name. .TP .B \-p, \-\-phrase [name] Paste a phrase with the specified name. .TP .B \-f, \-\-folder [name] Display a popup menu for the specified folder. .SH AUTHOR Chris Dekter. .PP This manual page was written by Chris Dekter . autokey-0.95.10/doc/scripting/000077500000000000000000000000001362232350500161175ustar00rootroot00000000000000autokey-0.95.10/doc/scripting/api-objects.txt000066400000000000000000000170421362232350500210640ustar00rootroot00000000000000lib.scripting lib.scripting-module.html lib.scripting.__package__ lib.scripting-module.html#__package__ lib.scripting.Engine lib.scripting.Engine-class.html lib.scripting.Engine.run_script lib.scripting.Engine-class.html#run_script lib.scripting.Engine.get_folder lib.scripting.Engine-class.html#get_folder lib.scripting.Engine.get_return_value lib.scripting.Engine-class.html#get_return_value lib.scripting.Engine.create_hotkey lib.scripting.Engine-class.html#create_hotkey lib.scripting.Engine.run_script_from_macro lib.scripting.Engine-class.html#run_script_from_macro lib.scripting.Engine.set_return_value lib.scripting.Engine-class.html#set_return_value lib.scripting.Engine.create_abbreviation lib.scripting.Engine-class.html#create_abbreviation lib.scripting.Engine.create_phrase lib.scripting.Engine-class.html#create_phrase lib.scripting.Engine.__init__ lib.scripting.Engine-class.html#__init__ lib.scripting.Engine.get_macro_arguments lib.scripting.Engine-class.html#get_macro_arguments lib.scripting.GtkClipboard lib.scripting.GtkClipboard-class.html lib.scripting.GtkClipboard.get_selection lib.scripting.GtkClipboard-class.html#get_selection lib.scripting.GtkClipboard.__receive lib.scripting.GtkClipboard-class.html#__receive lib.scripting.GtkClipboard.get_clipboard lib.scripting.GtkClipboard-class.html#get_clipboard lib.scripting.GtkClipboard.__fillClipboard lib.scripting.GtkClipboard-class.html#__fillClipboard lib.scripting.GtkClipboard.fill_clipboard lib.scripting.GtkClipboard-class.html#fill_clipboard lib.scripting.GtkClipboard.fill_selection lib.scripting.GtkClipboard-class.html#fill_selection lib.scripting.GtkClipboard.__execAsync lib.scripting.GtkClipboard-class.html#__execAsync lib.scripting.GtkClipboard.__fillSelection lib.scripting.GtkClipboard-class.html#__fillSelection lib.scripting.GtkClipboard.__init__ lib.scripting.GtkClipboard-class.html#__init__ lib.scripting.GtkDialog lib.scripting.GtkDialog-class.html lib.scripting.GtkDialog.input_dialog lib.scripting.GtkDialog-class.html#input_dialog lib.scripting.GtkDialog.info_dialog lib.scripting.GtkDialog-class.html#info_dialog lib.scripting.GtkDialog.list_menu lib.scripting.GtkDialog-class.html#list_menu lib.scripting.GtkDialog.open_file lib.scripting.GtkDialog-class.html#open_file lib.scripting.GtkDialog.password_dialog lib.scripting.GtkDialog-class.html#password_dialog lib.scripting.GtkDialog.list_menu_multi lib.scripting.GtkDialog-class.html#list_menu_multi lib.scripting.GtkDialog._run_zenity lib.scripting.GtkDialog-class.html#_run_zenity lib.scripting.GtkDialog.save_file lib.scripting.GtkDialog-class.html#save_file lib.scripting.GtkDialog.calendar lib.scripting.GtkDialog-class.html#calendar lib.scripting.GtkDialog.choose_directory lib.scripting.GtkDialog-class.html#choose_directory lib.scripting.Keyboard lib.scripting.Keyboard-class.html lib.scripting.Keyboard.press_key lib.scripting.Keyboard-class.html#press_key lib.scripting.Keyboard.fake_keypress lib.scripting.Keyboard-class.html#fake_keypress lib.scripting.Keyboard.send_key lib.scripting.Keyboard-class.html#send_key lib.scripting.Keyboard.send_keys lib.scripting.Keyboard-class.html#send_keys lib.scripting.Keyboard.release_key lib.scripting.Keyboard-class.html#release_key lib.scripting.Keyboard.wait_for_keypress lib.scripting.Keyboard-class.html#wait_for_keypress lib.scripting.Keyboard.__init__ lib.scripting.Keyboard-class.html#__init__ lib.scripting.Mouse lib.scripting.Mouse-class.html lib.scripting.Mouse.wait_for_click lib.scripting.Mouse-class.html#wait_for_click lib.scripting.Mouse.click_relative lib.scripting.Mouse-class.html#click_relative lib.scripting.Mouse.click_absolute lib.scripting.Mouse-class.html#click_absolute lib.scripting.Mouse.click_relative_self lib.scripting.Mouse-class.html#click_relative_self lib.scripting.Mouse.__init__ lib.scripting.Mouse-class.html#__init__ lib.scripting.QtClipboard lib.scripting.QtClipboard-class.html lib.scripting.QtClipboard.get_selection lib.scripting.QtClipboard-class.html#get_selection lib.scripting.QtClipboard.get_clipboard lib.scripting.QtClipboard-class.html#get_clipboard lib.scripting.QtClipboard.fill_clipboard lib.scripting.QtClipboard-class.html#fill_clipboard lib.scripting.QtClipboard.__fillClipboard lib.scripting.QtClipboard-class.html#__fillClipboard lib.scripting.QtClipboard.__getSelection lib.scripting.QtClipboard-class.html#__getSelection lib.scripting.QtClipboard.__getClipboard lib.scripting.QtClipboard-class.html#__getClipboard lib.scripting.QtClipboard.fill_selection lib.scripting.QtClipboard-class.html#fill_selection lib.scripting.QtClipboard.__execAsync lib.scripting.QtClipboard-class.html#__execAsync lib.scripting.QtClipboard.__fillSelection lib.scripting.QtClipboard-class.html#__fillSelection lib.scripting.QtClipboard.__init__ lib.scripting.QtClipboard-class.html#__init__ lib.scripting.QtDialog lib.scripting.QtDialog-class.html lib.scripting.QtDialog.input_dialog lib.scripting.QtDialog-class.html#input_dialog lib.scripting.QtDialog.info_dialog lib.scripting.QtDialog-class.html#info_dialog lib.scripting.QtDialog.list_menu lib.scripting.QtDialog-class.html#list_menu lib.scripting.QtDialog.open_file lib.scripting.QtDialog-class.html#open_file lib.scripting.QtDialog.password_dialog lib.scripting.QtDialog-class.html#password_dialog lib.scripting.QtDialog.list_menu_multi lib.scripting.QtDialog-class.html#list_menu_multi lib.scripting.QtDialog.combo_menu lib.scripting.QtDialog-class.html#combo_menu lib.scripting.QtDialog.save_file lib.scripting.QtDialog-class.html#save_file lib.scripting.QtDialog._run_kdialog lib.scripting.QtDialog-class.html#_run_kdialog lib.scripting.QtDialog.calendar lib.scripting.QtDialog-class.html#calendar lib.scripting.QtDialog.choose_directory lib.scripting.QtDialog-class.html#choose_directory lib.scripting.QtDialog.choose_colour lib.scripting.QtDialog-class.html#choose_colour lib.scripting.Store lib.scripting.Store-class.html lib.scripting.Store.set_global_value lib.scripting.Store-class.html#set_global_value lib.scripting.Store.get_value lib.scripting.Store-class.html#get_value lib.scripting.Store.set_value lib.scripting.Store-class.html#set_value lib.scripting.Store.get_global_value lib.scripting.Store-class.html#get_global_value lib.scripting.Store.remove_value lib.scripting.Store-class.html#remove_value lib.scripting.Store.remove_global_value lib.scripting.Store-class.html#remove_global_value lib.scripting.System lib.scripting.System-class.html lib.scripting.System.exec_command lib.scripting.System-class.html#exec_command lib.scripting.System.create_file lib.scripting.System-class.html#create_file lib.scripting.Window lib.scripting.Window-class.html lib.scripting.Window.wait_for_exist lib.scripting.Window-class.html#wait_for_exist lib.scripting.Window.activate lib.scripting.Window-class.html#activate lib.scripting.Window.move_to_desktop lib.scripting.Window-class.html#move_to_desktop lib.scripting.Window.get_active_geometry lib.scripting.Window-class.html#get_active_geometry lib.scripting.Window.get_active_class lib.scripting.Window-class.html#get_active_class lib.scripting.Window.wait_for_focus lib.scripting.Window-class.html#wait_for_focus lib.scripting.Window._run_wmctrl lib.scripting.Window-class.html#_run_wmctrl lib.scripting.Window.__init__ lib.scripting.Window-class.html#__init__ lib.scripting.Window.resize_move lib.scripting.Window-class.html#resize_move lib.scripting.Window.close lib.scripting.Window-class.html#close lib.scripting.Window.switch_desktop lib.scripting.Window-class.html#switch_desktop lib.scripting.Window.set_property lib.scripting.Window-class.html#set_property lib.scripting.Window.get_active_title lib.scripting.Window-class.html#get_active_title autokey-0.95.10/doc/scripting/class-tree.html000066400000000000000000000141621362232350500210530ustar00rootroot00000000000000 Class Hierarchy
 
[hide private]
[frames] | no frames]
[ Module Hierarchy | Class Hierarchy ]

Class Hierarchy

  • lib.scripting.Engine: Provides access to the internals of AutoKey.
  • lib.scripting.GtkClipboard: Read/write access to the X selection and clipboard - GTK version
  • lib.scripting.GtkDialog: Provides a simple interface for the display of some basic dialogs to collect information from the user.
  • lib.scripting.Keyboard: Provides access to the keyboard for event generation.
  • lib.scripting.Mouse: Provides access to send mouse clicks
  • lib.scripting.QtClipboard: Read/write access to the X selection and clipboard - QT version
  • lib.scripting.QtDialog: Provides a simple interface for the display of some basic dialogs to collect information from the user.
  • lib.scripting.System: Simplified access to some system commands.
  • lib.scripting.Window: Basic window management using wmctrl
  • object: The most base type
    • dict: dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object's (key, value) pairs dict(iterable) -> new dictionary initialized as if via: d = {} for k, v in iterable: d[k] = v dict(**kwargs) -> new dictionary initialized with the name=value pairs in the keyword argument list.
autokey-0.95.10/doc/scripting/crarr.png000066400000000000000000000005241362232350500177370ustar00rootroot00000000000000PNG  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.95.10/doc/scripting/epydoc.css000066400000000000000000000372271362232350500201270ustar00rootroot00000000000000 /* 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.95.10/doc/scripting/epydoc.js000066400000000000000000000245251362232350500177500ustar00rootroot00000000000000function 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.95.10/doc/scripting/frames.html000066400000000000000000000011231362232350500202570ustar00rootroot00000000000000 API Documentation autokey-0.95.10/doc/scripting/help.html000066400000000000000000000247451362232350500177510ustar00rootroot00000000000000 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.95.10/doc/scripting/identifier-index.html000066400000000000000000000721701362232350500222430ustar00rootroot00000000000000 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.95.10/doc/scripting/index.html000066400000000000000000000011231362232350500201110ustar00rootroot00000000000000 API Documentation autokey-0.95.10/doc/scripting/lib.scripting-module.html000066400000000000000000000206541362232350500230460ustar00rootroot00000000000000 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.95.10/doc/scripting/lib.scripting-pysrc.html000066400000000000000000011230671362232350500227240ustar00rootroot00000000000000 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.95.10/doc/scripting/lib.scripting.Engine-class.html000066400000000000000000000537141362232350500240750ustar00rootroot00000000000000 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.95.10/doc/scripting/lib.scripting.GtkClipboard-class.html000066400000000000000000000370021362232350500252250ustar00rootroot00000000000000 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.95.10/doc/scripting/lib.scripting.GtkDialog-class.html000066400000000000000000001004361362232350500245270ustar00rootroot00000000000000 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.95.10/doc/scripting/lib.scripting.Keyboard-class.html000066400000000000000000000430001362232350500244130ustar00rootroot00000000000000 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.95.10/doc/scripting/lib.scripting.Mouse-class.html000066400000000000000000000342451362232350500237560ustar00rootroot00000000000000 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.95.10/doc/scripting/lib.scripting.QtClipboard-class.html000066400000000000000000000371511362232350500250710ustar00rootroot00000000000000 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.95.10/doc/scripting/lib.scripting.QtDialog-class.html000066400000000000000000001220221362232350500243610ustar00rootroot00000000000000 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.95.10/doc/scripting/lib.scripting.Store-class.html000066400000000000000000000425431362232350500237620ustar00rootroot00000000000000 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.95.10/doc/scripting/lib.scripting.System-class.html000066400000000000000000000231731362232350500241500ustar00rootroot00000000000000 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.95.10/doc/scripting/lib.scripting.Window-class.html000066400000000000000000000744711362232350500241420ustar00rootroot00000000000000 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.95.10/doc/scripting/module-tree.html000066400000000000000000000070031362232350500212270ustar00rootroot00000000000000 Module Hierarchy
 
[hide private]
[frames] | no frames]
[ Module Hierarchy | Class Hierarchy ]

Module Hierarchy

autokey-0.95.10/doc/scripting/redirect.html000066400000000000000000000024011362232350500206030ustar00rootroot00000000000000Epydoc 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.95.10/doc/scripting/toc-everything.html000066400000000000000000000042201362232350500217520ustar00rootroot00000000000000 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.95.10/doc/scripting/toc-lib.scripting-module.html000066400000000000000000000037631362232350500236330ustar00rootroot00000000000000 scripting

Module scripting


Classes

Engine
GtkClipboard
GtkDialog
Keyboard
Mouse
QtClipboard
QtDialog
Store
System
Window

Variables

__package__

[hide private] autokey-0.95.10/doc/scripting/toc.html000066400000000000000000000023771362232350500176030ustar00rootroot00000000000000 Table of Contents

Table of Contents


Everything

Modules

lib.scripting

[hide private] autokey-0.95.10/extractDoc.py000066400000000000000000000030701362232350500160220ustar00rootroot00000000000000# -*- 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.95.10/lib/000077500000000000000000000000001362232350500141165ustar00rootroot00000000000000autokey-0.95.10/lib/autokey/000077500000000000000000000000001362232350500155775ustar00rootroot00000000000000autokey-0.95.10/lib/autokey/__init__.py000066400000000000000000000000001362232350500176760ustar00rootroot00000000000000autokey-0.95.10/lib/autokey/common.py000066400000000000000000000062641362232350500174510ustar00rootroot00000000000000#!/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 import dbus.service import logging 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") LOG_FILE = os.path.join(DATA_DIR, "autokey.log") MAX_LOG_SIZE = 5 * 1024 * 1024 # 5 megabytes MAX_LOG_COUNT = 3 LOG_FORMAT = "%(asctime)s %(levelname)s - %(name)s - %(message)s" APP_NAME = "autokey" CATALOG = "" VERSION = "0.95.10" 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 class AppService(dbus.service.Object): def __init__(self, app): busName = dbus.service.BusName('org.autokey.Service', bus=dbus.SessionBus()) dbus.service.Object.__init__(self, busName, "/AppService") self.app = app logging.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) autokey-0.95.10/lib/autokey/configmanager.py000066400000000000000000001204031362232350500207510ustar00rootroot00000000000000# -*- 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 typing import os import os.path import shutil import logging import glob import threading import re from pathlib import Path from autokey import common from autokey.iomediator.constants import X_RECORD_INTERFACE, MODIFIERS from autokey.iomediator import key import json _logger = logging.getLogger("config-manager") 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" IS_FIRST_RUN = "isFirstRun" SERVICE_RUNNING = "serviceRunning" MENU_TAKES_FOCUS = "menuTakesFocus" SHOW_TRAY_ICON = "showTrayIcon" SORT_BY_USAGE_COUNT = "sortByUsageCount" #DETECT_UNWANTED_ABBR = "detectUnwanted" PROMPT_TO_SAVE = "promptToSave" #PREDICTIVE_LENGTH = "predictiveLength" INPUT_SAVINGS = "inputSavings" ENABLE_QT4_WORKAROUND = "enableQT4Workaround" from .configmanager_constants import INTERFACE_TYPE 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" # Added by Trey Blancher (ectospasm) 2015-09-16 TRIGGER_BY_INITIAL = "triggerItemByInitial" SCRIPT_GLOBALS = "scriptGlobals" # TODO - Future functionality #TRACK_RECENT_ENTRY = "trackRecentEntry" #RECENT_ENTRY_COUNT = "recentEntryCount" #RECENT_ENTRY_MINLENGTH = "recentEntryMinLength" #RECENT_ENTRY_SUGGEST = "recentEntrySuggest" # Used to set or retrieve autostart related settings. These settings are separately handled by .desktop files in the # user autostart directory in $XDG_DATA_HOME/autostart, typically ~/.local/share/autostart. AutostartSettings = typing.NamedTuple("AutostartSettings", [ ("desktop_file_name", typing.Optional[str]), ("switch_show_configure", bool) ]) def get_config_manager(autoKeyApp, hadError=False): if not os.path.exists(CONFIG_DEFAULT_FOLDER): os.mkdir(CONFIG_DEFAULT_FOLDER) try: configManager = ConfigManager(autoKeyApp) except Exception as e: if hadError 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 get_config_manager(autoKeyApp, True) _logger.debug("Global settings: %r", ConfigManager.SETTINGS) return configManager 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 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 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 def convert_v07_to_v08(configData): oldVersion = configData["version"] os.rename(CONFIG_FILE, CONFIG_FILE + oldVersion) _logger.info("Converting v%s configuration data to v0.80.0", oldVersion) for folderData in configData["folders"]: _convertFolder(folderData, None) configData["folders"] = [] configData["version"] = common.VERSION configData["settings"][NOTIFICATION_ICON] = common.ICON_FILE_NOTIFICATION # Remove old backup file so we never retry the conversion if os.path.exists(CONFIG_FILE_BACKUP): os.remove(CONFIG_FILE_BACKUP) _logger.info("Conversion succeeded") def _convertFolder(folderData, parent): f = model.Folder("") f.inject_json_data(folderData) f.parent = parent f.persist() for subfolder in folderData["folders"]: _convertFolder(subfolder, f) for itemData in folderData["items"]: i = None if itemData["type"] == "script": i = model.Script("", "") i.code = itemData["code"] elif itemData["type"] == "phrase": i = model.Phrase("", "") i.phrase = itemData["phrase"] if i is not None: i.inject_json_data(itemData) i.parent = f i.persist() def convert_rename_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. """ 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("Migration task: Found old autostart entry: '{}'. Rename to: '{}'".format( old_autostart_file, new_file_name) ) old_autostart_file.rename(new_file_name) 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: {} } 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") myPhrases = model.Folder("My Phrases") myPhrases.set_hotkey([""], "") myPhrases.set_modes([model.TriggerMode.HOTKEY]) myPhrases.persist() f = model.Folder("Addresses") adr = model.Phrase("Home Address", "22 Avenue Street\nBrisbane\nQLD\n4000") adr.set_modes([model.TriggerMode.ABBREVIATION]) adr.add_abbreviation("adr") f.add_item(adr) myPhrases.add_folder(f) f.persist() adr.persist() p = model.Phrase("First phrase", "Test phrase number one!") p.set_modes([model.TriggerMode.PREDICTIVE]) p.set_window_titles(".* - gedit") myPhrases.add_item(p) myPhrases.add_item(model.Phrase("Second phrase", "Test phrase number two!")) myPhrases.add_item(model.Phrase("Third phrase", "Test phrase number three!")) self.folders.append(myPhrases) [p.persist() for p in myPhrases.items] sampleScripts = model.Folder("Sample Scripts") sampleScripts.persist() dte = model.Script("Insert Date", "") dte.code = """output = system.exec_command("date") keyboard.send_keys(output)""" sampleScripts.add_item(dte) lMenu = model.Script("List Menu", "") lMenu.code = """choices = ["something", "something else", "a third thing"] retCode, choice = dialog.list_menu(choices) if retCode == 0: keyboard.send_keys("You chose " + choice)""" sampleScripts.add_item(lMenu) sel = model.Script("Selection Test", "") sel.code = """text = clipboard.get_selection() keyboard.send_key("") keyboard.send_keys("The text %s was here previously" % text)""" sampleScripts.add_item(sel) abbrc = model.Script("Abbreviation from selection", "") abbrc.code = """import time time.sleep(0.25) contents = clipboard.get_selection() retCode, abbr = dialog.input_dialog("New Abbreviation", "Choose an abbreviation for the new phrase") if retCode == 0: if len(contents) > 20: title = contents[0:17] + "..." else: title = contents folder = engine.get_folder("My Phrases") engine.create_abbreviation(folder, title, abbr, contents)""" sampleScripts.add_item(abbrc) phrasec = model.Script("Phrase from selection", "") phrasec.code = """import time time.sleep(0.25) contents = clipboard.get_selection() if len(contents) > 20: title = contents[0:17] + "..." else: title = contents folder = engine.get_folder("My Phrases") engine.create_phrase(folder, title, contents)""" sampleScripts.add_item(phrasec) win = model.Script("Display window info", "") win.code = """# Displays the information of the next window to be left-clicked import time mouse.wait_for_click(1) time.sleep(0.2) winTitle = window.get_active_title() winClass = window.get_active_class() dialog.info_dialog("Window information", "Active window information:\\nTitle: '%s'\\nClass: '%s'" % (winTitle, winClass))""" win.show_in_tray_menu = True sampleScripts.add_item(win) self.folders.append(sampleScripts) [s.persist() for s in sampleScripts.items] # 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 = data["version"] if version < "0.80.0": try: convert_v07_to_v08(data) self.config_altered(True) except Exception as e: _logger.exception("Problem occurred during conversion.") _logger.error("Existing config file has been saved as %s%s", CONFIG_FILE, version) raise if version < "0.95.3": convert_rename_autostart_entries_for_v0_95_3() 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]) for entryPath in glob.glob(CONFIG_DEFAULT_FOLDER + "/*"): if os.path.isdir(entryPath): _logger.debug("Loading folder at '%s'", entryPath) f = model.Folder("", path=entryPath) f.load(None) self.folders.append(f) for folderPath in data["folders"]: f = model.Folder("", path=folderPath) f.load() self.folders.append(f) self.toggleServiceHotkey.load_from_serialized(data["toggleServiceHotkey"]) self.configHotkey.load_from_serialized(data["configHotkey"]) if self.VERSION < self.CLASS_VERSION: self.upgrade() self.config_altered(False) _logger.info("Successfully loaded configuration") 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 = model.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 = model.Phrase("", "", path=path) elif baseName.endswith(".py"): i = model.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 = model.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 upgrade(self): _logger.info("Checking if upgrade is needed from version %s", self.VERSION) # Always reset interface type when upgrading self.SETTINGS[INTERFACE_TYPE] = X_RECORD_INTERFACE _logger.info("Resetting interface type, new type: %s", self.SETTINGS[INTERFACE_TYPE]) if self.VERSION < '0.70.0': _logger.info("Doing upgrade to 0.70.0") for item in self.allItems: if isinstance(item, model.Phrase): item.sendMode = model.SendMode.KEYBOARD if self.VERSION < "0.82.3": self.SETTINGS[WORKAROUND_APP_REGEX] += "|krdc.Krdc" self.workAroundApps = re.compile(self.SETTINGS[WORKAROUND_APP_REGEX]) self.SETTINGS[SCRIPT_GLOBALS] = {} self.VERSION = common.VERSION self.config_altered(True) 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 model.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 model.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 model.TriggerMode.HOTKEY in item.modes: self.hotKeys.append(item) if model.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 = model.Folder(RECENT_ENTRIES_FOLDER) folder.set_hotkey([""], "") folder.set_modes([model.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]: self.recentEntries.pop(0) folder.items = [] for theEntry in self.recentEntries: if len(theEntry) > 17: description = theEntry[:17] + "..." else: description = theEntry p = model.Phrase(description, theEntry) if self.SETTINGS[RECENT_ENTRY_SUGGEST]: p.set_modes([model.TriggerMode.PREDICTIVE]) folder.add_item(p) self.config_altered(False) def check_abbreviation_unique(self, abbreviation, newFilterPattern, targetItem): """ Checks that the given abbreviation is not already in use. @param abbreviation: the abbreviation to check @param newFilterPattern: @param targetItem: the phrase for which the abbreviation to be used """ for item in self.allFolders: if model.TriggerMode.ABBREVIATION in item.modes: if abbreviation in item.abbreviations and item.filter_matches(newFilterPattern): return item is targetItem, item for item in self.allItems: if model.TriggerMode.ABBREVIATION in item.modes: if abbreviation in item.abbreviations and item.filter_matches(newFilterPattern): return item is targetItem, item return True, None """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 """ for item in self.allFolders: if model.TriggerMode.HOTKEY in item.modes: if item.modifiers == modifiers and item.hotKey == hotKey and item.filter_matches(newFilterPattern): return item is targetItem, item for item in self.allItems: if model.TriggerMode.HOTKEY in item.modes: if item.modifiers == modifiers and item.hotKey == hotKey and item.filter_matches(newFilterPattern): return item is targetItem, item for item in self.globalHotkeys: if item.enabled: if item.modifiers == modifiers and item.hotKey == hotKey and item.filter_matches(newFilterPattern): return item is targetItem, item return True, None # This import placed here to prevent circular import conflicts from . import model class GlobalHotkey(model.AbstractHotkey): """ A global application hotkey, configured from the advanced settings dialog. Allows a method call to be attached to the hotkey. """ def __init__(self): model.AbstractHotkey.__init__(self) self.enabled = False self.windowInfoRegex = None self.isRecursive = False self.parent = None def get_serializable(self): d = { "enabled": self.enabled } d.update(model.AbstractHotkey.get_serializable(self)) return d def load_from_serialized(self, data): model.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 model.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.95.10/lib/autokey/configmanager_constants.py000066400000000000000000000000411362232350500230400ustar00rootroot00000000000000INTERFACE_TYPE = "interfaceType" autokey-0.95.10/lib/autokey/gtkapp.py000066400000000000000000000254661362232350500174540ustar00rootroot00000000000000# -*- 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 . from . import common common.USING_QT = False import sys import traceback import os.path import signal import logging import logging.handlers import subprocess import optparse 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") 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 import configmanager as cm PROGRAM_NAME = _("AutoKey") # TODO: where does this _ named function come from? It must be one of those from x import * 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() p = optparse.OptionParser() p.add_option("-l", "--verbose", help="Enable verbose logging", action="store_true", default=False) p.add_option("-c", "--configure", help="Show the configuration window on startup", action="store_true", default=False) options, args = p.parse_args() try: # Create configuration directory if not os.path.exists(common.CONFIG_DIR): os.makedirs(common.CONFIG_DIR) # Create data directory (for log file) if not os.path.exists(common.DATA_DIR): os.makedirs(common.DATA_DIR) # Create run directory (for lock file) if not os.path.exists(common.RUN_DIR): os.makedirs(common.RUN_DIR) # Initialise logger rootLogger = logging.getLogger() if options.verbose: rootLogger.setLevel(logging.DEBUG) handler = logging.StreamHandler(sys.stdout) else: rootLogger.setLevel(logging.INFO) handler = logging.handlers.RotatingFileHandler(common.LOG_FILE, maxBytes=common.MAX_LOG_SIZE, backupCount=common.MAX_LOG_COUNT) handler.setFormatter(logging.Formatter(common.LOG_FORMAT)) rootLogger.addHandler(handler) if self.__verifyNotRunning(): self.__createLockFile() self.initialise(options.configure) except Exception as e: self.show_error_dialog(_("Fatal error starting AutoKey.\n") + str(e)) logging.exception("Fatal error starting AutoKey: " + str(e)) sys.exit(1) def __createLockFile(self): with open(common.LOCK_FILE, "w") as lock_file: lock_file.write(str(os.getpid())) def __verifyNotRunning(self): if os.path.exists(common.LOCK_FILE): pid = Application._read_pid_from_lock_file() # Check that the found PID is running and is autokey with subprocess.Popen(["ps", "-p", pid, "-o", "command"], stdout=subprocess.PIPE) as p: output = p.communicate()[0] if "autokey" in output.decode(): logging.debug("AutoKey is already running as pid %s", pid) bus = dbus.SessionBus() try: dbusService = bus.get_object("org.autokey.Service", "/AppService") dbusService.show_configure(dbus_interface="org.autokey.Service") sys.exit(0) except dbus.DBusException as e: logging.exception("Error communicating with Dbus service") self.show_error_dialog(_("AutoKey is already running as pid %s but is not responding") % pid, str(e)) sys.exit(1) return True @staticmethod def _read_pid_from_lock_file() -> str: with open(common.LOCK_FILE, 'r') as lock_file: return lock_file.read() def initialise(self, configure): logging.info("Initialising application") self.monitor = monitor.FileMonitor(self) self.configManager = cm.get_config_manager(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: logging.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 = common.AppService(self) if configure: self.show_configure() def init_global_hotkeys(self, configManager): logging.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): logging.debug("Created hotkey: %r %s", item.modifiers, item.hotKey) self.service.mediator.interface.grab_hotkey(item) def hotkey_removed(self, item): logging.debug("Removed hotkey: %r %s", item.modifiers, item.hotKey) self.service.mediator.interface.ungrab_hotkey(item) def path_created_or_modified(self, path): time.sleep(0.5) changed = self.configManager.path_created_or_modified(path) if changed and self.configWindow is not None: self.configWindow.config_modified() def path_removed(self, path): time.sleep(0.5) changed = self.configManager.path_removed(path) if changed and self.configWindow is not None: self.configWindow.config_modified() 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): logging.info("Shutting down") self.service.shutdown() self.monitor.stop() Gdk.threads_enter() Gtk.main_quit() Gdk.threads_leave() os.remove(common.LOCK_FILE) logging.debug("All shutdown tasks complete... quitting") def notify_error(self, message): """ Show an error notification popup. @param message: Message to show in the popup """ self.notifier.notify_error(message) 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. """ logging.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): logging.info("Entering main()") Gdk.threads_enter() Gtk.main() Gdk.threads_leave() def show_error_dialog(self, message, details=None): """ Convenience method for showing an error dialog. """ dlg = Gtk.MessageDialog(type=Gtk.MessageType.ERROR, 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 != '': dlg = Gtk.MessageDialog(type=Gtk.MessageType.INFO, buttons=Gtk.ButtonsType.OK, message_format=self.service.scriptRunner.error) self.service.scriptRunner.error = '' # revert the tray icon self.notifier.set_icon(cm.ConfigManager.SETTINGS[cm.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.95.10/lib/autokey/gtkui/000077500000000000000000000000001362232350500167225ustar00rootroot00000000000000autokey-0.95.10/lib/autokey/gtkui/__init__.py000066400000000000000000000000001362232350500210210ustar00rootroot00000000000000autokey-0.95.10/lib/autokey/gtkui/__main__.py000066400000000000000000000002531362232350500210140ustar00rootroot00000000000000import faulthandler faulthandler.enable() from autokey.gtkapp import Application def main(): a = Application() a.main() if __name__ == '__main__': main() autokey-0.95.10/lib/autokey/gtkui/configwindow.py000066400000000000000000001763701362232350500220070ustar00rootroot00000000000000# -*- 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 logging, os, webbrowser, time import threading from gi import require_version require_version('Gtk', '3.0') require_version('GtkSource', '3.0') from gi.repository import Gtk, Pango, GtkSource, Gdk, Gio import locale GETTEXT_DOMAIN = 'autokey' locale.setlocale(locale.LC_ALL, '') #for module in Gtk.glade, gettext: # module.bindtextdomain(GETTEXT_DOMAIN) # module.textdomain(GETTEXT_DOMAIN) from . import dialogs from .settingsdialog import SettingsDialog from .. import configmanager as cm from ..iomediator import Recorder from .. import model, common CONFIG_WINDOW_TITLE = "AutoKey" UI_DESCRIPTION_FILE = os.path.join(os.path.dirname(__file__), "data/menus.xml") _logger = logging.getLogger("configwindow") PROBLEM_MSG_PRIMARY = _("Some problems were found") PROBLEM_MSG_SECONDARY = _("%s\n\nYour changes have not been saved.") from .configwindow0 import get_ui # 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 def set_linkbutton(button, path): button.set_sensitive(True) if path.startswith(cm.CONFIG_DEFAULT_FOLDER): text = path.replace(cm.CONFIG_DEFAULT_FOLDER, _("(Default folder)")) else: text = path.replace(os.path.expanduser("~"), "~") button.set_label(text) button.set_uri("file://" + path) label = button.get_child() 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 model.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 model.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 model.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) 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 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 # Validate ret = [] configManager = self.parentWindow.app.configManager for abbr in abbreviations: unique, conflicting = configManager.check_abbreviation_unique(abbr, filterExpression, self.currentItem) if not unique: msg = _("The abbreviation '%s' is already in use by the %s") % (abbr, str(conflicting)) f = conflicting.get_applicable_regex() if f is not None: msg += _(" for windows matching '%s'.") % f.pattern ret.append(msg) unique, conflicting = configManager.check_hotkey_unique(modifiers, key, filterExpression, self.currentItem) if not unique: msg = _("The hotkey '%s' is already in use by the %s") % (conflicting.get_hotkey_string(), str(conflicting)) f = conflicting.get_applicable_regex() if f is not None: msg += _(" for windows matching '%s'.") % f.pattern ret.append(msg) return ret # ---- 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") label = self.linkButton.get_child() label.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)")) else: set_linkbutton(self.linkButton, self.currentFolder.path) 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.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") label = self.linkButton.get_child() label.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("classic")) 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)")) else: set_linkbutton(self.linkButton, self.currentItem.path) 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 = Recorder(self) dlg = dialogs.RecordDialog(self.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") vbox = builder.get_object("settingsVbox") self.settingsWidget = SettingsWidget(parentWindow) vbox.pack_start(self.settingsWidget.ui, False, False, 0) # Populate combo l = list(model.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)")) else: set_linkbutton(self.linkButton, self.currentItem.path) l = list(model.SEND_MODES.keys()) l.sort() for k, v in model.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 = model.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 = 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), #("Settings", None, _("_Settings"), None, None, None), #("advanced", Gtk.STOCK_PREFERENCES, _("_Advanced Settings"), "", _("Advanced configuration options"), self.on_advanced_settings), ("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.SHOW_TOOLBAR]) toolbar.get_style_context().add_class(Gtk.STYLE_CLASS_PRIMARY_TOOLBAR) 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_iter(rootIter) self.on_tree_selection_changed(self.treeView) self.treeView.columns_autosize() width, height = cm.ConfigManager.SETTINGS[cm.WINDOW_DEFAULT_SIZE] self.set_default_size(width, height) self.hpaned.set_position(cm.ConfigManager.SETTINGS[cm.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): if not self.__warnedOfChanges: Gdk.threads_enter() msg = _("Changes made in other programs will not be displayed until you\ close and reopen the AutoKey window.\nThis message is only shown once per session.") dlg = Gtk.MessageDialog(self.ui, type=Gtk.MessageType.QUESTION, buttons=Gtk.ButtonsType.OK, message_format= _("Configuration has been changed on disk.")) dlg.format_secondary_text(msg) dlg.run() dlg.destroy() Gdk.threads_leave() self.__warnedOfChanges = True def update_actions(self, items, changed): if len(items) == 0: canCreate = False canCopy = False canRecord = False canMacro = False canPlay = False enableAny = False else: canCreate = isinstance(items[0], model.Folder) and len(items) == 1 canCopy = True canRecord = (not isinstance(items[0], model.Folder)) and len(items) == 1 canMacro = isinstance(items[0], model.Phrase) and len(items) == 1 canPlay = isinstance(items[0], model.Script) and len(items) == 1 enableAny = True for item in items: if isinstance(item, model.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) 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_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 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.WINDOW_DEFAULT_SIZE] = self.get_size() cm.ConfigManager.SETTINGS[cm.HPANE_POSITION] = self.hpaned.get_position() 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.WINDOW_DEFAULT_SIZE] = self.get_size() cm.ConfigManager.SETTINGS[cm.HPANE_POSITION] = self.hpaned.get_position() 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, model.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 = model.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 = model.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 = model.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, model.Phrase): newObj = model.Phrase('', '') else: newObj = model.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, model.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, model.Phrase): newObj = model.Phrase('', '') else: newObj = model.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, model.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): if model.TriggerMode.HOTKEY in theItem.modes: self.app.hotkey_removed(theItem) if isinstance(theItem, model.Folder): for subFolder in theItem.folders: self.__deleteHotkeys(subFolder) for item in theItem.items: if model.TriggerMode.HOTKEY in item.modes: self.app.hotkey_removed(item) 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.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) # 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, model.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): 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() 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] if isinstance(selectedObject, model.Folder): self.stack.set_current_page(1) self.folderPage.load(selectedObject) elif isinstance(selectedObject, model.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, model.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, model.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, model.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, model.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) 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.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, model.Folder): return self.folderPage elif isinstance(self.selectedObject, model.Phrase): return self.phrasePage elif isinstance(self.selectedObject, model.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, model.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, model.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, model.Folder) and (isinstance(item2, model.Phrase) or isinstance(item2, model.Script)): return -1 elif isinstance(item2, model.Folder) and (isinstance(item1, model.Phrase) or isinstance(item1, model.Script)): return 1 else: # return cmp(str(item1), str(item2)) a, b = str(item1), str(item2) return (a > b) - (a < b) autokey-0.95.10/lib/autokey/gtkui/configwindow0.py000066400000000000000000000003521362232350500220510ustar00rootroot00000000000000import 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.95.10/lib/autokey/gtkui/data/000077500000000000000000000000001362232350500176335ustar00rootroot00000000000000autokey-0.95.10/lib/autokey/gtkui/data/abbrsettings.xml000066400000000000000000000424341362232350500230530ustar00rootroot00000000000000 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.95.10/lib/autokey/gtkui/data/autokey.svg000066400000000000000000000322171362232350500220420ustar00rootroot00000000000000 image/svg+xml autokey-0.95.10/lib/autokey/gtkui/data/blankpage.xml000066400000000000000000000010541362232350500223010ustar00rootroot00000000000000 True 0 0 5 5 True autokey-0.95.10/lib/autokey/gtkui/data/detectdialog.xml000066400000000000000000000260621362232350500230130ustar00rootroot00000000000000 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.95.10/lib/autokey/gtkui/data/folderpage.xml000066400000000000000000000112071362232350500224660ustar00rootroot00000000000000 True False 0 0 5 5 True False (Unsaved) False True True True True False none 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.95.10/lib/autokey/gtkui/data/hotkeysettings.xml000066400000000000000000000257261362232350500234550ustar00rootroot00000000000000 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 Shift False True True False False False True 2 Super False True True False False False True 3 Hyper False True True False False False True 4 Meta False True True False False False True 5 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.95.10/lib/autokey/gtkui/data/mainwindow.xml000066400000000000000000000073221362232350500225350ustar00rootroot00000000000000 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.95.10/lib/autokey/gtkui/data/menus.xml000066400000000000000000000050111362232350500215010ustar00rootroot00000000000000 autokey-0.95.10/lib/autokey/gtkui/data/phrasepage.xml000066400000000000000000000170411362232350500224770ustar00rootroot00000000000000 True False 5 5 True False 5 (Unsaved) False True True True True False none 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.95.10/lib/autokey/gtkui/data/recorddialog.xml000066400000000000000000000214651362232350500230230ustar00rootroot00000000000000 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.95.10/lib/autokey/gtkui/data/renamedialog.xml000066400000000000000000000155571362232350500230210ustar00rootroot00000000000000 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.95.10/lib/autokey/gtkui/data/scriptpage.xml000066400000000000000000000142611362232350500225220ustar00rootroot00000000000000 True False 5 5 True False 5 (Unsaved) False True True True True False none 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.95.10/lib/autokey/gtkui/data/settingsdialog.xml000066400000000000000000001134751362232350500234100ustar00rootroot00000000000000 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 <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.95.10/lib/autokey/gtkui/data/settingswidget.xml000066400000000000000000000233711362232350500234270ustar00rootroot00000000000000 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.95.10/lib/autokey/gtkui/data/windowfiltersettings.xml000066400000000000000000000200121362232350500246460ustar00rootroot00000000000000 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.95.10/lib/autokey/gtkui/dialogs.py000066400000000000000000000531431362232350500207240ustar00rootroot00000000000000# -*- 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 re from gi.repository import Gtk, Gdk #import gettext import locale GETTEXT_DOMAIN = 'autokey' locale.setlocale(locale.LC_ALL, '') #for module in Gtk.glade, gettext: # module.bindtextdomain(GETTEXT_DOMAIN) # module.textdomain(GETTEXT_DOMAIN) __all__ = ["validate", "EMPTY_FIELD_REGEX", "AbbrSettingsDialog", "HotkeySettingsDialog", "WindowFilterSettingsDialog", "RecordDialog"] from .. import model, iomediator from ..iomediator.key import Key # from . import configwindow from .configwindow0 import get_ui WORD_CHAR_OPTIONS = { "All non-word": model.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 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 model.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(model.extract_wordchars(wordCharRegex)) self.wordCharCombo.set_active(len(WORD_CHAR_OPTIONS)) if isinstance(item, model.Folder): self.omitTriggerCheckbox.hide() else: self.omitTriggerCheckbox.show() self.omitTriggerCheckbox.set_active(item.omitTrigger) if isinstance(item, model.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(model.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(model.make_wordchar_re(option)) if not isinstance(item, model.Folder): item.omitTrigger = self.omitTriggerCheckbox.get_active() if isinstance(item, model.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.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") DialogBase.__init__(self) def load(self, item): self.targetItem = item self.setButton.set_sensitive(True) if model.TriggerMode.HOTKEY in item.modes: self.controlButton.set_active(Key.CONTROL in item.modifiers) self.altButton.set_active(Key.ALT in item.modifiers) self.shiftButton.set_active(Key.SHIFT in item.modifiers) self.superButton.set_active(Key.SUPER in item.modifiers) self.hyperButton.set_active(Key.HYPER in item.modifiers) self.metaButton.set_active(Key.META in item.modifiers) key = item.hotKey if key in self.KEY_MAP: keyText = self.KEY_MAP[key] else: keyText = key self._setKeyLabel(keyText) self.key = keyText else: self.reset() def save(self, item): item.modes.append(model.TriggerMode.HOTKEY) # Build modifier list modifiers = self.build_modifiers() keyText = self.key if keyText in self.REVERSE_KEY_MAP: key = self.REVERSE_KEY_MAP[keyText] else: key = keyText assert key is not None, "Attempt to set hotkey with no key" item.set_hotkey(modifiers, key) def reset(self): self.controlButton.set_active(False) self.altButton.set_active(False) self.shiftButton.set_active(False) self.superButton.set_active(False) self.hyperButton.set_active(False) self.metaButton.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.controlButton.set_active(Key.CONTROL in modifiers) self.altButton.set_active(Key.ALT in modifiers) self.shiftButton.set_active(Key.SHIFT in modifiers) self.superButton.set_active(Key.SUPER in modifiers) self.hyperButton.set_active(Key.HYPER in modifiers) self.metaButton.set_active(Key.META in 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 = [] if self.controlButton.get_active(): modifiers.append(Key.CONTROL) if self.altButton.get_active(): modifiers.append(Key.ALT) if self.shiftButton.get_active(): modifiers.append(Key.SHIFT) if self.superButton.get_active(): modifiers.append(Key.SUPER) if self.hyperButton.get_active(): modifiers.append(Key.HYPER) if self.metaButton.get_active(): modifiers.append(Key.META) modifiers.sort() return modifiers def _setKeyLabel(self, key): self.keyLabel.set_text(_("Key: ") + key) def valid(self): if not validate(self.key is not None, _("You must specify a key for the hotkey."), None, self.ui): return False return True def on_setButton_pressed(self, widget, data=None): self.setButton.set_sensitive(False) self.keyLabel.set_text(_("Press a key...")) self.grabber = iomediator.KeyGrabber(self) self.grabber.start() class GlobalHotkeyDialog(HotkeySettingsDialog): def load(self, item): self.targetItem = item if item.enabled: self.controlButton.set_active(Key.CONTROL in item.modifiers) self.altButton.set_active(Key.ALT in item.modifiers) self.shiftButton.set_active(Key.SHIFT in item.modifiers) self.superButton.set_active(Key.SUPER in item.modifiers) self.hyperButton.set_active(Key.HYPER in item.modifiers) self.metaButton.set_active(Key.META in item.modifiers) key = item.hotKey if key in self.KEY_MAP: keyText = self.KEY_MAP[key] else: keyText = key self._setKeyLabel(keyText) self.key = keyText else: self.reset() def save(self, item): # Build modifier list modifiers = self.build_modifiers() keyText = self.key if keyText in self.REVERSE_KEY_MAP: key = self.REVERSE_KEY_MAP[keyText] else: key = keyText assert key is not None, "Attempt to set hotkey with no key" item.set_hotkey(modifiers, key) 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 validate( self.key is not None, _("You must specify a key for the hotkey."), None, self.ui): 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, model.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): try: item.set_window_titles(self.get_filter_text()) except re.error as e: # TODO warn the user somehow. Currently just ignores the regex # instead of saving it. pass item.set_filter_recursive(self.get_is_recursive()) 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 = iomediator.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.95.10/lib/autokey/gtkui/notifier.py000066400000000000000000000212561362232350500211210ustar00rootroot00000000000000# -*- 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 gi gi.require_version('Gtk', '3.0') gi.require_version('Notify', '0.7') gi.require_version('AppIndicator3', '0.1') from gi.repository import Gtk, Gdk, Notify, AppIndicator3 import gettext from . import popupmenu from .. import configmanager as cm 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") self.app = autokeyApp self.configManager = autokeyApp.service.configManager self.indicator = AppIndicator3.Indicator.new("AutoKey", cm.ConfigManager.SETTINGS[cm.NOTIFICATION_ICON], AppIndicator3.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.SHOW_TRAY_ICON]: self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE) else: self.indicator.set_status(AppIndicator3.IndicatorStatus.PASSIVE) def hide_icon(self): self.indicator.set_status(AppIndicator3.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): self.show_notify(message, Gtk.STOCK_DIALOG_ERROR) self.errorItem.show() self.indicator.set_status(AppIndicator3.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. So open the main window first to circumvent issues. # TODO: If the GTK GUI gets rewritten to not always destroy it’s UI, remove this comment and workaround. self.on_show_configure(widget, data) self.app.show_script_error(self.app.configWindow.ui) 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(AppIndicator3.IndicatorStatus.PASSIVE) cm.ConfigManager.SETTINGS[cm.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.95.10/lib/autokey/gtkui/popupmenu.py000066400000000000000000000111651362232350500213300ustar00rootroot00000000000000# -*- 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, logging from gi.repository import Gtk, Gdk from .. import configmanager as cm _logger = logging.getLogger("phrase-menu") 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 if cm.ConfigManager.SETTINGS[cm.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 cm.ConfigManager.SETTINGS[cm.TRIGGER_BY_INITIAL]: _logger.debug("Triggering menu item by first initial") self.triggerInitial = 1 else: _logger.debug("Triggering menu item by position in list") self.triggerInitial = 0 if len(folders) == 1 and not items and onDesktop: # Only one folder - create menu with just its folders and items for folder in folders[0].folders: menuItem = Gtk.MenuItem(label=self.__getMnemonic(folder.title, onDesktop)) menuItem.set_submenu(PopupMenu(service, folder.folders, folder.items, onDesktop)) menuItem.set_use_underline(True) self.append(menuItem) if len(folders[0].folders) > 0: self.append(Gtk.SeparatorMenuItem()) self.__addItemsToSelf(folders[0].items, service, onDesktop) else: # Create phrase folder section for folder in folders: menuItem = Gtk.MenuItem(label=self.__getMnemonic(folder.title, onDesktop)) menuItem.set_submenu(PopupMenu(service, folder.folders, folder.items, False)) menuItem.set_use_underline(True) self.append(menuItem) if len(folders) > 0: self.append(Gtk.SeparatorMenuItem()) self.__addItemsToSelf(items, service, onDesktop) self.show_all() 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, service, onDesktop): # Create phrase section if cm.ConfigManager.SETTINGS[cm.SORT_BY_USAGE_COUNT]: items.sort(key=lambda obj: obj.usageCount, reverse=True) else: items.sort(key=lambda obj: str(obj)) i = 1 for item in items: #if onDesktop: # menuItem = Gtk.MenuItem(item.get_description(service.lastStackState), False) #else: 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.95.10/lib/autokey/gtkui/settingsdialog.py000066400000000000000000000227521362232350500223240ustar00rootroot00000000000000# -*- 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 from autokey import configmanager as cm, common from autokey.iomediator.key import Key from .dialogs import GlobalHotkeyDialog from .configwindow0 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.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.SHOW_TRAY_ICON]) self.iconStyleCombo.set_active(ICON_NAME_LIST.index(cm.ConfigManager.SETTINGS[cm.NOTIFICATION_ICON])) self.autoStartCheckbox.set_active(cm.get_autostart().desktop_file_name is not None) self.autosaveCheckbox.set_active(not cm.ConfigManager.SETTINGS[cm.PROMPT_TO_SAVE]) self.showTrayCheckbox.set_active(cm.ConfigManager.SETTINGS[cm.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.TRIGGER_BY_INITIAL]) self.sortByUsageCheckbox.set_active(cm.ConfigManager.SETTINGS[cm.SORT_BY_USAGE_COUNT]) self.enableUndoCheckbox.set_active(cm.ConfigManager.SETTINGS[cm.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(): cm.set_autostart_entry(cm.AutostartSettings("autokey-gtk.desktop", False)) else: cm.delete_autostart_entry() cm.ConfigManager.SETTINGS[cm.PROMPT_TO_SAVE] = not self.autosaveCheckbox.get_active() cm.ConfigManager.SETTINGS[cm.SHOW_TRAY_ICON] = self.showTrayCheckbox.get_active() #cm.ConfigManager.SETTINGS[MENU_TAKES_FOCUS] = self.allowKbNavCheckbox.get_active() cm.ConfigManager.SETTINGS[cm.SORT_BY_USAGE_COUNT] = self.sortByUsageCheckbox.get_active() # Added by Trey Blancher (ectospasm) 2015-09-16 cm.ConfigManager.SETTINGS[cm.TRIGGER_BY_INITIAL] = self.triggerItemByInitial.get_active() cm.ConfigManager.SETTINGS[cm.UNDO_USING_BACKSPACE] = self.enableUndoCheckbox.get_active() cm.ConfigManager.SETTINGS[cm.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.95.10/lib/autokey/interface.py000066400000000000000000001524541362232350500201240ustar00rootroot00000000000000# -*- 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"] from abc import abstractmethod import typing import threading import select import logging import queue import subprocess import time if typing.TYPE_CHECKING: from autokey.iomediator import IoMediator from autokey import model # 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 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 = logging.getLogger("interface") 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) 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 logging.getLogger().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: model.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 is None: 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 is None: 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: model.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.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: 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 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) 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.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.exception("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.iomediator.constants import MODIFIERS from autokey.iomediator.key import Key from autokey import 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.95.10/lib/autokey/iomediator/000077500000000000000000000000001362232350500177335ustar00rootroot00000000000000autokey-0.95.10/lib/autokey/iomediator/__init__.py000066400000000000000000000015331362232350500220460ustar00rootroot00000000000000# 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 . """ IoMediator public interface """ from ._windowgrabber import WindowGrabber from ._waiter import Waiter from ._keygrabber import Recorder, KeyGrabber from ._iomediator import IoMediator autokey-0.95.10/lib/autokey/iomediator/_iomediator.py000066400000000000000000000221211362232350500225760ustar00rootroot00000000000000import threading import queue import logging from ..configmanager import ConfigManager from ..configmanager_constants import INTERFACE_TYPE from ..interface import XRecordInterface, AtSpiInterface from autokey.model import SendMode from .key import Key from .constants import X_RECORD_INTERFACE, KEY_SPLIT_RE, MODIFIERS, HELD_MODIFIERS CURRENT_INTERFACE = None _logger = logging.getLogger("iomediator") 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] # 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, keyCode, 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((keyCode, window_info)) def run(self): while True: keyCode, window_info = self.queue.get() if keyCode is None and window_info is None: break numLock = self.modifiers[Key.NUMLOCK] modifiers = self.__getModifiersOn() shifted = self.modifiers[Key.CAPSLOCK] ^ self.modifiers[Key.SHIFT] key = self.interface.lookup_string(keyCode, shifted, numLock, self.modifiers[Key.ALT_GR]) rawKey = self.interface.lookup_string(keyCode, False, False, False) for target in self.listeners: target.handle_keypress(rawKey, modifiers, key, window_info) self.queue.task_done() def handle_mouse_click(self, rootX, rootY, relX, relY, button, windowInfo): for target in self.listeners: target.handle_mouseclick(rootX, rootY, relX, relY, button, windowInfo) # 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.__clearModifiers() 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.__reapplyModifiers() def paste_string(self, string, pasteCommand: SendMode): if len(string) > 0: _logger.debug("Send via clipboard") self.interface.send_string_clipboard(string, pasteCommand) 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, keyName): keyName = keyName.replace('\n', "") self.interface.send_key(keyName) def press_key(self, keyName): keyName = keyName.replace('\n', "") self.interface.fake_keydown(keyName) def release_key(self, keyName): keyName = keyName.replace('\n', "") self.interface.fake_keyup(keyName) def fake_keypress(self, keyName): keyName = keyName.replace('\n', "") self.interface.fake_keypress(keyName) 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 __clearModifiers(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 __reapplyModifiers(self): for modifier in self.releasedModifiers: self.interface.press_key(modifier) def __getModifiersOn(self): modifiers = [] for modifier in HELD_MODIFIERS: if self.modifiers[modifier]: modifiers.append(modifier) modifiers.sort() return modifiers autokey-0.95.10/lib/autokey/iomediator/_keygrabber.py000066400000000000000000000071631362232350500225700ustar00rootroot00000000000000import datetime import time from .constants import MODIFIERS from ._iomediator import IoMediator from .key import Key from . import _iomediator class KeyGrabber: """ Keygrabber used by the hotkey settings dialog to grab the key pressed """ def __init__(self, parent): self.targetParent = parent def start(self): # In QT version, sometimes the mouseclick 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, rawKey, modifiers, key, *args): if rawKey not in MODIFIERS: IoMediator.listeners.remove(self) self.targetParent.set_key(rawKey, modifiers) _iomediator.CURRENT_INTERFACE.ungrab_keyboard() def handle_mouseclick(self, rootX, rootY, relX, relY, button, windowInfo): IoMediator.listeners.remove(self) _iomediator.CURRENT_INTERFACE.ungrab_keyboard() self.targetParent.cancel_grab() class Recorder(KeyGrabber): """ Recorder used by the record macro functionality """ def __init__(self, parent): KeyGrabber.__init__(self, parent) self.insideKeys = False def start(self, delay): time.sleep(0.1) IoMediator.listeners.append(self) self.targetParent.start_record() self.startTime = time.time() self.delay = delay self.delayFinished = False def start_withgrab(self): time.sleep(0.1) IoMediator.listeners.append(self) self.targetParent.start_record() self.startTime = time.time() self.delay = 0 self.delayFinished = True _iomediator.CURRENT_INTERFACE.grab_keyboard() def stop(self): if self in IoMediator.listeners: IoMediator.listeners.remove(self) if self.insideKeys: self.targetParent.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.targetParent.end_key_sequence() self.insideKeys = False def set_record_keyboard(self, doIt): self.recordKeyboard = doIt def set_record_mouse(self, doIt): self.recordMouse = doIt def __delayPassed(self): if not self.delayFinished: now = time.time() delta = datetime.datetime.utcfromtimestamp(now - self.startTime) self.delayFinished = (delta.second > self.delay) return self.delayFinished def handle_keypress(self, rawKey, modifiers, key, *args): if self.recordKeyboard and self.__delayPassed(): if not self.insideKeys: self.insideKeys = True self.targetParent.start_key_sequence() modifierCount = len(modifiers) if modifierCount > 1 or (modifierCount == 1 and Key.SHIFT not in modifiers) or \ (Key.SHIFT in modifiers and len(rawKey) > 1): self.targetParent.append_hotkey(rawKey, modifiers) elif key not in MODIFIERS: self.targetParent.append_key(key) def handle_mouseclick(self, rootX, rootY, relX, relY, button, windowInfo): if self.recordMouse and self.__delayPassed(): if self.insideKeys: self.insideKeys = False self.targetParent.end_key_sequence() self.targetParent.append_mouseclick(relX, relY, button, windowInfo[0]) autokey-0.95.10/lib/autokey/iomediator/_waiter.py000066400000000000000000000015531362232350500217430ustar00rootroot00000000000000import threading from ._iomediator import IoMediator class Waiter: """ Waits for a specified event to occur """ def __init__(self, rawKey, modifiers, button, timeOut): IoMediator.listeners.append(self) self.rawKey = rawKey self.modifiers = modifiers self.button = button self.event = threading.Event() self.timeOut = timeOut if modifiers is not None: self.modifiers.sort() def wait(self): return self.event.wait(self.timeOut) def handle_keypress(self, rawKey, modifiers, key, *args): if rawKey == self.rawKey and modifiers == self.modifiers: IoMediator.listeners.remove(self) self.event.set() def handle_mouseclick(self, rootX, rootY, relX, relY, button, windowInfo): if button == self.button: self.event.set() autokey-0.95.10/lib/autokey/iomediator/_windowgrabber.py000066400000000000000000000024161362232350500233030ustar00rootroot00000000000000# -*- 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, rawKey, modifiers, key, *args): pass def handle_mouseclick(self, rootX, rootY, relX, relY, button, windowInfo): IoMediator.listeners.remove(self) self.dialog.receive_window_info(windowInfo) autokey-0.95.10/lib/autokey/iomediator/constants.py000066400000000000000000000004641362232350500223250ustar00rootroot00000000000000import re from .key import Key X_RECORD_INTERFACE = "XRecord" 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, Key.SUPER, Key.SHIFT, Key.HYPER, Key.META] autokey-0.95.10/lib/autokey/iomediator/key.py000066400000000000000000000060501362232350500210760ustar00rootroot00000000000000# 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) autokey-0.95.10/lib/autokey/macro.py000066400000000000000000000104271362232350500172560ustar00rootroot00000000000000import datetime from abc import abstractmethod from autokey.iomediator.constants import KEY_SPLIT_RE from autokey.iomediator.key import Key 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 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()) 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 def process_expansion(self, expansion): parts = KEY_SPLIT_RE.split(expansion.string) for macro in self.macros: macro.process(parts) expansion.string = ''.join(parts) class AbstractMacro: 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 _can_process(self, token): if KEY_SPLIT_RE.match(token): return token[1:-1].split(' ', 1)[0] == self.ID else: return False def _get_args(self, token): l = token[:-1].split(' ') ret = {} if len(l) > 1: for arg in l[1:]: key, val = arg.split('=', 1) ret[key] = val for k, v in self.ARGS: if k not in ret: raise Exception("Missing mandatory argument '{}' for macro '{}'".format(k, self.ID)) return ret def process(self, parts): for i in range(len(parts)): if self._can_process(parts[i]): self.do_process(parts, i) @abstractmethod def do_process(self, parts, i): pass class CursorMacro(AbstractMacro): ID = "cursor" TITLE = _("Position cursor") ARGS = [] def do_process(self, parts, i): try: lefts = len(''.join(parts[i+1:])) parts.append(Key.LEFT * lefts) parts[i] = '' except IndexError: pass 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, parts, i): args = self._get_args(parts[i]) self.engine.run_script_from_macro(args) parts[i] = self.engine.get_return_value() class DateMacro(AbstractMacro): ID = "date" TITLE = _("Insert date") ARGS = [("format", _("Format"))] def do_process(self, parts, i): format_ = self._get_args(parts[i])["format"] date = datetime.datetime.now().strftime(format_) parts[i] = date class FileContentsMacro(AbstractMacro): ID = "file" TITLE = _("Insert file contents") ARGS = [("name", _("File name"))] def do_process(self, parts, i): name = self._get_args(parts[i])["name"] with open(name, "r") as inputFile: parts[i] = inputFile.read() autokey-0.95.10/lib/autokey/model.py000066400000000000000000001176131362232350500172620ustar00rootroot00000000000000# -*- 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 errno import re import os import os.path import glob import logging import json import typing import enum from autokey import configmanager as cm from autokey.iomediator.key import Key, NAVIGATION_KEYS from autokey.iomediator.constants import KEY_SPLIT_RE from autokey.scripting_Store import Store _logger = logging.getLogger("model") 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 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] 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 # type: Item ): 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): self.abbreviations.append(abbr) 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:] 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): if otherFilter is None or self.get_applicable_regex() is None: return True return otherFilter == self.get_applicable_regex().pattern 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 class AbstractHotkey(AbstractWindowFilter): def __init__(self): self.modifiers = [] self.hotKey = None 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 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 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 = [] # type: typing.List[Item] 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 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.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 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) 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.omitTrigger = False self.matchCase = False self.parent = None self.show_in_tray_menu = False self.sendMode = SendMode.KEYBOARD 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 if CURSOR_POSITION_TOKEN in expansion.string: # TODO: Undefined local variable? 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 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: Folder): 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 + "')" Item = typing.Union[Folder, Phrase, Script] autokey-0.95.10/lib/autokey/monitor.py000066400000000000000000000104261362232350500176430ustar00rootroot00000000000000#!/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 logging import os.path import time from pyinotify import WatchManager, Notifier, EventsCodes, ProcessEvent _logger = logging.getLogger("inotify") 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.95.10/lib/autokey/qtapp.py000066400000000000000000000333641362232350500173070ustar00rootroot00000000000000#!/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 . from . import common common.USING_QT = True import sys import os.path import logging import logging.handlers import subprocess import queue import time import dbus import argparse 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 from autokey import service, monitor 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 import configmanager as cm from autokey.qtui.dbus_service import AppService 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 ) def generate_argument_parser() -> argparse.ArgumentParser: 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" ) return parser 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() parser = generate_argument_parser() self.args = parser.parse_args() try: self._create_storage_directories() self._configure_root_logger() except Exception as e: logging.exception("Fatal error starting AutoKey: " + str(e)) self.show_error_dialog("Fatal error starting AutoKey.", str(e)) sys.exit(1) logging.info("Initialising application") self.setWindowIcon(QIcon.fromTheme(common.ICON_FILE, ui_common.load_icon(ui_common.AutoKeyIcon.AUTOKEY))) try: # Initialise logger if self._verify_not_running(): self._create_lock_file() self.monitor = monitor.FileMonitor(self) self.configManager = cm.get_config_manager(self) self.service = service.Service(self) self.serviceDisabled = False self._try_start_service() self.notifier = Notifier(self) self.configWindow = ConfigWindow(self) self.monitor.start() # Initialise user code dir if self.configManager.userCodeDir is not None: sys.path.append(self.configManager.userCodeDir) logging.debug("Creating DBus service") self.dbus_service = AppService(self) logging.debug("Service created") self.show_configure_signal.connect(self.show_configure, Qt.QueuedConnection) if cm.ConfigManager.SETTINGS[cm.IS_FIRST_RUN]: cm.ConfigManager.SETTINGS[cm.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: logging.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: logging.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)) def _configure_root_logger(self): """Initialise logging system""" root_logger = logging.getLogger() root_logger.setLevel(logging.DEBUG) if self.args.verbose: handler = logging.StreamHandler(sys.stdout) else: handler = logging.handlers.RotatingFileHandler( common.LOG_FILE, maxBytes=common.MAX_LOG_SIZE, backupCount=common.MAX_LOG_COUNT ) handler.setLevel(logging.INFO) handler.setFormatter(logging.Formatter(common.LOG_FORMAT)) root_logger.addHandler(handler) @staticmethod def _create_storage_directories(): """Create various storage directories, if those do not exist.""" # Create configuration directory if not os.path.exists(common.CONFIG_DIR): os.makedirs(common.CONFIG_DIR) # Create data directory (for log file) if not os.path.exists(common.DATA_DIR): os.makedirs(common.DATA_DIR) # Create run directory (for lock file) if not os.path.exists(common.RUN_DIR): os.makedirs(common.RUN_DIR) @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 os.path.exists(common.LOCK_FILE): 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: logging.exception("AutoKey pid file contains garbage instead of a usable process id: " + pid) sys.exit(1) # Check that the found PID is running and is autokey with subprocess.Popen(["ps", "-p", pid, "-o", "command"], stdout=subprocess.PIPE) as p: output = p.communicate()[0].decode() if "autokey" in output: logging.debug("AutoKey is already running as pid " + pid) 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: logging.exception("Error communicating with Dbus service") self.show_error_dialog( message="AutoKey is already running as pid {} but is not responding".format(pid), details=str(e)) sys.exit(1) return True def init_global_hotkeys(self, configManager): logging.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): logging.debug("Created hotkey: %r %s", item.modifiers, item.hotKey) self.service.mediator.interface.grab_hotkey(item) def hotkey_removed(self, item): logging.debug("Removed hotkey: %r %s", item.modifiers, item.hotKey) self.service.mediator.interface.ungrab_hotkey(item) def path_created_or_modified(self, path): time.sleep(0.5) changed = self.configManager.path_created_or_modified(path) if changed and self.configWindow is not None: self.configWindow.config_modified() def path_removed(self, path): time.sleep(0.5) changed = self.configManager.path_removed(path) if changed and self.configWindow is not None: self.configWindow.config_modified() 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. """ logging.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? logging.debug("All shutdown tasks complete... quitting") def notify_error(self, message): """ Show an error notification popup. @param message: Message to show in the popup """ self.exec_in_main(self.notifier.notify_error, message) 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. """ logging.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 message_box = QMessageBox( QMessageBox.Critical, "Error", message, QMessageBox.Ok, None ) if details: message_box.setDetailedText(details) message_box.exec_() def show_script_error(self): """ Show the last script error (if any) """ # TODO: i18n if self.service.scriptRunner.error: details = self.service.scriptRunner.error self.service.scriptRunner.error = '' else: details = "No error information available" QMessageBox.information(None, "View Script Error Details", details) 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: logging.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.95.10/lib/autokey/qtui/000077500000000000000000000000001362232350500165615ustar00rootroot00000000000000autokey-0.95.10/lib/autokey/qtui/__init__.py000066400000000000000000000000001362232350500206600ustar00rootroot00000000000000autokey-0.95.10/lib/autokey/qtui/__main__.py000066400000000000000000000025551362232350500206620ustar00rootroot00000000000000# 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.95.10/lib/autokey/qtui/autokey_treewidget.py000066400000000000000000000161761362232350500230520ustar00rootroot00000000000000# 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 from autokey import model 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: model.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: model.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: model.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[model.Folder, model.Phrase, model.Script] ItemWidgetType = Union[FolderWidgetItem, PhraseWidgetItem, ScriptWidgetItem] class WidgetItemFactory: def __init__(self, root_folders: List[model.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: model.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, model.Folder): return FolderWidgetItem(parent, item) elif isinstance(item, model.Phrase): return PhraseWidgetItem(parent, item) elif isinstance(item, model.Script): return ScriptWidgetItem(parent, item) autokey-0.95.10/lib/autokey/qtui/centralwidget.py000066400000000000000000000607071362232350500220010ustar00rootroot00000000000000# 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 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 from autokey import iomediator from autokey import model from autokey import configmanager as cm from . import common as ui_common from . import autokey_treewidget as ak_tree logger = ui_common.logger.getChild("CentralWidget") # type: logging.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 = iomediator.Recorder(self.scriptPage) self.cutCopiedItems = [] for column_index in range(3): self.treeWidget.setColumnWidth(column_index, cm.ConfigManager.SETTINGS[cm.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.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.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, model.Folder): self.stack.setCurrentIndex(0) self.folderPage.load(model_item) elif isinstance(model_item, model.Phrase): self.stack.setCurrentIndex(1) self.phrasePage.load(model_item) elif isinstance(model_item, model.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 = model.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 = model.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 = model.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 = model.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, model.Phrase): new_obj = model.Phrase('', '') else: new_obj = model.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, model.Phrase): new_obj = model.Phrase('', '') new_obj.copy(source_object) new_item = ak_tree.PhraseWidgetItem(parent_item, new_obj) else: new_obj = model.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, model.Folder): new_item = ak_tree.FolderWidgetItem(parent_item, item) ak_tree.WidgetItemFactory.process_folder(new_item, item) parent.add_folder(item) elif isinstance(item, model.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, model.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, model.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, model.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() # type: typing.Optional[model.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, model.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): if model.TriggerMode.HOTKEY in removed_item.modes: self.window().app.hotkey_removed(removed_item) if isinstance(removed_item, model.Folder): for subFolder in removed_item.folders: self.__deleteHotkeys(subFolder) for item in removed_item.items: if model.TriggerMode.HOTKEY in item.modes: self.window().app.hotkey_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 root_logger = logging.getLogger() 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.95.10/lib/autokey/qtui/common.py000066400000000000000000000135771362232350500204400ustar00rootroot00000000000000# 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 logging import os.path import pathlib import enum import functools from PyQt5.QtCore import QFile, QSize from PyQt5.QtGui import QFont, QIcon, QPixmap, QPainter, QColor from PyQt5.QtWidgets import QMessageBox, QLabel from PyQt5 import uic from PyQt5.QtSvg import QSvgRenderer from autokey import configmanager as cm 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) EMPTY_FIELD_REGEX = re.compile(r"^ *$", re.UNICODE) logger = logging.getLogger("root").getChild("Qt-GUI") # type: logging.Logger def monospace_font() -> QFont: """ Returns a monospace font used in the code editor widgets. :return: QFont instance having a monospace font. """ 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.CONFIG_DEFAULT_FOLDER): text = path.replace(cm.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.95.10/lib/autokey/qtui/compiled_resources.pyi000066400000000000000000000017051362232350500231750ustar00rootroot00000000000000# 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.95.10/lib/autokey/qtui/configwindow.py000066400000000000000000000331471362232350500216400ustar00rootroot00000000000000# 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 logging import threading import time import webbrowser from PyQt5.QtGui import QIcon, QKeySequence, QCloseEvent from PyQt5.QtWidgets import QApplication, QAction, QMenu import autokey.common import autokey.qtui.common from autokey import configmanager as cm from autokey import model from .settings import SettingsDialog from . import dialogs PROBLEM_MSG_PRIMARY = "Some problems were found" PROBLEM_MSG_SECONDARY = "%1\n\nYour changes have not been saved." _logger = autokey.qtui.common.logger.getChild("configwindow") # type: logging.Logger class ConfigWindow(*autokey.qtui.common.inherits_from_ui_file_with_name("mainwindow")): def __init__(self, app: QApplication): super().__init__() self.setupUi(self) self.about_dialog = dialogs.AboutAutokeyDialog(self) 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 _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_error.triggered.connect(self.app.notifier.reset_tray_icon) self.action_show_last_script_error.triggered.connect(self.app.show_script_error) 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], model.Folder) and len(items) == 1 can_copy = True for item in items: if isinstance(item, model.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], model.Script) and len(items) == 1) self.action_run_script.setEnabled(isinstance(items[0], model.Script) and len(items) == 1) self.menu_insert_macros.setEnabled(isinstance(items[0], model.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.HPANE_POSITION] = self.central_widget.splitter.sizes()[0] + 4 cm.ConfigManager.SETTINGS[cm.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() # logging.getLogger().removeHandler(self.central_widget.logHandler) 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): t = threading.Thread(target=self._run_script) t.start() def _run_script(self): script = self.central_widget.get_selected_item()[0] time.sleep(2) # Fix the GUI tooltip for action_run_script when changing this! self.app.service.scriptRunner.execute(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.95.10/lib/autokey/qtui/data/000077500000000000000000000000001362232350500174725ustar00rootroot00000000000000autokey-0.95.10/lib/autokey/qtui/data/api.txt000066400000000000000000000074511362232350500210130ustar00rootroot00000000000000engine.create_abbreviation(folder, description, abbr, contents) Create a text abbreviation engine.create_hotkey(folder, description, modifiers, key, contents) Create a text hotkey engine.create_phrase(folder, description, contents) Create a text phrase 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 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(keyString) Send a sequence of keys via keyboard events keyboard.wait_for_keypress(self, key, modifiers=[], timeOut=10.0) Wait for a keypress or key combination 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.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.95.10/lib/autokey/qtui/dbus_service.py000066400000000000000000000042111362232350500216060ustar00rootroot00000000000000# 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.95.10/lib/autokey/qtui/dialogs/000077500000000000000000000000001362232350500202035ustar00rootroot00000000000000autokey-0.95.10/lib/autokey/qtui/dialogs/__init__.py000066400000000000000000000025551362232350500223230ustar00rootroot00000000000000# 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" ] 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 autokey-0.95.10/lib/autokey/qtui/dialogs/abbrsettings.py000066400000000000000000000212701362232350500232460ustar00rootroot00000000000000# 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. """ import logging from PyQt5 import QtCore from PyQt5.QtWidgets import QListWidgetItem, QDialogButtonBox from autokey.qtui import common as ui_common from autokey import model logger = ui_common.logger.getChild("Abbreviation Settings Dialog") # type: logging.Logger WORD_CHAR_OPTIONS = { "All non-word": model.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 model.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(model.extract_wordchars(wordCharRegex)) self.wordCharCombo.setCurrentIndex(len(WORD_CHAR_OPTIONS)) if isinstance(item, model.Folder): self.omitTriggerCheckbox.setVisible(False) else: self.omitTriggerCheckbox.setVisible(True) self.omitTriggerCheckbox.setChecked(item.omitTrigger) if isinstance(item, model.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(model.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(model.make_wordchar_re(option)) if not isinstance(item, model.Folder): item.omitTrigger = self.omitTriggerCheckbox.isChecked() if isinstance(item, model.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.95.10/lib/autokey/qtui/dialogs/about_autokey_dialog.py000066400000000000000000000025431362232350500247530ustar00rootroot00000000000000# 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.95.10/lib/autokey/qtui/dialogs/detectdialog.py000066400000000000000000000041641362232350500232120ustar00rootroot00000000000000# 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.95.10/lib/autokey/qtui/dialogs/hotkeysettings.py000066400000000000000000000174531362232350500236530ustar00rootroot00000000000000# 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 logging import typing from PyQt5.QtCore import pyqtSignal from PyQt5.QtWidgets import QDialogButtonBox from autokey.qtui import common as ui_common from autokey import iomediator, model, configmanager as cm from autokey.iomediator.key import Key logger = ui_common.logger.getChild("Hotkey Settings Dialog") # type: logging.Logger Item = typing.Union[model.Folder, model.Script, model.Phrase] class HotkeySettingsDialog(*ui_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 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(self) self.grabber.start() def load(self, item: Item): self.target_item = item if model.TriggerMode.HOTKEY in item.modes: self.mod_control_button.setChecked(Key.CONTROL in item.modifiers) self.mod_alt_button.setChecked(Key.ALT in item.modifiers) self.mod_shift_button.setChecked(Key.SHIFT in item.modifiers) self.mod_super_button.setChecked(Key.SUPER in item.modifiers) self.mod_hyper_button.setChecked(Key.HYPER in item.modifiers) self.mod_meta_button.setChecked(Key.META in item.modifiers) key = item.hotKey if key in self.KEY_MAP: key_text = self.KEY_MAP[key] else: key_text = key self._update_key(key_text) logger.debug("Loaded item {}, key: {}, modifiers: {}".format(item, key_text, item.modifiers)) else: self.reset() def save(self, item): item.modes.append(model.TriggerMode.HOTKEY) # Build modifier list modifiers = self.build_modifiers() if self.key in self.REVERSE_KEY_MAP: key = self.REVERSE_KEY_MAP[self.key] else: key = self.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 reset(self): self.mod_control_button.setChecked(False) self.mod_alt_button.setChecked(False) self.mod_shift_button.setChecked(False) self.mod_super_button.setChecked(False) self.mod_hyper_button.setChecked(False) self.mod_meta_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 = [] # type: typing.List[Key] if key in self.KEY_MAP: key = self.KEY_MAP[key] self._update_key(key) self.mod_control_button.setChecked(Key.CONTROL in modifiers) self.mod_alt_button.setChecked(Key.ALT in modifiers) self.mod_shift_button.setChecked(Key.SHIFT in modifiers) self.mod_super_button.setChecked(Key.SUPER in modifiers) self.mod_hyper_button.setChecked(Key.HYPER in modifiers) self.mod_meta_button.setChecked(Key.META in 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 = [] if self.mod_control_button.isChecked(): modifiers.append(Key.CONTROL) if self.mod_alt_button.isChecked(): modifiers.append(Key.ALT) if self.mod_shift_button.isChecked(): modifiers.append(Key.SHIFT) if self.mod_super_button.isChecked(): modifiers.append(Key.SUPER) if self.mod_hyper_button.isChecked(): modifiers.append(Key.HYPER) if self.mod_meta_button.isChecked(): modifiers.append(Key.META) 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 if item.enabled: self.mod_control_button.setChecked(Key.CONTROL in item.modifiers) self.mod_alt_button.setChecked(Key.ALT in item.modifiers) self.mod_shift_button.setChecked(Key.SHIFT in item.modifiers) self.mod_super_button.setChecked(Key.SUPER in item.modifiers) self.mod_hyper_button.setChecked(Key.HYPER in item.modifiers) self.mod_meta_button.setChecked(Key.META in item.modifiers) key = item.hotKey if key in self.KEY_MAP: key_text = self.KEY_MAP[key] else: key_text = key self._update_key(key_text) else: self.reset() def save(self, item: cm.GlobalHotkey): # Build modifier list modifiers = self.build_modifiers() if self.key in self.REVERSE_KEY_MAP: key = self.REVERSE_KEY_MAP[self.key] else: key = self.key if key is None: raise RuntimeError("Attempt to set hotkey with no key") item.set_hotkey(modifiers, key) autokey-0.95.10/lib/autokey/qtui/dialogs/recorddialog.py000066400000000000000000000037641362232350500232250ustar00rootroot00000000000000# 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 import logging logger = ui_common.logger.getChild("Record Dialog") # type: logging.Logger 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.95.10/lib/autokey/qtui/dialogs/windowfiltersettings.py000066400000000000000000000065471362232350500250670ustar00rootroot00000000000000# 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 from autokey.qtui import common as ui_common from .detectdialog import DetectDialog from autokey import iomediator from autokey import model # TODO: Once the port to Qt5 is done, enable the clearButtonEnable property for the line edit in the UI editor. # TODO: Pure Qt4 does not support the line edit clear button, so this functionality is currently unavailable. class WindowFilterSettingsDialog(*ui_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: iomediator.WindowGrabber def load(self, item: model.Item): self.target_item = item if not isinstance(item, model.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): try: item.set_window_titles(self.get_filter_text()) except re.error: # TODO: Warn user. Currently just doesn't save regex on error. pass item.set_filter_recursive(self.get_is_recursive()) 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(self) self.grabber.start() # --- event handlers --- def slotButtonClicked(self, button): if button == QDialog.Cancel: self.load(self.targetItem) QDialog.slotButtonClicked(self, button) autokey-0.95.10/lib/autokey/qtui/folderpage.py000066400000000000000000000061231362232350500212450ustar00rootroot00000000000000# 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 import typing from PyQt5.QtWidgets import QMessageBox import autokey.qtui.common as ui_common from autokey import configmanager as cm from autokey.model import Folder if typing.TYPE_CHECKING: import logging logger = ui_common.logger.getChild("FolderPage") # type: logging.Logger 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.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.95.10/lib/autokey/qtui/notifier.py000066400000000000000000000220741362232350500207570ustar00rootroot00000000000000# 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 logging 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 from autokey import configmanager as cm if TYPE_CHECKING: from autokey.qtapp import Application TOOLTIP_RUNNING = "AutoKey - running" TOOLTIP_PAUSED = "AutoKey - paused" logger = ui_common.logger.getChild("System-tray-notifier") # type: logging.Logger 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.SERVICE_RUNNING]) self.app.monitoring_disabled.connect(self.update_tool_tip) if cm.ConfigManager.SETTINGS[cm.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." ) self.action_view_script_error.triggered.connect(self.app.show_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.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.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.95.10/lib/autokey/qtui/phrasepage.py000066400000000000000000000120441362232350500212530ustar00rootroot00000000000000# 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 from autokey import model 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: model.Phrase for val in sorted(model.SEND_MODES.keys()): self.sendModeCombo.addItem(val) self.initialising = False def load(self, phrase: model.Phrase): self.current_phrase = phrase self.phraseText.setPlainText(phrase.phrase) self.showInTrayCheckbox.setChecked(phrase.show_in_tray_menu) for k, v in model.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 = model.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.95.10/lib/autokey/qtui/popupmenu.py000066400000000000000000000140751362232350500211720ustar00rootroot00000000000000# 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 import logging from PyQt5.QtCore import Qt, pyqtSignal from PyQt5.QtGui import QIcon from PyQt5.QtWidgets import QMenu, QAction, QWidget from autokey import configmanager as cm import autokey.model import autokey.service FolderList = List[autokey.model.Folder] Item = Union[autokey.model.Script, autokey.model.Phrase] _logger = logging.getLogger("phrase-menu") 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.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.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.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): return QIcon.fromTheme("text-x-python") elif isinstance(item, autokey.model.Phrase): return QIcon.fromTheme("text-x-generic") else: error_msg = "ItemAction got unknown item. Expected Union[autokey.model.Script, autokey.model.Phrase], " \ "got '{}'".format(str(type(item))) _logger.error(error_msg) raise ValueError(error_msg) autokey-0.95.10/lib/autokey/qtui/resources/000077500000000000000000000000001362232350500205735ustar00rootroot00000000000000autokey-0.95.10/lib/autokey/qtui/resources/resources.qrc000066400000000000000000000017461362232350500233240ustar00rootroot00000000000000 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 icons/autokey.png icons/autokey.svg icons/autokey-status.svg icons/autokey-status-dark.svg icons/autokey-status-error.svg autokey-0.95.10/lib/autokey/qtui/resources/ui/000077500000000000000000000000001362232350500212105ustar00rootroot00000000000000autokey-0.95.10/lib/autokey/qtui/resources/ui/abbrsettings.ui000066400000000000000000000140231362232350500242360ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/resources/ui/about_autokey_dialog.ui000066400000000000000000002052331362232350500257460ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/resources/ui/centralwidget.ui000066400000000000000000000071601362232350500244070ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/resources/ui/detectdialog.ui000066400000000000000000000107161362232350500242040ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/resources/ui/enginesettings.ui000066400000000000000000000071401362232350500245770ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/resources/ui/folderpage.ui000066400000000000000000000044231362232350500236620ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/resources/ui/generalsettings.ui000066400000000000000000000203621362232350500247500ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/resources/ui/hotkeysettings.ui000066400000000000000000000117211362232350500246350ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/resources/ui/mainwindow.ui000066400000000000000000000416771362232350500237420ustar00rootroot00000000000000 MainWindow 0 0 784 587 AutoKey 0 0 784 30 Fi&le &New ../../../../../../../.designer/backup../../../../../../../.designer/backup Edi&t T&ools &Insert Macro Setti&ngs Hel&p toolBar Qt::ToolButtonFollowStyle TopToolBarArea true ../../../../../../../.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 ../../../../../../../.designer/backup../../../../../../../.designer/backup &Show last script error Show the last occured script error, if any. 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.95.10/lib/autokey/qtui/resources/ui/phrasepage.ui000066400000000000000000000063051362232350500236720ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/resources/ui/record_dialog.ui000066400000000000000000000064071362232350500243530ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/resources/ui/scriptpage.ui000066400000000000000000000046571362232350500237240ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/resources/ui/settingsdialog.ui000066400000000000000000000116521362232350500245740ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/resources/ui/settingswidget.ui000066400000000000000000000100251362232350500246110ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/resources/ui/specialhotkeysettings.ui000066400000000000000000000075401362232350500262020ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/resources/ui/window_filter_settings_dialog.ui000066400000000000000000000065511362232350500276710ustar00rootroot00000000000000 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.95.10/lib/autokey/qtui/scriptpage.py000066400000000000000000000133631362232350500213020ustar00rootroot00000000000000# 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 from autokey import model 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: model.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: model.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.95.10/lib/autokey/qtui/settings/000077500000000000000000000000001362232350500204215ustar00rootroot00000000000000autokey-0.95.10/lib/autokey/qtui/settings/__init__.py000066400000000000000000000016351362232350500225370ustar00rootroot00000000000000# 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.95.10/lib/autokey/qtui/settings/engine.py000066400000000000000000000100641362232350500222410ustar00rootroot00000000000000# 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 import logging import typing from PyQt5.QtCore import pyqtSlot from PyQt5.QtWidgets import QFileDialog, QWidget, QApplication from autokey.qtui import common if typing.TYPE_CHECKING: from autokey.configmanager import ConfigManager logger = common.logger.getChild("AutoKey configuration") # type: logging.Logger 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.95.10/lib/autokey/qtui/settings/general.py000066400000000000000000000157331362232350500224210ustar00rootroot00000000000000# 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 logging from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QWidget, QComboBox from autokey import configmanager as cm from autokey.iomediator.key import Key import autokey.qtui.common as ui_common import autokey.common as common logger = ui_common.logger.getChild("General settings widget") # type: logging.Logger 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.PROMPT_TO_SAVE]) self.show_tray_checkbox.setChecked(cm.ConfigManager.SETTINGS[cm.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.SORT_BY_USAGE_COUNT]) self.enable_undo_checkbox.setChecked(cm.ConfigManager.SETTINGS[cm.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 = cm.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.PROMPT_TO_SAVE] = not self.autosave_checkbox.isChecked() cm.ConfigManager.SETTINGS[cm.SHOW_TRAY_ICON] = self.show_tray_checkbox.isChecked() # cm.ConfigManager.SETTINGS[cm.MENU_TAKES_FOCUS] = self.allow_kb_nav_checkbox.isChecked() cm.ConfigManager.SETTINGS[cm.SORT_BY_USAGE_COUNT] = self.sort_by_usage_checkbox.isChecked() cm.ConfigManager.SETTINGS[cm.UNDO_USING_BACKSPACE] = self.enable_undo_checkbox.isChecked() cm.ConfigManager.SETTINGS[cm.NOTIFICATION_ICON] = self.system_tray_icon_theme_combobox.currentData(Qt.UserRole) self._save_disable_capslock_setting() self._save_autostart_settings() 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: cm.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.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 = cm.AutostartSettings(desktop_entry, show_main_window) if new_settings != self.autostart_settings: # Only write if settings changed to preserve eventual user-made modifications. cm.set_autostart_entry(new_settings) autokey-0.95.10/lib/autokey/qtui/settings/settingsdialog.py000066400000000000000000000051221362232350500240130ustar00rootroot00000000000000# 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 logging 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 = common.logger.getChild("Settings Dialog") # type: logging.Logger 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.95.10/lib/autokey/qtui/settings/specialhotkeys.py000066400000000000000000000120051362232350500240200ustar00rootroot00000000000000# 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: import logging from autokey.qtapp import Application logger = ui_common.logger.getChild("Special Hotkey Settings widget") # type: logging.Logger 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.95.10/lib/autokey/qtui/settingswidget.py000066400000000000000000000230331362232350500222000ustar00rootroot00000000000000# 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 from autokey.qtui.common import inherits_from_ui_file_with_name from autokey.qtui.dialogs import HotkeySettingsDialog, AbbrSettingsDialog, WindowFilterSettingsDialog from autokey import model 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: model.Item self.abbreviation_enabled = False self.hotkey_enabled = False self.window_filter_enabled = False def load(self, item: model.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: model.Item): self.abbr_settings_dialog.load(item) item_has_abbreviation = model.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: model.Item): self.hotkey_settings_dialog.load(item) item_has_hotkey = model.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: model.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 model.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) 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.95.10/lib/autokey/scripting.py000066400000000000000000001373341362232350500201660ustar00rootroot00000000000000# -*- 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 subprocess import threading import time import re from typing import NamedTuple, Union, List from autokey import common, model from autokey import iomediator if common.USING_QT: from PyQt5.QtGui import QClipboard from PyQt5.QtWidgets import QApplication else: from gi.repository import Gtk, Gdk class ColourData(NamedTuple("ColourData", (("r", int), ("g", int), ("b", int)))): """Colour data type for colour chooser dialogs.""" @property def hex_code(self) -> str: 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])))): """""" @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 class Keyboard: """ Provides access to the keyboard for event generation. """ def __init__(self, mediator): self.mediator = mediator def send_keys(self, keyString): """ Send a sequence of keys via keyboard events Usage: C{keyboard.send_keys(keyString)} @param keyString: string of keys (including special keys) to send """ assert type(keyString) is str self.mediator.interface.begin_send() try: self.mediator.send_string(keyString) 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 = iomediator.Waiter(key, modifiers, None, timeOut) return w.wait() class Mouse: """ Provides access to send mouse clicks """ def __init__(self, mediator: iomediator.IoMediator): self.mediator = mediator self.interface = 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 = iomediator.Waiter(None, None, button, timeOut) w.wait() 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) class System: """ Simplified access to some system commands. """ def exec_command(self, 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] if output[-1] == "\n": # Most shell output has a new line at the end, which we # don't want. Drop the trailing newline character output = output[:-1] if p.returncode: raise subprocess.CalledProcessError(p.returncode, output) return output else: subprocess.Popen(command, shell=True, bufsize=-1) def create_file(self, fileName, 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(fileName, "w") as written_file: written_file.write(contents) 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. A note on 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) class QtClipboard: """ Read/write access to the X selection and clipboard - QT version """ def __init__(self, app): self.clipBoard = QApplication.clipboard() self.app = app 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): 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 __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): self.text = self.clipBoard.text(QClipboard.Clipboard) self.sem.release() def __execAsync(self, callback, *args): self.sem = threading.Semaphore(0) self.app.exec_in_main(callback, *args) self.sem.acquire() class GtkClipboard: """ Read/write access to the X selection and clipboard - GTK version """ def __init__(self, app): self.clipBoard = Gtk.Clipboard.get(Gdk.SELECTION_CLIPBOARD) self.selection = Gtk.Clipboard.get(Gdk.SELECTION_PRIMARY) self.app = app 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): 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 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") 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 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. """ def __init__(self, configManager, runner): self.configManager = configManager self.runner = runner self.monitor = configManager.app.monitor self.__returnValue = '' def get_folder(self, title): """ 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. """ for folder in self.configManager.allFolders: if folder.title == title: return folder return None def create_phrase(self, folder, description, contents): """ Create a text phrase Usage: C{engine.create_phrase(folder, description, contents)} A new phrase with no abbreviation or hotkey is created in the specified folder @param folder: folder to place the abbreviation in, retrieved using C{engine.get_folder()} @param description: description for the phrase @param contents: the expansion text """ self.monitor.suspend() p = model.Phrase(description, contents) folder.add_item(p) p.persist() self.monitor.unsuspend() self.configManager.config_altered(False) def create_abbreviation(self, folder, description, abbr, contents): """ Create a text abbreviation 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): raise Exception("The specified abbreviation is already in use") self.monitor.suspend() p = model.Phrase(description, contents) p.modes.append(model.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): """ 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): raise Exception("The specified hotkey and modifier combination is already in use") self.monitor.suspend() p = model.Phrase(description, contents) p.modes.append(model.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): """ Run an existing script using its description to look it up Usage: C{engine.run_script(description)} @param description: description of the script to run @raise Exception: if the specified script does not exist """ targetScript = None for item in self.configManager.allItems: if item.description == description and isinstance(item, model.Script): targetScript = item if targetScript is not None: self.runner.run_subscript(targetScript) else: raise Exception("No script with description '%s' found" % description) def run_script_from_macro(self, args): """ Used internally by AutoKey for phrase macros """ self.__macroArgs = args["args"].split(',') try: self.run_script(args["name"]) except Exception as e: self.set_return_value("{ERROR: %s}" % str(e)) 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.__macroArgs 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.__returnValue = val def get_return_value(self): """ Used internally by AutoKey for phrase macros """ ret = self.__returnValue self.__returnValue = '' return ret autokey-0.95.10/lib/autokey/scripting_Store.py000066400000000000000000000026101362232350500213260ustar00rootroot00000000000000class Store(dict): """ Allows persistent storage of values between invocations of the script. """ 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.95.10/lib/autokey/scripting_highlevel.py000066400000000000000000000112261362232350500222040ustar00rootroot00000000000000import time import os import subprocess import tempfile import imghdr import struct class PatternNotFound(Exception): pass # numeric representation of the mouse buttons. For use in visgrep. LEFT = 1 MIDDLE = 2 RIGHT = 3 def visgrep(scr: str, pat: str, tolerance: int = 0) -> int: """ visgrep(scr: str, pat: str, tolerance: int = 0) -> int Visual grep of scr for pattern pat. Requires xautomation (http://hoopajoo.net/projects/xautomation.html). visgrep("screen.png", "pat.png") Exceptions raised: ValueError, PatternNotFound, FileNotFoundError :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. If negative or not convertible to int, raises ValueError. :returns: coordinates of the topleft point of the match, if any. Raises 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: """ get_png_dim(filepath:str) -> (int) Finds the dimension of a PNG. :param filepath: file path of the PNG. :returns: (width, height). """ 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=''): subprocess.call(['xte', '-x', display, "mousemove {} {}".format(int(x), int(y))]) def mouse_rmove(x: int, y: int, display: str=''): subprocess.call(['xte', '-x', display, "mousermove {} {}".format(int(x), int(y))]) def mouse_click(button: int, display: str=''): subprocess.call(['xte', '-x', display, "mouseclick {}".format(int(button))]) def mouse_pos(): 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 imagemagick, xautomation, 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. Exception PatternNotFound is raised when the pattern is not found on the screen. :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. """ 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 help for 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.95.10/lib/autokey/service.py000066400000000000000000000430301362232350500176110ustar00rootroot00000000000000#!/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 traceback import collections import time import logging from autokey import common from autokey.iomediator.key import Key, KEY_FIND_RE from autokey.iomediator import IoMediator from .macro import MacroManager from . import scripting, model, scripting_Store, scripting_highlevel from .configmanager import ConfigManager, SERVICE_RUNNING, SCRIPT_GLOBALS, save_config, UNDO_USING_BACKSPACE import threading logger = logging.getLogger("service") MAX_STACK_LENGTH = 150 def threaded(f): def wrapper(*args): t = threading.Thread(target=f, args=args, name="Phrase-thread") t.setDaemon(False) t.start() wrapper.__name__ = f.__name__ wrapper.__dict__ = f.__dict__ wrapper.__doc__ = f.__doc__ 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[SERVICE_RUNNING] = False self.mediator = None self.app = app self.inputStack = collections.deque(maxlen=MAX_STACK_LENGTH) self.lastStackState = '' self.lastMenu = None def start(self): self.mediator = IoMediator(self) self.mediator.interface.initialise() self.mediator.interface.start() self.mediator.start() ConfigManager.SETTINGS[SERVICE_RUNNING] = True self.scriptRunner = ScriptRunner(self.mediator, self.app) self.phraseRunner = PhraseRunner(self) scripting_Store.Store.GLOBALS = ConfigManager.SETTINGS[SCRIPT_GLOBALS] logger.info("Service now marked as running") def unpause(self): ConfigManager.SETTINGS[SERVICE_RUNNING] = True logger.info("Unpausing - service now marked as running") def pause(self): ConfigManager.SETTINGS[SERVICE_RUNNING] = False logger.info("Pausing - service now marked as stopped") def is_running(self): return ConfigManager.SETTINGS[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() # 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: model.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: 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, model.Phrase, "phrase") self.phraseRunner.execute(phrase) def run_script(self, name): script = self.__findItem(name, model.Script, "script") self.scriptRunner.execute(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[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, model.Phrase): self.phraseRunner.execute(item, buffer) else: self.scriptRunner.execute(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: model.Phrase, buffer=''): mediator = self.service.mediator # type: IoMediator mediator.interface.begin_send() try: expansion = phrase.build_phrase(buffer) self.macroManager.process_expansion(expansion) self.contains_special_keys = self.phrase_contains_special_keys(expansion) mediator.send_backspace(expansion.backspaces) if phrase.sendMode == model.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: model.Expansion) -> bool: """ Determine if the expansion contains any special keys, including those resulting from any processed macros (