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