This repository was archived by the owner on Mar 19, 2021. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathdraft_ui.py
More file actions
213 lines (172 loc) · 6.64 KB
/
draft_ui.py
File metadata and controls
213 lines (172 loc) · 6.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
from typing import Optional
import re
from talon.experimental.textarea import (
TextArea,
Span,
DarkThemeLabels,
LightThemeLabels
)
word_matcher = re.compile(r"([^\s]+)(\s*)")
def calculate_text_anchors(text, cursor_position, anchor_labels=None):
"""
Produces an iterator of (anchor, start_word_index, end_word_index, last_space_index)
tuples from the given text. Each tuple indicates a particular point you may want to
reference when editing along with some useful ranges you may want to operate on.
- text is the text you want to process.
- cursor_position is the current position of the cursor, anchors will be placed around
this.
- anchor_labels is a list of characters you want to use for your labels.
- *index is just a character offset from the start of the string (e.g. the first character is at index 0)
- end_word_index is the index of the character after the last one included in the
anchor. That is, you can use it with a slice directly like [start:end]
- anchor is a short piece of text you can use to identify it (e.g. 'a', or '1').
"""
anchor_labels = anchor_labels or "abcdefghijklmnopqrstuvwxyz"
if len(text) == 0:
return []
# Find all the word spans
matches = []
cursor_idx = None
for match in word_matcher.finditer(text):
matches.append((
match.start(),
match.end() - len(match.group(2)),
match.end()
))
if matches[-1][0] <= cursor_position and matches[-1][2] >= cursor_position:
cursor_idx = len(matches) - 1
# Now work out what range of those matches are getting an anchor. The aim is
# to centre the anchors around the cursor position, but also to use all the
# anchors.
anchors_before_cursor = len(anchor_labels) // 2
anchor_start_idx = max(0, cursor_idx - anchors_before_cursor)
anchor_end_idx = min(len(matches), anchor_start_idx + len(anchor_labels))
anchor_start_idx = max(0, anchor_end_idx - len(anchor_labels))
# Now add anchors to the selected matches
for i, anchor in zip(range(anchor_start_idx, anchor_end_idx), anchor_labels):
word_start, word_end, whitespace_end = matches[i]
yield (
anchor,
word_start,
word_end,
whitespace_end
)
class DraftManager:
"""
API to the draft window
"""
def __init__(self):
self.area = TextArea()
self.area.title = "Talon Draft"
self.area.value = ""
self.area.register("label", self._update_labels)
self.set_styling()
def set_styling(
self,
theme="dark",
text_size=20,
label_size=20,
label_color=None
):
"""
Allow settings the style of the draft window. Will dynamically
update the style based on the passed in parameters.
"""
area_theme = DarkThemeLabels if theme == "dark" else LightThemeLabels
theme_changes = {
"text_size": text_size,
"label_size": label_size,
}
if label_color is not None:
theme_changes["label"] = label_color
self.area.theme = area_theme(**theme_changes)
def show(self, text: Optional[str] = None):
"""
Show the window. If text is None then keep the old contents,
otherwise set the text to the given value.
"""
if text is not None:
self.area.value = text
self.area.show()
def hide(self):
"""
Hide the window.
"""
self.area.hide()
def get_text(self) -> str:
"""
Gets the context of the text area
"""
return self.area.value
def get_rect(self) -> "talon.types.Rect":
"""
Get the Rect for the window
"""
return self.area.rect
def reposition(
self,
xpos: Optional[int] = None,
ypos: Optional[int] = None,
width: Optional[int] = None,
height: Optional[int] = None,
):
"""
Move the window or resize it without having to change all properties.
"""
rect = self.area.rect
if xpos is not None:
rect.x = xpos
if ypos is not None:
rect.y = ypos
if width is not None:
rect.width = width
if height is not None:
rect.height = height
self.area.rect = rect
def select_text(
self, start_anchor, end_anchor=None, include_trailing_whitespace=False
):
"""
Selects the word corresponding to start_anchor. If end_anchor supplied, selects
from start_anchor to the end of end_anchor. If include_trailing_whitespace=True
then also selects trailing space characters (useful for delete).
"""
start_index, end_index, last_space_index = self.anchor_to_range(start_anchor)
if end_anchor is not None:
_, end_index, last_space_index = self.anchor_to_range(end_anchor)
if include_trailing_whitespace:
end_index = last_space_index
self.area.sel = Span(start_index, end_index)
def position_caret(self, anchor, after=False):
"""
Positions the caret before the given anchor. If after=True position it directly after.
"""
start_index, end_index, _ = self.anchor_to_range(anchor)
index = end_index if after else start_index
self.area.sel = index
def anchor_to_range(self, anchor):
anchors_data = calculate_text_anchors(self._get_visible_text(), self.area.sel.left)
for loop_anchor, start_index, end_index, last_space_index in anchors_data:
if anchor == loop_anchor:
return (start_index, end_index, last_space_index)
raise RuntimeError(f"Couldn't find anchor {anchor}")
def _update_labels(self, _visible_text):
"""
Updates the position of the labels displayed on top of each word
"""
anchors_data = calculate_text_anchors(self._get_visible_text(), self.area.sel.left)
return [
(Span(start_index, end_index), anchor)
for anchor, start_index, end_index, _ in anchors_data
]
def _get_visible_text(self):
# Placeholder for a future method of getting this
return self.area.value
if False:
# Some code for testing, change above False to True and edit as desired
draft_manager = DraftManager()
draft_manager.show(
"This is a line of text\nand another line of text and some more text so that the line gets so long that it wraps a bit.\nAnd a final sentence"
)
draft_manager.reposition(xpos=100, ypos=100)
draft_manager.select_text("c")