1 /* TextEditor.cpp
2  *
3  * Copyright (C) 1997-2021 Paul Boersma, 2010 Franz Brausse
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 "TextEditor.h"
20 #include "machine.h"
21 #include "../kar/longchar.h"
22 #include "EditorM.h"
23 #include "../kar/UnicodeData.h"
24 
25 Thing_implement (TextEditor, Editor, 0);
26 
27 #include "prefs_define.h"
28 #include "TextEditor_prefs.h"
29 #include "prefs_install.h"
30 #include "TextEditor_prefs.h"
31 #include "prefs_copyToInstance.h"
32 #include "TextEditor_prefs.h"
33 
34 static CollectionOf <structTextEditor> theReferencesToAllOpenTextEditors;
35 
36 /***** TextEditor methods *****/
37 
v_destroy()38 void structTextEditor :: v_destroy () noexcept {
39 	our openDialog.reset();   // don't delay till delete
40 	our saveDialog.reset();   // don't delay till delete
41 	theReferencesToAllOpenTextEditors. undangleItem (this);
42 	TextEditor_Parent :: v_destroy ();
43 }
44 
v_nameChanged()45 void structTextEditor :: v_nameChanged () {
46 	if (v_fileBased ()) {
47 		bool dirtinessAlreadyShown = GuiWindow_setDirty (our windowForm, our dirty);
48 		static MelderString windowTitle;
49 		if (our name [0] == U'\0') {
50 			MelderString_copy (& windowTitle, U"(untitled");
51 			if (dirty && ! dirtinessAlreadyShown)
52 				MelderString_append (& windowTitle, U", modified");
53 			MelderString_append (& windowTitle, U")");
54 		} else {
55 			MelderString_copy (& windowTitle, U"File ", MelderFile_messageName (& our file));
56 			if (dirty && ! dirtinessAlreadyShown)
57 				MelderString_append (& windowTitle, U" (modified)");
58 		}
59 		GuiShell_setTitle (our windowForm, windowTitle.string);
60 		//MelderString_copy (& windowTitle, our dirty && ! dirtinessAlreadyShown ? U"*" : U"", our name [0] == U'\0' ? U"(untitled)" : MelderFile_name (& our file));
61 	} else {
62 		TextEditor_Parent :: v_nameChanged ();
63 	}
64 }
65 
openDocument(TextEditor me,MelderFile file)66 static void openDocument (TextEditor me, MelderFile file) {
67 	for (integer ieditor = 1; ieditor <= theReferencesToAllOpenTextEditors.size; ieditor ++) {
68 		TextEditor editor = theReferencesToAllOpenTextEditors.at [ieditor];
69 		if (editor != me && MelderFile_equal (file, & editor -> file)) {
70 			Editor_raise (editor);
71 			/*
72 				Destruction alarm!
73 				When we combine the destruction of an object with the presentation of a message,
74 				we shall always follow the "build message -- destroy -- show message" paradigm.
75 				Actually, in this case this is not only safe, but also crucial,
76 				because at the time of writing (2019-04-28) the owner of `file` is owned by `me`,
77 				so that destroying `me` would dangle `file`.
78 			*/
79 			Melder_appendError (U"Text file ", file, U" is already open.");
80 			forget (me);
81 			Melder_flushError ();
82 			return;
83 		}
84 	}
85 	autostring32 text = MelderFile_readText (file);
86 	GuiText_setString (my textWidget, text.get());
87 	/*
88 	 * GuiText_setString has invoked the changeCallback,
89 	 * which has set `my dirty` to `true`. Fix this.
90 	 */
91 	my dirty = false;
92 	MelderFile_copy (file, & my file);
93 	Thing_setName (me, Melder_fileToPath (file));
94 }
95 
newDocument(TextEditor me)96 static void newDocument (TextEditor me) {
97 	GuiText_setString (my textWidget, U"");   // implicitly sets my dirty to `true`
98 	my dirty = false;
99 	if (my v_fileBased ())
100 		Thing_setName (me, U"");
101 }
102 
saveDocument(TextEditor me,MelderFile file)103 static void saveDocument (TextEditor me, MelderFile file) {
104 	autostring32 text = GuiText_getString (my textWidget);
105 	MelderFile_writeText (file, text.get(), Melder_getOutputEncoding ());
106 	my dirty = false;
107 	MelderFile_copy (file, & my file);
108 	if (my v_fileBased ())
109 		Thing_setName (me, Melder_fileToPath (file));
110 }
111 
closeDocument(TextEditor me)112 static void closeDocument (TextEditor me) {
113 	forget (me);
114 }
115 
cb_open_ok(UiForm sendingForm,integer,Stackel,conststring32,Interpreter,conststring32,bool,void * void_me)116 static void cb_open_ok (UiForm sendingForm, integer /* narg */, Stackel /* args */, conststring32 /* sendingString */,
117 	Interpreter /* interpreter */, conststring32 /* invokingButtonTitle */, bool /* modified */, void *void_me)
118 {
119 	iam (TextEditor);
120 	MelderFile file = UiFile_getFile (sendingForm);
121 	openDocument (me, file);
122 }
123 
cb_showOpen(EditorCommand cmd)124 static void cb_showOpen (EditorCommand cmd) {
125 	TextEditor me = (TextEditor) cmd -> d_editor;
126 	if (! my openDialog)
127 		my openDialog = UiInfile_create (my windowForm, U"Open", cb_open_ok, me, nullptr, nullptr, false);
128 	UiInfile_do (my openDialog.get());
129 }
130 
cb_saveAs_ok(UiForm sendingForm,integer,Stackel,conststring32,Interpreter,conststring32,bool,void * void_me)131 static void cb_saveAs_ok (UiForm sendingForm, integer /* narg */, Stackel /* args */, conststring32 /* sendingString */,
132 	Interpreter /* interpreter */, conststring32 /* invokingButtonTitle */, bool /* modified */, void *void_me)
133 {
134 	iam (TextEditor);
135 	MelderFile file = UiFile_getFile (sendingForm);
136 	saveDocument (me, file);
137 }
138 
menu_cb_saveAs(TextEditor me,EDITOR_ARGS_DIRECT)139 static void menu_cb_saveAs (TextEditor me, EDITOR_ARGS_DIRECT) {
140 	if (! my saveDialog)
141 		my saveDialog = UiOutfile_create (my windowForm, U"Save", cb_saveAs_ok, me, nullptr, nullptr);
142 	char32 defaultName [300];
143 	Melder_sprint (defaultName,300, ! my v_fileBased () ? U"info.txt" : my name [0] ? MelderFile_name (& my file) : U"");
144 	UiOutfile_do (my saveDialog.get(), defaultName);
145 }
146 
gui_button_cb_saveAndOpen(EditorCommand cmd,GuiButtonEvent)147 static void gui_button_cb_saveAndOpen (EditorCommand cmd, GuiButtonEvent /* event */) {
148 	TextEditor me = (TextEditor) cmd -> d_editor;
149 	GuiThing_hide (my dirtyOpenDialog);
150 	if (my name [0]) {
151 		try {
152 			saveDocument (me, & my file);
153 		} catch (MelderError) {
154 			Melder_flushError ();
155 			return;
156 		}
157 		cb_showOpen (cmd);
158 	} else {
159 		menu_cb_saveAs (me, cmd, nullptr, 0, nullptr, nullptr, nullptr);
160 	}
161 }
162 
gui_button_cb_cancelOpen(EditorCommand cmd,GuiButtonEvent)163 static void gui_button_cb_cancelOpen (EditorCommand cmd, GuiButtonEvent /* event */) {
164 	TextEditor me = (TextEditor) cmd -> d_editor;
165 	GuiThing_hide (my dirtyOpenDialog);
166 }
167 
gui_button_cb_discardAndOpen(EditorCommand cmd,GuiButtonEvent)168 static void gui_button_cb_discardAndOpen (EditorCommand cmd, GuiButtonEvent /* event */) {
169 	TextEditor me = (TextEditor) cmd -> d_editor;
170 	GuiThing_hide (my dirtyOpenDialog);
171 	cb_showOpen (cmd);
172 }
173 
menu_cb_open(TextEditor me,EDITOR_ARGS_CMD)174 static void menu_cb_open (TextEditor me, EDITOR_ARGS_CMD) {
175 	if (my dirty) {
176 		if (! my dirtyOpenDialog) {
177 			int buttonWidth = 120, buttonSpacing = 20;
178 			my dirtyOpenDialog = GuiDialog_create (my windowForm,
179 				150, 70,
180 				Gui_LEFT_DIALOG_SPACING + 3 * buttonWidth + 2 * buttonSpacing + Gui_RIGHT_DIALOG_SPACING,
181 				Gui_TOP_DIALOG_SPACING + Gui_TEXTFIELD_HEIGHT + Gui_VERTICAL_DIALOG_SPACING_SAME + 2 * Gui_BOTTOM_DIALOG_SPACING + Gui_PUSHBUTTON_HEIGHT,
182 				U"Text changed", nullptr, nullptr, GuiDialog_MODAL);
183 			GuiLabel_createShown (my dirtyOpenDialog,
184 				Gui_LEFT_DIALOG_SPACING, - Gui_RIGHT_DIALOG_SPACING,
185 				Gui_TOP_DIALOG_SPACING, Gui_TOP_DIALOG_SPACING + Gui_LABEL_HEIGHT,
186 				U"The text has changed! Save changes?", 0);
187 			int x = Gui_LEFT_DIALOG_SPACING, y = - Gui_BOTTOM_DIALOG_SPACING;
188 			GuiButton_createShown (my dirtyOpenDialog,
189 				x, x + buttonWidth, y - Gui_PUSHBUTTON_HEIGHT, y,
190 				U"Discard & Open", gui_button_cb_discardAndOpen, cmd, 0);
191 			x += buttonWidth + buttonSpacing;
192 			GuiButton_createShown (my dirtyOpenDialog,
193 				x, x + buttonWidth, y - Gui_PUSHBUTTON_HEIGHT, y,
194 				U"Cancel", gui_button_cb_cancelOpen, cmd, 0);
195 			x += buttonWidth + buttonSpacing;
196 			GuiButton_createShown (my dirtyOpenDialog,
197 				x, x + buttonWidth, y - Gui_PUSHBUTTON_HEIGHT, y,
198 				U"Save & Open", gui_button_cb_saveAndOpen, cmd, 0);
199 		}
200 		GuiThing_show (my dirtyOpenDialog);
201 	} else {
202 		cb_showOpen (cmd);
203 	}
204 }
205 
gui_button_cb_saveAndNew(EditorCommand cmd,GuiButtonEvent)206 static void gui_button_cb_saveAndNew (EditorCommand cmd, GuiButtonEvent /* event */) {
207 	TextEditor me = (TextEditor) cmd -> d_editor;
208 	GuiThing_hide (my dirtyNewDialog);
209 	if (my name [0]) {
210 		try {
211 			saveDocument (me, & my file);
212 		} catch (MelderError) {
213 			Melder_flushError ();
214 			return;
215 		}
216 		newDocument (me);
217 	} else {
218 		menu_cb_saveAs (me, cmd, nullptr, 0, nullptr, nullptr, nullptr);
219 	}
220 }
221 
gui_button_cb_cancelNew(EditorCommand cmd,GuiButtonEvent)222 static void gui_button_cb_cancelNew (EditorCommand cmd, GuiButtonEvent /* event */) {
223 	TextEditor me = (TextEditor) cmd -> d_editor;
224 	GuiThing_hide (my dirtyNewDialog);
225 }
226 
gui_button_cb_discardAndNew(EditorCommand cmd,GuiButtonEvent)227 static void gui_button_cb_discardAndNew (EditorCommand cmd, GuiButtonEvent /* event */) {
228 	TextEditor me = (TextEditor) cmd -> d_editor;
229 	GuiThing_hide (my dirtyNewDialog);
230 	newDocument (me);
231 }
232 
menu_cb_new(TextEditor me,EDITOR_ARGS_CMD)233 static void menu_cb_new (TextEditor me, EDITOR_ARGS_CMD) {
234 	if (my v_fileBased () && my dirty) {
235 		if (! my dirtyNewDialog) {
236 			int buttonWidth = 120, buttonSpacing = 20;
237 			my dirtyNewDialog = GuiDialog_create (my windowForm,
238 				150, 70, Gui_LEFT_DIALOG_SPACING + 3 * buttonWidth + 2 * buttonSpacing + Gui_RIGHT_DIALOG_SPACING,
239 					Gui_TOP_DIALOG_SPACING + Gui_TEXTFIELD_HEIGHT + Gui_VERTICAL_DIALOG_SPACING_SAME + 2 * Gui_BOTTOM_DIALOG_SPACING + Gui_PUSHBUTTON_HEIGHT,
240 				U"Text changed", nullptr, nullptr, GuiDialog_MODAL);
241 			GuiLabel_createShown (my dirtyNewDialog,
242 				Gui_LEFT_DIALOG_SPACING, - Gui_RIGHT_DIALOG_SPACING,
243 				Gui_TOP_DIALOG_SPACING, Gui_TOP_DIALOG_SPACING + Gui_LABEL_HEIGHT,
244 				U"The text has changed! Save changes?", 0);
245 			int x = Gui_LEFT_DIALOG_SPACING, y = - Gui_BOTTOM_DIALOG_SPACING;
246 			GuiButton_createShown (my dirtyNewDialog,
247 				x, x + buttonWidth, y - Gui_PUSHBUTTON_HEIGHT, y,
248 				U"Discard & New", gui_button_cb_discardAndNew, cmd, 0);
249 			x += buttonWidth + buttonSpacing;
250 			GuiButton_createShown (my dirtyNewDialog,
251 				x, x + buttonWidth, y - Gui_PUSHBUTTON_HEIGHT, y,
252 				U"Cancel", gui_button_cb_cancelNew, cmd, 0);
253 			x += buttonWidth + buttonSpacing;
254 			GuiButton_createShown (my dirtyNewDialog,
255 				x, x + buttonWidth, y - Gui_PUSHBUTTON_HEIGHT, y,
256 				U"Save & New", gui_button_cb_saveAndNew, cmd, 0);
257 		}
258 		GuiThing_show (my dirtyNewDialog);
259 	} else {
260 		newDocument (me);
261 	}
262 }
263 
gui_button_cb_cancelReopen(EditorCommand cmd,GuiButtonEvent)264 static void gui_button_cb_cancelReopen (EditorCommand cmd, GuiButtonEvent /* event */) {
265 	TextEditor me = (TextEditor) cmd -> d_editor;
266 	GuiThing_hide (my dirtyReopenDialog);
267 }
268 
gui_button_cb_discardAndReopen(EditorCommand cmd,GuiButtonEvent)269 static void gui_button_cb_discardAndReopen (EditorCommand cmd, GuiButtonEvent /* event */) {
270 	TextEditor me = (TextEditor) cmd -> d_editor;
271 	GuiThing_hide (my dirtyReopenDialog);
272 	openDocument (me, & my file);
273 }
274 
menu_cb_reopen(TextEditor me,EDITOR_ARGS_CMD)275 static void menu_cb_reopen (TextEditor me, EDITOR_ARGS_CMD) {
276 	Melder_assert (my v_fileBased());
277 	if (my name [0] == U'\0') {
278 		Melder_throw (U"Cannot reopen from disk, because the text has never been saved yet.");
279 	}
280 	if (my dirty) {
281 		if (! my dirtyReopenDialog) {
282 			int buttonWidth = 250, buttonSpacing = 20;
283 			my dirtyReopenDialog = GuiDialog_create (my windowForm,
284 				150, 70, Gui_LEFT_DIALOG_SPACING + 2 * buttonWidth + 1 * buttonSpacing + Gui_RIGHT_DIALOG_SPACING,
285 					Gui_TOP_DIALOG_SPACING + Gui_TEXTFIELD_HEIGHT + Gui_VERTICAL_DIALOG_SPACING_SAME + 2 * Gui_BOTTOM_DIALOG_SPACING + Gui_PUSHBUTTON_HEIGHT,
286 				U"Text changed", nullptr, nullptr, GuiDialog_MODAL);
287 			GuiLabel_createShown (my dirtyReopenDialog,
288 				Gui_LEFT_DIALOG_SPACING, - Gui_RIGHT_DIALOG_SPACING,
289 				Gui_TOP_DIALOG_SPACING, Gui_TOP_DIALOG_SPACING + Gui_LABEL_HEIGHT,
290 				U"The text in the editor contains changes! Reopen nevertheless?", 0);
291 			int x = Gui_LEFT_DIALOG_SPACING, y = - Gui_BOTTOM_DIALOG_SPACING;
292 			GuiButton_createShown (my dirtyReopenDialog,
293 				x, x + buttonWidth, y - Gui_PUSHBUTTON_HEIGHT, y,
294 				U"Keep visible version", gui_button_cb_cancelReopen, cmd, GuiButton_CANCEL);
295 			x += buttonWidth + buttonSpacing;
296 			GuiButton_createShown (my dirtyReopenDialog,
297 				x, x + buttonWidth, y - Gui_PUSHBUTTON_HEIGHT, y,
298 				U"Replace with version from disk", gui_button_cb_discardAndReopen, cmd, GuiButton_DEFAULT);
299 		}
300 		GuiThing_show (my dirtyReopenDialog);
301 	} else {
302 		try {
303 			openDocument (me, & my file);
304 		} catch (MelderError) {
305 			Melder_flushError ();
306 			return;
307 		}
308 	}
309 }
310 
menu_cb_clear(TextEditor me,EDITOR_ARGS_DIRECT)311 static void menu_cb_clear (TextEditor me, EDITOR_ARGS_DIRECT) {
312 	my v_clear ();
313 }
314 
menu_cb_save(TextEditor me,EDITOR_ARGS_CMD)315 static void menu_cb_save (TextEditor me, EDITOR_ARGS_CMD) {
316 	if (my name [0]) {
317 		try {
318 			saveDocument (me, & my file);
319 		} catch (MelderError) {
320 			Melder_flushError ();
321 			return;
322 		}
323 	} else {
324 		menu_cb_saveAs (me, cmd, nullptr, 0, nullptr, nullptr, nullptr);
325 	}
326 }
327 
gui_button_cb_saveAndClose(TextEditor me,GuiButtonEvent)328 static void gui_button_cb_saveAndClose (TextEditor me, GuiButtonEvent /* event */) {
329 	GuiThing_hide (my dirtyCloseDialog);
330 	if (my name [0]) {
331 		try {
332 			saveDocument (me, & my file);
333 		} catch (MelderError) {
334 			Melder_flushError ();
335 			return;
336 		}
337 		closeDocument (me);
338 	} else {
339 		menu_cb_saveAs (me, Editor_getMenuCommand (me, U"File", U"Save as..."), nullptr, 0, nullptr, nullptr, nullptr);
340 	}
341 }
342 
gui_button_cb_cancelClose(TextEditor me,GuiButtonEvent)343 static void gui_button_cb_cancelClose (TextEditor me, GuiButtonEvent /* event */) {
344 	GuiThing_hide (my dirtyCloseDialog);
345 }
346 
gui_button_cb_discardAndClose(TextEditor me,GuiButtonEvent)347 static void gui_button_cb_discardAndClose (TextEditor me, GuiButtonEvent /* event */) {
348 	GuiThing_hide (my dirtyCloseDialog);
349 	closeDocument (me);
350 }
351 
v_goAway()352 void structTextEditor :: v_goAway () {
353 	if (v_fileBased () && dirty) {
354 		if (! dirtyCloseDialog) {
355 			int buttonWidth = 120, buttonSpacing = 20;
356 			dirtyCloseDialog = GuiDialog_create (our windowForm,
357 				150, 70, Gui_LEFT_DIALOG_SPACING + 3 * buttonWidth + 2 * buttonSpacing + Gui_RIGHT_DIALOG_SPACING,
358 					Gui_TOP_DIALOG_SPACING + Gui_TEXTFIELD_HEIGHT + Gui_VERTICAL_DIALOG_SPACING_SAME + 2 * Gui_BOTTOM_DIALOG_SPACING + Gui_PUSHBUTTON_HEIGHT,
359 				U"Text changed", nullptr, nullptr, GuiDialog_MODAL);
360 			GuiLabel_createShown (dirtyCloseDialog,
361 				Gui_LEFT_DIALOG_SPACING, - Gui_RIGHT_DIALOG_SPACING,
362 				Gui_TOP_DIALOG_SPACING, Gui_TOP_DIALOG_SPACING + Gui_LABEL_HEIGHT,
363 				U"The text has changed! Save changes?", 0);
364 			int x = Gui_LEFT_DIALOG_SPACING, y = - Gui_BOTTOM_DIALOG_SPACING;
365 			GuiButton_createShown (dirtyCloseDialog,
366 				x, x + buttonWidth, y - Gui_PUSHBUTTON_HEIGHT, y,
367 				U"Discard & Close", gui_button_cb_discardAndClose, this, 0);
368 			x += buttonWidth + buttonSpacing;
369 			GuiButton_createShown (dirtyCloseDialog,
370 				x, x + buttonWidth, y - Gui_PUSHBUTTON_HEIGHT, y,
371 				U"Cancel", gui_button_cb_cancelClose, this, 0);
372 			x += buttonWidth + buttonSpacing;
373 			GuiButton_createShown (dirtyCloseDialog,
374 				x, x + buttonWidth, y - Gui_PUSHBUTTON_HEIGHT, y,
375 				U"Save & Close", gui_button_cb_saveAndClose, this, 0);
376 		}
377 		if (our dirtyNewDialog)
378 			GuiThing_hide (our dirtyNewDialog);
379 		if (our dirtyOpenDialog)
380 			GuiThing_hide (our dirtyOpenDialog);
381 		if (our dirtyReopenDialog)
382 			GuiThing_hide (our dirtyReopenDialog);
383 		GuiThing_show (dirtyCloseDialog);
384 	} else {
385 		closeDocument (this);
386 	}
387 }
388 
menu_cb_undo(TextEditor me,EDITOR_ARGS_DIRECT)389 static void menu_cb_undo (TextEditor me, EDITOR_ARGS_DIRECT) {
390 	GuiText_undo (my textWidget);
391 }
392 
menu_cb_redo(TextEditor me,EDITOR_ARGS_DIRECT)393 static void menu_cb_redo (TextEditor me, EDITOR_ARGS_DIRECT) {
394 	GuiText_redo (my textWidget);
395 }
396 
menu_cb_cut(TextEditor me,EDITOR_ARGS_DIRECT)397 static void menu_cb_cut (TextEditor me, EDITOR_ARGS_DIRECT) {
398 	GuiText_cut (my textWidget);  // use ((XmAnyCallbackStruct *) call) -> event -> xbutton. time
399 }
400 
menu_cb_copy(TextEditor me,EDITOR_ARGS_DIRECT)401 static void menu_cb_copy (TextEditor me, EDITOR_ARGS_DIRECT) {
402 	GuiText_copy (my textWidget);
403 }
404 
menu_cb_paste(TextEditor me,EDITOR_ARGS_DIRECT)405 static void menu_cb_paste (TextEditor me, EDITOR_ARGS_DIRECT) {
406 	GuiText_paste (my textWidget);
407 }
408 
menu_cb_erase(TextEditor me,EDITOR_ARGS_DIRECT)409 static void menu_cb_erase (TextEditor me, EDITOR_ARGS_DIRECT) {
410 	GuiText_remove (my textWidget);
411 }
412 
getSelectedLines(TextEditor me,integer * firstLine,integer * lastLine)413 static bool getSelectedLines (TextEditor me, integer *firstLine, integer *lastLine) {
414 	integer left, right;
415 	autostring32 text = GuiText_getStringAndSelectionPosition (my textWidget, & left, & right);
416 	integer textLength = str32len (text.get());
417 	Melder_assert (left >= 0);
418 	Melder_assert (left <= right);
419 	if (right > textLength)
420 		Melder_fatal (U"The end of the selection is at position ", right,
421 			U", which is beyond the end of the text, which is at position ", textLength, U".");
422 	integer i = 0;
423 	*firstLine = 1;
424 	/*
425 		Cycle through the text in order to see how many linefeeds we pass.
426 	*/
427 	for (; i < left; i ++)
428 		if (text [i] == U'\n')
429 			(*firstLine) ++;
430 	if (left == right)
431 		return false;
432 	*lastLine = *firstLine;
433 	for (; i < right; i ++)
434 		if (text [i] == U'\n')
435 			(*lastLine) ++;
436 	return true;
437 }
438 
439 static autostring32 theFindString, theReplaceString;
do_find(TextEditor me)440 static void do_find (TextEditor me) {
441 	if (! theFindString)
442 		return;   // e.g. when the user does "Find again" before having done any "Find"
443 	integer left, right;
444 	autostring32 text = GuiText_getStringAndSelectionPosition (my textWidget, & left, & right);
445 	char32 *location = str32str (& text [right], theFindString.get());
446 	if (location) {
447 		integer index = location - text.get();
448 		GuiText_setSelection (my textWidget, index, index + str32len (theFindString.get()));
449 		GuiText_scrollToSelection (my textWidget);
450 		#ifdef _WIN32
451 			GuiThing_show (my windowForm);
452 		#endif
453 	} else {
454 		/*
455 			Try from the start of the document.
456 		*/
457 		location = str32str (text.get(), theFindString.get());
458 		if (location) {
459 			integer index = location - text.get();
460 			GuiText_setSelection (my textWidget, index, index + str32len (theFindString.get()));
461 			GuiText_scrollToSelection (my textWidget);
462 			#ifdef _WIN32
463 				GuiThing_show (my windowForm);
464 			#endif
465 		} else {
466 			Melder_beep ();
467 		}
468 	}
469 }
470 
do_replace(TextEditor me)471 static void do_replace (TextEditor me) {
472 	if (! theReplaceString) return;   // e.g. when the user does "Replace again" before having done any "Replace"
473 	autostring32 selection = GuiText_getSelection (my textWidget);
474 	if (! Melder_equ (selection.get(), theFindString.get())) {
475 		do_find (me);
476 		return;
477 	}
478 	integer left, right;
479 	autostring32 text = GuiText_getStringAndSelectionPosition (my textWidget, & left, & right);
480 	GuiText_replace (my textWidget, left, right, theReplaceString.get());
481 	GuiText_setSelection (my textWidget, left, left + str32len (theReplaceString.get()));
482 	GuiText_scrollToSelection (my textWidget);
483 	#ifdef _WIN32
484 		GuiThing_show (my windowForm);
485 	#endif
486 }
487 
menu_cb_find(TextEditor me,EDITOR_ARGS_FORM)488 static void menu_cb_find (TextEditor me, EDITOR_ARGS_FORM) {
489 	EDITOR_FORM (U"Find", nullptr)
490 		TEXTFIELD (findString, U"Find", U"", 5)
491 	EDITOR_OK
492 		if (theFindString) SET_STRING (findString, theFindString.get());
493 	EDITOR_DO
494 		theFindString = Melder_dup (findString);
495 		#ifdef macintosh
496 			/*
497 				Perhaps don't use the system-wide Find pasteboard,
498 				by which other applications can see what you are searching for in Praat's text windows.
499 				Remember ever showing your app in Xcode to somebody,
500 				revealing to your onlooker the name of the person you last looked up in your email?
501 			*/
502 			// NSPasteboard * theFindPasteBoard = [NSPasteboard pasteboardWithName: NSPasteboardNameFind   create: NO];
503 			// [theFindPasteBoard ...]
504 		#endif
505 		do_find (me);
506 	EDITOR_END
507 }
508 
menu_cb_findAgain(TextEditor me,EDITOR_ARGS_DIRECT)509 static void menu_cb_findAgain (TextEditor me, EDITOR_ARGS_DIRECT) {
510 	do_find (me);
511 }
512 
menu_cb_useSelectionForFind(TextEditor me,EDITOR_ARGS_DIRECT)513 static void menu_cb_useSelectionForFind (TextEditor me, EDITOR_ARGS_DIRECT) {
514 	theFindString = GuiText_getSelection (my textWidget);
515 }
516 
menu_cb_replace(TextEditor me,EDITOR_ARGS_FORM)517 static void menu_cb_replace (TextEditor me, EDITOR_ARGS_FORM) {
518 	EDITOR_FORM (U"Find", nullptr)
519 		LABEL (U"This is a \"slow\" find-and-replace method;")
520 		LABEL (U"if the selected text is identical to the Find string,")
521 		LABEL (U"the selected text will be replaced by the Replace string;")
522 		LABEL (U"otherwise, the next occurrence of the Find string will be selected.")
523 		LABEL (U"So you typically need two clicks on Apply to get a text replaced.")
524 		TEXTFIELD (findString, U"Find", U"", 5)
525 		TEXTFIELD (replaceString, U"Replace with", U"", 5)
526 	EDITOR_OK
527 		if (theFindString) SET_STRING (findString, theFindString.get());
528 		if (theReplaceString) SET_STRING (replaceString, theReplaceString.get());
529 	EDITOR_DO
530 		theFindString = Melder_dup (findString);
531 		theReplaceString = Melder_dup (replaceString);
532 		do_replace (me);
533 	EDITOR_END
534 }
535 
menu_cb_replaceAgain(TextEditor me,EDITOR_ARGS_DIRECT)536 static void menu_cb_replaceAgain (TextEditor me, EDITOR_ARGS_DIRECT) {
537 	do_replace (me);
538 }
539 
menu_cb_whereAmI(TextEditor me,EDITOR_ARGS_DIRECT)540 static void menu_cb_whereAmI (TextEditor me, EDITOR_ARGS_DIRECT) {
541 	integer numberOfLinesLeft, numberOfLinesRight;
542 	if (! getSelectedLines (me, & numberOfLinesLeft, & numberOfLinesRight)) {
543 		Melder_information (U"The cursor is on line ", numberOfLinesLeft, U".");
544 	} else if (numberOfLinesLeft == numberOfLinesRight) {
545 		Melder_information (U"The selection is on line ", numberOfLinesLeft, U".");
546 	} else {
547 		Melder_information (U"The selection runs from line ", numberOfLinesLeft, U" to line ", numberOfLinesRight, U".");
548 	}
549 }
550 
menu_cb_goToLine(TextEditor me,EDITOR_ARGS_FORM)551 static void menu_cb_goToLine (TextEditor me, EDITOR_ARGS_FORM) {
552 	EDITOR_FORM (U"Go to line", nullptr)
553 		NATURAL (lineToGo, U"Line", U"1")
554 	EDITOR_OK
555 		integer firstLine, lastLine;
556 		getSelectedLines (me, & firstLine, & lastLine);
557 		SET_INTEGER (lineToGo, firstLine)
558 	EDITOR_DO
559 		autostring32 text = GuiText_getString (my textWidget);
560 		integer currentLine = 1;
561 		integer left = 0, right = 0;
562 		if (lineToGo == 1) {
563 			for (; text [right] != U'\n' && text [right] != U'\0'; right ++) { }
564 		} else {
565 			for (; text [left] != U'\0'; left ++) {
566 				if (text [left] == U'\n') {
567 					currentLine ++;
568 					if (currentLine == lineToGo) {
569 						left ++;
570 						for (right = left; text [right] != U'\n' && text [right] != U'\0'; right ++) { }
571 						break;
572 					}
573 				}
574 			}
575 		}
576 		if (left == str32len (text.get())) {
577 			right = left;
578 		} else if (text [right] == U'\n') {
579 			right ++;
580 		}
581 		GuiText_setSelection (my textWidget, left, right);
582 		GuiText_scrollToSelection (my textWidget);
583 	EDITOR_END
584 }
585 
menu_cb_convertToCString(TextEditor me,EDITOR_ARGS_DIRECT)586 static void menu_cb_convertToCString (TextEditor me, EDITOR_ARGS_DIRECT) {
587 	autostring32 text = GuiText_getString (my textWidget);
588 	char32 buffer [2] = U" ";
589 	const conststring32 hex [16] = { U"0", U"1", U"2", U"3", U"4", U"5", U"6", U"7", U"8", U"9", U"A", U"B", U"C", U"D", U"E", U"F" };
590 	MelderInfo_open ();
591 	MelderInfo_write (U"\"");
592 	for (char32 *p = & text [0]; *p != U'\0'; p ++) {
593 		char32 kar = *p;
594 		if (kar == U'\n') {
595 			MelderInfo_write (U"\\n\"\n\"");
596 		} else if (kar == U'\t') {
597 			MelderInfo_write (U"   ");
598 		} else if (kar == U'\"') {
599 			MelderInfo_write (U"\\\"");
600 		} else if (kar == U'\\') {
601 			MelderInfo_write (U"\\\\");
602 		} else if (kar > 127) {
603 			if (kar <= 0x00FFFF) {
604 				MelderInfo_write (U"\\u", hex [kar >> 12], hex [(kar >> 8) & 0x00'000F], hex [(kar >> 4) & 0x00'000F], hex [kar & 0x00'000F]);
605 			} else {
606 				MelderInfo_write (U"\\U", hex [kar >> 28], hex [(kar >> 24) & 0x00'000F], hex [(kar >> 20) & 0x00'000F], hex [(kar >> 16) & 0x00'000F],
607 					hex [(kar >> 12) & 0x00'000F], hex [(kar >> 8) & 0x00'000F], hex [(kar >> 4) & 0x00'000F], hex [kar & 0x00'000F]);
608 			}
609 		} else {
610 			buffer [0] = *p;
611 			MelderInfo_write (& buffer [0]);
612 		}
613 	}
614 	MelderInfo_write (U"\"");
615 	MelderInfo_close ();
616 }
617 
618 /***** 'Font' menu *****/
619 
updateSizeMenu(TextEditor me)620 static void updateSizeMenu (TextEditor me) {
621 	if (my fontSizeButton_10) GuiMenuItem_check (my fontSizeButton_10, my p_fontSize == 10.0);
622 	if (my fontSizeButton_12) GuiMenuItem_check (my fontSizeButton_12, my p_fontSize == 12.0);
623 	if (my fontSizeButton_14) GuiMenuItem_check (my fontSizeButton_14, my p_fontSize == 14.0);
624 	if (my fontSizeButton_18) GuiMenuItem_check (my fontSizeButton_18, my p_fontSize == 18.0);
625 	if (my fontSizeButton_24) GuiMenuItem_check (my fontSizeButton_24, my p_fontSize == 24.0);
626 }
setFontSize(TextEditor me,double fontSize)627 static void setFontSize (TextEditor me, double fontSize) {
628 	GuiText_setFontSize (my textWidget, fontSize);
629 	my pref_fontSize () = my p_fontSize = fontSize;
630 	updateSizeMenu (me);
631 }
632 
menu_cb_10(TextEditor me,EDITOR_ARGS_DIRECT)633 static void menu_cb_10 (TextEditor me, EDITOR_ARGS_DIRECT) { setFontSize (me, 10.0); }
menu_cb_12(TextEditor me,EDITOR_ARGS_DIRECT)634 static void menu_cb_12 (TextEditor me, EDITOR_ARGS_DIRECT) { setFontSize (me, 12.0); }
menu_cb_14(TextEditor me,EDITOR_ARGS_DIRECT)635 static void menu_cb_14 (TextEditor me, EDITOR_ARGS_DIRECT) { setFontSize (me, 14.0); }
menu_cb_18(TextEditor me,EDITOR_ARGS_DIRECT)636 static void menu_cb_18 (TextEditor me, EDITOR_ARGS_DIRECT) { setFontSize (me, 18.0); }
menu_cb_24(TextEditor me,EDITOR_ARGS_DIRECT)637 static void menu_cb_24 (TextEditor me, EDITOR_ARGS_DIRECT) { setFontSize (me, 24.0); }
menu_cb_fontSize(TextEditor me,EDITOR_ARGS_FORM)638 static void menu_cb_fontSize (TextEditor me, EDITOR_ARGS_FORM) {
639 	EDITOR_FORM (U"Text window: Font size", nullptr)
640 		POSITIVE (fontSize, U"Font size (points)", U"12")
641 	EDITOR_OK
642 		SET_REAL (fontSize, my p_fontSize);
643 	EDITOR_DO
644 		setFontSize (me, fontSize);
645 	EDITOR_END
646 }
647 
gui_text_cb_changed(TextEditor me,GuiTextEvent)648 static void gui_text_cb_changed (TextEditor me, GuiTextEvent /* event */) {
649 	if (! my dirty) {
650 		my dirty = true;
651 		my v_nameChanged ();
652 	}
653 }
654 
v_createChildren()655 void structTextEditor :: v_createChildren () {
656 	textWidget = GuiText_createShown (our windowForm, 0, 0, Machine_getMenuBarBottom (), 0, GuiText_SCROLLED);
657 	GuiText_setChangedCallback (textWidget, gui_text_cb_changed, this);
658 }
659 
v_createMenus()660 void structTextEditor :: v_createMenus () {
661 	TextEditor_Parent :: v_createMenus ();
662 
663 	if (v_fileBased ()) {
664 		Editor_addCommand (this, U"File", U"New", 'N', menu_cb_new);
665 		Editor_addCommand (this, U"File", U"Open...", 'O', menu_cb_open);
666 		Editor_addCommand (this, U"File", U"Reopen from disk", GuiMenu_SHIFT | 'O', menu_cb_reopen);
667 	} else {
668 		Editor_addCommand (this, U"File", U"Clear", 'N', menu_cb_clear);
669 	}
670 	Editor_addCommand (this, U"File", U"-- save --", 0, nullptr);
671 	if (v_fileBased ()) {
672 		Editor_addCommand (this, U"File", U"Save", 'S', menu_cb_save);
673 		Editor_addCommand (this, U"File", U"Save as...", 0, menu_cb_saveAs);
674 	} else {
675 		Editor_addCommand (this, U"File", U"Save as...", 'S', menu_cb_saveAs);
676 	}
677 	Editor_addCommand (this, U"File", U"-- close --", 0, nullptr);
678 	GuiText_setUndoItem (textWidget, Editor_addCommand (this, U"Edit", U"Undo", 'Z', menu_cb_undo));
679 	GuiText_setRedoItem (textWidget, Editor_addCommand (this, U"Edit", U"Redo", 'Y', menu_cb_redo));
680 	Editor_addCommand (this, U"Edit", U"-- cut copy paste --", 0, nullptr);
681 	Editor_addCommand (this, U"Edit", U"Cut", 'X', menu_cb_cut);
682 	Editor_addCommand (this, U"Edit", U"Copy", 'C', menu_cb_copy);
683 	Editor_addCommand (this, U"Edit", U"Paste", 'V', menu_cb_paste);
684 	Editor_addCommand (this, U"Edit", U"Erase", 0, menu_cb_erase);
685 
686 	Editor_addMenu (this, U"Search", 0);
687 	Editor_addCommand (this, U"Search", U"Find...", 'F', menu_cb_find);
688 	Editor_addCommand (this, U"Search", U"Find again", 'G', menu_cb_findAgain);
689 	Editor_addCommand (this, U"Search", U"Replace...", GuiMenu_SHIFT | 'F', menu_cb_replace);
690 	Editor_addCommand (this, U"Search", U"Replace again", GuiMenu_SHIFT | 'G', menu_cb_replaceAgain);
691 	Editor_addCommand (this, U"Search", U"Use selection for find", 'E', menu_cb_useSelectionForFind);
692 	Editor_addCommand (this, U"Search", U"-- line --", 0, nullptr);
693 	Editor_addCommand (this, U"Search", U"Where am I?", 0, menu_cb_whereAmI);
694 	Editor_addCommand (this, U"Search", U"Go to line...", 'L', menu_cb_goToLine);
695 
696 	Editor_addMenu (this, U"Convert", 0);
697 	Editor_addCommand (this, U"Convert", U"Convert to C string", 0, menu_cb_convertToCString);
698 
699 	Editor_addMenu (this, U"Font", 0);
700 	Editor_addCommand (this, U"Font", U"Font size...", 0, menu_cb_fontSize);
701 	fontSizeButton_10 = Editor_addCommand (this, U"Font", U"10", GuiMenu_CHECKBUTTON, menu_cb_10);
702 	fontSizeButton_12 = Editor_addCommand (this, U"Font", U"12", GuiMenu_CHECKBUTTON, menu_cb_12);
703 	fontSizeButton_14 = Editor_addCommand (this, U"Font", U"14", GuiMenu_CHECKBUTTON, menu_cb_14);
704 	fontSizeButton_18 = Editor_addCommand (this, U"Font", U"18", GuiMenu_CHECKBUTTON, menu_cb_18);
705 	fontSizeButton_24 = Editor_addCommand (this, U"Font", U"24", GuiMenu_CHECKBUTTON, menu_cb_24);
706 }
707 
TextEditor_init(TextEditor me,conststring32 initialText)708 void TextEditor_init (TextEditor me, conststring32 initialText) {
709 	Editor_init (me, 0, 0, 600, 400, U"", nullptr);
710 	setFontSize (me, my p_fontSize);
711 	if (initialText) {
712 		GuiText_setString (my textWidget, initialText);
713 		my dirty = false;   // was set to true in valueChanged callback
714 		Thing_setName (me, U"");
715 	}
716 	theReferencesToAllOpenTextEditors. addItem_ref (me);
717 }
718 
TextEditor_create(conststring32 initialText)719 autoTextEditor TextEditor_create (conststring32 initialText) {
720 	try {
721 		autoTextEditor me = Thing_new (TextEditor);
722 		TextEditor_init (me.get(), initialText);
723 		return me;
724 	} catch (MelderError) {
725 		Melder_throw (U"Text window not created.");
726 	}
727 }
728 
TextEditor_showOpen(TextEditor me)729 void TextEditor_showOpen (TextEditor me) {
730 	cb_showOpen (Editor_getMenuCommand (me, U"File", U"Open..."));
731 }
732 
733 /* End of file TextEditor.cpp */
734