chiark / gitweb /
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 }