basalt-appkit 0.0.0-rc5 → 0.2.0

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.
Files changed (43) hide show
  1. package/native/CMakeLists.txt +16 -2
  2. package/native/appkit/AppKitAppWindow.h +24 -0
  3. package/native/appkit/AppKitImageLoader.h +48 -1
  4. package/native/appkit/AppKitImageLoader.mm +228 -10
  5. package/native/appkit/AppKitMenuBar.mm +58 -0
  6. package/native/appkit/AppKitMountingManager.h +34 -0
  7. package/native/appkit/AppKitMountingManager.mm +582 -12
  8. package/native/appkit/AppKitScrollView.h +48 -0
  9. package/native/appkit/AppKitScrollView.mm +210 -0
  10. package/native/appkit/AppKitSkiaPeer.h +15 -0
  11. package/native/appkit/AppKitTextInput.h +38 -1
  12. package/native/appkit/AppKitTextInput.mm +235 -8
  13. package/native/appkit/AppKitTextPeer.h +60 -0
  14. package/native/appkit/AppKitTextPeer.mm +307 -0
  15. package/native/appkit/AppKitTouchDispatcher.h +56 -0
  16. package/native/appkit/AppKitTouchDispatcher.mm +140 -0
  17. package/native/appkit/AppKitWindowControl.mm +38 -3
  18. package/native/appkit/ComponentRegistryAppKit.mm +28 -3
  19. package/native/appkit/CoreTextLayout.h +64 -2
  20. package/native/appkit/CoreTextLayout.mm +431 -71
  21. package/native/appkit/CoreTextLayoutManager.mm +57 -6
  22. package/native/appkit/FontRegistryCoreText.mm +6 -5
  23. package/native/appkit/PlatformServicesAppKit.mm +101 -20
  24. package/native/appkit/RnAppKitView.h +492 -0
  25. package/native/appkit/RnAppKitView.mm +2266 -23
  26. package/native/appkit/RnTextLayout.h +106 -0
  27. package/native/appkit/RnTextLayout.mm +321 -2
  28. package/native/appkit/main_appkit.mm +174 -3
  29. package/native/tests/test_appkit_accessibility.mm +289 -0
  30. package/native/tests/test_appkit_borders.mm +975 -0
  31. package/native/tests/test_appkit_fonts.mm +203 -0
  32. package/native/tests/test_appkit_image.mm +392 -17
  33. package/native/tests/test_appkit_input.mm +209 -0
  34. package/native/tests/test_appkit_mounting.mm +360 -0
  35. package/native/tests/test_appkit_scrollbar.mm +125 -0
  36. package/native/tests/test_appkit_scrollview.mm +371 -0
  37. package/native/tests/test_appkit_selection.mm +349 -0
  38. package/native/tests/test_appkit_services.mm +51 -1
  39. package/native/tests/test_appkit_text.mm +1628 -0
  40. package/native/tests/test_appkit_textinput.mm +819 -0
  41. package/native/tests/test_appkit_titlebar.mm +43 -0
  42. package/native/tests/test_appkit_view.mm +609 -0
  43. package/package.json +4 -3
@@ -0,0 +1,349 @@
1
+ // Selecting a paragraph's text with a pointer, on AppKit.
2
+ //
3
+ // The same three questions the GTK suite asks, over the same claims, because
4
+ // the point of asking twice is that the two hosts agree: where a point lands in
5
+ // the text, what the highlight looks like, and what a press and a drag do. The
6
+ // state machine behind the third is core/TextSelection.h and has its own tests.
7
+ //
8
+ // The pixels matter here more than usual. A selection is a wash under the
9
+ // glyphs, so no tree dump can show it: a highlight drawn at the wrong offset,
10
+ // or not drawn at all, looks identical to every assertion that is not about
11
+ // colour at a point.
12
+
13
+ #include "TestHarness.h"
14
+
15
+ #include "AppKitMountingManager.h"
16
+ #include "AppKitTouchDispatcher.h"
17
+ #include "CoreTextLayout.h"
18
+ #include "PlatformServices.h"
19
+ #include "RnAppKitView.h"
20
+ #include "RnTextLayout.h"
21
+ #include "TextSelection.h"
22
+
23
+ #include <react/renderer/attributedstring/AttributedString.h>
24
+ #include <react/renderer/attributedstring/ParagraphAttributes.h>
25
+
26
+ #include <sstream>
27
+ #include <string>
28
+
29
+ namespace {
30
+
31
+ RnTextLayout *paragraphLayout(const char *text, float width) {
32
+ facebook::react::TextAttributes attributes;
33
+ attributes.fontSize = 24.0F;
34
+ facebook::react::AttributedString::Fragment fragment;
35
+ fragment.string = text;
36
+ fragment.textAttributes = attributes;
37
+ facebook::react::AttributedString string;
38
+ string.appendFragment(std::move(fragment));
39
+ (void)width;
40
+ return basalt::buildTextLayout(string, facebook::react::ParagraphAttributes{});
41
+ }
42
+
43
+ // A view with a paragraph in it, sized and selectable.
44
+ RnAppKitView *paragraphView(const char *text, CGFloat width, CGFloat height, BOOL selectable) {
45
+ RnAppKitView *view = [RnAppKitView viewWithTag:7];
46
+ view.frame = NSMakeRect(0, 0, width, height);
47
+ [view setRnTextLayout:paragraphLayout(text, static_cast<float>(width))];
48
+ [view setRnTextSelectable:selectable];
49
+ return view;
50
+ }
51
+
52
+ // How many pixels in a band are the selection's blue, which is a wash over
53
+ // white: the blue channel ends up well clear of the other two.
54
+ //
55
+ // The paragraph alone rather than a whole view: the highlight is drawn by the
56
+ // layout, and a view's own drawRect: wants a layer-backed window this does not
57
+ // need.
58
+ int highlightPixels(RnTextLayout *layout, CGSize size, int fromX, int toX) {
59
+ CGColorSpaceRef space = CGColorSpaceCreateWithName(kCGColorSpaceSRGB);
60
+ CGContextRef context = CGBitmapContextCreate(nullptr, (size_t)size.width, (size_t)size.height, 8,
61
+ 0, space, kCGImageAlphaPremultipliedLast);
62
+ CGColorSpaceRelease(space);
63
+ CGContextSetRGBFillColor(context, 1, 1, 1, 1);
64
+ CGContextFillRect(context, CGRectMake(0, 0, size.width, size.height));
65
+ [layout drawInContext:context size:size];
66
+
67
+ auto *pixels = static_cast<unsigned char *>(CGBitmapContextGetData(context));
68
+ const size_t stride = CGBitmapContextGetBytesPerRow(context);
69
+ int count = 0;
70
+ for (int x = fromX; x < toX; x++) {
71
+ for (int y = 0; y < (int)size.height; y++) {
72
+ const unsigned char red = pixels[(size_t)y * stride + (size_t)x * 4];
73
+ const unsigned char green = pixels[(size_t)y * stride + (size_t)x * 4 + 1];
74
+ const unsigned char blue = pixels[(size_t)y * stride + (size_t)x * 4 + 2];
75
+ if (blue > red + 20 && blue > green + 20) {
76
+ count++;
77
+ }
78
+ }
79
+ }
80
+ CGContextRelease(context);
81
+ return count;
82
+ }
83
+
84
+ } // namespace
85
+
86
+ TEST(appkit_selection_the_index_at_a_point_follows_the_text) {
87
+ @autoreleasepool {
88
+ RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
89
+
90
+ EXPECT_EQ((long)[view rnTextIndexAtPoint:CGPointMake(0, 10)], 0L);
91
+ EXPECT_EQ((long)[view rnTextIndexAtPoint:CGPointMake(399, 10)],
92
+ (long)std::string("Selectable text").size());
93
+
94
+ const NSInteger middle = [view rnTextIndexAtPoint:CGPointMake(90, 10)];
95
+ EXPECT(middle > 0);
96
+ EXPECT(middle < (NSInteger)std::string("Selectable text").size());
97
+ }
98
+ }
99
+
100
+ TEST(appkit_selection_a_view_with_no_paragraph_has_no_index) {
101
+ @autoreleasepool {
102
+ RnAppKitView *view = [RnAppKitView viewWithTag:3];
103
+ EXPECT_EQ((long)[view rnTextIndexAtPoint:CGPointMake(10, 10)], -1L);
104
+ }
105
+ }
106
+
107
+ TEST(appkit_selection_the_selected_text_is_what_the_clipboard_would_take) {
108
+ @autoreleasepool {
109
+ RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
110
+ EXPECT([view rnSelectedText] == nil);
111
+
112
+ [view setRnTextSelectionStart:0 length:10];
113
+ EXPECT_EQ(std::string([view rnSelectedText].UTF8String), std::string("Selectable"));
114
+
115
+ // A range that runs past the end of a paragraph rebuilt under it: clamped
116
+ // rather than read past, which is a crash on a shorter string.
117
+ [view setRnTextSelectionStart:11 length:1000];
118
+ EXPECT_EQ(std::string([view rnSelectedText].UTF8String), std::string("text"));
119
+ }
120
+ }
121
+
122
+ TEST(appkit_selection_the_highlight_is_drawn_under_the_selected_range) {
123
+ @autoreleasepool {
124
+ const CGSize size = CGSizeMake(400, 60);
125
+ RnTextLayout *layout = paragraphLayout("Selectable text", 400);
126
+
127
+ // Nothing selected: no wash anywhere, which is the control for the two
128
+ // assertions below.
129
+ EXPECT_EQ(highlightPixels(layout, size, 0, 400), 0);
130
+
131
+ // The first four characters, which at 24pt is the left end of the line and
132
+ // nowhere near the right.
133
+ layout.selection = NSMakeRange(0, 4);
134
+ EXPECT(highlightPixels(layout, size, 0, 60) > 100);
135
+ EXPECT_EQ(highlightPixels(layout, size, 250, 400), 0);
136
+
137
+ // Clearing it takes the wash away rather than leaving the last one drawn.
138
+ layout.selection = NSMakeRange(0, 0);
139
+ EXPECT_EQ(highlightPixels(layout, size, 0, 400), 0);
140
+ }
141
+ }
142
+
143
+ TEST(appkit_selection_a_paragraph_that_stops_being_selectable_loses_its_highlight) {
144
+ @autoreleasepool {
145
+ RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
146
+ [view setRnTextSelectionStart:0 length:6];
147
+ [view setRnTextSelectable:NO];
148
+ EXPECT_EQ((long)[view rnTextSelectionLength], 0L);
149
+ }
150
+ }
151
+
152
+ TEST(appkit_selection_a_rebuilt_paragraph_keeps_a_selection_of_the_same_text) {
153
+ @autoreleasepool {
154
+ RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
155
+ [view setRnTextSelectionStart:0 length:10];
156
+
157
+ // Every mutation that touches a paragraph builds a new layout, a parent
158
+ // re-rendering being enough, so dropping the selection each time would make
159
+ // it impossible to keep one in a live app.
160
+ [view setRnTextLayout:paragraphLayout("Selectable text", 400)];
161
+ EXPECT_EQ((long)[view rnTextSelectionLength], 10L);
162
+
163
+ // Different text is a different string, and the old offsets would highlight
164
+ // whatever now sits at them.
165
+ [view setRnTextLayout:paragraphLayout("Something else entirely", 400)];
166
+ EXPECT_EQ((long)[view rnTextSelectionLength], 0L);
167
+ }
168
+ }
169
+
170
+ TEST(appkit_selection_a_drag_across_selectable_text_selects_it) {
171
+ @autoreleasepool {
172
+ basalt::AppKitMountingManager manager;
173
+ RnAppKitView *root = manager.createSurfaceRoot(1);
174
+ root.frame = NSMakeRect(0, 0, 400, 60);
175
+
176
+ RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
177
+ [root addSubview:view];
178
+
179
+ basalt::AppKitTouchDispatcher dispatcher(&manager, root);
180
+
181
+ // Ten steps from the left edge to the middle of the line, which is a sweep
182
+ // rather than a tap and so claims the gesture.
183
+ dispatcher.synthesiseDrag(2, 10, 150, 10, 10);
184
+ EXPECT([view rnTextSelectionLength] > 0);
185
+ EXPECT_EQ((long)[view rnTextSelectionStart], 0L);
186
+
187
+ // What `Copy` and Cmd+C would take, through the provider the dispatcher
188
+ // installs: the same text the view reports.
189
+ EXPECT_EQ(basalt::selectedText(), std::string([view rnSelectedText].UTF8String));
190
+
191
+ // And a tap afterwards clears it, which is how a person dismisses a
192
+ // highlight.
193
+ dispatcher.synthesiseTap(200, 10);
194
+ EXPECT_EQ((long)[view rnTextSelectionLength], 0L);
195
+ EXPECT(basalt::selectedText().empty());
196
+
197
+ manager.destroySurfaceRoot(1);
198
+ }
199
+ }
200
+
201
+ TEST(appkit_selection_a_drag_across_text_that_is_not_selectable_selects_nothing) {
202
+ @autoreleasepool {
203
+ basalt::AppKitMountingManager manager;
204
+ RnAppKitView *root = manager.createSurfaceRoot(1);
205
+ root.frame = NSMakeRect(0, 0, 400, 60);
206
+
207
+ RnAppKitView *view = paragraphView("Selectable text", 400, 60, NO);
208
+ [root addSubview:view];
209
+
210
+ basalt::AppKitTouchDispatcher dispatcher(&manager, root);
211
+ dispatcher.synthesiseDrag(2, 10, 150, 10, 10);
212
+ EXPECT_EQ((long)[view rnTextSelectionLength], 0L);
213
+
214
+ manager.destroySurfaceRoot(1);
215
+ }
216
+ }
217
+
218
+ TEST(appkit_selection_a_backwards_drag_selects_from_where_it_ended) {
219
+ @autoreleasepool {
220
+ basalt::AppKitMountingManager manager;
221
+ RnAppKitView *root = manager.createSurfaceRoot(1);
222
+ root.frame = NSMakeRect(0, 0, 400, 60);
223
+
224
+ RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
225
+ [root addSubview:view];
226
+
227
+ basalt::AppKitTouchDispatcher dispatcher(&manager, root);
228
+ dispatcher.synthesiseDrag(150, 10, 2, 10, 10);
229
+ EXPECT_EQ((long)[view rnTextSelectionStart], 0L);
230
+ EXPECT([view rnTextSelectionLength] > 0);
231
+
232
+ manager.destroySurfaceRoot(1);
233
+ }
234
+ }
235
+
236
+ TEST(appkit_selection_the_copy_role_copies_the_selection) {
237
+ @autoreleasepool {
238
+ // What Cmd+C performs on this host: the application menu's Copy item
239
+ // carries the key equivalent and performs the role, so there is no second
240
+ // key path. The role used to mean "whatever the responder chain will take
241
+ // a `copy:` for", which is a field and never a paragraph.
242
+ basalt::AppKitMountingManager manager;
243
+ RnAppKitView *root = manager.createSurfaceRoot(1);
244
+ root.frame = NSMakeRect(0, 0, 400, 60);
245
+
246
+ RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
247
+ [root addSubview:view];
248
+
249
+ basalt::AppKitTouchDispatcher dispatcher(&manager, root);
250
+ const std::string original = basalt::clipboardText();
251
+
252
+ dispatcher.synthesiseDrag(2, 10, 150, 10, 10);
253
+ const std::string expected = std::string([view rnSelectedText].UTF8String);
254
+ EXPECT(!expected.empty());
255
+
256
+ basalt::performMenuRole("copy");
257
+ EXPECT_EQ(basalt::clipboardText(), expected);
258
+
259
+ basalt::setClipboardText(original);
260
+ manager.destroySurfaceRoot(1);
261
+ }
262
+ }
263
+
264
+ // --- What a screen reader can read -------------------------------------------
265
+ //
266
+ // The other half of this entry. A paragraph is an RnTextLayout drawn into a
267
+ // view, so VoiceOver had a label for it and nothing to move through; these are
268
+ // the NSAccessibility answers added for that, and the selection one is the
269
+ // pair of the GTK suite's. Indices are UTF-16 code units everywhere here,
270
+ // which is what NSAttributedString counts and what VoiceOver asks in.
271
+
272
+ namespace {
273
+
274
+ // An emoji outside the basic plane, which is two UTF-16 code units: text where
275
+ // a count of characters and a count of code units disagree, so an assertion
276
+ // cannot pass by accident.
277
+ const char *const kWithEmoji = "a \xf0\x9f\x8e\xb8 b";
278
+
279
+ } // namespace
280
+
281
+ TEST(appkit_selection_a_paragraph_hands_a_screen_reader_its_text) {
282
+ RnAppKitView *view = paragraphView("hello world", 400, 60, YES);
283
+ EXPECT_EQ((int)[view accessibilityNumberOfCharacters], 11);
284
+ EXPECT_EQ(std::string([[view accessibilityStringForRange:NSMakeRange(6, 5)] UTF8String]),
285
+ std::string("world"));
286
+ // Past the end is clamped rather than throwing, which is what VoiceOver asks
287
+ // for the moment a paragraph shrinks under it.
288
+ EXPECT_EQ(std::string([[view accessibilityStringForRange:NSMakeRange(6, 99)] UTF8String]),
289
+ std::string("world"));
290
+ EXPECT_EQ(std::string([[view accessibilityStringForRange:NSMakeRange(99, 1)] UTF8String]),
291
+ std::string(""));
292
+ // The attributed form carries the run's font, which is what the plain one
293
+ // cannot say.
294
+ NSAttributedString *rich = [view accessibilityAttributedStringForRange:NSMakeRange(0, 5)];
295
+ EXPECT_EQ((int)rich.length, 5);
296
+ EXPECT(rich.length > 0 && [rich attribute:NSFontAttributeName atIndex:0 effectiveRange:nullptr] != nil);
297
+ }
298
+
299
+ TEST(appkit_selection_a_view_with_no_paragraph_has_nothing_to_read) {
300
+ RnAppKitView *view = [RnAppKitView viewWithTag:3];
301
+ EXPECT_EQ((int)[view accessibilityNumberOfCharacters], 0);
302
+ EXPECT_EQ(std::string([[view accessibilityStringForRange:NSMakeRange(0, 5)] UTF8String]),
303
+ std::string(""));
304
+ EXPECT_EQ((int)[view accessibilitySelectedTextRange].length, 0);
305
+ }
306
+
307
+ TEST(appkit_selection_a_selection_is_what_a_screen_reader_is_told_is_selected) {
308
+ RnAppKitView *view = paragraphView("Selectable text", 400, 60, YES);
309
+ [view setRnTextSelectionStart:0 length:10];
310
+
311
+ EXPECT_EQ(std::string([[view accessibilitySelectedText] UTF8String]), std::string("Selectable"));
312
+ const NSRange range = [view accessibilitySelectedTextRange];
313
+ EXPECT_EQ((int)range.location, 0);
314
+ EXPECT_EQ((int)range.length, 10);
315
+ // The same span said twice: what the clipboard would take is what VoiceOver
316
+ // is told is selected.
317
+ EXPECT_EQ(std::string([[view accessibilityStringForRange:range] UTF8String]),
318
+ std::string([[view accessibilitySelectedText] UTF8String]));
319
+
320
+ [view setRnTextSelectionStart:0 length:0];
321
+ EXPECT_EQ((int)[view accessibilitySelectedTextRange].length, 0);
322
+ EXPECT_EQ(std::string([[view accessibilitySelectedText] UTF8String]), std::string(""));
323
+ }
324
+
325
+ TEST(appkit_selection_a_screen_reader_can_ask_which_line_an_index_is_on) {
326
+ // Narrow enough to wrap, so there is more than one line to be on.
327
+ RnAppKitView *view = paragraphView("hello wonderful world", 120, 200, YES);
328
+ const NSRange first = [view accessibilityRangeForLine:0];
329
+ EXPECT_EQ((int)first.location, 0);
330
+ EXPECT(first.length > 0);
331
+ EXPECT(first.length < 21);
332
+ EXPECT_EQ((int)[view accessibilityLineForIndex:0], 0);
333
+ // An index on the second line is on a later line than one on the first,
334
+ // which is the claim that does not depend on where this font wraps.
335
+ EXPECT([view accessibilityLineForIndex:(NSInteger)NSMaxRange(first)] > 0);
336
+ // A line nobody has is NSNotFound rather than an empty range at zero.
337
+ EXPECT_EQ((int)[view accessibilityRangeForLine:99].location, (int)NSNotFound);
338
+ }
339
+
340
+ TEST(appkit_selection_an_index_is_a_whole_character_rather_than_half_a_pair) {
341
+ RnAppKitView *view = paragraphView(kWithEmoji, 400, 60, YES);
342
+ // "a ", then the guitar, which is two code units and one character.
343
+ const NSRange composed = [view accessibilityRangeForIndex:2];
344
+ EXPECT_EQ((int)composed.location, 2);
345
+ EXPECT_EQ((int)composed.length, 2);
346
+ // Asked about the second half, the answer is still the whole thing.
347
+ EXPECT_EQ((int)[view accessibilityRangeForIndex:3].location, 2);
348
+ EXPECT_EQ((int)[view accessibilityRangeForIndex:99].location, (int)NSNotFound);
349
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  #include "TestHarness.h"
7
7
 
8
+ #include "MenuModel.h"
8
9
  #include "PlatformServices.h"
9
10
 
10
11
  #import <Cocoa/Cocoa.h>
@@ -52,6 +53,55 @@ TEST(can_open_url_answers_by_scheme) {
52
53
  // What the alert needs proving about it is that it does not block: it is called
53
54
  // from the JavaScript thread, and a modal run loop there would freeze every
54
55
  // mount, timer and animation frame until somebody clicked. That is checked by
55
- // js/alert.js instead, which keeps a 300ms heartbeat running and shows an alert
56
+ // e2e/alert.js instead, which keeps a 300ms heartbeat running and shows an alert
56
57
  // half a second in -- the ticks carry straight on past it, which a blocking
57
58
  // implementation could not do.
59
+
60
+ // A menu role reaching the field that has focus, in an app that is not the
61
+ // active one. See AppKitAppWindow.h.
62
+ //
63
+ // `performMenuRole` used to be one line, `[NSApp sendAction:selector to:nil
64
+ // from:nil]`, which walks the responder chain from the **key window's** first
65
+ // responder. A background app has no key window, so a `paste` role reached
66
+ // nothing at all -- and this suite is a background app, which is what makes the
67
+ // test possible without a harness around it.
68
+ //
69
+ // Uses the real clipboard and puts back what was there, the same courtesy
70
+ // `clipboard_round_trips` above extends.
71
+ TEST(a_menu_role_reaches_the_focused_field_without_the_app_being_active) {
72
+ @autoreleasepool {
73
+ const std::string original = basalt::clipboardText();
74
+
75
+ NSWindow *window = [[NSWindow alloc]
76
+ initWithContentRect:NSMakeRect(0, 0, 320, 80)
77
+ styleMask:NSWindowStyleMaskTitled
78
+ backing:NSBackingStoreBuffered
79
+ defer:NO];
80
+ NSTextField *field = [[NSTextField alloc] initWithFrame:NSMakeRect(10, 10, 300, 24)];
81
+ [window.contentView addSubview:field];
82
+ // Not released when closed, which is the same line main_appkit.mm needs for
83
+ // the windows the host makes and for the same reason: `close` on a window
84
+ // whose `releasedWhenClosed` is YES -- the default for one built in code --
85
+ // releases it while ARC still holds a reference, and the second release
86
+ // comes when the pool drains.
87
+ //
88
+ // That is undefined behaviour, so it did what undefined behaviour does: it
89
+ // passed here and segfaulted on CI's Mac, inside `objc_autoreleasePoolPop`
90
+ // with no test name, until the harness started flushing.
91
+ window.releasedWhenClosed = NO;
92
+ [window orderFront:nil];
93
+ // First responder without the app being active, which AppKit allows: what
94
+ // it does not do is make the window *key*.
95
+ EXPECT([window makeFirstResponder:field]);
96
+ EXPECT(NSApp.keyWindow == nil || NSApp.keyWindow == window);
97
+
98
+ basalt::setClipboardText("pasted by a role");
99
+ basalt::performMenuRole("paste");
100
+
101
+ EXPECT_EQ(std::string(field.stringValue.UTF8String), std::string("pasted by a role"));
102
+
103
+ [window orderOut:nil];
104
+ [window close];
105
+ basalt::setClipboardText(original);
106
+ }
107
+ }