1 /* GuiText.cpp
2 *
3 * Copyright (C) 1993-2019 Paul Boersma, 2013 Tom Naughton
4 *
5 * This code is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 2 of the License, or (at
8 * your option) any later version.
9 *
10 * This code is distributed in the hope that it will be useful, but
11 * WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
13 * See the GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this work. If not, see <http://www.gnu.org/licenses/>.
17 */
18
19 #include "GuiP.h"
20 #include <locale.h>
21
22 Thing_implement (GuiText, GuiControl, 0);
23
24 #if motif
25 #define iam_text \
26 Melder_assert (widget -> widgetClass == xmTextWidgetClass); \
27 GuiText me = (GuiText) widget -> userData
28 #else
29 #define iam_text \
30 GuiText me = (GuiText) _GuiObject_getUserData (widget)
31 #endif
32
33 #if motif
34
35 static HFONT font10, font12, font14, font18, font24;
36
37 /*
38 * (1) KEYBOARD FOCUS
39 *
40 * (1.1) In Motif, the native GUI system handles all that we want:
41 * every window with text widgets has one text focus widget,
42 * which will receive global text focus when the window is activated.
43 * The global text focus is visible to the user.
44 * The focus changes whenever the user clicks in a text widget that does not have focus.
45 *
46 * (1.2) In Windows, the native GUI system handles almost all of the above.
47 * The exception is that windows have no own text focus widgets;
48 * there is only a single global text focus. This means that when the user
49 * clicks on a non-active window, none of the text widgets in this window
50 * will automatically receive text focus. Yet, the user expects automatic text focus behaviour
51 * (click a window, then type immediately) in text edit windows (like Praat's script editor)
52 * and in windows that have a single text widget (like Praat's TextGrid editor).
53 * For this reason, the WM_COMMAND message handler in Gui.c intercepts the EN_SETFOCUS notification.
54 * This handler calls _GuiText_handleFocusReception (), which records
55 * the new text focus widget in its window. When a window is activated,
56 * we set the global focus explicitly to this window's own text focus widget,
57 * by calling _GuiText_setTheTextFocus ().
58 *
59 * (1.3) On Macintosh, we have to handle all of this explicitly.
60 *
61 * (1.4) On Win and Mac, we implement a feature not available in Motif:
62 * the use of Command-X, Command-C, and Command-V to cut, copy, and paste in text widgets,
63 * even in dialogs, where there are no menus for which these three commands could be keyboard shortcuts.
64 * For this reason, _GuiText_handleFocusReception () also stores the global text focus,
65 * so that the keyboard shortcut handler in Gui.c knows what widget to cut from, copy from, or paste into.
66 * (It is true that Windows itself stores the global text focus, but this is not always a text widget;
67 * we want it to always be a text widget, e.g. in the TextGrid editor it is always the text widget,
68 * never the drawing area, that receives key strokes. In Motif, we will have to program this text
69 * preference explicitly; see the discussion in FunctionEditor.cpp.)
70 */
71
_GuiText_handleFocusReception(GuiObject widget)72 void _GuiText_handleFocusReception (GuiObject widget) {
73 /*
74 * On Windows, this is called:
75 * 1. on a user click in a text widget: WM_COMMAND -> EN_SETFOCUS;
76 * 2. on window activation: _GuiText_setTheTextFocus () -> SetFocus -> WM_COMMAND -> EN_SETFOCUS;
77 * 3. on a user click in a push button or toggle button, which would otherwise draw the
78 * focus away from the text widgets: WM_COMMAND -> _GuiText_setTheTextFocus ().
79 *
80 * On Macintosh, this is called:
81 * 1. on a user click in a text widget: handleControlClick & handleTextEditClick -> _GuiText_setTheTextFocus ();
82 * 2. on window activation: handleActivateEvent -> _GuiText_setTheTextFocus ().
83 */
84 widget -> shell -> textFocus = widget; /* see (1.2) */
85 theGui.textFocus = widget; /* see (1.4) */
86 }
87
_GuiText_handleFocusLoss(GuiObject widget)88 void _GuiText_handleFocusLoss (GuiObject widget) {
89 /*
90 * me is going out of sight;
91 * it must stop having global focus.
92 */
93 /*
94 * On Windows, this is called:
95 * 1. on window deactivation
96 * 2. on window closure
97 * 3. on text unmanaging
98 * 4. on window unmanaging
99 *
100 * On Macintosh, this is called:
101 * 1. on window deactivation
102 * 2. on window closure
103 * 3. on text unmanaging
104 * 4. on window unmanaging
105 */
106 if (widget == theGui.textFocus)
107 theGui.textFocus = nullptr;
108 }
109
_GuiText_setTheTextFocus(GuiObject widget)110 void _GuiText_setTheTextFocus (GuiObject widget) {
111 if (! widget || theGui.textFocus == widget
112 || ! widget -> managed) return; // perhaps not-yet-managed; test: open Praat's DataEditor with a Sound, then type
113 #if gtk
114 gtk_widget_grab_focus (GTK_WIDGET (widget)); // not used: gtk is not 1 when motif is 1
115 #elif motif
116 SetFocus (widget -> window); // will send an EN_SETFOCUS notification, which will call _GuiText_handleFocusReception ()
117 #endif
118 }
119
120 /*
121 * CHANGE NOTIFICATION
122 */
_GuiText_handleValueChanged(GuiObject widget)123 void _GuiText_handleValueChanged (GuiObject widget) {
124 iam_text;
125 if (my d_changedCallback) {
126 struct structGuiTextEvent event { me };
127 my d_changedCallback (my d_changedBoss, & event);
128 }
129 }
130
_GuiText_unmanage(GuiObject widget)131 void _GuiText_unmanage (GuiObject widget) {
132 _GuiText_handleFocusLoss (widget);
133 _GuiNativeControl_hide (widget);
134 /*
135 * The caller will set the unmanage flag to zero, and remanage the parent.
136 */
137 }
138
139 /*
140 * VISIBILITY
141 */
142
_GuiWinText_destroy(GuiObject widget)143 void _GuiWinText_destroy (GuiObject widget) {
144 if (widget == theGui.textFocus)
145 theGui.textFocus = nullptr; // remove dangling reference
146 if (widget == widget -> shell -> textFocus)
147 widget -> shell -> textFocus = nullptr; // remove dangling reference
148 iam_text;
149 DestroyWindow (widget -> window);
150 forget (me); // NOTE: my widget is not destroyed here
151 }
_GuiWinText_map(GuiObject widget)152 void _GuiWinText_map (GuiObject widget) {
153 iam_text;
154 ShowWindow (widget -> window, SW_SHOW);
155 }
156
NativeText_getLength(GuiObject widget)157 static integer NativeText_getLength (GuiObject widget) {
158 return Edit_GetTextLength (widget -> window); // in UTF-16 code units
159 }
160
161 /*
162 * SELECTION
163 */
164
NativeText_getSelectionRange(GuiObject widget,integer * out_left,integer * out_right)165 static bool NativeText_getSelectionRange (GuiObject widget, integer *out_left, integer *out_right) {
166 Melder_assert (MEMBER (widget, Text));
167 DWORD left, right;
168 SendMessage (widget -> window, EM_GETSEL, (WPARAM) & left, (LPARAM) & right);
169 if (out_left) *out_left = left;
170 if (out_right) *out_right = right;
171 return right > left;
172 }
173
174 /*
175 * PACKAGE
176 */
177
_GuiText_init()178 void _GuiText_init () {
179 }
180
_GuiText_exit()181 void _GuiText_exit () {
182 }
183
184 #endif
185
186 #if gtk || motif
187 /*
188 * Undo/Redo history functions
189 */
190
_GuiText_delete(GuiObject widget,int from_pos,int to_pos)191 static void _GuiText_delete (GuiObject widget, int from_pos, int to_pos) {
192 #if gtk
193 if (G_OBJECT_TYPE (G_OBJECT (widget)) == GTK_TYPE_ENTRY) {
194 gtk_editable_delete_text (GTK_EDITABLE (widget), from_pos, to_pos);
195 } else {
196 GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
197 GtkTextIter from_it, to_it;
198 gtk_text_buffer_get_iter_at_offset (buffer, & from_it, from_pos);
199 gtk_text_buffer_get_iter_at_offset (buffer, & to_it, to_pos);
200 gtk_text_buffer_delete_interactive (buffer, & from_it, & to_it,
201 gtk_text_view_get_editable (GTK_TEXT_VIEW (widget)));
202 gtk_text_buffer_place_cursor (buffer, & to_it);
203 }
204 #elif motif
205 #endif
206 }
207
_GuiText_insert(GuiObject widget,int from_pos,int to_pos,const history_data text)208 static void _GuiText_insert (GuiObject widget, int from_pos, int to_pos, const history_data text) {
209 #if gtk
210 if (G_OBJECT_TYPE (G_OBJECT (widget)) == GTK_TYPE_ENTRY) {
211 gint from_pos_gint = from_pos;
212 gtk_editable_insert_text (GTK_EDITABLE (widget), text, to_pos - from_pos, & from_pos_gint);
213 } else {
214 GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget));
215 GtkTextIter it;
216 gtk_text_buffer_get_iter_at_offset (buffer, & it, from_pos);
217 gtk_text_buffer_insert_interactive (buffer, & it, text, to_pos - from_pos,
218 gtk_text_view_get_editable (GTK_TEXT_VIEW (widget)));
219 gtk_text_buffer_get_iter_at_offset (buffer, & it, to_pos);
220 gtk_text_buffer_place_cursor (buffer, & it);
221 }
222 #elif motif
223 #endif
224 }
225
226 /* Tests the previous elements of the history for mergability with the one to insert given via parameters.
227 * If successful, it returns a pointer to the last valid entry in the merged history list, otherwise
228 * returns nullptr.
229 * Specifically the function expands the previous insert/delete event(s)
230 * - with the current, if current also is an insert/delete event and the ranges of previous and current event match
231 * - with the previous delete and current insert event, in case the ranges of both event-pairs respectively match
232 */
history_addAndMerge(GuiText me,history_data text_new,integer first,integer last,bool deleted)233 static history_entry * history_addAndMerge (GuiText me, history_data text_new, integer first, integer last, bool deleted) {
234 history_entry *he = nullptr;
235
236 if (! my d_prev)
237 return nullptr;
238
239 if (my d_prev->type_del == deleted) {
240 // extend the last history event by this one
241 if (my d_prev->first == last) {
242 // most common for backspace key presses
243 he = my d_prev;
244 text_new = (char *) realloc (text_new, sizeof (*text_new) * (he->last - first + 1));
245 memcpy (text_new + last - first, he->text, sizeof (*text_new) * (he->last - he->first + 1));
246 free (he->text);
247 he->text = text_new;
248 he->first = first;
249
250 } else if (my d_prev->last == first) {
251 // most common for ordinary text insertion
252 he = my d_prev;
253 he->text = (char *) realloc (he->text, sizeof (*he->text) * (last - he->first + 1));
254 memcpy (he->text + he->last - he->first, text_new, sizeof (*he->text) * (last - first + 1));
255 free (text_new);
256 he->last = last;
257
258 } else if (deleted && my d_prev->first == first) {
259 // most common for delete key presses
260 he = my d_prev;
261 he->text = (char *) realloc (he->text, sizeof (*he->text) * (last - first + he->last - he->first + 1));
262 memcpy (he->text + he->last - he->first, text_new, sizeof (*he->text) * (last - first + 1));
263 free (text_new);
264 he->last = last + he->last - he->first;
265 }
266 } else {
267 // prev->type_del != deleted, no simple expansion possible, check for double expansion
268 if (! deleted && my d_prev->prev && my d_prev->prev->prev) {
269 history_entry *del_one = my d_prev;
270 history_entry *ins_mult = del_one->prev;
271 history_entry *del_mult = ins_mult->prev;
272 integer from1 = del_mult->first, to1 = del_mult->last;
273 integer from2 = ins_mult->first, to2 = ins_mult->last;
274 integer from3 = del_one->first, to3 = del_one->last;
275 if (from3 == first && to3 == last && from2 == from1 && to2 == to1 && to1 == first &&
276 ! ins_mult->type_del && del_mult->type_del) {
277 // most common for overwriting text
278 /* So the layout is as follows:
279 *
280 * del_mult ins_mult del_one current (parameters)
281 * [del, from1, to1, "uvw"] [ins, from1, to1, "abc"] [del, to1, to3, "x"] [ins, to1, to3, "d"]
282 * n >= 1 characters n characters 1 character 1 character
283 *
284 * So merge those four events into two events by expanding del_mult by del_one and ins_mult by current */
285 del_mult->text = (char *) realloc (del_mult->text, sizeof (*del_mult->text) * (to3 - from1 + 1));
286 ins_mult->text = (char *) realloc (ins_mult->text, sizeof (*ins_mult->text) * (to3 - from1 + 1));
287 memcpy (del_mult->text + to1 - from1, del_one->text, sizeof (*del_mult->text) * (to3 - to1 + 1));
288 memcpy (ins_mult->text + to1 - from1, text_new , sizeof (*del_mult->text) * (to3 - to1 + 1));
289 del_mult->last = to3;
290 ins_mult->last = to3;
291 free (del_one->text);
292 free (del_one);
293 free (text_new);
294 my d_prev = he = ins_mult;
295 }
296 }
297 }
298
299 return he;
300 }
301
302 /* Inserts a new history action, thereby removing any remaining 'redo' steps;
303 * text_new a newly allocated string that will be freed by a history function
304 * (history_add or history_clear)
305 */
history_add(GuiText me,history_data text_new,integer first,integer last,bool deleted)306 static void history_add (GuiText me, history_data text_new, integer first, integer last, bool deleted) {
307
308 // delete all newer entries; from here on there is no 'Redo' until the next 'Undo' is performed
309 history_entry *old_hnext = my d_next, *hnext;
310 while (old_hnext) {
311 hnext = old_hnext->next;
312 free (old_hnext->text);
313 free (old_hnext);
314 old_hnext = hnext;
315 }
316 my d_next = nullptr;
317
318 history_entry *he = history_addAndMerge (me, text_new, first, last, deleted);
319 if (! he) {
320 he = (history_entry *) malloc (sizeof *he);
321 he->first = first;
322 he->last = last;
323 he->type_del = deleted;
324 he->text = text_new;
325
326 he->prev = my d_prev;
327 he->next = nullptr;
328 if (my d_prev)
329 my d_prev->next = he;
330 }
331 my d_prev = he;
332 he->next = nullptr;
333
334 if (my d_undo_item) GuiThing_setSensitive (my d_undo_item, true);
335 if (my d_redo_item) GuiThing_setSensitive (my d_redo_item, false);
336 }
337
history_has_undo(GuiText me)338 static bool history_has_undo (GuiText me) {
339 return !! my d_prev;
340 }
341
history_has_redo(GuiText me)342 static bool history_has_redo (GuiText me) {
343 return !! my d_next;
344 }
345
history_do(GuiText me,bool undo)346 static void history_do (GuiText me, bool undo) {
347 history_entry *he = undo ? my d_prev : my d_next;
348 if (! he) // TODO: this function should not be called in that case
349 return;
350
351 my d_history_change = 1;
352 if (undo ^ he->type_del) {
353 _GuiText_delete (my d_widget, he->first, he->last);
354 } else {
355 _GuiText_insert (my d_widget, he->first, he->last, he->text);
356 }
357 my d_history_change = 0;
358
359 if (undo) {
360 my d_next = my d_prev;
361 my d_prev = my d_prev->prev;
362 } else {
363 my d_prev = my d_next;
364 my d_next = my d_next->next;
365 }
366
367 if (my d_undo_item) GuiThing_setSensitive (my d_undo_item, history_has_undo (me));
368 if (my d_redo_item) GuiThing_setSensitive (my d_redo_item, history_has_redo (me));
369 }
370
history_clear(GuiText me)371 static void history_clear (GuiText me) {
372 history_entry *h1, *h2;
373
374 h1 = my d_prev;
375 while (h1) {
376 h2 = h1->prev;
377 free (h1->text);
378 free (h1);
379 h1 = h2;
380 }
381 my d_prev = nullptr;
382
383 h1 = my d_next;
384 while (h1) {
385 h2 = h1->next;
386 free (h1->text);
387 free (h1);
388 h1 = h2;
389 }
390 my d_next = nullptr;
391
392 if (my d_undo_item) GuiThing_setSensitive (my d_undo_item, false);
393 if (my d_redo_item) GuiThing_setSensitive (my d_redo_item, false);
394 }
395 #endif
396
397 /*
398 * CALLBACKS
399 */
400
401 #if gtk
_GuiGtkEntry_history_delete_cb(GtkEditable * ed,gint from,gint to,gpointer void_me)402 static void _GuiGtkEntry_history_delete_cb (GtkEditable *ed, gint from, gint to, gpointer void_me) {
403 iam (GuiText);
404 trace (U"begin");
405 if (my d_history_change) return;
406 history_add (me, gtk_editable_get_chars (GTK_EDITABLE (ed), from, to), from, to, 1);
407 }
408
_GuiGtkEntry_history_insert_cb(GtkEditable * ed,gchar * utf8_text,gint len,gint * from,gpointer void_me)409 static void _GuiGtkEntry_history_insert_cb (GtkEditable *ed, gchar *utf8_text, gint len, gint *from, gpointer void_me) {
410 (void) ed;
411 iam (GuiText);
412 trace (U"begin");
413 if (my d_history_change) return;
414 gchar *text = (gchar *) malloc (sizeof (gchar) * (len + 1));
415 strcpy (text, utf8_text);
416 history_add (me, text, *from, *from + len, 0);
417 }
418
_GuiGtkTextBuf_history_delete_cb(GtkTextBuffer * buffer,GtkTextIter * from,GtkTextIter * to,gpointer void_me)419 static void _GuiGtkTextBuf_history_delete_cb (GtkTextBuffer *buffer, GtkTextIter *from, GtkTextIter *to, gpointer void_me) {
420 iam (GuiText);
421 trace (U"begin");
422 if (my d_history_change) return;
423 int from_pos = gtk_text_iter_get_offset (from);
424 int to_pos = gtk_text_iter_get_offset (to);
425 history_add (me, gtk_text_buffer_get_text (buffer, from, to, false), from_pos, to_pos, 1);
426 }
427
_GuiGtkTextBuf_history_insert_cb(GtkTextBuffer * buffer,GtkTextIter * from,gchar * utf8_text,gint len,gpointer void_me)428 static void _GuiGtkTextBuf_history_insert_cb (GtkTextBuffer *buffer, GtkTextIter *from, gchar *utf8_text, gint len, gpointer void_me) {
429 (void) buffer;
430 iam (GuiText);
431 trace (U"begin");
432 if (my d_history_change) return;
433 int from_pos = gtk_text_iter_get_offset (from);
434 gchar *text = (gchar *) malloc (sizeof (gchar) * (len + 1));
435 strcpy (text, utf8_text);
436 history_add (me, text, from_pos, from_pos + len, 0);
437 }
438
_GuiGtkText_valueChangedCallback(GuiObject widget,gpointer void_me)439 static void _GuiGtkText_valueChangedCallback (GuiObject widget, gpointer void_me) {
440 iam (GuiText);
441 trace (U"begin");
442 Melder_assert (me);
443 if (my d_changedCallback) {
444 struct structGuiTextEvent event { me };
445 my d_changedCallback (my d_changedBoss, & event);
446 }
447 }
448
_GuiGtkText_destroyCallback(GuiObject widget,gpointer void_me)449 static void _GuiGtkText_destroyCallback (GuiObject widget, gpointer void_me) {
450 (void) widget;
451 iam (GuiText);
452 Melder_assert (me);
453 Melder_assert (my classInfo == classGuiText);
454 trace (U"begin");
455 if (my d_undo_item) {
456 trace (U"undo");
457 //g_object_unref (my d_undo_item -> d_widget);
458 }
459 if (my d_redo_item) {
460 trace (U"redo");
461 //g_object_unref (my d_redo_item -> d_widget);
462 }
463 my d_undo_item = nullptr;
464 my d_redo_item = nullptr;
465 trace (U"history");
466 history_clear (me);
467 forget (me);
468 }
469 #elif motif
470 #elif cocoa
471 @implementation GuiCocoaTextField {
472 GuiText d_userData;
473 }
474 - (void) dealloc { // override
475 GuiText me = self -> d_userData;
476 forget (me);
477 trace (U"deleting a text field");
478 [super dealloc];
479 }
480 - (GuiThing) getUserData {
481 return self -> d_userData;
482 }
483 - (void) setUserData: (GuiThing) userData {
484 Melder_assert (userData == nullptr || Thing_isa (userData, classGuiText));
485 d_userData = static_cast <GuiText> (userData);
486 }
487 - (void) textDidChange: (NSNotification *) notification {
488 (void) notification;
489 GuiText me = self -> d_userData;
490 if (me && my d_changedCallback) {
491 struct structGuiTextEvent event { me };
492 my d_changedCallback (my d_changedBoss, & event);
493 }
494 }
495 @end
496 @implementation GuiCocoaTextView {
497 GuiText d_userData;
498 }
499 - (void) dealloc { // override
500 GuiText me = self -> d_userData;
501 forget (me);
502 trace (U"deleting a text view");
503 [super dealloc];
504 }
505 - (GuiThing) getUserData {
506 return self -> d_userData;
507 }
508 - (void) setUserData: (GuiThing) userData {
509 Melder_assert (userData == nullptr || Thing_isa (userData, classGuiText));
510 self -> d_userData = static_cast <GuiText> (userData);
511 }
512 /*
513 The NSTextViewDelegate protocol.
514 While NSTextDelegate simply has textDidChange:, that method doesn't seem to always respond when the text is changed programmatically.
515 Hence the addition of textView:shouldChangeTextInRange.
516 It's not unthinkable that some changes could appear twice.
517 */
518 - (void) textDidChange: (NSNotification *) notification { // method doesn't seem to respond when the text is changed programmatically
519 (void) notification;
520 GuiText me = self -> d_userData;
521 if (me && my d_changedCallback) {
522 struct structGuiTextEvent event { me };
523 my d_changedCallback (my d_changedBoss, & event);
524 }
525 }
526 - (BOOL) textView: (NSTextView *) aTextView shouldChangeTextInRange: (NSRange) affectedCharRange replacementString: (NSString *) replacementString {
527 (void) aTextView;
528 (void) affectedCharRange;
529 (void) replacementString;
530 trace (U"changing text to: ", Melder_peek8to32 ([replacementString UTF8String]));
531 GuiText me = self -> d_userData;
532 if (me && my d_changedCallback) {
533 struct structGuiTextEvent event { me };
534 my d_changedCallback (my d_changedBoss, & event);
535 }
536 return YES;
537 }
538 @end
539 #endif
540
GuiText_create(GuiForm parent,int left,int right,int top,int bottom,uint32 flags)541 GuiText GuiText_create (GuiForm parent, int left, int right, int top, int bottom, uint32 flags) {
542 autoGuiText me = Thing_new (GuiText);
543 my d_shell = parent -> d_shell;
544 my d_parent = parent;
545 #if gtk
546 trace (U"before creating a GTK text widget: locale is ", Melder_peek8to32 (setlocale (LC_ALL, nullptr)));
547 if (flags & GuiText_SCROLLED) {
548 GuiObject scrolled = gtk_scrolled_window_new (nullptr, nullptr);
549 gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
550 my d_widget = gtk_text_view_new ();
551 gtk_container_add (GTK_CONTAINER (scrolled), GTK_WIDGET (my d_widget));
552 gtk_widget_show (GTK_WIDGET (scrolled));
553 gtk_text_view_set_editable (GTK_TEXT_VIEW (my d_widget), ! (flags & GuiText_NONEDITABLE));
554 const GtkWrapMode gtkWrapMode =
555 ( flags & GuiText_CHARWRAP ? GTK_WRAP_CHAR : flags & GuiText_INKWRAP ? GTK_WRAP_WORD_CHAR : GTK_WRAP_NONE );
556 gtk_text_view_set_wrap_mode (GTK_TEXT_VIEW (my d_widget), gtkWrapMode);
557 GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (my d_widget));
558 g_signal_connect (G_OBJECT (buffer), "delete-range", G_CALLBACK (_GuiGtkTextBuf_history_delete_cb), me.get());
559 g_signal_connect (G_OBJECT (buffer), "insert-text", G_CALLBACK (_GuiGtkTextBuf_history_insert_cb), me.get());
560 g_signal_connect (G_OBJECT (buffer), "changed", G_CALLBACK (_GuiGtkText_valueChangedCallback), me.get());
561 _GuiObject_setUserData (my d_widget, me.get());
562 _GuiObject_setUserData (scrolled, me.get());
563 my v_positionInForm (scrolled, left, right, top, bottom, parent);
564 } else {
565 my d_widget = gtk_entry_new ();
566 gtk_editable_set_editable (GTK_EDITABLE (my d_widget), ! (flags & GuiText_NONEDITABLE));
567 g_signal_connect (G_OBJECT (my d_widget), "delete-text", G_CALLBACK (_GuiGtkEntry_history_delete_cb), me.get());
568 g_signal_connect (G_OBJECT (my d_widget), "insert-text", G_CALLBACK (_GuiGtkEntry_history_insert_cb), me.get());
569 g_signal_connect (GTK_EDITABLE (my d_widget), "changed", G_CALLBACK (_GuiGtkText_valueChangedCallback), me.get());
570 //gtk_widget_set_can_default (my d_widget, false);
571 _GuiObject_setUserData (my d_widget, me.get());
572 my v_positionInForm (my d_widget, left, right, top, bottom, parent);
573 gtk_entry_set_activates_default (GTK_ENTRY (my d_widget), true);
574 }
575 trace (U"after creating a GTK text widget: locale is ", Melder_peek8to32 (setlocale (LC_ALL, nullptr)));
576 my d_prev = nullptr;
577 my d_next = nullptr;
578 my d_history_change = 0;
579 my d_undo_item = nullptr;
580 my d_redo_item = nullptr;
581 g_signal_connect (G_OBJECT (my d_widget), "destroy", G_CALLBACK (_GuiGtkText_destroyCallback), me.get());
582 #elif motif
583 my d_widget = _Gui_initializeWidget (xmTextWidgetClass, parent -> d_widget, flags & GuiText_SCROLLED ? U"scrolledText" : U"text");
584 _GuiObject_setUserData (my d_widget, me.get());
585 my d_editable = (flags & GuiText_NONEDITABLE) == 0;
586 my d_widget -> window = CreateWindow (L"edit", nullptr, WS_CHILD | WS_BORDER
587 | ( flags & GuiText_ANYWRAP ? ES_AUTOVSCROLL : ES_AUTOHSCROLL )
588 | ES_MULTILINE | WS_CLIPSIBLINGS
589 | ( flags & GuiText_SCROLLED ? WS_VSCROLL | ( flags & GuiText_ANYWRAP ? 0 : WS_HSCROLL ) : 0 ),
590 my d_widget -> x, my d_widget -> y, my d_widget -> width, my d_widget -> height,
591 my d_widget -> parent -> window, (HMENU) 1, theGui.instance, nullptr);
592 SetWindowLongPtr (my d_widget -> window, GWLP_USERDATA, (LONG_PTR) my d_widget);
593 if (! font10) {
594 font10 = CreateFont (13, 0, 0, 0, 0, 0, 0, 0, DEFAULT_CHARSET, 0, 0, 0, 0/*FIXED_PITCH | FF_MODERN*/, /*L"Doulos SIL"*/L"Courier New");
595 font12 = CreateFont (16, 0, 0, 0, 0, 0, 0, 0, DEFAULT_CHARSET, 0, 0, 0, 0/*FIXED_PITCH | FF_MODERN*/, /*L"Doulos SIL"*/L"Courier New");
596 font14 = CreateFont (19, 0, 0, 0, 0, 0, 0, 0, DEFAULT_CHARSET, 0, 0, 0, 0/*FIXED_PITCH | FF_MODERN*/, /*L"Doulos SIL"*/L"Courier New");
597 font18 = CreateFont (24, 0, 0, 0, 0, 0, 0, 0, DEFAULT_CHARSET, 0, 0, 0, 0/*FIXED_PITCH | FF_MODERN*/, /*L"Doulos SIL"*/L"Courier New");
598 font24 = CreateFont (32, 0, 0, 0, 0, 0, 0, 0, DEFAULT_CHARSET, 0, 0, 0, 0/*FIXED_PITCH | FF_MODERN*/, /*L"Doulos SIL"*/L"Courier New");
599 }
600 SetWindowFont (my d_widget -> window, font12 /*theScrolledHint ? font : GetStockFont (ANSI_VAR_FONT)*/, false);
601 Edit_LimitText (my d_widget -> window, 0);
602 my v_positionInForm (my d_widget, left, right, top, bottom, parent);
603 /*
604 The first created text widget shall attract the input focus.
605 */
606 if (! my d_widget -> shell -> textFocus)
607 my d_widget -> shell -> textFocus = my d_widget; // even if not-yet-managed. But in that case it will not receive global focus
608 #elif cocoa
609 const bool isMultilineTextField = ( flags & GuiText_ANYWRAP );
610 /*
611 The following can be set to true once we can simulate an NSTextField with an NSTextView.
612 Stuff still missing includes:
613 - automatic selection of the whole field when the user tabs
614 - a blue outline around the field
615 - an Enter would take away the focus from the NSTextView,
616 which might be circumvented by incorrect code described at GuiCocoaApplication::sendEvent() in GuiMenu.cpp
617 (LAST CHECKED 2021-02-03)
618 */
619 const bool multiLineTextFieldsUseNSTextView = false; // set to true to try out scrollable multi-line text fields
620 const bool weWouldLikeScrolling = ( flags & GuiText_SCROLLED );
621 const bool weHaveTroubleImplementingScrolling = ( isMultilineTextField && ! multiLineTextFieldsUseNSTextView );
622 if (weWouldLikeScrolling && ! weHaveTroubleImplementingScrolling) {
623 my d_cocoaScrollView = [[GuiCocoaScrolledWindow alloc] init];
624 [my d_cocoaScrollView setUserData: nullptr]; // because those user data can only be GuiScrolledWindow
625 my d_widget = my d_cocoaScrollView;
626 my v_positionInForm (my d_widget, left, right, top, bottom, parent);
627 [my d_cocoaScrollView setBorderType: NSBezelBorder];
628 [my d_cocoaScrollView setHasHorizontalScroller: ! (flags & GuiText_ANYWRAP)];
629 [my d_cocoaScrollView setHasVerticalScroller: YES];
630 //[my d_cocoaScrollView setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable];
631 NSSize contentSize = [my d_cocoaScrollView contentSize];
632 my d_cocoaTextView = [[GuiCocoaTextView alloc] initWithFrame: NSMakeRect (0, 0, contentSize. width, contentSize. height)];
633 [my d_cocoaTextView setUserData: me.get()];
634 if (Melder_systemVersion < 101100) {
635 [my d_cocoaTextView setMinSize: NSMakeSize (0.0, contentSize.height)];
636 } else {
637 [my d_cocoaTextView setMinSize: NSMakeSize (contentSize. width, contentSize.height)]; // El Capitan Developer Beta 2
638 }
639 [my d_cocoaTextView setMaxSize: NSMakeSize (FLT_MAX, FLT_MAX)];
640 if ((true)) {
641 /*
642 The Info window and the Script window have the problem that if a tab occurs after 336 points,
643 the text breaks to the next line.
644 The probable cause is found in NSParagraphStyle:
645 (1) "The NSTextTab objects, sorted by location, define the tab stops for the paragraph style.
646 The default value is an array of 12 left-aligned tabs at 28-point intervals."
647 (2) The default value of defaultTabInterval ("Tabs after the last specified in tabStops are
648 placed at integer multiples of this distance (if positive).") is 0.0,
649 probably meaning that the next tab stop has to be sought on the next line.
650 We therefore try to prevent the unwanted line break by setting defaultTabInterval to 28.0.
651 */
652 //NSMutableParagraphStyle *paragraphStyle = [[my d_cocoaTextView defaultParagraphStyle] mutableCopy]; // this one doesn't work (in 10.14.6)
653 NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
654 [paragraphStyle setParagraphStyle: [my d_cocoaTextView defaultParagraphStyle]]; // should be superfluous
655 [paragraphStyle setDefaultTabInterval: 28.0];
656 if (!! (flags & GuiText_CHARWRAP))
657 [paragraphStyle setLineBreakMode: NSLineBreakByCharWrapping];
658 else if (!! (flags & GuiText_INKWRAP))
659 [paragraphStyle setLineBreakMode: NSLineBreakByWordWrapping];
660 [my d_cocoaTextView setDefaultParagraphStyle: paragraphStyle];
661 [paragraphStyle release];
662 /*
663 The trick above works only when we insert text by setString, not when we edit the text manually.
664 However, we can correctly edit manually *after* setString has been called with a non-empty string (see below).
665 */
666 /*
667 We can experiment with setting additional tab stops at 400 and 500 points.
668 */
669 //[paragraphStyle setTabStops: [NSArray array]];
670 //NSDictionary *emptyDictionary = [[NSDictionary alloc] init];
671 //NSTextTab *tab400 = [[NSTextTab alloc] initWithTextAlignment: NSTextAlignmentLeft location: 400.0 options: emptyDictionary];
672 //NSTextTab *tab400 = [[NSTextTab alloc] initWithType: NSLeftTabStopType location: 400.0];
673 //[paragraphStyle addTabStop: tab400];
674 //NSTextTab *tab500 = [[NSTextTab alloc] initWithTextAlignment: NSTextAlignmentLeft location: 500.0 options: emptyDictionary];
675 //NSTextTab *tab500 = [[NSTextTab alloc] initWithType: NSLeftTabStopType location: 500.0];
676 //[paragraphStyle addTabStop: tab500];
677 /*
678 We can experiment with attributed strings.
679 This won't work here, perhaps because the length of the string, and hence the "range", will change later.
680 */
681 //NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];
682 //[attributes setObject: paragraphStyle forKey: NSParagraphStyleAttributeName];
683 //[[my d_cocoaTextView textStorage] addAttributes: attributes range: NSMakeRange (0, [[[my d_cocoaTextView textStorage] string] length])];
684 }
685 [my d_cocoaTextView setVerticallyResizable: YES];
686 [my d_cocoaTextView setHorizontallyResizable: YES];
687 //[my d_cocoaTextView setAutoresizingMask: NSViewWidthSizable | NSViewHeightSizable];
688 if (! (flags & GuiText_ANYWRAP))
689 [[my d_cocoaTextView textContainer] setContainerSize: NSMakeSize (FLT_MAX, FLT_MAX)];
690 [[my d_cocoaTextView textContainer] setWidthTracksTextView: NO];
691 [my d_cocoaScrollView setDocumentView: my d_cocoaTextView]; // the scroll view will own the text view?
692 [my d_cocoaTextView release]; // so we release the text view itself
693 if (! (flags & GuiText_ANYWRAP))
694 [[my d_cocoaScrollView window] makeFirstResponder: my d_cocoaTextView]; // in dialog
695 if (flags & GuiText_ANYWRAP) {
696 my d_macFontSize = 13.0;
697 static NSFont *theSystemTextFont;
698 if (! theSystemTextFont)
699 theSystemTextFont = [[NSFont systemFontOfSize: 13.0] retain];
700 [my d_cocoaTextView setFont: theSystemTextFont];
701 } else {
702 my d_macFontSize = 12.0;
703 static NSFont *theFixedWidthTextFont;
704 if (! theFixedWidthTextFont)
705 theFixedWidthTextFont = [[NSFont fontWithName: @"Menlo" size: 12.0] retain];
706 [my d_cocoaTextView setFont: theFixedWidthTextFont];
707 }
708 [my d_cocoaTextView setAllowsUndo: YES];
709 [my d_cocoaTextView turnOffLigatures: nil];
710 [my d_cocoaTextView setSmartInsertDeleteEnabled: NO];
711 [my d_cocoaTextView setAutomaticQuoteSubstitutionEnabled: NO];
712 [my d_cocoaTextView setAutomaticTextReplacementEnabled: NO];
713 [my d_cocoaTextView setAutomaticDashSubstitutionEnabled: NO];
714 [my d_cocoaTextView setDelegate: my d_cocoaTextView];
715 if (flags & GuiText_ANYWRAP) {
716 /*
717 The following two statements are not sufficient
718 to make an NSTextView act like an NSTextField.
719 (LAST CHECKED 2021-12-03)
720 */
721 [my d_cocoaTextView setFieldEditor: YES]; // so that it takes part in tab navigation
722 [my d_cocoaTextView setSelectable: YES]; // so that it takes part in tab navigation
723 }
724 /*
725 Regrettably, we have to implement the following HACK
726 to prevent tab-based line breaks even when editing manually.
727 */
728 [my d_cocoaTextView setString: @" "];
729 [my d_cocoaTextView setString: @""];
730 } else {
731 my d_widget = [[GuiCocoaTextField alloc] init];
732 my v_positionInForm (my d_widget, left, right, top, bottom, parent);
733 [(GuiCocoaTextField *) my d_widget setUserData: me.get()];
734 [(NSTextField *) my d_widget setEditable: YES];
735 static NSFont *theTextFont;
736 if (! theTextFont)
737 theTextFont = [[NSFont systemFontOfSize: 13.0] retain];
738 [(NSTextField *) my d_widget setFont: theTextFont];
739 }
740 #endif
741
742 return me.releaseToAmbiguousOwner();
743 }
744
GuiText_createShown(GuiForm parent,int left,int right,int top,int bottom,uint32 flags)745 GuiText GuiText_createShown (GuiForm parent, int left, int right, int top, int bottom, uint32 flags) {
746 GuiText me = GuiText_create (parent, left, right, top, bottom, flags);
747 GuiThing_show (me);
748 return me;
749 }
750
GuiText_copy(GuiText me)751 void GuiText_copy (GuiText me) {
752 #if gtk
753 if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_ENTRY) {
754 gtk_editable_copy_clipboard (GTK_EDITABLE (my d_widget));
755 } else if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_TEXT_VIEW) {
756 GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (my d_widget));
757 GtkClipboard *cb = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
758 gtk_text_buffer_copy_clipboard (buffer, cb);
759 }
760 #elif motif
761 if (! NativeText_getSelectionRange (my d_widget, nullptr, nullptr))
762 return;
763 SendMessage (my d_widget -> window, WM_COPY, 0, 0);
764 #elif cocoa
765 if (my d_cocoaTextView) {
766 [my d_cocoaTextView copy: nil];
767 } else {
768 [[[(GuiCocoaTextField *) my d_widget window] fieldEditor: NO forObject: nil] copy: nil];
769 }
770 #endif
771 }
772
GuiText_cut(GuiText me)773 void GuiText_cut (GuiText me) {
774 #if gtk
775 if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_ENTRY) {
776 gtk_editable_cut_clipboard (GTK_EDITABLE (my d_widget));
777 } else if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_TEXT_VIEW) {
778 GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (my d_widget));
779 GtkClipboard *cb = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
780 gtk_text_buffer_cut_clipboard (buffer, cb, gtk_text_view_get_editable (GTK_TEXT_VIEW (my d_widget)));
781 }
782 #elif motif
783 if (! my d_editable || ! NativeText_getSelectionRange (my d_widget, nullptr, nullptr))
784 return;
785 SendMessage (my d_widget -> window, WM_CUT, 0, 0); // this will send the EN_CHANGE message, hence no need to call the valueChangedCallbacks
786 UpdateWindow (my d_widget -> window);
787 #elif cocoa
788 if (my d_cocoaTextView) {
789 [my d_cocoaTextView cut: nil];
790 } else {
791 [[[(GuiCocoaTextField *) my d_widget window] fieldEditor: NO forObject: nil] cut: nil];
792 }
793 #endif
794 }
795
GuiText_getSelection(GuiText me)796 autostring32 GuiText_getSelection (GuiText me) {
797 #if gtk
798 // first = gtk_text_iter_get_offset (& start);
799 // last = gtk_text_iter_get_offset (& end);
800 if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_ENTRY) {
801 gint start, end;
802 gtk_editable_get_selection_bounds (GTK_EDITABLE (my d_widget), & start, & end);
803 if (end > start) { // at least one character selected?
804 gchar *text = gtk_editable_get_chars (GTK_EDITABLE (my d_widget), start, end);
805 autostring32 result = Melder_8to32 (text);
806 g_free (text);
807 return result;
808 }
809 } else if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_TEXT_VIEW) {
810 GtkTextBuffer *textBuffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (my d_widget));
811 if (gtk_text_buffer_get_has_selection (textBuffer)) { // at least one character selected?
812 GtkTextIter start, end;
813 gtk_text_buffer_get_selection_bounds (textBuffer, & start, & end);
814 gchar *text = gtk_text_buffer_get_text (textBuffer, & start, & end, true);
815 autostring32 result = Melder_8to32 (text);
816 g_free (text);
817 return result;
818 }
819 }
820 #elif motif
821 integer startW, endW;
822 (void) NativeText_getSelectionRange (my d_widget, & startW, & endW);
823 if (endW > startW) { // at least one character selected?
824 /*
825 Get all text.
826 */
827 integer lengthW = NativeText_getLength (my d_widget); // in UTF-16 code units
828 WCHAR *bufferW = Melder_malloc_f (WCHAR, lengthW + 1);
829 GetWindowTextW (my d_widget -> window, bufferW, lengthW + 1);
830 /*
831 Zoom in on selection.
832 */
833 lengthW = endW - startW;
834 memmove (bufferW, bufferW + startW, lengthW * sizeof (WCHAR)); // not because of realloc, but because of free!
835 bufferW [lengthW] = U'\0';
836 autostring32 result = Melder_dup_f (Melder_peekWto32 (bufferW));
837 (void) Melder_killReturns_inplace (result.get()); // AFTER zooming!
838 return result;
839 }
840 #elif cocoa
841 integer start, end;
842 autostring32 selection = GuiText_getStringAndSelectionPosition (me, & start, & end);
843 integer length = end - start;
844 if (length > 0) {
845 autostring32 result (length, true);
846 memcpy (result.get(), & selection [start], integer_to_uinteger (length) * sizeof (char32));
847 result [length] = U'\0';
848 (void) Melder_killReturns_inplace (result.get());
849 return result;
850 }
851 #endif
852 return autostring32(); // zero characters selected
853 }
854
GuiText_getString(GuiText me)855 autostring32 GuiText_getString (GuiText me) {
856 integer first, last;
857 return GuiText_getStringAndSelectionPosition (me, & first, & last);
858 }
859
GuiText_getStringAndSelectionPosition(GuiText me,integer * first,integer * last)860 autostring32 GuiText_getStringAndSelectionPosition (GuiText me, integer *first, integer *last) {
861 #if gtk
862 if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_ENTRY) {
863 gint first_gint, last_gint;
864 gtk_editable_get_selection_bounds (GTK_EDITABLE (my d_widget), & first_gint, & last_gint); // expressed in Unicode code points!
865 *first = first_gint;
866 *last = last_gint;
867 return Melder_8to32 (gtk_entry_get_text (GTK_ENTRY (my d_widget)));
868 } else if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_TEXT_VIEW) {
869 GtkTextBuffer *textBuffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (my d_widget));
870 GtkTextIter start, end;
871 gtk_text_buffer_get_start_iter (textBuffer, & start);
872 gtk_text_buffer_get_end_iter (textBuffer, & end);
873 gchar *text = gtk_text_buffer_get_text (textBuffer, & start, & end, true); // TODO: Hidden chars ook maar doen he?
874 autostring32 result = Melder_8to32 (text);
875 g_free (text);
876 gtk_text_buffer_get_selection_bounds (textBuffer, & start, & end);
877 *first = gtk_text_iter_get_offset (& start);
878 *last = gtk_text_iter_get_offset (& end);
879 return result;
880 }
881 return autostring32();
882 #elif motif
883 integer lengthW = NativeText_getLength (my d_widget);
884 autostringW bufferW (lengthW , true);
885 GetWindowTextW (my d_widget -> window, bufferW.get(), lengthW + 1);
886 integer firstW, lastW;
887 (void) NativeText_getSelectionRange (my d_widget, & firstW, & lastW);
888
889 integer differenceFirst = 0;
890 for (integer i = 0; i < firstW; i ++) {
891 if (bufferW [i] == 13 && (bufferW [i + 1] == L'\n' || bufferW [i + 1] == 0x0085))
892 differenceFirst ++;
893 if (bufferW [i] >= 0xDC00 && bufferW [i] <= 0xDFFF)
894 differenceFirst ++;
895 }
896 *first = firstW - differenceFirst;
897
898 integer differenceLast = differenceFirst;
899 for (integer i = firstW; i < lastW; i ++) {
900 if (bufferW [i] == 13 && (bufferW [i + 1] == L'\n' || bufferW [i + 1] == 0x0085))
901 differenceLast ++;
902 if (bufferW [i] >= 0xDC00 && bufferW [i] <= 0xDFFF)
903 differenceLast ++;
904 }
905 *last = lastW - differenceLast;
906
907 autostring32 result = Melder_dup_f (Melder_peekWto32 (bufferW.get()));
908 (void) Melder_killReturns_inplace (result.get());
909 return result;
910 #elif cocoa
911 NSString *nsString = ( my d_cocoaTextView ?
912 [my d_cocoaTextView string] :
913 [(NSTextField *) my d_widget stringValue] );
914 autostring16 buffer16 = Melder_32to16 (Melder_peek8to32 ([nsString UTF8String]));
915 NSText *nsText = ( my d_cocoaTextView ?
916 my d_cocoaTextView :
917 [[(NSTextField *) my d_widget window] fieldEditor: NO forObject: nil] );
918 NSRange nsRange = [nsText selectedRange];
919 *first = uinteger_to_integer (nsRange. location);
920 *last = *first + uinteger_to_integer (nsRange. length);
921 /*
922 The UTF-16 string may contain sequences of carriage return and newline,
923 for instance whenever a text has been copy-pasted from Microsoft Word,
924 in which case the carriage return has to be deleted and `first` and/or `last`
925 may have to be decremented.
926 */
927 integer differenceFirst = 0;
928 for (integer i = 0; i < *first; i ++) {
929 if (buffer16 [i] == 13 && (buffer16 [i + 1] == L'\n' || buffer16 [i + 1] == 0x0085))
930 differenceFirst ++;
931 if (buffer16 [i] >= 0xDC00 && buffer16 [i] <= 0xDFFF)
932 differenceFirst ++;
933 }
934 integer differenceLast = differenceFirst;
935 for (integer i = *first; i < *last; i ++) {
936 if (buffer16 [i] == 13 && (buffer16 [i + 1] == L'\n' || buffer16 [i + 1] == 0x0085))
937 differenceLast ++;
938 if (buffer16 [i] >= 0xDC00 && buffer16 [i] <= 0xDFFF)
939 differenceLast ++;
940 }
941 *first -= differenceFirst;
942 *last -= differenceLast;
943 autostring32 result = Melder_dup_f (Melder_peek16to32 (buffer16.get()));
944 (void) Melder_killReturns_inplace (result.get());
945 return result;
946 #else
947 return autostring32();
948 #endif
949 }
950
GuiText_paste(GuiText me)951 void GuiText_paste (GuiText me) {
952 #if gtk
953 if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_ENTRY) {
954 gtk_editable_paste_clipboard (GTK_EDITABLE (my d_widget));
955 } else if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_TEXT_VIEW) {
956 GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (my d_widget));
957 GtkClipboard *cb = gtk_clipboard_get (GDK_SELECTION_CLIPBOARD);
958 gtk_text_buffer_paste_clipboard (buffer, cb, nullptr, gtk_text_view_get_editable (GTK_TEXT_VIEW (my d_widget)));
959 }
960 #elif motif
961 if (! my d_editable)
962 return;
963 SendMessage (my d_widget -> window, WM_PASTE, 0, 0); // this will send the EN_CHANGE message, hence no need to call the valueChangedCallbacks
964 UpdateWindow (my d_widget -> window);
965 #elif cocoa
966 if (my d_cocoaTextView) {
967 [my d_cocoaTextView pasteAsPlainText: nil];
968 } else {
969 [(NSTextView *) [[(GuiCocoaTextField *) my d_widget window] fieldEditor: NO forObject: nil] pasteAsPlainText: nil];
970 }
971 #endif
972 }
973
GuiText_redo(GuiText me)974 void GuiText_redo (GuiText me) {
975 #if gtk || motif
976 history_do (me, 0);
977 #elif cocoa
978 if (my d_cocoaTextView) {
979 [[my d_cocoaTextView undoManager] redo];
980 }
981 #endif
982 }
983
GuiText_remove(GuiText me)984 void GuiText_remove (GuiText me) {
985 #if gtk
986 if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_ENTRY) {
987 gtk_editable_delete_selection (GTK_EDITABLE (my d_widget));
988 } else if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_TEXT_VIEW) {
989 GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (my d_widget));
990 gtk_text_buffer_delete_selection (buffer, true, gtk_text_view_get_editable (GTK_TEXT_VIEW (my d_widget)));
991 }
992 #elif motif
993 if (! my d_editable || ! NativeText_getSelectionRange (my d_widget, nullptr, nullptr)) return;
994 SendMessage (my d_widget -> window, WM_CLEAR, 0, 0); // this will send the EN_CHANGE message, hence no need to call the valueChangedCallbacks
995 UpdateWindow (my d_widget -> window);
996 #elif cocoa
997 if (my d_cocoaTextView) {
998 [my d_cocoaTextView delete: nil];
999 }
1000 #endif
1001 }
1002
GuiText_replace(GuiText me,integer from_pos,integer to_pos,conststring32 text)1003 void GuiText_replace (GuiText me, integer from_pos, integer to_pos, conststring32 text) {
1004 #if gtk
1005 const gchar *newText = Melder_peek32to8 (text);
1006 if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_ENTRY) {
1007 gtk_editable_delete_text (GTK_EDITABLE (my d_widget), from_pos, to_pos);
1008 gint from_pos_gint = from_pos;
1009 gtk_editable_insert_text (GTK_EDITABLE (my d_widget), newText, g_utf8_strlen (newText, -1), & from_pos_gint);
1010 } else if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_TEXT_VIEW) {
1011 GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (my d_widget));
1012 GtkTextIter from_it, to_it;
1013 gtk_text_buffer_get_iter_at_offset (buffer, & from_it, from_pos);
1014 gtk_text_buffer_get_iter_at_offset (buffer, & to_it, to_pos);
1015 gtk_text_buffer_delete_interactive (buffer, & from_it, & to_it,
1016 gtk_text_view_get_editable (GTK_TEXT_VIEW (my d_widget)));
1017 gtk_text_buffer_insert_interactive (buffer, & from_it, newText, g_utf8_strlen (newText, -1),
1018 gtk_text_view_get_editable (GTK_TEXT_VIEW (my d_widget)));
1019 }
1020 #elif motif
1021 Melder_assert (MEMBER (my d_widget, Text));
1022 autostring32 winText (2 * str32len (text), true); // all newlines
1023 char32 *to = & winText [0];
1024 /*
1025 Replace all LF with CR/LF.
1026 */
1027 for (const char32 *from = & text [0]; *from != U'\0'; from ++, to ++)
1028 if (*from == U'\n') { *to = 13; * ++ to = U'\n'; } else *to = *from;
1029 *to = U'\0';
1030 /*
1031 We DON'T replace any text without selecting it, so we can deselect any other text,
1032 thus allowing ourselves to select [from_pos, to_pos] and use the REPLACESEL message.
1033 */
1034 GuiText_setSelection (me, from_pos, to_pos);
1035 Edit_ReplaceSel (my d_widget -> window, Melder_peek32toW (winText.get()));
1036 UpdateWindow (my d_widget -> window);
1037 #elif cocoa
1038 if (my d_cocoaTextView) {
1039 integer numberOfLeadingHighUnicodeValues = 0, numberOfSelectedHighUnicodeValues = 0;
1040 {// scope
1041 autostring32 oldText = GuiText_getString (me);
1042 for (integer i = 0; i < from_pos; i ++) if (oldText [i] > 0xFFFF) numberOfLeadingHighUnicodeValues ++;
1043 for (integer i = from_pos; i < to_pos; i ++) if (oldText [i] > 0xFFFF) numberOfSelectedHighUnicodeValues ++;
1044 }
1045 from_pos += numberOfLeadingHighUnicodeValues;
1046 to_pos += numberOfLeadingHighUnicodeValues + numberOfSelectedHighUnicodeValues;
1047 NSRange nsRange = NSMakeRange (integer_to_uinteger (from_pos), integer_to_uinteger (to_pos - from_pos));
1048 NSString *nsString = (NSString *) Melder_peek32toCfstring (text);
1049 [my d_cocoaTextView shouldChangeTextInRange: nsRange replacementString: nsString]; // ignore the returned BOOL: only interested in the side effect of having undo support
1050 [[my d_cocoaTextView textStorage] replaceCharactersInRange: nsRange withString: nsString]; // this messes up the widget...
1051 [my d_cocoaTextView setFont: [NSFont fontWithName: @"Menlo" size: my d_macFontSize]]; // ... so we reapply the font size (HACK 2021-05-07)
1052 [my d_cocoaTextView setTextColor: [NSColor textColor]]; // ... and the foreground colour as well (HACK 2021-05-07)
1053 }
1054 #endif
1055 }
1056
GuiText_scrollToSelection(GuiText me)1057 void GuiText_scrollToSelection (GuiText me) {
1058 #if gtk
1059 GtkTextBuffer *textBuffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (my d_widget));
1060 GtkTextIter start, end;
1061 gtk_text_buffer_get_selection_bounds (textBuffer, & start, & end);
1062 //GtkTextMark *mark = gtk_text_buffer_create_mark (textBuffer, nullptr, & start, true);
1063 gtk_text_view_scroll_to_iter (GTK_TEXT_VIEW (my d_widget), & start, 0.1, false, 0.0, 0.0);
1064 //gtk_text_view_scroll_to_mark (GTK_TEXT_VIEW (my d_widget), mark, 0.1, false, 0.0, 0.0);
1065 #elif motif
1066 Edit_ScrollCaret (my d_widget -> window);
1067 #elif cocoa
1068 if (my d_cocoaTextView)
1069 [my d_cocoaTextView scrollRangeToVisible: [my d_cocoaTextView selectedRange]];
1070 #endif
1071 }
1072
GuiText_setChangedCallback(GuiText me,GuiText_ChangedCallback changedCallback,Thing changedBoss)1073 void GuiText_setChangedCallback (GuiText me, GuiText_ChangedCallback changedCallback, Thing changedBoss) {
1074 my d_changedCallback = changedCallback;
1075 my d_changedBoss = changedBoss;
1076 }
1077
GuiText_setFontSize(GuiText me,double size)1078 void GuiText_setFontSize (GuiText me, double size) {
1079 #if gtk
1080 GtkStyleContext *styleContext = gtk_widget_get_style_context (GTK_WIDGET (my d_widget));
1081 const PangoFontDescription *fontDesc = gtk_style_context_get_font (styleContext, GTK_STATE_FLAG_NORMAL);
1082 PangoFontDescription *copy = pango_font_description_copy (fontDesc);
1083 pango_font_description_set_absolute_size (copy, size * PANGO_SCALE);
1084 gtk_widget_override_font (GTK_WIDGET (my d_widget), copy);
1085 pango_font_description_free (copy);
1086 #elif motif
1087 // a trick to update the window. BUG: why doesn't UpdateWindow seem to suffice?
1088 integer first, last;
1089 autostring32 text = GuiText_getStringAndSelectionPosition (me, & first, & last);
1090 GuiText_setString (me, U""); // erase all
1091 UpdateWindow (my d_widget -> window);
1092 if (size <= 10.0)
1093 SetWindowFont (my d_widget -> window, font10, false);
1094 else if (size <= 12.0)
1095 SetWindowFont (my d_widget -> window, font12, false);
1096 else if (size <= 14.0)
1097 SetWindowFont (my d_widget -> window, font14, false);
1098 else if (size <= 18.0)
1099 SetWindowFont (my d_widget -> window, font18, false);
1100 else
1101 SetWindowFont (my d_widget -> window, font24, false);
1102 GuiText_setString (me, text.get());
1103 GuiText_setSelection (me, first, last);
1104 UpdateWindow (my d_widget -> window);
1105 #elif cocoa
1106 my d_macFontSize = size;
1107 if (my d_cocoaTextView)
1108 [my d_cocoaTextView setFont: [NSFont fontWithName: @"Menlo" size: size]];
1109 #endif
1110 }
1111
GuiText_setRedoItem(GuiText me,GuiMenuItem item)1112 void GuiText_setRedoItem (GuiText me, GuiMenuItem item) {
1113 #if gtk
1114 if (my d_redo_item)
1115 //g_object_unref (my d_redo_item -> d_widget);
1116 my d_redo_item = item;
1117 if (my d_redo_item) {
1118 //g_object_ref (my d_redo_item -> d_widget);
1119 GuiThing_setSensitive (my d_redo_item, history_has_redo (me));
1120 }
1121 #elif motif
1122 #elif cocoa
1123 #endif
1124 }
1125
GuiText_setSelection(GuiText me,integer first,integer last)1126 void GuiText_setSelection (GuiText me, integer first, integer last) {
1127 if (my d_widget) {
1128 #if gtk
1129 if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_ENTRY) {
1130 gtk_editable_select_region (GTK_EDITABLE (my d_widget), first, last);
1131 } else if (G_OBJECT_TYPE (G_OBJECT (my d_widget)) == GTK_TYPE_TEXT_VIEW) {
1132 GtkTextBuffer *buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (my d_widget));
1133 GtkTextIter from_it, to_it;
1134 gtk_text_buffer_get_iter_at_offset (buffer, & from_it, first);
1135 gtk_text_buffer_get_iter_at_offset (buffer, & to_it, last);
1136 gtk_text_buffer_select_range (buffer, & from_it, & to_it);
1137 }
1138 #elif motif
1139 autostring32 text = GuiText_getString (me);
1140 if (first < 0) first = 0;
1141 if (last < 0) last = 0;
1142 integer length = str32len (text.get());
1143 if (first >= length) first = length;
1144 if (last >= length) last = length;
1145 /*
1146 * 'first' and 'last' are the positions of the selection in the text when separated by LF alone.
1147 * We have to convert this to the positions that the selection has in a text separated by CR/LF sequences.
1148 */
1149 integer numberOfLeadingLineBreaks = 0, numberOfSelectedLineBreaks = 0;
1150 for (integer i = 0; i < first; i ++) if (text [i] == U'\n') numberOfLeadingLineBreaks ++;
1151 for (integer i = first; i < last; i ++) if (text [i] == U'\n') numberOfSelectedLineBreaks ++;
1152 /*
1153 On Windows, characters are counted in UTF-16 units, whereas 'first' and 'last' are in UTF-32 units. Convert.
1154 */
1155 integer numberOfLeadingHighUnicodeValues = 0, numberOfSelectedHighUnicodeValues = 0;
1156 for (integer i = 0; i < first; i ++)
1157 if (text [i] > 0xFFFF)
1158 numberOfLeadingHighUnicodeValues ++;
1159 for (integer i = first; i < last; i ++)
1160 if (text [i] > 0xFFFF)
1161 numberOfSelectedHighUnicodeValues ++;
1162
1163 first += numberOfLeadingLineBreaks;
1164 last += numberOfLeadingLineBreaks + numberOfSelectedLineBreaks;
1165 first += numberOfLeadingHighUnicodeValues;
1166 last += numberOfLeadingHighUnicodeValues + numberOfSelectedHighUnicodeValues;
1167
1168 Edit_SetSel (my d_widget -> window, first, last);
1169 UpdateWindow (my d_widget -> window);
1170 #elif cocoa
1171 /*
1172 On Cocoa, characters are counted in UTF-16 units, whereas 'first' and 'last' are in UTF-32 units. Convert.
1173 */
1174 autostring32 text = GuiText_getString (me);
1175 /*
1176 The following line is needed in case GuiText_getString removed carriage returns.
1177 */
1178 GuiText_setString (me, text.get());
1179 integer numberOfLeadingHighUnicodeValues = 0, numberOfSelectedHighUnicodeValues = 0;
1180 for (integer i = 0; i < first; i ++)
1181 if (text [i] > 0xFFFF)
1182 numberOfLeadingHighUnicodeValues ++;
1183 for (integer i = first; i < last; i ++)
1184 if (text [i] > 0xFFFF)
1185 numberOfSelectedHighUnicodeValues ++;
1186 first += numberOfLeadingHighUnicodeValues;
1187 last += numberOfLeadingHighUnicodeValues + numberOfSelectedHighUnicodeValues;
1188
1189 if (my d_cocoaTextView) {
1190 [my d_cocoaTextView setSelectedRange: NSMakeRange (integer_to_uinteger (first), integer_to_uinteger (last - first))];
1191 }
1192 #endif
1193 }
1194 }
1195
GuiText_setString(GuiText me,conststring32 text,bool undoable)1196 void GuiText_setString (GuiText me, conststring32 text, bool undoable) {
1197 #if gtk
1198 if (G_OBJECT_TYPE (my d_widget) == GTK_TYPE_ENTRY) {
1199 gtk_entry_set_text (GTK_ENTRY (my d_widget), Melder_peek32to8 (text));
1200 } else if (G_OBJECT_TYPE (my d_widget) == GTK_TYPE_TEXT_VIEW) {
1201 GtkTextBuffer *textBuffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (my d_widget));
1202 const gchar *textUtf8 = Melder_peek32to8 (text);
1203 //gtk_text_buffer_set_text (textBuffer, textUtf8, strlen (textUtf8)); // length in bytes!
1204 GtkTextIter start, end;
1205 gtk_text_buffer_get_start_iter (textBuffer, & start);
1206 gtk_text_buffer_get_end_iter (textBuffer, & end);
1207 gtk_text_buffer_delete_interactive (textBuffer, & start, & end, gtk_text_view_get_editable (GTK_TEXT_VIEW (my d_widget)));
1208 gtk_text_buffer_insert_interactive (textBuffer, & start, textUtf8, strlen (textUtf8), gtk_text_view_get_editable (GTK_TEXT_VIEW (my d_widget)));
1209 }
1210 #elif motif
1211 autostring32 winText (2 * str32len (text), true); // all new lines
1212 char32 *to = & winText [0];
1213 /*
1214 Replace all LF with CR/LF.
1215 */
1216 for (const char32 *from = & text [0]; *from != U'\0'; from ++, to ++)
1217 if (*from == U'\n') { *to = 13; * ++ to = U'\n'; } else *to = *from;
1218 *to = U'\0';
1219 SetWindowTextW (my d_widget -> window, Melder_peek32toW (winText.get()));
1220 UpdateWindow (my d_widget -> window);
1221 #elif cocoa
1222 trace (U"title");
1223 if (my d_cocoaTextView) {
1224 NSRange nsRange = NSMakeRange (0, [[my d_cocoaTextView textStorage] length]);
1225 NSString *nsString = (NSString *) Melder_peek32toCfstring (text);
1226 if (undoable)
1227 [my d_cocoaTextView shouldChangeTextInRange: nsRange replacementString: nsString]; // to make this action undoable
1228 //[[my d_cocoaTextView textStorage] replaceCharactersInRange: nsRange withString: nsString];
1229 if (true) {
1230 [my d_cocoaTextView setString: nsString];
1231 } else {
1232 NSMutableParagraphStyle * aMutableParagraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
1233 [aMutableParagraphStyle setTabStops: [NSArray array]];
1234 NSTextTab *tab400 = [[NSTextTab alloc] initWithType: NSLeftTabStopType location: 400.0];
1235 [aMutableParagraphStyle addTabStop: tab400];
1236 NSTextTab *tab500 = [[NSTextTab alloc] initWithType: NSLeftTabStopType location: 500.0];
1237 [aMutableParagraphStyle addTabStop: tab500];
1238 NSMutableAttributedString * attributedString = [[NSMutableAttributedString alloc] initWithString: nsString];
1239 [attributedString addAttribute: NSParagraphStyleAttributeName value: aMutableParagraphStyle range: NSMakeRange (0, [nsString length])];
1240 [[my d_cocoaTextView textStorage] setAttributedString: attributedString];
1241 }
1242 [my d_cocoaTextView scrollRangeToVisible: NSMakeRange ([[my d_cocoaTextView textStorage] length], 0)]; // to the end
1243 //[[my d_cocoaTextView window] setViewsNeedDisplay: YES];
1244 //[[my d_cocoaTextView window] display];
1245 } else {
1246 [(NSTextField *) my d_widget setStringValue: (NSString *) Melder_peek32toCfstring (text)];
1247 }
1248 #endif
1249 }
1250
GuiText_setUndoItem(GuiText me,GuiMenuItem item)1251 void GuiText_setUndoItem (GuiText me, GuiMenuItem item) {
1252 #if gtk
1253 if (my d_undo_item) {
1254 //g_object_unref (my d_undo_item -> d_widget);
1255 }
1256 my d_undo_item = item;
1257 if (my d_undo_item) {
1258 //g_object_ref (my d_undo_item -> d_widget);
1259 GuiThing_setSensitive (my d_undo_item, history_has_undo (me));
1260 }
1261 #elif motif
1262 #elif cocoa
1263 #endif
1264 }
1265
GuiText_undo(GuiText me)1266 void GuiText_undo (GuiText me) {
1267 #if gtk
1268 history_do (me, 1);
1269 #elif motif
1270 #elif cocoa
1271 if (my d_cocoaTextView) {
1272 [[my d_cocoaTextView undoManager] undo];
1273 }
1274 #endif
1275 }
1276
1277 /* End of file GuiText.cpp */
1278