toga-code-editor 0.1.1__py3-none-any.whl
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.
- toga_code_editor/__init__.py +16 -0
- toga_code_editor/code_editor.py +169 -0
- toga_code_editor/highlighting.py +199 -0
- toga_code_editor/platforms/__init__.py +0 -0
- toga_code_editor/platforms/android.py +217 -0
- toga_code_editor/platforms/cocoa.py +264 -0
- toga_code_editor/platforms/dummy.py +21 -0
- toga_code_editor/platforms/iOS.py +314 -0
- toga_code_editor-0.1.1.dist-info/METADATA +128 -0
- toga_code_editor-0.1.1.dist-info/RECORD +14 -0
- toga_code_editor-0.1.1.dist-info/WHEEL +5 -0
- toga_code_editor-0.1.1.dist-info/entry_points.txt +11 -0
- toga_code_editor-0.1.1.dist-info/licenses/LICENSE +27 -0
- toga_code_editor-0.1.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from bisect import bisect_right
|
|
2
|
+
|
|
3
|
+
from rubicon.objc import (
|
|
4
|
+
SEL,
|
|
5
|
+
NSPoint,
|
|
6
|
+
NSRange,
|
|
7
|
+
NSRect,
|
|
8
|
+
ObjCClass,
|
|
9
|
+
objc_method,
|
|
10
|
+
objc_property,
|
|
11
|
+
)
|
|
12
|
+
from toga_cocoa.colors import native_color
|
|
13
|
+
from toga_cocoa.libs import (
|
|
14
|
+
NSAttributedString,
|
|
15
|
+
NSBezelBorder,
|
|
16
|
+
NSColor,
|
|
17
|
+
NSFontAttributeName,
|
|
18
|
+
NSFontManager,
|
|
19
|
+
NSForegroundColorAttributeName,
|
|
20
|
+
NSMutableDictionary,
|
|
21
|
+
NSNotificationCenter,
|
|
22
|
+
NSScrollView,
|
|
23
|
+
NSViewBoundsDidChangeNotification,
|
|
24
|
+
NSViewHeightSizable,
|
|
25
|
+
NSViewWidthSizable,
|
|
26
|
+
)
|
|
27
|
+
from toga_cocoa.widgets.multilinetextinput import MultilineTextInput, TogaTextView
|
|
28
|
+
|
|
29
|
+
from ..highlighting import to_utf16_spans, utf16_line_starts
|
|
30
|
+
|
|
31
|
+
NSRulerView = ObjCClass("NSRulerView")
|
|
32
|
+
|
|
33
|
+
# NSRulerOrientation
|
|
34
|
+
NSVerticalRuler = 1
|
|
35
|
+
# NSFontTraitMask
|
|
36
|
+
NSItalicFontMask = 1 << 0
|
|
37
|
+
NSBoldFontMask = 1 << 1
|
|
38
|
+
|
|
39
|
+
GUTTER_PADDING = 6
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class TogaCodeTextView(TogaTextView):
|
|
43
|
+
@objc_method
|
|
44
|
+
def textDidChange_(self, notification) -> None:
|
|
45
|
+
self.interface._schedule_rehighlight()
|
|
46
|
+
self.interface.on_change()
|
|
47
|
+
self.impl.text_changed()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class TogaLineNumberView(NSRulerView):
|
|
51
|
+
impl = objc_property(object, weak=True)
|
|
52
|
+
|
|
53
|
+
@objc_method
|
|
54
|
+
def boundsDidChange_(self, notification) -> None:
|
|
55
|
+
# The text scrolled; the visible line numbers changed with it.
|
|
56
|
+
self.setNeedsDisplay(True)
|
|
57
|
+
|
|
58
|
+
@objc_method
|
|
59
|
+
def drawHashMarksAndLabelsInRect_(self, rect: NSRect) -> None:
|
|
60
|
+
self.impl.draw_line_numbers()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class CodeEditor(MultilineTextInput):
|
|
64
|
+
def create(self):
|
|
65
|
+
# Mirrors toga_cocoa's MultilineTextInput.create(), swapping in
|
|
66
|
+
# TogaCodeTextView, turning off prose features, and adding a ruler.
|
|
67
|
+
self.native = NSScrollView.alloc().init()
|
|
68
|
+
self.native.hasVerticalScroller = True
|
|
69
|
+
self.native.hasHorizontalScroller = False
|
|
70
|
+
self.native.autohidesScrollers = False
|
|
71
|
+
self.native.borderType = NSBezelBorder
|
|
72
|
+
|
|
73
|
+
self.native_text = TogaCodeTextView.alloc().init()
|
|
74
|
+
self.native_text.interface = self.interface
|
|
75
|
+
self.native_text.impl = self
|
|
76
|
+
self.native_text.delegate = self.native_text
|
|
77
|
+
|
|
78
|
+
self.native_text.editable = True
|
|
79
|
+
self.native_text.selectable = True
|
|
80
|
+
self.native_text.allowsUndo = True
|
|
81
|
+
self.native_text.verticallyResizable = True
|
|
82
|
+
self.native_text.horizontallyResizable = False
|
|
83
|
+
self.native_text.usesAdaptiveColorMappingForDarkAppearance = True
|
|
84
|
+
self.native_text.setAutomaticQuoteSubstitutionEnabled(False)
|
|
85
|
+
self.native_text.setAutomaticDashSubstitutionEnabled(False)
|
|
86
|
+
# Code is not prose.
|
|
87
|
+
self.native_text.setContinuousSpellCheckingEnabled(False)
|
|
88
|
+
self.native_text.setGrammarCheckingEnabled(False)
|
|
89
|
+
self.native_text.setAutomaticSpellingCorrectionEnabled(False)
|
|
90
|
+
self.native_text.setAutomaticTextReplacementEnabled(False)
|
|
91
|
+
self.native_text.setAutomaticTextCompletionEnabled(False)
|
|
92
|
+
|
|
93
|
+
self.native_text.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable
|
|
94
|
+
self.native.documentView = self.native_text
|
|
95
|
+
|
|
96
|
+
# Reading layoutManager opts the view into TextKit 1, which is what the
|
|
97
|
+
# gutter's line-fragment queries need. Do it once, up front.
|
|
98
|
+
self.layout_manager = self.native_text.layoutManager
|
|
99
|
+
|
|
100
|
+
self.ruler = TogaLineNumberView.alloc().initWithScrollView(
|
|
101
|
+
self.native, orientation=NSVerticalRuler
|
|
102
|
+
)
|
|
103
|
+
self.ruler.impl = self
|
|
104
|
+
self.ruler.clientView = self.native_text
|
|
105
|
+
self.ruler.reservedThicknessForMarkers = 0
|
|
106
|
+
self.ruler.reservedThicknessForAccessoryView = 0
|
|
107
|
+
# Since macOS 14 views no longer clip by default, and AppKit hands the ruler
|
|
108
|
+
# a dirty rect that reaches the window's top edge, so its rule line would be
|
|
109
|
+
# drawn above the scroll view. Earlier systems always clip.
|
|
110
|
+
if self.ruler.respondsToSelector_(SEL("setClipsToBounds:")):
|
|
111
|
+
self.ruler.clipsToBounds = True
|
|
112
|
+
self.native.hasVerticalRuler = True
|
|
113
|
+
self.native.verticalRulerView = self.ruler
|
|
114
|
+
self.native.rulersVisible = True
|
|
115
|
+
|
|
116
|
+
# Redraw the gutter whenever the clip view scrolls.
|
|
117
|
+
self.native.contentView.postsBoundsChangedNotifications = True
|
|
118
|
+
NSNotificationCenter.defaultCenter.addObserver(
|
|
119
|
+
self.ruler,
|
|
120
|
+
selector=SEL("boundsDidChange:"),
|
|
121
|
+
name=NSViewBoundsDidChangeNotification,
|
|
122
|
+
object=self.native.contentView,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# NSTextView.font and .textColor read back the *first character's* attributes
|
|
126
|
+
# once the storage is styled, so the base style is tracked here instead.
|
|
127
|
+
self.base_font = self.native_text.font
|
|
128
|
+
self.base_color = NSColor.textColor
|
|
129
|
+
|
|
130
|
+
self.theme = {}
|
|
131
|
+
self.attributes = {}
|
|
132
|
+
self.spans = []
|
|
133
|
+
self.line_starts = [0]
|
|
134
|
+
|
|
135
|
+
self.add_constraints()
|
|
136
|
+
|
|
137
|
+
# Inherited MultilineTextInput methods that must keep the gutter and colors in sync
|
|
138
|
+
|
|
139
|
+
def set_value(self, value):
|
|
140
|
+
super().set_value(value)
|
|
141
|
+
self.text_changed()
|
|
142
|
+
|
|
143
|
+
def set_font(self, font):
|
|
144
|
+
super().set_font(font)
|
|
145
|
+
self.base_font = font._impl.native
|
|
146
|
+
self.rebuild_attributes()
|
|
147
|
+
self.apply_highlights()
|
|
148
|
+
self.text_changed()
|
|
149
|
+
|
|
150
|
+
def set_color(self, value):
|
|
151
|
+
super().set_color(value)
|
|
152
|
+
self.base_color = native_color(value) or NSColor.textColor
|
|
153
|
+
self.apply_highlights()
|
|
154
|
+
|
|
155
|
+
# CodeEditor backend contract
|
|
156
|
+
|
|
157
|
+
def set_theme(self, theme):
|
|
158
|
+
self.theme = theme
|
|
159
|
+
self.rebuild_attributes()
|
|
160
|
+
|
|
161
|
+
def set_highlights(self, spans):
|
|
162
|
+
self.spans = spans
|
|
163
|
+
self.apply_highlights()
|
|
164
|
+
|
|
165
|
+
def set_show_line_numbers(self, value):
|
|
166
|
+
self.native.rulersVisible = value
|
|
167
|
+
|
|
168
|
+
# Highlighting
|
|
169
|
+
|
|
170
|
+
def rebuild_attributes(self):
|
|
171
|
+
base_font = self.base_font
|
|
172
|
+
manager = NSFontManager.sharedFontManager
|
|
173
|
+
self.attributes = {}
|
|
174
|
+
for kind, style in self.theme.items():
|
|
175
|
+
attributes = NSMutableDictionary.alloc().init()
|
|
176
|
+
attributes[NSForegroundColorAttributeName] = native_color(style.color)
|
|
177
|
+
traits = 0
|
|
178
|
+
if style.bold:
|
|
179
|
+
traits |= NSBoldFontMask
|
|
180
|
+
if style.italic:
|
|
181
|
+
traits |= NSItalicFontMask
|
|
182
|
+
if traits:
|
|
183
|
+
attributes[NSFontAttributeName] = manager.convertFont(
|
|
184
|
+
base_font, toHaveTrait=traits
|
|
185
|
+
)
|
|
186
|
+
self.attributes[kind] = attributes
|
|
187
|
+
|
|
188
|
+
def apply_highlights(self):
|
|
189
|
+
storage = self.native_text.textStorage
|
|
190
|
+
length = storage.length()
|
|
191
|
+
full_range = NSRange(0, length)
|
|
192
|
+
# Between a native edit and the debounced re-highlight the spans describe the
|
|
193
|
+
# previous text, so clamp them: a range past the end raises NSRangeException
|
|
194
|
+
# inside AppKit, which cannot be caught from Python. Convert before editing
|
|
195
|
+
# so a Python error cannot leave the storage mid-edit either.
|
|
196
|
+
spans = [
|
|
197
|
+
(span.start, min(span.end, length), self.attributes.get(span.kind))
|
|
198
|
+
for span in to_utf16_spans(self.get_value(), self.spans)
|
|
199
|
+
if span.start < length
|
|
200
|
+
]
|
|
201
|
+
storage.beginEditing()
|
|
202
|
+
# Reset to the base font and color, then paint each span.
|
|
203
|
+
storage.addAttribute(
|
|
204
|
+
NSFontAttributeName, value=self.base_font, range=full_range
|
|
205
|
+
)
|
|
206
|
+
storage.addAttribute(
|
|
207
|
+
NSForegroundColorAttributeName, value=self.base_color, range=full_range
|
|
208
|
+
)
|
|
209
|
+
for start, end, attributes in spans:
|
|
210
|
+
if attributes is not None and end > start:
|
|
211
|
+
storage.addAttributes(attributes, range=NSRange(start, end - start))
|
|
212
|
+
storage.endEditing()
|
|
213
|
+
|
|
214
|
+
# Gutter
|
|
215
|
+
|
|
216
|
+
def text_changed(self):
|
|
217
|
+
self.line_starts = utf16_line_starts(self.get_value())
|
|
218
|
+
digits = len(str(len(self.line_starts)))
|
|
219
|
+
label = self.gutter_label("0" * digits)
|
|
220
|
+
thickness = label.size().width + 2 * GUTTER_PADDING
|
|
221
|
+
if self.ruler.ruleThickness != thickness:
|
|
222
|
+
self.ruler.ruleThickness = thickness
|
|
223
|
+
self.ruler.setNeedsDisplay(True)
|
|
224
|
+
|
|
225
|
+
def gutter_label(self, text):
|
|
226
|
+
attributes = NSMutableDictionary.alloc().init()
|
|
227
|
+
attributes[NSFontAttributeName] = self.base_font
|
|
228
|
+
attributes[NSForegroundColorAttributeName] = NSColor.secondaryLabelColor
|
|
229
|
+
return NSAttributedString.alloc().initWithString(text, attributes=attributes)
|
|
230
|
+
|
|
231
|
+
def draw_line_numbers(self):
|
|
232
|
+
layout = self.layout_manager
|
|
233
|
+
container = self.native_text.textContainer
|
|
234
|
+
visible = self.native_text.visibleRect
|
|
235
|
+
inset = self.native_text.textContainerInset
|
|
236
|
+
text_length = self.native_text.textStorage.length()
|
|
237
|
+
|
|
238
|
+
glyph_range = layout.glyphRangeForBoundingRect(
|
|
239
|
+
visible, inTextContainer=container
|
|
240
|
+
)
|
|
241
|
+
char_range = layout.characterRangeForGlyphRange(
|
|
242
|
+
glyph_range, actualGlyphRange=None
|
|
243
|
+
)
|
|
244
|
+
first_line = max(bisect_right(self.line_starts, char_range.location) - 1, 0)
|
|
245
|
+
last_char = char_range.location + char_range.length
|
|
246
|
+
thickness = self.ruler.ruleThickness
|
|
247
|
+
|
|
248
|
+
for number, start in enumerate(
|
|
249
|
+
self.line_starts[first_line:], start=first_line + 1
|
|
250
|
+
):
|
|
251
|
+
if start > last_char:
|
|
252
|
+
break
|
|
253
|
+
if start == text_length:
|
|
254
|
+
# The empty line after a trailing newline, or an empty document.
|
|
255
|
+
fragment = layout.extraLineFragmentRect
|
|
256
|
+
else:
|
|
257
|
+
glyph = layout.glyphIndexForCharacterAtIndex(start)
|
|
258
|
+
fragment = layout.lineFragmentRectForGlyphAtIndex(
|
|
259
|
+
glyph, effectiveRange=None
|
|
260
|
+
)
|
|
261
|
+
label = self.gutter_label(str(number))
|
|
262
|
+
x = thickness - GUTTER_PADDING - label.size().width
|
|
263
|
+
y = fragment.origin.y - visible.origin.y + inset.height
|
|
264
|
+
label.drawAtPoint(NSPoint(x, y))
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from toga_dummy.widgets.multilinetextinput import MultilineTextInput
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CodeEditor(MultilineTextInput):
|
|
5
|
+
def create(self):
|
|
6
|
+
self._action("create CodeEditor")
|
|
7
|
+
|
|
8
|
+
def set_theme(self, theme):
|
|
9
|
+
self._set_value("theme", theme)
|
|
10
|
+
|
|
11
|
+
def set_highlights(self, spans):
|
|
12
|
+
self._set_value("highlights", spans)
|
|
13
|
+
|
|
14
|
+
def set_show_line_numbers(self, value):
|
|
15
|
+
self._set_value("show_line_numbers", value)
|
|
16
|
+
|
|
17
|
+
def simulate_change(self):
|
|
18
|
+
# Mirror the real backends: the re-highlight is scheduled before the
|
|
19
|
+
# user's handler runs.
|
|
20
|
+
self.interface._schedule_rehighlight()
|
|
21
|
+
self.interface.on_change()
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
from bisect import bisect_right
|
|
2
|
+
|
|
3
|
+
from rubicon.objc import (
|
|
4
|
+
CGPoint,
|
|
5
|
+
CGRect,
|
|
6
|
+
NSMutableDictionary,
|
|
7
|
+
NSRange,
|
|
8
|
+
objc_method,
|
|
9
|
+
send_message,
|
|
10
|
+
send_super,
|
|
11
|
+
)
|
|
12
|
+
from rubicon.objc.types import NSInteger, UIEdgeInsets
|
|
13
|
+
from toga_iOS.colors import native_color
|
|
14
|
+
from toga_iOS.libs import (
|
|
15
|
+
NSAttributedString,
|
|
16
|
+
NSFontAttributeName,
|
|
17
|
+
NSForegroundColorAttributeName,
|
|
18
|
+
NSLayoutAttributeBottom,
|
|
19
|
+
NSLayoutAttributeLeading,
|
|
20
|
+
NSLayoutAttributeTop,
|
|
21
|
+
NSLayoutAttributeTrailing,
|
|
22
|
+
NSLayoutConstraint,
|
|
23
|
+
NSLayoutRelationEqual,
|
|
24
|
+
UIColor,
|
|
25
|
+
UIFont,
|
|
26
|
+
UIFontDescriptorTraitBold,
|
|
27
|
+
UIFontDescriptorTraitItalic,
|
|
28
|
+
UILabel,
|
|
29
|
+
)
|
|
30
|
+
from toga_iOS.widgets.multilinetextinput import (
|
|
31
|
+
MultilineTextInput,
|
|
32
|
+
TogaMultilineTextView,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
from ..highlighting import to_utf16_spans, utf16_line_starts
|
|
36
|
+
|
|
37
|
+
UIColor.declare_class_property("labelColor")
|
|
38
|
+
UIColor.declare_class_property("secondaryLabelColor")
|
|
39
|
+
|
|
40
|
+
# UIViewContentMode
|
|
41
|
+
UIViewContentModeRedraw = 3
|
|
42
|
+
# UITextAutocapitalizationType / UITextAutocorrectionType / UITextSpellCheckingType /
|
|
43
|
+
# UITextSmartQuotesType / UITextSmartDashesType / UITextSmartInsertDeleteType
|
|
44
|
+
UITextAutocapitalizationTypeNone = 0
|
|
45
|
+
UITextAutocorrectionTypeNo = 1
|
|
46
|
+
UITextSpellCheckingTypeNo = 1
|
|
47
|
+
UITextSmartQuotesTypeNo = 1
|
|
48
|
+
UITextSmartDashesTypeNo = 1
|
|
49
|
+
UITextSmartInsertDeleteTypeNo = 1
|
|
50
|
+
|
|
51
|
+
GUTTER_PADDING = 6
|
|
52
|
+
PLACEHOLDER_LEADING = 4.0
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TogaCodeTextView(TogaMultilineTextView):
|
|
56
|
+
@objc_method
|
|
57
|
+
def textViewDidChange_(self, text_view):
|
|
58
|
+
self.interface._schedule_rehighlight()
|
|
59
|
+
self.interface.on_change()
|
|
60
|
+
self.impl.text_changed()
|
|
61
|
+
|
|
62
|
+
@objc_method
|
|
63
|
+
def drawRect_(self, rect: CGRect) -> None:
|
|
64
|
+
send_super(__class__, self, "drawRect:", rect, argtypes=[CGRect])
|
|
65
|
+
self.impl.draw_line_numbers()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class CodeEditor(MultilineTextInput):
|
|
69
|
+
def create(self):
|
|
70
|
+
# Mirrors toga_iOS's MultilineTextInput.create(), swapping in
|
|
71
|
+
# TogaCodeTextView and turning off prose features.
|
|
72
|
+
self.native = TogaCodeTextView.alloc().init()
|
|
73
|
+
self.native.interface = self.interface
|
|
74
|
+
self.native.impl = self
|
|
75
|
+
self.native.delegate = self.native
|
|
76
|
+
self.native.contentMode = UIViewContentModeRedraw
|
|
77
|
+
# UITextView forwards its UITextInputTraits setters to an internal object, so
|
|
78
|
+
# rubicon cannot see them as properties and attribute assignment is silently
|
|
79
|
+
# dropped. Send the messages directly.
|
|
80
|
+
for selector, value in (
|
|
81
|
+
("setAutocapitalizationType:", UITextAutocapitalizationTypeNone),
|
|
82
|
+
("setAutocorrectionType:", UITextAutocorrectionTypeNo),
|
|
83
|
+
("setSpellCheckingType:", UITextSpellCheckingTypeNo),
|
|
84
|
+
("setSmartQuotesType:", UITextSmartQuotesTypeNo),
|
|
85
|
+
("setSmartDashesType:", UITextSmartDashesTypeNo),
|
|
86
|
+
("setSmartInsertDeleteType:", UITextSmartInsertDeleteTypeNo),
|
|
87
|
+
):
|
|
88
|
+
send_message(
|
|
89
|
+
self.native, selector, value, restype=None, argtypes=[NSInteger]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Reading layoutManager opts the view into TextKit 1, which is what the
|
|
93
|
+
# gutter's line-fragment queries need. Do it once, up front.
|
|
94
|
+
self.layout_manager = self.native.layoutManager
|
|
95
|
+
|
|
96
|
+
# Placeholder isn't natively supported, so we create our own
|
|
97
|
+
self.placeholder_label = UILabel.alloc().init()
|
|
98
|
+
self.placeholder_label.translatesAutoresizingMaskIntoConstraints = False
|
|
99
|
+
self.placeholder_label.font = self.native.font
|
|
100
|
+
self.placeholder_label.alpha = 0.5
|
|
101
|
+
self.native.addSubview(self.placeholder_label)
|
|
102
|
+
self.constrain_placeholder_label()
|
|
103
|
+
self.native.placeholder_label = self.placeholder_label
|
|
104
|
+
|
|
105
|
+
# UITextView.font and .textColor read back the *first character's* attributes
|
|
106
|
+
# once the storage is styled, so the base style is tracked here instead.
|
|
107
|
+
self.base_font = self.native.font
|
|
108
|
+
self.base_color = UIColor.labelColor
|
|
109
|
+
|
|
110
|
+
self.theme = {}
|
|
111
|
+
self.attributes = {}
|
|
112
|
+
self.spans = []
|
|
113
|
+
self.line_starts = [0]
|
|
114
|
+
self.show_line_numbers = True
|
|
115
|
+
self.gutter_width = 0
|
|
116
|
+
self.default_inset = self.native.textContainerInset
|
|
117
|
+
|
|
118
|
+
self.add_constraints()
|
|
119
|
+
self.text_changed()
|
|
120
|
+
|
|
121
|
+
def constrain_placeholder_label(self):
|
|
122
|
+
# Same constraints as toga_iOS, but keeping a reference to the leading one
|
|
123
|
+
# so the gutter can push the placeholder right.
|
|
124
|
+
self.placeholder_leading = NSLayoutConstraint.constraintWithItem(
|
|
125
|
+
self.placeholder_label,
|
|
126
|
+
attribute__1=NSLayoutAttributeLeading,
|
|
127
|
+
relatedBy=NSLayoutRelationEqual,
|
|
128
|
+
toItem=self.native,
|
|
129
|
+
attribute__2=NSLayoutAttributeLeading,
|
|
130
|
+
multiplier=1.0,
|
|
131
|
+
constant=PLACEHOLDER_LEADING,
|
|
132
|
+
)
|
|
133
|
+
trailing_constraint = NSLayoutConstraint.constraintWithItem(
|
|
134
|
+
self.placeholder_label,
|
|
135
|
+
attribute__1=NSLayoutAttributeTrailing,
|
|
136
|
+
relatedBy=NSLayoutRelationEqual,
|
|
137
|
+
toItem=self.native,
|
|
138
|
+
attribute__2=NSLayoutAttributeTrailing,
|
|
139
|
+
multiplier=1.0,
|
|
140
|
+
constant=0,
|
|
141
|
+
)
|
|
142
|
+
top_constraint = NSLayoutConstraint.constraintWithItem(
|
|
143
|
+
self.placeholder_label,
|
|
144
|
+
attribute__1=NSLayoutAttributeTop,
|
|
145
|
+
relatedBy=NSLayoutRelationEqual,
|
|
146
|
+
toItem=self.native,
|
|
147
|
+
attribute__2=NSLayoutAttributeTop,
|
|
148
|
+
multiplier=1.0,
|
|
149
|
+
constant=8.0,
|
|
150
|
+
)
|
|
151
|
+
bottom_constraint = NSLayoutConstraint.constraintWithItem(
|
|
152
|
+
self.placeholder_label,
|
|
153
|
+
attribute__1=NSLayoutAttributeBottom,
|
|
154
|
+
relatedBy=NSLayoutRelationEqual,
|
|
155
|
+
toItem=self.native,
|
|
156
|
+
attribute__2=NSLayoutAttributeBottom,
|
|
157
|
+
multiplier=1.0,
|
|
158
|
+
constant=0,
|
|
159
|
+
)
|
|
160
|
+
self.native.addConstraints(
|
|
161
|
+
[
|
|
162
|
+
self.placeholder_leading,
|
|
163
|
+
trailing_constraint,
|
|
164
|
+
top_constraint,
|
|
165
|
+
bottom_constraint,
|
|
166
|
+
]
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Inherited MultilineTextInput methods that must keep the gutter and colors in sync
|
|
170
|
+
|
|
171
|
+
def set_value(self, value):
|
|
172
|
+
super().set_value(value)
|
|
173
|
+
self.text_changed()
|
|
174
|
+
|
|
175
|
+
def set_font(self, font):
|
|
176
|
+
super().set_font(font)
|
|
177
|
+
self.base_font = font._impl.native
|
|
178
|
+
self.rebuild_attributes()
|
|
179
|
+
self.apply_highlights()
|
|
180
|
+
self.text_changed()
|
|
181
|
+
|
|
182
|
+
def set_color(self, value):
|
|
183
|
+
super().set_color(value)
|
|
184
|
+
self.base_color = native_color(value) or UIColor.labelColor
|
|
185
|
+
self.apply_highlights()
|
|
186
|
+
|
|
187
|
+
# CodeEditor backend contract
|
|
188
|
+
|
|
189
|
+
def set_theme(self, theme):
|
|
190
|
+
self.theme = theme
|
|
191
|
+
self.rebuild_attributes()
|
|
192
|
+
|
|
193
|
+
def set_highlights(self, spans):
|
|
194
|
+
self.spans = spans
|
|
195
|
+
self.apply_highlights()
|
|
196
|
+
|
|
197
|
+
def set_show_line_numbers(self, value):
|
|
198
|
+
self.show_line_numbers = value
|
|
199
|
+
self.text_changed()
|
|
200
|
+
|
|
201
|
+
# Highlighting
|
|
202
|
+
|
|
203
|
+
def rebuild_attributes(self):
|
|
204
|
+
base_font = self.base_font
|
|
205
|
+
self.attributes = {}
|
|
206
|
+
for kind, style in self.theme.items():
|
|
207
|
+
attributes = NSMutableDictionary.alloc().init()
|
|
208
|
+
attributes[NSForegroundColorAttributeName] = native_color(style.color)
|
|
209
|
+
traits = 0
|
|
210
|
+
if style.bold:
|
|
211
|
+
traits |= UIFontDescriptorTraitBold
|
|
212
|
+
if style.italic:
|
|
213
|
+
traits |= UIFontDescriptorTraitItalic
|
|
214
|
+
if traits and base_font is not None:
|
|
215
|
+
# If there is no font with the requested traits, this returns None.
|
|
216
|
+
font = UIFont.fontWithDescriptor(
|
|
217
|
+
base_font.fontDescriptor.fontDescriptorWithSymbolicTraits(traits),
|
|
218
|
+
size=base_font.pointSize,
|
|
219
|
+
)
|
|
220
|
+
if font is not None:
|
|
221
|
+
attributes[NSFontAttributeName] = font
|
|
222
|
+
self.attributes[kind] = attributes
|
|
223
|
+
|
|
224
|
+
def apply_highlights(self):
|
|
225
|
+
storage = self.native.textStorage
|
|
226
|
+
length = storage.length()
|
|
227
|
+
full_range = NSRange(0, length)
|
|
228
|
+
# Between a native edit and the debounced re-highlight the spans describe the
|
|
229
|
+
# previous text, so clamp them: a range past the end raises NSRangeException
|
|
230
|
+
# inside UIKit, which cannot be caught from Python.
|
|
231
|
+
spans = [
|
|
232
|
+
(span.start, min(span.end, length), self.attributes.get(span.kind))
|
|
233
|
+
for span in to_utf16_spans(self.get_value(), self.spans)
|
|
234
|
+
if span.start < length
|
|
235
|
+
]
|
|
236
|
+
storage.beginEditing()
|
|
237
|
+
# Reset to the base font and color, then paint each span.
|
|
238
|
+
if self.base_font is not None:
|
|
239
|
+
storage.addAttribute(
|
|
240
|
+
NSFontAttributeName, value=self.base_font, range=full_range
|
|
241
|
+
)
|
|
242
|
+
storage.addAttribute(
|
|
243
|
+
NSForegroundColorAttributeName, value=self.base_color, range=full_range
|
|
244
|
+
)
|
|
245
|
+
for start, end, attributes in spans:
|
|
246
|
+
if attributes is not None and end > start:
|
|
247
|
+
storage.addAttributes(attributes, range=NSRange(start, end - start))
|
|
248
|
+
storage.endEditing()
|
|
249
|
+
|
|
250
|
+
# Gutter
|
|
251
|
+
|
|
252
|
+
def text_changed(self):
|
|
253
|
+
self.line_starts = utf16_line_starts(self.get_value())
|
|
254
|
+
if self.show_line_numbers:
|
|
255
|
+
digits = len(str(len(self.line_starts)))
|
|
256
|
+
width = self.gutter_label("0" * digits).size().width + 2 * GUTTER_PADDING
|
|
257
|
+
else:
|
|
258
|
+
width = 0
|
|
259
|
+
if width != self.gutter_width:
|
|
260
|
+
self.gutter_width = width
|
|
261
|
+
inset = self.default_inset
|
|
262
|
+
self.native.textContainerInset = UIEdgeInsets(
|
|
263
|
+
inset.top, inset.left + width, inset.bottom, inset.right
|
|
264
|
+
)
|
|
265
|
+
self.placeholder_leading.constant = PLACEHOLDER_LEADING + width
|
|
266
|
+
self.native.setNeedsDisplay()
|
|
267
|
+
|
|
268
|
+
def gutter_label(self, text):
|
|
269
|
+
attributes = NSMutableDictionary.alloc().init()
|
|
270
|
+
if self.base_font is not None:
|
|
271
|
+
attributes[NSFontAttributeName] = self.base_font
|
|
272
|
+
attributes[NSForegroundColorAttributeName] = UIColor.secondaryLabelColor
|
|
273
|
+
return NSAttributedString.alloc().initWithString(text, attributes=attributes)
|
|
274
|
+
|
|
275
|
+
def draw_line_numbers(self):
|
|
276
|
+
if not self.show_line_numbers:
|
|
277
|
+
return
|
|
278
|
+
layout = self.layout_manager
|
|
279
|
+
container = self.native.textContainer
|
|
280
|
+
inset = self.native.textContainerInset
|
|
281
|
+
text_length = self.native.textStorage.length()
|
|
282
|
+
|
|
283
|
+
# bounds.origin is the scroll offset; the container is inset within it.
|
|
284
|
+
bounds = self.native.bounds
|
|
285
|
+
visible = CGRect(
|
|
286
|
+
CGPoint(bounds.origin.x - inset.left, bounds.origin.y - inset.top),
|
|
287
|
+
bounds.size,
|
|
288
|
+
)
|
|
289
|
+
glyph_range = layout.glyphRangeForBoundingRect(
|
|
290
|
+
visible, inTextContainer=container
|
|
291
|
+
)
|
|
292
|
+
char_range = layout.characterRangeForGlyphRange(
|
|
293
|
+
glyph_range, actualGlyphRange=None
|
|
294
|
+
)
|
|
295
|
+
first_line = max(bisect_right(self.line_starts, char_range.location) - 1, 0)
|
|
296
|
+
last_char = char_range.location + char_range.length
|
|
297
|
+
|
|
298
|
+
for number, start in enumerate(
|
|
299
|
+
self.line_starts[first_line:], start=first_line + 1
|
|
300
|
+
):
|
|
301
|
+
if start > last_char:
|
|
302
|
+
break
|
|
303
|
+
if start == text_length:
|
|
304
|
+
# The empty line after a trailing newline, or an empty document.
|
|
305
|
+
fragment = layout.extraLineFragmentRect
|
|
306
|
+
else:
|
|
307
|
+
glyph = layout.glyphIndexForCharacterAtIndex(start)
|
|
308
|
+
fragment = layout.lineFragmentRectForGlyphAtIndex(
|
|
309
|
+
glyph, effectiveRange=None
|
|
310
|
+
)
|
|
311
|
+
label = self.gutter_label(str(number))
|
|
312
|
+
x = self.gutter_width - GUTTER_PADDING - label.size().width
|
|
313
|
+
y = fragment.origin.y + inset.top
|
|
314
|
+
label.drawAtPoint(CGPoint(x, y))
|