KeyboardPaster 0.1.24__tar.gz → 0.1.26__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/KeyboardPaster.egg-info/PKG-INFO +25 -3
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/KeyboardPaster.egg-info/SOURCES.txt +6 -1
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/KeyboardPaster.egg-info/requires.txt +2 -2
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/PKG-INFO +25 -3
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/README.md +22 -0
- keyboardpaster-0.1.26/keyboardpaster/app.py +503 -0
- keyboardpaster-0.1.26/keyboardpaster/bootstrap.py +20 -0
- keyboardpaster-0.1.26/keyboardpaster/desktop.py +96 -0
- keyboardpaster-0.1.26/keyboardpaster/keyboard_input.py +705 -0
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/keyboardpaster/keyboard_layout_detector.py +22 -0
- keyboardpaster-0.1.26/keyboardpaster/keyboardpaster_app.kv +324 -0
- keyboardpaster-0.1.26/keyboardpaster/res/images/keyboardpaster.png +0 -0
- keyboardpaster-0.1.26/keyboardpaster/shared_resources.py +15 -0
- keyboardpaster-0.1.26/keyboardpaster.desktop +10 -0
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/setup.py +13 -4
- keyboardpaster-0.1.24/keyboardpaster/app.py +0 -407
- keyboardpaster-0.1.24/keyboardpaster/keyboardpaster_app.kv +0 -173
- keyboardpaster-0.1.24/keyboardpaster/shared_resources.py +0 -1
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/KeyboardPaster.egg-info/dependency_links.txt +0 -0
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/KeyboardPaster.egg-info/entry_points.txt +0 -0
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/KeyboardPaster.egg-info/top_level.txt +0 -0
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/LICENSE +0 -0
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/keyboardpaster/__init__.py +0 -0
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/keyboardpaster/modules/__init__.py +0 -0
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/keyboardpaster/modules/autoupdate.py +0 -0
- {keyboardpaster-0.1.24 → keyboardpaster-0.1.26}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: KeyboardPaster
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.26
|
|
4
4
|
Summary: A Python package to type text using the keyboard module
|
|
5
5
|
Home-page: https://github.com/KnutssonDevelopment/KeyboardPaster
|
|
6
6
|
Author: Brian Knutsson
|
|
@@ -19,8 +19,8 @@ Requires-Python: >=3.10
|
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Requires-Dist: pynput
|
|
22
|
-
Requires-Dist: kivy
|
|
23
|
-
Requires-Dist: kivymd
|
|
22
|
+
Requires-Dist: kivy<2.4,>=2.2
|
|
23
|
+
Requires-Dist: kivymd<2,>=1.2
|
|
24
24
|
Requires-Dist: requests
|
|
25
25
|
Provides-Extra: windows
|
|
26
26
|
Requires-Dist: pywin32; extra == "windows"
|
|
@@ -48,10 +48,32 @@ A Python package to type text using the keyboard module. This is useful in situa
|
|
|
48
48
|
- Paste 10 different strings using the keyboard
|
|
49
49
|
- Save string for future usage
|
|
50
50
|
|
|
51
|
+
## Install (Linux / pipx)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pipx install keyboardpaster
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
KivyMD is pinned to 1.x so pipx does not need to compile `pycairo`.
|
|
58
|
+
|
|
59
|
+
If you install KivyMD 2.x yourself, or a dependency pulls in `pycairo`, install Cairo headers first:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
sudo apt install libcairo2-dev pkg-config
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
On GNOME Wayland you also need `ydotool` / `ydotoold` for typing into other apps.
|
|
66
|
+
|
|
51
67
|
## Future features
|
|
52
68
|
- Paste images as base64 code
|
|
53
69
|
|
|
54
70
|
## Changes
|
|
71
|
+
### v0.1.26
|
|
72
|
+
- BUGFIX: Pin KivyMD to 1.x so pipx install does not require pycairo/Cairo headers
|
|
73
|
+
|
|
74
|
+
### v0.1.25
|
|
75
|
+
- BUGFIX: Dropbox menu resulted in crash when multiple profiles were available
|
|
76
|
+
|
|
55
77
|
### v0.1.24
|
|
56
78
|
- BUGFIX: Corrected Python requirement
|
|
57
79
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
LICENSE
|
|
2
2
|
README.md
|
|
3
|
+
keyboardpaster.desktop
|
|
3
4
|
setup.py
|
|
4
5
|
KeyboardPaster.egg-info/PKG-INFO
|
|
5
6
|
KeyboardPaster.egg-info/SOURCES.txt
|
|
@@ -9,8 +10,12 @@ KeyboardPaster.egg-info/requires.txt
|
|
|
9
10
|
KeyboardPaster.egg-info/top_level.txt
|
|
10
11
|
keyboardpaster/__init__.py
|
|
11
12
|
keyboardpaster/app.py
|
|
13
|
+
keyboardpaster/bootstrap.py
|
|
14
|
+
keyboardpaster/desktop.py
|
|
15
|
+
keyboardpaster/keyboard_input.py
|
|
12
16
|
keyboardpaster/keyboard_layout_detector.py
|
|
13
17
|
keyboardpaster/keyboardpaster_app.kv
|
|
14
18
|
keyboardpaster/shared_resources.py
|
|
15
19
|
keyboardpaster/modules/__init__.py
|
|
16
|
-
keyboardpaster/modules/autoupdate.py
|
|
20
|
+
keyboardpaster/modules/autoupdate.py
|
|
21
|
+
keyboardpaster/res/images/keyboardpaster.png
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: KeyboardPaster
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.26
|
|
4
4
|
Summary: A Python package to type text using the keyboard module
|
|
5
5
|
Home-page: https://github.com/KnutssonDevelopment/KeyboardPaster
|
|
6
6
|
Author: Brian Knutsson
|
|
@@ -19,8 +19,8 @@ Requires-Python: >=3.10
|
|
|
19
19
|
Description-Content-Type: text/markdown
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Requires-Dist: pynput
|
|
22
|
-
Requires-Dist: kivy
|
|
23
|
-
Requires-Dist: kivymd
|
|
22
|
+
Requires-Dist: kivy<2.4,>=2.2
|
|
23
|
+
Requires-Dist: kivymd<2,>=1.2
|
|
24
24
|
Requires-Dist: requests
|
|
25
25
|
Provides-Extra: windows
|
|
26
26
|
Requires-Dist: pywin32; extra == "windows"
|
|
@@ -48,10 +48,32 @@ A Python package to type text using the keyboard module. This is useful in situa
|
|
|
48
48
|
- Paste 10 different strings using the keyboard
|
|
49
49
|
- Save string for future usage
|
|
50
50
|
|
|
51
|
+
## Install (Linux / pipx)
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pipx install keyboardpaster
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
KivyMD is pinned to 1.x so pipx does not need to compile `pycairo`.
|
|
58
|
+
|
|
59
|
+
If you install KivyMD 2.x yourself, or a dependency pulls in `pycairo`, install Cairo headers first:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
sudo apt install libcairo2-dev pkg-config
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
On GNOME Wayland you also need `ydotool` / `ydotoold` for typing into other apps.
|
|
66
|
+
|
|
51
67
|
## Future features
|
|
52
68
|
- Paste images as base64 code
|
|
53
69
|
|
|
54
70
|
## Changes
|
|
71
|
+
### v0.1.26
|
|
72
|
+
- BUGFIX: Pin KivyMD to 1.x so pipx install does not require pycairo/Cairo headers
|
|
73
|
+
|
|
74
|
+
### v0.1.25
|
|
75
|
+
- BUGFIX: Dropbox menu resulted in crash when multiple profiles were available
|
|
76
|
+
|
|
55
77
|
### v0.1.24
|
|
56
78
|
- BUGFIX: Corrected Python requirement
|
|
57
79
|
|
|
@@ -6,10 +6,32 @@ A Python package to type text using the keyboard module. This is useful in situa
|
|
|
6
6
|
- Paste 10 different strings using the keyboard
|
|
7
7
|
- Save string for future usage
|
|
8
8
|
|
|
9
|
+
## Install (Linux / pipx)
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pipx install keyboardpaster
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
KivyMD is pinned to 1.x so pipx does not need to compile `pycairo`.
|
|
16
|
+
|
|
17
|
+
If you install KivyMD 2.x yourself, or a dependency pulls in `pycairo`, install Cairo headers first:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
sudo apt install libcairo2-dev pkg-config
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
On GNOME Wayland you also need `ydotool` / `ydotoold` for typing into other apps.
|
|
24
|
+
|
|
9
25
|
## Future features
|
|
10
26
|
- Paste images as base64 code
|
|
11
27
|
|
|
12
28
|
## Changes
|
|
29
|
+
### v0.1.26
|
|
30
|
+
- BUGFIX: Pin KivyMD to 1.x so pipx install does not require pycairo/Cairo headers
|
|
31
|
+
|
|
32
|
+
### v0.1.25
|
|
33
|
+
- BUGFIX: Dropbox menu resulted in crash when multiple profiles were available
|
|
34
|
+
|
|
13
35
|
### v0.1.24
|
|
14
36
|
- BUGFIX: Corrected Python requirement
|
|
15
37
|
|
|
@@ -0,0 +1,503 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import json
|
|
3
|
+
import subprocess
|
|
4
|
+
import importlib.resources
|
|
5
|
+
import keyboardpaster
|
|
6
|
+
|
|
7
|
+
from keyboardpaster.bootstrap import configure_kivy_environment
|
|
8
|
+
|
|
9
|
+
configure_kivy_environment()
|
|
10
|
+
|
|
11
|
+
from kivymd.app import MDApp
|
|
12
|
+
from kivy.properties import ObjectProperty, StringProperty, BooleanProperty
|
|
13
|
+
from kivy.uix.textinput import TextInput
|
|
14
|
+
from kivy.lang import Builder
|
|
15
|
+
from kivy.clock import Clock
|
|
16
|
+
from kivy.core.window import Window
|
|
17
|
+
from kivy.factory import Factory
|
|
18
|
+
from kivy.metrics import dp
|
|
19
|
+
from kivy.uix.dropdown import DropDown
|
|
20
|
+
from kivymd.uix.selectioncontrol.selectioncontrol import MDCheckbox
|
|
21
|
+
from kivymd.uix.button.button import MDRaisedButton
|
|
22
|
+
from kivymd.uix.dialog import MDDialog
|
|
23
|
+
from kivymd.uix.textfield import MDTextField
|
|
24
|
+
|
|
25
|
+
from keyboardpaster.keyboard_input import type_string
|
|
26
|
+
from keyboardpaster.keyboard_layout_detector import get_keyboard_layout
|
|
27
|
+
from keyboardpaster.modules.autoupdate import autoupdate
|
|
28
|
+
from keyboardpaster.shared_resources import app_version, app_icon_path
|
|
29
|
+
from keyboardpaster.desktop import ensure_user_desktop_entry
|
|
30
|
+
|
|
31
|
+
SPECIAL_CHARS_SHIFT = {
|
|
32
|
+
'EN_US': {
|
|
33
|
+
'~': '`', '!': '1', '@': '2', '#': '3', '$': '4',
|
|
34
|
+
'%': '5', '^': '6', '&': '7', '*': '8', '(': '9',
|
|
35
|
+
')': '0', '_': '-', '+': '=', '{': '[', '}': ']',
|
|
36
|
+
'|': '\\', ':': ';', '"': "'", '<': ',', '>': '.',
|
|
37
|
+
'?': '/'
|
|
38
|
+
},
|
|
39
|
+
'DA_DK': {
|
|
40
|
+
'½': '§', '!': '1', '"': '2', '#': '3', '¤': '4', '%': '5',
|
|
41
|
+
'&': '6', '/': '7', '(': '8', ')': '9', '=': '0',
|
|
42
|
+
'?': '+', '`': '§', ':': ';', ';': ',', '>': '<', '_': '/',
|
|
43
|
+
'*': '\\'
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
SPECIAL_CHARS_ALT_GR = {
|
|
48
|
+
'EN_US': {},
|
|
49
|
+
'DA_DK': {
|
|
50
|
+
'$': '4', '£': '3', '@': '2', '{': '7', '}': '0', '[': '8',
|
|
51
|
+
']': '9', '|': '´', '€': 'E',
|
|
52
|
+
# ¨ key (physical ]); used by pynput path. ydotool uses DA_DK_YDOTOOL_COMBO.
|
|
53
|
+
'~': ']',
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Dynamic window sizing around slots (matches keyboardpaster_app.kv dp values)
|
|
58
|
+
WINDOW_WIDTH = int(dp(960))
|
|
59
|
+
HEADER_HEIGHT = int(dp(68))
|
|
60
|
+
FOOTER_HEIGHT = int(dp(72))
|
|
61
|
+
BODY_TOP_PADDING = int(dp(12))
|
|
62
|
+
SLOT_HEIGHT = int(dp(56))
|
|
63
|
+
SLOT_SPACING = int(dp(10))
|
|
64
|
+
SLOT_AREA_PADDING_V = int(dp(16)) # container padding top+bottom
|
|
65
|
+
MAX_WINDOW_HEIGHT = int(dp(720))
|
|
66
|
+
|
|
67
|
+
"""
|
|
68
|
+
SPECIAL_CHARS_SPACE = {
|
|
69
|
+
'EN_US': {},
|
|
70
|
+
'DA_DK': {
|
|
71
|
+
'^': '¨'
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
def type_string_with_delay(
|
|
77
|
+
text: str,
|
|
78
|
+
start_delay: float = 3.0,
|
|
79
|
+
mod_delay: float = 0.1,
|
|
80
|
+
keypress_delay: float = 0.1,
|
|
81
|
+
layout: str = 'EN_US',
|
|
82
|
+
end_line: bool = False,
|
|
83
|
+
on_error=None,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Types the given text using the keyboard module after a defined start delay, with an optional delay between keypresses.
|
|
87
|
+
|
|
88
|
+
:param text: The text to be typed.
|
|
89
|
+
:param start_delay: The delay before typing starts in seconds. Default is 3.0 seconds.
|
|
90
|
+
:param keypress_delay: The delay between keypresses in seconds. Default is 0.1 seconds.
|
|
91
|
+
:param layout: The keyboard layout to use. Default is 'EN_US'.
|
|
92
|
+
:param end_line: Should end the paste with a ENTER press.
|
|
93
|
+
:param on_error: Optional callback invoked if typing fails after the start delay.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
def type_with_delay_callback(dt):
|
|
97
|
+
try:
|
|
98
|
+
special_chars_shift = SPECIAL_CHARS_SHIFT.get(layout, SPECIAL_CHARS_SHIFT["EN_US"])
|
|
99
|
+
special_chars_alt_gr = SPECIAL_CHARS_ALT_GR.get(layout, SPECIAL_CHARS_ALT_GR["EN_US"])
|
|
100
|
+
type_string(
|
|
101
|
+
text,
|
|
102
|
+
delay=keypress_delay,
|
|
103
|
+
mod_delay=mod_delay,
|
|
104
|
+
layout=layout,
|
|
105
|
+
end_line=end_line,
|
|
106
|
+
special_chars_shift=special_chars_shift,
|
|
107
|
+
special_chars_alt_gr=special_chars_alt_gr,
|
|
108
|
+
)
|
|
109
|
+
except Exception as error:
|
|
110
|
+
if on_error:
|
|
111
|
+
on_error(error)
|
|
112
|
+
else:
|
|
113
|
+
raise
|
|
114
|
+
|
|
115
|
+
Clock.schedule_once(type_with_delay_callback, start_delay)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class KeyboardPasterApp(MDApp):
|
|
119
|
+
layout = StringProperty('EN_US')
|
|
120
|
+
version_text = StringProperty(f"v{app_version}")
|
|
121
|
+
start_delay = ObjectProperty(None)
|
|
122
|
+
layout = 'EN_US'
|
|
123
|
+
_updating_slots = BooleanProperty(False)
|
|
124
|
+
|
|
125
|
+
def __init__(self):
|
|
126
|
+
super().__init__()
|
|
127
|
+
self.config = None
|
|
128
|
+
self.json_config = 'saved_inputs.json'
|
|
129
|
+
# Strong ref: KV ids only keep a WeakProxy, so DropDown was GC'd after dismiss.
|
|
130
|
+
self.profile_dropdown = DropDown()
|
|
131
|
+
|
|
132
|
+
def build(self):
|
|
133
|
+
self.theme_cls.primary_palette = "Amber"
|
|
134
|
+
self.theme_cls.accent_palette = "Orange"
|
|
135
|
+
self.theme_cls.theme_style = "Dark"
|
|
136
|
+
Window.clearcolor = (0.08, 0.08, 0.08, 1)
|
|
137
|
+
|
|
138
|
+
self.read_config()
|
|
139
|
+
|
|
140
|
+
self.detect_keyboard_layout()
|
|
141
|
+
Clock.schedule_once(self.set_profile, 1)
|
|
142
|
+
self.title = f"Keyboard Paster v{app_version}"
|
|
143
|
+
Window.size = (WINDOW_WIDTH, self._height_for_slot_count(1))
|
|
144
|
+
icon = app_icon_path()
|
|
145
|
+
if icon is not None:
|
|
146
|
+
Window.set_icon(str(icon))
|
|
147
|
+
|
|
148
|
+
with importlib.resources.path(keyboardpaster, "keyboardpaster_app.kv") as kv_file_path:
|
|
149
|
+
return Builder.load_file(str(kv_file_path))
|
|
150
|
+
|
|
151
|
+
def on_stop(self):
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def _height_for_slot_count(slot_count: int) -> int:
|
|
156
|
+
n = max(1, slot_count)
|
|
157
|
+
slots_area = SLOT_AREA_PADDING_V + n * SLOT_HEIGHT + max(0, n - 1) * SLOT_SPACING
|
|
158
|
+
return HEADER_HEIGHT + BODY_TOP_PADDING + slots_area + FOOTER_HEIGHT
|
|
159
|
+
|
|
160
|
+
def update_window_height(self):
|
|
161
|
+
desired = self._height_for_slot_count(len(self._ordered_slots()))
|
|
162
|
+
height = min(desired, MAX_WINDOW_HEIGHT)
|
|
163
|
+
if Window.size[0] != WINDOW_WIDTH or Window.size[1] != height:
|
|
164
|
+
Window.size = (WINDOW_WIDTH, height)
|
|
165
|
+
|
|
166
|
+
def _slot_container(self):
|
|
167
|
+
return self.root.ids["input_fields_container"]
|
|
168
|
+
|
|
169
|
+
def _ordered_slots(self):
|
|
170
|
+
# Kivy children are reverse insertion order.
|
|
171
|
+
return list(reversed(self._slot_container().children))
|
|
172
|
+
|
|
173
|
+
@staticmethod
|
|
174
|
+
def _slot_text_input(slot):
|
|
175
|
+
for child in slot.children:
|
|
176
|
+
if isinstance(child, TextInput):
|
|
177
|
+
return child
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def _slot_secret_checkbox(slot):
|
|
182
|
+
for child in slot.children:
|
|
183
|
+
if isinstance(child, MDCheckbox) and getattr(child, "secret_checkbox", False):
|
|
184
|
+
return child
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
def _renumber_slots(self):
|
|
188
|
+
for index, slot in enumerate(self._ordered_slots()):
|
|
189
|
+
slot.text_input_id = f"input_text_{index}"
|
|
190
|
+
|
|
191
|
+
def add_slot(self, text: str = "", secret: bool = False):
|
|
192
|
+
container = self._slot_container()
|
|
193
|
+
index = len(container.children)
|
|
194
|
+
slot = Factory.InputFieldButton()
|
|
195
|
+
slot.text_input_id = f"input_text_{index}"
|
|
196
|
+
container.add_widget(slot)
|
|
197
|
+
|
|
198
|
+
text_input = self._slot_text_input(slot)
|
|
199
|
+
secret_cb = self._slot_secret_checkbox(slot)
|
|
200
|
+
|
|
201
|
+
previous = self._updating_slots
|
|
202
|
+
self._updating_slots = True
|
|
203
|
+
try:
|
|
204
|
+
if text_input is not None:
|
|
205
|
+
text_input.text = text
|
|
206
|
+
text_input.password = bool(secret)
|
|
207
|
+
if secret_cb is not None:
|
|
208
|
+
secret_cb.active = bool(secret)
|
|
209
|
+
finally:
|
|
210
|
+
self._updating_slots = previous
|
|
211
|
+
|
|
212
|
+
return slot
|
|
213
|
+
|
|
214
|
+
def clear_slots(self):
|
|
215
|
+
self._slot_container().clear_widgets()
|
|
216
|
+
|
|
217
|
+
def ensure_trailing_empty_slot(self, *_args):
|
|
218
|
+
if self._updating_slots:
|
|
219
|
+
return
|
|
220
|
+
|
|
221
|
+
self._updating_slots = True
|
|
222
|
+
try:
|
|
223
|
+
slots = self._ordered_slots()
|
|
224
|
+
if not slots:
|
|
225
|
+
self.add_slot()
|
|
226
|
+
self._renumber_slots()
|
|
227
|
+
return
|
|
228
|
+
|
|
229
|
+
# Remove extra trailing empty slots (keep exactly one empty at the end).
|
|
230
|
+
while len(slots) > 1:
|
|
231
|
+
last = slots[-1]
|
|
232
|
+
prev = slots[-2]
|
|
233
|
+
last_text = self._slot_text_input(last)
|
|
234
|
+
prev_text = self._slot_text_input(prev)
|
|
235
|
+
last_empty = last_text is None or not last_text.text.strip()
|
|
236
|
+
prev_empty = prev_text is None or not prev_text.text.strip()
|
|
237
|
+
if last_empty and prev_empty:
|
|
238
|
+
self._slot_container().remove_widget(last)
|
|
239
|
+
slots = self._ordered_slots()
|
|
240
|
+
continue
|
|
241
|
+
break
|
|
242
|
+
|
|
243
|
+
last = self._ordered_slots()[-1]
|
|
244
|
+
last_text = self._slot_text_input(last)
|
|
245
|
+
if last_text is not None and last_text.text.strip():
|
|
246
|
+
self.add_slot()
|
|
247
|
+
|
|
248
|
+
self._renumber_slots()
|
|
249
|
+
finally:
|
|
250
|
+
self._updating_slots = False
|
|
251
|
+
self.update_window_height()
|
|
252
|
+
|
|
253
|
+
def on_slot_text(self, _instance, _value):
|
|
254
|
+
self.ensure_trailing_empty_slot()
|
|
255
|
+
|
|
256
|
+
def read_config(self):
|
|
257
|
+
try:
|
|
258
|
+
with open(self.json_config, "r", encoding='utf-8') as file:
|
|
259
|
+
self.config = json.load(file)
|
|
260
|
+
except FileNotFoundError:
|
|
261
|
+
self.config = None
|
|
262
|
+
except AttributeError:
|
|
263
|
+
self.config = None
|
|
264
|
+
except json.JSONDecodeError:
|
|
265
|
+
self.config = None
|
|
266
|
+
except ValueError:
|
|
267
|
+
self.config = None
|
|
268
|
+
except KeyError:
|
|
269
|
+
self.config = None
|
|
270
|
+
|
|
271
|
+
if not self.config:
|
|
272
|
+
self.config = {'profiles': {'DEFAULT': {}}}
|
|
273
|
+
elif 'profiles' in self.config:
|
|
274
|
+
pass
|
|
275
|
+
elif 'input_text_0' in self.config:
|
|
276
|
+
values = self.config
|
|
277
|
+
self.config = {'profiles': {'DEFAULT': values}}
|
|
278
|
+
else:
|
|
279
|
+
self.config = {'profiles': {'DEFAULT': {}}}
|
|
280
|
+
|
|
281
|
+
def write_config(self):
|
|
282
|
+
with open(self.json_config, "w", encoding='utf-8') as file:
|
|
283
|
+
json.dump(self.config, file)
|
|
284
|
+
|
|
285
|
+
@staticmethod
|
|
286
|
+
def close_dialog(button_instance):
|
|
287
|
+
# Navigate up the widget tree until we find an MDDialog instance
|
|
288
|
+
current_widget = button_instance
|
|
289
|
+
while current_widget and not isinstance(current_widget, MDDialog):
|
|
290
|
+
current_widget = current_widget.parent
|
|
291
|
+
|
|
292
|
+
# If we found an MDDialog instance, dismiss it
|
|
293
|
+
if isinstance(current_widget, MDDialog):
|
|
294
|
+
current_widget.dismiss()
|
|
295
|
+
|
|
296
|
+
def add_profile(self, profile_name, button_instance):
|
|
297
|
+
# Access the text from the input box
|
|
298
|
+
print(f"New profile name: {profile_name}")
|
|
299
|
+
self.config['profiles'][profile_name] = dict()
|
|
300
|
+
self.set_profile(None, profile_name)
|
|
301
|
+
|
|
302
|
+
# Close the dialog using the button instance
|
|
303
|
+
self.close_dialog(button_instance)
|
|
304
|
+
|
|
305
|
+
def create_new_profile(self):
|
|
306
|
+
self.close_dropdown()
|
|
307
|
+
|
|
308
|
+
profile_input = MDTextField(
|
|
309
|
+
hint_text="Enter profile name",
|
|
310
|
+
size_hint_x=0.8,
|
|
311
|
+
pos_hint={"center_x": 0.5}
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
dialog = MDDialog(
|
|
315
|
+
title="New Profile",
|
|
316
|
+
type="custom",
|
|
317
|
+
content_cls=profile_input,
|
|
318
|
+
buttons=[
|
|
319
|
+
MDRaisedButton(
|
|
320
|
+
text="CANCEL",
|
|
321
|
+
on_release=self.close_dialog
|
|
322
|
+
),
|
|
323
|
+
MDRaisedButton(
|
|
324
|
+
text="OK",
|
|
325
|
+
on_release=lambda x: self.add_profile(profile_input.text, x)
|
|
326
|
+
)
|
|
327
|
+
],
|
|
328
|
+
)
|
|
329
|
+
dialog.open()
|
|
330
|
+
|
|
331
|
+
def close_dropdown(self):
|
|
332
|
+
self.profile_dropdown.dismiss()
|
|
333
|
+
|
|
334
|
+
def handle_dropdown(self, profile):
|
|
335
|
+
self.set_profile(None, profile)
|
|
336
|
+
self.close_dropdown()
|
|
337
|
+
|
|
338
|
+
def generate_profile_selector(self):
|
|
339
|
+
profile_dropdown = self.profile_dropdown
|
|
340
|
+
profile_selector = self.root.ids['profile_selector']
|
|
341
|
+
try:
|
|
342
|
+
profiles = list(self.config['profiles'].keys())
|
|
343
|
+
except KeyError:
|
|
344
|
+
profiles = ['DEFAULT']
|
|
345
|
+
max_length = 7
|
|
346
|
+
|
|
347
|
+
profile_dropdown.clear_widgets()
|
|
348
|
+
|
|
349
|
+
btn = MDRaisedButton()
|
|
350
|
+
btn.text = '- New -'
|
|
351
|
+
btn.size_hint_y = None
|
|
352
|
+
btn.bind(on_release=lambda x: self.create_new_profile())
|
|
353
|
+
profile_dropdown.add_widget(btn)
|
|
354
|
+
|
|
355
|
+
for profile in profiles:
|
|
356
|
+
if len(profile) > max_length:
|
|
357
|
+
max_length = len(profile)
|
|
358
|
+
|
|
359
|
+
if profile != profile_selector.text.strip():
|
|
360
|
+
btn = MDRaisedButton()
|
|
361
|
+
btn.text = profile
|
|
362
|
+
btn.size_hint_y = None
|
|
363
|
+
btn.bind(on_release=lambda x, p=profile: self.handle_dropdown(p))
|
|
364
|
+
profile_dropdown.add_widget(btn)
|
|
365
|
+
|
|
366
|
+
number_of_whitespaces = max_length - len(profile_selector.text.strip())
|
|
367
|
+
profile_selector.text = profile_selector.text + (number_of_whitespaces + 1) * " "
|
|
368
|
+
|
|
369
|
+
def set_profile(self, dt=None, profile=None):
|
|
370
|
+
profile_selector = self.root.ids['profile_selector']
|
|
371
|
+
|
|
372
|
+
if not self.config or 'profiles' not in self.config or not self.config['profiles']:
|
|
373
|
+
self.config = self.config or {}
|
|
374
|
+
self.config['profiles'] = {'DEFAULT': {}}
|
|
375
|
+
|
|
376
|
+
if profile is None:
|
|
377
|
+
profile_name = list(self.config['profiles'].keys())[0]
|
|
378
|
+
else:
|
|
379
|
+
profile_name = profile
|
|
380
|
+
|
|
381
|
+
profile_selector.text = profile_name
|
|
382
|
+
self.load_inputs()
|
|
383
|
+
self.generate_profile_selector()
|
|
384
|
+
|
|
385
|
+
def load_inputs(self):
|
|
386
|
+
profile_selector = self.root.ids['profile_selector']
|
|
387
|
+
profile_name = profile_selector.text.strip()
|
|
388
|
+
|
|
389
|
+
profile_values = {}
|
|
390
|
+
if self.config and profile_name in self.config.get('profiles', {}):
|
|
391
|
+
profile_values = self.config['profiles'][profile_name] or {}
|
|
392
|
+
|
|
393
|
+
def sort_key(item):
|
|
394
|
+
key = item[0]
|
|
395
|
+
try:
|
|
396
|
+
return int(str(key).rsplit("_", 1)[-1])
|
|
397
|
+
except ValueError:
|
|
398
|
+
return 0
|
|
399
|
+
|
|
400
|
+
filled = []
|
|
401
|
+
for _name, value in sorted(profile_values.items(), key=sort_key):
|
|
402
|
+
if isinstance(value, (list, tuple)) and len(value) >= 2:
|
|
403
|
+
text, secret_state = value[0], value[1]
|
|
404
|
+
else:
|
|
405
|
+
text, secret_state = value, False
|
|
406
|
+
if text:
|
|
407
|
+
filled.append((text, bool(secret_state)))
|
|
408
|
+
|
|
409
|
+
self.clear_slots()
|
|
410
|
+
self._updating_slots = True
|
|
411
|
+
try:
|
|
412
|
+
for text, secret_state in filled:
|
|
413
|
+
self.add_slot(text=text, secret=secret_state)
|
|
414
|
+
finally:
|
|
415
|
+
self._updating_slots = False
|
|
416
|
+
self.ensure_trailing_empty_slot()
|
|
417
|
+
self.update_window_height()
|
|
418
|
+
|
|
419
|
+
def save_inputs(self):
|
|
420
|
+
profile_selector = self.root.ids['profile_selector']
|
|
421
|
+
profile_name = profile_selector.text.strip()
|
|
422
|
+
|
|
423
|
+
input_fields = {}
|
|
424
|
+
for slot in self._ordered_slots():
|
|
425
|
+
text_input = self._slot_text_input(slot)
|
|
426
|
+
if text_input is None or not text_input.text:
|
|
427
|
+
continue
|
|
428
|
+
secret_cb = self._slot_secret_checkbox(slot)
|
|
429
|
+
secret_state = bool(secret_cb.active) if secret_cb is not None else False
|
|
430
|
+
input_fields[slot.text_input_id] = (text_input.text, secret_state)
|
|
431
|
+
|
|
432
|
+
if not self.config:
|
|
433
|
+
self.config = {'profiles': {}}
|
|
434
|
+
if 'profiles' not in self.config:
|
|
435
|
+
self.config['profiles'] = {}
|
|
436
|
+
self.config['profiles'][profile_name] = input_fields
|
|
437
|
+
self.write_config()
|
|
438
|
+
|
|
439
|
+
def type_text(self, input_text, _checkbox):
|
|
440
|
+
if not input_text:
|
|
441
|
+
# print("No text found")
|
|
442
|
+
return
|
|
443
|
+
|
|
444
|
+
if _checkbox.state == "down":
|
|
445
|
+
end_line = True
|
|
446
|
+
else:
|
|
447
|
+
end_line = False
|
|
448
|
+
|
|
449
|
+
start_delay = float(self.root.ids["start_delay"].value)
|
|
450
|
+
mod_delay = float(self.root.ids["mod_delay"].value)
|
|
451
|
+
type_string_with_delay(
|
|
452
|
+
input_text,
|
|
453
|
+
start_delay=start_delay,
|
|
454
|
+
mod_delay=mod_delay,
|
|
455
|
+
keypress_delay=0.02,
|
|
456
|
+
layout=self.layout,
|
|
457
|
+
end_line=end_line,
|
|
458
|
+
on_error=self.show_type_error,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
def show_type_error(self, error):
|
|
462
|
+
MDDialog(title="Kan ikke sende tastaturinput", text=str(error)).open()
|
|
463
|
+
|
|
464
|
+
@staticmethod
|
|
465
|
+
def copy_text(input_text, _checkbox):
|
|
466
|
+
if not input_text:
|
|
467
|
+
# print("No text found")
|
|
468
|
+
return
|
|
469
|
+
|
|
470
|
+
cmd = 'echo ' + input_text.strip() + '|clip'
|
|
471
|
+
subprocess.check_call(cmd, shell=True)
|
|
472
|
+
|
|
473
|
+
@staticmethod
|
|
474
|
+
def hide_text(_input_text, _checkbox):
|
|
475
|
+
if _checkbox.state == "down":
|
|
476
|
+
_input_text.password = True
|
|
477
|
+
else:
|
|
478
|
+
_input_text.password = False
|
|
479
|
+
|
|
480
|
+
def set_layout(self, layout):
|
|
481
|
+
self.layout = layout
|
|
482
|
+
|
|
483
|
+
def update_slider_label(self, value):
|
|
484
|
+
rounded_value = round(value, 1)
|
|
485
|
+
self.root.ids["mod_delay"].value = rounded_value
|
|
486
|
+
|
|
487
|
+
def detect_keyboard_layout(self):
|
|
488
|
+
layout_code = get_keyboard_layout()
|
|
489
|
+
|
|
490
|
+
if bool(re.match(r'da|dk', layout_code, re.I)):
|
|
491
|
+
self.layout = 'DA_DK'
|
|
492
|
+
else: # Default to English (US) layout
|
|
493
|
+
self.layout = 'EN_US'
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
def main():
|
|
497
|
+
ensure_user_desktop_entry()
|
|
498
|
+
autoupdate()
|
|
499
|
+
KeyboardPasterApp().run()
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
if __name__ == "__main__":
|
|
503
|
+
main()
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import os
|
|
2
|
+
|
|
3
|
+
from keyboardpaster.shared_resources import APP_WM_CLASS, app_icon_path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def configure_kivy_environment() -> None:
|
|
7
|
+
if getattr(configure_kivy_environment, "_done", False):
|
|
8
|
+
return
|
|
9
|
+
|
|
10
|
+
os.environ["SDL_MOUSE_FOCUS_CLICKTHROUGH"] = "1"
|
|
11
|
+
os.environ.setdefault("SDL_VIDEO_X11_WMCLASS", APP_WM_CLASS)
|
|
12
|
+
os.environ.setdefault("SDL_VIDEO_WAYLAND_WMCLASS", APP_WM_CLASS)
|
|
13
|
+
|
|
14
|
+
icon = app_icon_path()
|
|
15
|
+
if icon is not None:
|
|
16
|
+
from kivy.config import Config
|
|
17
|
+
|
|
18
|
+
Config.set("kivy", "window_icon", str(icon))
|
|
19
|
+
|
|
20
|
+
configure_kivy_environment._done = True # type: ignore[attr-defined]
|