https://iconduck.com/icons/12138/keyboard
This simple program was developed to implement keyboard layout switching using just one key. It utilizes the win32api, thus it will only work on Windows systems.
How it works
The program provides a convenient way to switch between keyboard layouts using a single key. Instead of the usual method of using a key combination (like Alt + Shift), this program allows users to assign the right CTRL and right SHIFT keys to specific layouts, and the left CTRL key for a round-robin switching method.
- Start program from the script main.py.
- Read configuration settings from the config.ini by config_reader.py.
- Attach listeners (keyboard_listener.py) on keys: (left) CTRL right CTRL right SHIFT
- Start keyboard monitor (keyboard_layout_monitor.py): Start the tray icon (tray_icon.py) with the current layout flag.
Layout monitoring
While the program is running it displays current keyboard layout as a flag:
The tray icon feature was implemented using the pystray library in the script tray_icon.py.
Flag icons should be placed in the flags directory. Files should be in PNG format and named after the countries they represent in English.
Keyboard layout is linked to each window so that when you switch between different windows, the keyboard layout will also change according to the last set layout for that window. I didn't find a way to hook into the 'keyboard layout change' event using win32api, so I implemented regular monitoring of the current layout.
If there is no flag icon corresponding to the country of the current layout, an autogenerated pattern is displayed:
The periodicity of checking the keyboard layout can be set by the 'layout check interval' option in the config.ini. If it is not set, then the default value from the constant CHECK_INTERVAL in keyboard_layout_monitor.py is used.
The check process is running in the daemon thread, so it doesn't interfere with other program logic execution. As a daemon thread, it will be automatically killed when the program exits, but the stop method is implemented as well.
Hotkeys
When the user presses down any key, the _onkey_press method of all listeners is triggered (keyboard_listener.py). The listener attached to that key stores the time of event, while all other listeners reset this value. This filters out the event where the user presses another key while holding a hotkey, for example, pressing the 'C' key while holding down 'CTRL', so the hotkey event will be skipped during this time.
When the user releases the key, the method _onkey_release of the specific listener is called. If the user holds the key for too long, nothing happens; otherwise, a new keyboard layout will be set.
Change keyboard layout
The feature to change the keyboard layout is implemented using win32api in the script keyboard_layout_controller.py.
To change the keyboard layout, we need to retrieve the current foreground window and send a specific message to it requesting a layout change. The window may either accept or reject this request. For instance, the "Task Manager" rejects layout changes, and it may not be possible to fix this behavior. It's important to check if the window has a parent, as child windows reject keyboard language changes due to their dependency on the parent window's layout settings. Therefore, if our window has a parent, we need to address the request to its parent window.
Keyboard layout ID is a 32-bit integer value and consists of two parts:
- layout ID
- country ID (language ID)
To request a change in keyboard layout, we can use either the full layout ID or just the language ID part.
Some examples of different layout IDs:
- QWERTY-layout for US-English lang (EN): 0x4090409
- QWERTZ-layout for German lang (DE): 0x4070407
- ЙЦУКЕН-layout for Ukrainian lang (UK): -0xf57fbde
Last 16 bits of a full layout ID represent the country or the language:
- 0x409 - EN, English (United States)
- 0x407 - DE, German (Germany)
- 0x422 - UK, Ukrainian (Ukraine)
How do I get 0x422 from -0xf57fbde, you may wonder. Let's represent this value in the proper negative 32-bit format:
-0xF57FBDE = 0xF0A80422 - 0000 1111 ' 0101 0111 ' 1111 1011 ' 1101 1110 = 1111 0000 ' 1010 1000 ' 0000 0100 ' 0010 0010
- Read more about bin negatives: Two's complement
You can find all supported languages along with their IDs and country names here:
Pause and continue listening the hotkeys
- Right-click on the tray icon
- You will see the popup-menu
- Click on the "Pause"
Icon will become darker:
Hotkeys are cancelled and will not change the keyboard layout.
Despite this, the flags icon will still be changed according to the current layout; only the attachment of hotkeys is cancelled.
To unpause - click "Continue":
Exit from the program
- Right click on the tray icon
- You will see the popup-menu
- Click on the "Exit"
Build process
This program may be built into an exe-file using pyinstaller:
pip install pyinstaller pyinstaller --noconsole main.py
- for more details see: https://pyinstaller.org/en/stable/usage.html
But for proper building and copying of important resources, there is a special build script - build.py, which:
- Removes previous files of the previous distribution build.
- Prepares the icon if set: Copies specified icon and saves it in the proper format (see icon_generator.py -> copy_icon()). Generates an icon with two partially overlapped flags (see icon_generator.py -> generate_icon())
- Runs pyinstaller to create an executable file with the specifications provided in main.spec.
- Copies resources specified in the RESOURCES constant, such as images from flags/*.png and the config.ini file.
- Creates an archive for distribution.
Configuration
Through the config.ini file, you can configure:
- Binding keyboard language (layout) to the right SHIFT key - option "right shift". It may be either in decimal (1058) or hexadecimal (0x422) format. See 'Language ID' at Microsoft Learn.
- Binding keyboard language (layout) to the right CTRL key - option "right ctrl". It may be either in decimal (1058) or hexadecimal (0x422) format. See Language ID at Microsoft Learn.
- How often should the program check if the keyboard layout has changed to update the tray icon - option "layout check interval". It may be either an integer or a float (minimum value is 0.1).
- Duration of holding the hot-key to consider it a timeout - option "key press timeout". It may be either an integer or a float (minimum value is 0.1).
Compatibility
This program is designed to work on Windows systems only due to its dependency on win32api.
Possible issues (language doesn't switch)
- When you run the program for the first time, select any window and then try using the hotkey.
- The program cannot change the keyboard language for the "Task Manager" as it rejects the request.
- Clicking on certain icons in the tray may cause the hotkey to stop working -> just select some window.
- You hold a hotkey for too long -> you may increase timeout value in the config.ini - key press timeout.
- If you press another key while holding a hotkey - this is a normal behavior, just to prevent unintentional layout changes.
References
- Sequence diagrams were created using the web-editor: https://sequencediagram.org/
- py-scripts to exe-file - pyinstaller: Using PyInstaller
- threading in Python: Thread-based parallelism
- tray icon in Python: pystray
License
GNU License GPL v3.0 and later
Disclaimer
This program is provided as-is, without any warranty. Use it at your own risk.
Sometimes, antivirus software may mistakenly flag this program as a Trojan "Win64:Evo-Gen". However, I assure you that it's a false positive alarm. This occasionally happens with Python programs that have been compiled using PyInstaller.
Sources
Releases
No comments:
Post a Comment