Import upstream version 5.3.
[mup] / mup / mupmate / Edit.C
1 /* Copyright (c) 2006 by Arkkra Enterprises */
2 /* All rights reserved */
3
4 // Code for Edit menu on main toolbar
5
6
7 #include "globals.H"
8 #include "Edit.H"
9 #include "utils.H"
10 #include <FL/Enumerations.H>
11 #include <FL/fl_ask.H>
12 #include <stdlib.h>
13 #include <string.h>
14 #include <stdio.h>
15
16
17 //-------------------Find_dialog class--------------------------------
18 // This class is for the window that pops up when user does a "Find"
19
20 // Constructor creates the window and all the widgets inside it
21
22 Find_dialog::Find_dialog(void)
23 : Fl_Double_Window(430, 170, "Find")
24 {
25 pattern_p = new Fl_Input(90, 20, 175, 20, "Find what:");
26 // We have ungray the "Find Next" when user enters a pattern,
27 // so set up callback for that
28 pattern_p->callback(Pattern_cb, this);
29 pattern_p->when(FL_WHEN_CHANGED);
30 pattern_p->tooltip("Enter the pattern you want to\n"
31 "search for in your Mup input.");
32
33 replace_with_p = new Fl_Input(90, 60, 175, 20, "Replace with");
34 replace_with_p->tooltip("Enter the replacement text.");
35 replace_with_p->hide();
36
37 casematch_p = new Fl_Check_Button(10, 85, 100, 20, "Match case");
38 casematch_p->tooltip("If checked, upper/lower case must match.\n"
39 "If not checked, case is ignored.");
40
41 direction_p = new Fl_Box(FL_ENGRAVED_BOX, 120, 55, 125, 35, "");
42 up_p = new Fl_Round_Button(130, 60, 40, 20, "Up");
43 up_p->type(FL_RADIO_BUTTON);
44 up_p->value(0);
45 up_p->tooltip("Search upward from current place.");
46 up_p->callback(change_cb, this);
47 down_p = new Fl_Round_Button(175, 60, 60, 20, "Down");
48 down_p->type(FL_RADIO_BUTTON);
49 down_p->value(1);
50 down_p->tooltip("Search downward from current place.");
51 down_p->callback(change_cb, this);
52
53 next_p = new Fl_Return_Button(285, 10, 125, 30, "Find Next");
54 next_p->when(FL_WHEN_RELEASE);
55 next_p->callback(FindNext_cb, this);
56 next_p->deactivate();
57
58 replace_p = new Fl_Button(285, 45, 125, 30, "Replace");
59 replace_p->tooltip("Replace the current instance of the pattern\n"
60 "with the replacement text.");
61 replace_p->callback(Replace_cb, this);
62 replace_p->when(FL_WHEN_RELEASE);
63 replace_p->deactivate();
64 replace_p->hide();
65
66 replace_all_p = new Fl_Button(285, 80, 125, 30, "Replace All");
67 replace_all_p->tooltip("Replace all instances of the pattern\n"
68 "with the replacement text.");
69 replace_all_p->callback(ReplaceAll_cb, this);
70 replace_all_p->when(FL_WHEN_RELEASE);
71 replace_all_p->deactivate();
72 replace_all_p->hide();
73
74 cancel_p = new Fl_Button(285, 115, 125, 30, "Cancel");
75 cancel_p->shortcut(FL_Escape);
76 cancel_p->when(FL_WHEN_RELEASE);
77 cancel_p->callback(Cancel_cb, this);
78
79 // Arrange for destructor to clean up new-ed widgets
80 end();
81
82 // Arrange for window manager closes to do Cancel.
83 callback(Cancel_cb, this);
84 when(FL_WHEN_NEVER);
85 }
86
87 Find_dialog::~Find_dialog()
88 {
89 }
90
91
92 // The class can be used for either Find or Replace.
93 // These next two methods set which of those two personalities
94 // the window has.
95
96 void
97 Find_dialog::as_Find()
98 {
99 label("Find");
100 cancel_p->resize(cancel_p->x(), 55, cancel_p->w(), cancel_p->h());
101 casematch_p->resize(casematch_p->x(), 65, casematch_p->w(), casematch_p->h());
102 resize(x(), y(), w(), 120);
103 direction_p->show();
104 up_p->show();
105 down_p->show();
106 replace_with_p->hide();
107 replace_p->hide();
108 replace_all_p->hide();
109 is_replace = false;
110 }
111
112 void
113 Find_dialog::as_Replace()
114 {
115 label("Replace");
116 cancel_p->resize(cancel_p->x(), 115, cancel_p->w(), cancel_p->h());
117 casematch_p->resize(casematch_p->x(), 115, casematch_p->w(), casematch_p->h());
118 resize(x(), y(), w(), 155);
119 direction_p->hide();
120 up_p->hide();
121 down_p->hide();
122 replace_with_p->show();
123 replace_p->show();
124 replace_all_p->show();
125 is_replace = true;
126 }
127
128
129 // Callback for when user clicks "Find Next" button after filling in
130 // the dialog.
131
132 CALL_BACK(Find_dialog, FindNext)
133 {
134 int start = editor_p->insert_position();
135 bool found;
136 int where;
137 if (down_p->value() == 1 || is_replace) {
138 found = editor_p->buffer()->search_forward(
139 start + 1, pattern_p->value(),
140 &where, casematch_p->value());
141 }
142 else {
143 found = editor_p->buffer()->search_backward(
144 start - 1, pattern_p->value(),
145 &where, casematch_p->value());
146 }
147
148 if ( ! found ) {
149 // It seems fltk does not find a pattern if it is exactly
150 // at the beginning for an upward search or exactly
151 // at the end for a downward search. That surely can't
152 // be right, so add special checks for that. If fltk
153 // fixes that some day, we'll never hit this case,
154 // so this should still be compatible.
155 int patlength = pattern_p->size();
156 int bufflength = editor_p->buffer()->length();
157 where = (down_p->value() ? bufflength - patlength : 0);
158 // If pattern is longer than buffer, then no match possible.
159 // If already at pattern,
160 // we already found the end one last time.
161 if (patlength <= bufflength &&
162 where != editor_p->insert_position() - patlength) {
163 if (casematch_p->value()) {
164 if (strncmp(pattern_p->value(),
165 editor_p->buffer()->text()
166 + where, patlength) == 0) {
167 found = 1;
168 }
169 }
170 else {
171 if (strncasecmp(pattern_p->value(),
172 editor_p->buffer()->text()
173 + where, patlength) == 0) {
174 found = 1;
175 }
176 }
177 }
178
179 if ( ! found ) {
180 fl_alert("Cannot find \"%s\"", pattern_p->value());
181 gray_out();
182
183 // The main editor window should now
184 // be made active, rather than the Find window.
185 editor_p->take_focus();
186 }
187 }
188 if (found) {
189 editor_p->buffer()->highlight(where, where + pattern_p->size());
190 editor_p->insert_position(where + pattern_p->size());
191 editor_p->show_insert_position();
192 }
193 }
194
195
196 // Callback for when user clicks "Replace"
197
198 CALL_BACK(Find_dialog, Replace)
199 {
200 // See if we are already at the pattern to replace due to
201 // a previous Replace/Find Next
202 int start, end, isRect, rectStart, rectEnd;
203 bool at_pattern = false;
204 if (editor_p->buffer()->highlight_position(&start, &end, &isRect,
205 &rectStart, &rectEnd)) {
206 int place = editor_p->insert_position();
207 if (place == end && (end - start == pattern_p->size())) {
208 if (casematch_p->value()) {
209
210 at_pattern = (strncmp(pattern_p->value(),
211 editor_p->buffer()->text() + start,
212 pattern_p->size()) == 0);
213 }
214 else {
215 at_pattern = (strncasecmp(pattern_p->value(),
216 editor_p->buffer()->text() + start,
217 pattern_p->size()) == 0);
218 }
219 }
220 }
221
222 if (at_pattern) {
223 editor_p->buffer()->unhighlight();
224 editor_p->buffer()->replace(start, end, replace_with_p->value());
225 }
226
227 FindNext();
228 }
229
230
231 // Callback for when use clicks "Replace All"
232
233 CALL_BACK(Find_dialog, ReplaceAll)
234 {
235 // We want to be able to "undo" the entire "Replace All"
236 // so we make a copy of the buffer, make all the changes in the
237 // copy and then replace the original with the altered copy.
238 Fl_Text_Buffer altered_buff;
239 altered_buff.copy(editor_p->buffer(), 0, editor_p->buffer()->length(), 0);
240 int start; // where to begin each search
241 bool found = true; // if matching pattern found on current search
242 int where; // offset into buffer where match occurred
243 int new_cursor_pos;
244 bool replaced_something = false; // if any matches found at all
245
246 new_cursor_pos = editor_p->insert_position();
247 for (start = 0; found; start = where + replace_with_p->size()) {
248 if ((found = altered_buff.search_forward(
249 start, pattern_p->value(),
250 &where, casematch_p->value()))
251 != 0) {
252 altered_buff.replace(where, where + pattern_p->size(),
253 replace_with_p->value());
254 new_cursor_pos = where + replace_with_p->size();
255 replaced_something = true;
256 }
257 }
258 // Kludge because pattern at very end is not found.
259 // See more complete explanation in FindNext().
260 where = altered_buff.length() - pattern_p->size();
261 if (where >= 0) {
262 found = false;
263 if (casematch_p->value()) {
264 if (strcmp(pattern_p->value(),
265 altered_buff.text() + where) == 0) {
266 found = true;
267 }
268 }
269 else {
270 if (strcasecmp(pattern_p->value(),
271 altered_buff.text() + where) == 0) {
272 found = true;
273 }
274 }
275 if (found) {
276 altered_buff.replace(where, where + pattern_p->size(),
277 replace_with_p->value());
278 new_cursor_pos = where + replace_with_p->size();
279 replaced_something = true;
280 }
281 }
282
283 if (replaced_something) {
284 editor_p->buffer()->replace(0, editor_p->buffer()->length(),
285 altered_buff.text());
286 editor_p->insert_position(new_cursor_pos);
287 }
288 else {
289 fl_alert("No instances of pattern to replace.");
290 }
291 replace_all_p->deactivate();
292 }
293
294
295 // Callback for when user clicks "Cancel" in the "Find" window.
296 // Hides the window.
297
298 CALL_BACK(Find_dialog, Cancel)
299 {
300 editor_p->buffer()->unhighlight();
301 hide();
302 }
303
304
305 // If user did Find Next until no more instances found,
306 // the Find Next and Replace buttons will get grayed out,
307 // But if they then change search direction, we need to reactivate them
308 // since the pattern might be found in that direction.
309 // They must also be ungrayed if the contents of the editor buffer change,
310 // since the new text might contain the pattern.
311
312 CALL_BACK(Find_dialog, change)
313 {
314 next_p->activate();
315 replace_p->activate();
316 replace_all_p->activate();
317 }
318
319
320 // Callback for when user changes pattern, to know whether to gray or ungray
321 // Find Next button or not.
322
323 CALL_BACK(Find_dialog, Pattern)
324 {
325 if (pattern_p->size() > 0) {
326 next_p->activate();
327 replace_p->activate();
328 replace_all_p->activate();
329 }
330 else {
331 gray_out();
332 }
333 }
334
335 // Horrible kludge. If the "Find" or "Replace" is grayed out because there
336 // are no more instances of the pattern in the current direction,
337 // and then user moves the cursor somewhere else, it's possible the pattern
338 // may then be findable. But modify_callback does not get called for
339 // a change in cursor position. So we poll to see if the cursor position
340 // changed since the last check, and if so, ungray.
341 // Fortunately, we can limit this to only when the button are grayed,
342 // which shouldn't be too often.
343
344 void
345 Find_dialog::gray_out(void)
346 {
347 next_p->deactivate();
348 replace_p->deactivate();
349 replace_all_p->deactivate();
350 // Remember where we are and set up to poll for changes
351 last_cursor_position = editor_p->insert_position();
352 Fl::add_timeout(0.5, cursor_change_check, this);
353 }
354
355 void
356 Find_dialog::cursor_change_check(void * data)
357 {
358 Find_dialog * obj_p = (Find_dialog *) data;
359 if (obj_p->editor_p->insert_position() != obj_p->last_cursor_position
360 && obj_p->pattern_p->size() > 0) {
361 obj_p->change();
362 }
363 else {
364 Fl::repeat_timeout(0.5, cursor_change_check, data);
365 }
366 }
367
368
369 // Class needs access to the editor; this lets it know which editor
370 // instance to use, and which main window it is associated with.
371
372 void
373 Find_dialog::set_editor(Fl_Text_Editor * ed)
374 {
375 editor_p = ed;
376 }
377
378
379 // Returns the current "Find" pattern entered by user,
380 // or "" if they have not yet entered any such pattern
381
382 const char *
383 Find_dialog::get_pattern()
384 {
385 if (pattern_p->value() == 0) {
386 return("");
387 }
388 else {
389 return(pattern_p->value());
390 }
391 }
392
393
394 //---------------- GoTo class---------------------------------------------
395
396 GoTo_dialog::GoTo_dialog(void)
397 : Fl_Double_Window(225, 95, "Goto line")
398 {
399 linenum_p = new Positive_Int_Input(115, 10, 60, 30, "Line Number:");
400 linenum_p->tooltip("Enter the line number of the line\n"
401 "you want to make the current line.");
402
403 ok_p = new Fl_Return_Button(25, 50, 75, 30, "OK");
404 ok_p->when(FL_WHEN_RELEASE);
405 ok_p->callback(OK_cb, this);
406
407 cancel_p = new Fl_Button(125, 50, 75, 30, "Cancel");
408 cancel_p->shortcut(FL_Escape);
409 cancel_p->when(FL_WHEN_RELEASE);
410 cancel_p->callback(Cancel_cb, this);
411
412 // Arrange for destructor to clean up new-ed widgets
413 end();
414
415 // Arrange for window manager closes to do Cancel.
416 callback(Cancel_cb, this);
417 when(FL_WHEN_NEVER);
418 }
419
420 GoTo_dialog::~GoTo_dialog()
421 {
422 }
423
424
425 // Callback for when user clicks "OK" in GoTo dialog
426
427 CALL_BACK(GoTo_dialog, OK)
428 {
429 // Find end of valid range
430 int last_line = editor_p->buffer()->count_lines(0,
431 editor_p->buffer()->length());
432 // FLTK line numbers start at 0, so we have to subtract 1 from
433 // number supplied by user.
434 int desired_line = (int) strtol(linenum_p->value(), 0, 0) - 1;
435 if (desired_line < 0 || desired_line > last_line) {
436 fl_alert("Line number out of range");
437 return;
438 }
439
440 // Find appropriate new cursor position and move cursor there
441 int newposition = editor_p->buffer()->skip_lines(0, desired_line);
442 editor_p->insert_position(newposition);
443 editor_p->show_insert_position();
444 hide();
445 }
446
447
448 // Callback if user cancel Go To
449
450 CALL_BACK(GoTo_dialog, Cancel)
451 {
452 hide();
453 }
454
455
456 // Code that calls constructor should then call this to tell
457 // us which editor instance to act upon.
458
459 void
460 GoTo_dialog::set_editor(Fl_Text_Editor * ed)
461 {
462 editor_p = ed;
463 }
464
465
466 // Initialize contents on GoTo field to the current line number
467
468 void
469 GoTo_dialog::set_current_line()
470 {
471 char num_as_string[16];
472 // fltk numbers lines from 0, so add 1 to get what user expects
473 (void) sprintf(num_as_string, "%d",
474 editor_p->buffer()->count_lines(0, editor_p->insert_position()) + 1);
475 linenum_p->value(num_as_string);
476 }
477
478
479 //------------------Edit class-----------------------------------------------
480 // Implements the items in the Edit menu on the main toolbar
481
482
483 Edit::Edit()
484 {
485 find_p = 0;
486 goto_p = 0;
487 wrote_to_clipboard = false;
488 }
489
490
491 Edit::~Edit()
492 {
493 if (find_p != 0) {
494 delete find_p;
495 find_p = 0;
496 }
497 if (goto_p != 0) {
498 delete goto_p;
499 goto_p = 0;
500 }
501 }
502
503
504 //---Undo menu item---------------
505
506 CALL_BACK(Edit, Undo)
507 {
508 buffer_p->undo();
509 }
510
511
512 //---Cut menu item---------------
513
514 CALL_BACK(Edit, Cut)
515 {
516 Fl_Text_Editor::kf_cut('x', editor_p);
517 set_can_paste();
518 }
519
520
521 //---Copy menu item---------------
522
523 CALL_BACK(Edit, Copy)
524 {
525 Fl_Text_Editor::kf_copy('c', editor_p);
526 set_can_paste();
527 }
528
529
530 //---Paste menu item---------------
531
532 CALL_BACK(Edit, Paste)
533 {
534 Fl_Text_Editor::kf_paste('v', editor_p);
535 }
536
537
538 //---Delete menu item---------------
539
540 CALL_BACK(Edit, Delete)
541 {
542 buffer_p->remove_selection();
543 buffer_p->unselect();
544 }
545
546
547 //---Find menu item---------------
548
549 CALL_BACK(Edit, Find)
550 {
551 if (find_p == 0) {
552 // First time, create widget
553 find_p = new Find_dialog();
554 find_p->set_editor(editor_p);
555 }
556 find_p->as_Find();
557 find_p->show();
558 }
559
560
561 //---Find Next menu item---------------
562
563 CALL_BACK(Edit, FindNext)
564 {
565 if (find_p == 0 || strlen(find_p->get_pattern()) == 0) {
566 // No pattern specified yet; turn into Find
567 Find();
568 return;
569 }
570 find_p->FindNext();
571 }
572
573
574 //---Replace menu item---------------
575
576 CALL_BACK(Edit, Replace)
577 {
578 if (find_p == 0) {
579 // First time, create widget
580 find_p = new Find_dialog();
581 find_p->set_editor(editor_p);
582 }
583 find_p->as_Replace();
584 find_p->show();
585 }
586
587
588 //---Go To menu item---------------
589
590 CALL_BACK(Edit, GoTo)
591 {
592 if (goto_p == 0) {
593 // First time, create widget
594 goto_p = new GoTo_dialog();
595 goto_p->set_editor(editor_p);
596 }
597 goto_p->set_current_line();
598 goto_p->show();
599 }
600
601
602 //---Select All menu item---------------
603
604 CALL_BACK(Edit, SelectAll)
605 {
606 buffer_p->select(0, buffer_p->length());
607 }
608
609
610 //---- Callback for when editor window is modified.
611 // Grayed out find/replace should be ungrayed, because the modified
612 // text might now match.
613
614 void
615 Edit::modify_cb(int, int, int, int, const char *, void * data)
616 {
617 if ( ((Edit *)data)->find_p != 0) {
618 ((Edit *)data)->find_p->change();
619 }
620 }
621
622
623 // Class needs access to the editor; this lets it know which editor
624 // instance to use.
625
626 void
627 Edit::set_editor(Fl_Text_Editor * ed)
628 {
629 editor_p = ed;
630 buffer_p = editor_p->buffer();
631 }
632
633 // Ungray Paste button
634
635 void
636 Edit::set_can_paste(void)
637 {
638 wrote_to_clipboard = true;
639 editor_p->buffer()->call_modify_callbacks();
640 }