213 lines
6.6 KiB
Python
213 lines
6.6 KiB
Python
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")
|