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