chiark / gitweb /
Merge branch 'arkkra' into shiny
[mup] / mup / mupmate / Edit.C
diff --git a/mup/mupmate/Edit.C b/mup/mupmate/Edit.C
new file mode 100644 (file)
index 0000000..f85e632
--- /dev/null
@@ -0,0 +1,640 @@
+/* Copyright (c) 2006 by Arkkra Enterprises */
+/* All rights reserved */
+
+// Code for Edit menu on main toolbar
+
+
+#include "globals.H"
+#include "Edit.H"
+#include "utils.H"
+#include <FL/Enumerations.H>
+#include <FL/fl_ask.H>
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+
+
+//-------------------Find_dialog class--------------------------------
+// This class is for the window that pops up when user does a "Find"
+
+// Constructor creates the window and all the widgets inside it
+
+Find_dialog::Find_dialog(void)
+       : Fl_Double_Window(430, 170, "Find")
+{
+       pattern_p = new Fl_Input(90, 20, 175, 20, "Find what:");
+       // We have ungray the "Find Next" when user enters a pattern,
+       // so set up callback for that
+       pattern_p->callback(Pattern_cb, this);
+       pattern_p->when(FL_WHEN_CHANGED);
+       pattern_p->tooltip("Enter the pattern you want to\n"
+                       "search for in your Mup input.");
+
+       replace_with_p = new Fl_Input(90, 60, 175, 20, "Replace with");
+       replace_with_p->tooltip("Enter the replacement text.");
+       replace_with_p->hide();
+
+       casematch_p = new Fl_Check_Button(10, 85, 100, 20, "Match case");
+       casematch_p->tooltip("If checked, upper/lower case must match.\n"
+                       "If not checked, case is ignored.");
+
+       direction_p = new Fl_Box(FL_ENGRAVED_BOX, 120, 55, 125, 35, "");
+       up_p = new Fl_Round_Button(130, 60, 40, 20, "Up");
+       up_p->type(FL_RADIO_BUTTON);
+       up_p->value(0);
+       up_p->tooltip("Search upward from current place.");
+       up_p->callback(change_cb, this);
+       down_p = new Fl_Round_Button(175, 60, 60, 20, "Down");
+       down_p->type(FL_RADIO_BUTTON);
+       down_p->value(1);
+       down_p->tooltip("Search downward from current place.");
+       down_p->callback(change_cb, this);
+
+       next_p = new Fl_Return_Button(285, 10, 125, 30, "Find Next");
+       next_p->when(FL_WHEN_RELEASE);
+       next_p->callback(FindNext_cb, this);
+       next_p->deactivate();
+
+       replace_p = new Fl_Button(285, 45, 125, 30, "Replace");
+       replace_p->tooltip("Replace the current instance of the pattern\n"
+                       "with the replacement text.");
+       replace_p->callback(Replace_cb, this);
+       replace_p->when(FL_WHEN_RELEASE);
+       replace_p->deactivate();
+       replace_p->hide();
+
+       replace_all_p = new Fl_Button(285, 80, 125, 30, "Replace All");
+       replace_all_p->tooltip("Replace all instances of the pattern\n"
+                       "with the replacement text.");
+       replace_all_p->callback(ReplaceAll_cb, this);
+       replace_all_p->when(FL_WHEN_RELEASE);
+       replace_all_p->deactivate();
+       replace_all_p->hide();
+
+       cancel_p = new Fl_Button(285, 115, 125, 30, "Cancel");
+       cancel_p->shortcut(FL_Escape);
+       cancel_p->when(FL_WHEN_RELEASE);
+       cancel_p->callback(Cancel_cb, this);
+
+       // Arrange for destructor to clean up new-ed widgets
+       end();
+
+       // Arrange for window manager closes to do Cancel.
+       callback(Cancel_cb, this);
+       when(FL_WHEN_NEVER);
+}
+
+Find_dialog::~Find_dialog()
+{
+}
+
+
+// The class can be used for either Find or Replace.
+// These next two methods set which of those two personalities
+// the window has.
+
+void
+Find_dialog::as_Find()
+{
+       label("Find");
+       cancel_p->resize(cancel_p->x(), 55, cancel_p->w(), cancel_p->h());
+       casematch_p->resize(casematch_p->x(), 65, casematch_p->w(), casematch_p->h());
+       resize(x(), y(), w(), 120);
+       direction_p->show();
+       up_p->show();
+       down_p->show();
+       replace_with_p->hide();
+       replace_p->hide();
+       replace_all_p->hide();
+       is_replace = false;
+}
+
+void
+Find_dialog::as_Replace()
+{
+       label("Replace");
+       cancel_p->resize(cancel_p->x(), 115, cancel_p->w(), cancel_p->h());
+       casematch_p->resize(casematch_p->x(), 115, casematch_p->w(), casematch_p->h());
+       resize(x(), y(), w(), 155);
+       direction_p->hide();
+       up_p->hide();
+       down_p->hide();
+       replace_with_p->show();
+       replace_p->show();
+       replace_all_p->show();
+       is_replace = true;
+}
+
+
+// Callback for when user clicks "Find Next" button after filling in
+// the dialog.
+
+CALL_BACK(Find_dialog, FindNext)
+{
+       int start = editor_p->insert_position();
+       bool found;
+       int where;
+       if (down_p->value() == 1 || is_replace) {
+               found = editor_p->buffer()->search_forward(
+                               start + 1, pattern_p->value(),
+                               &where, casematch_p->value());
+       }
+       else {
+               found = editor_p->buffer()->search_backward(
+                               start - 1, pattern_p->value(),
+                               &where, casematch_p->value());
+       }
+
+       if ( ! found ) {
+               // It seems fltk does not find a pattern if it is exactly
+               // at the beginning for an upward search or exactly
+               // at the end for a downward search. That surely can't
+               // be right, so add special checks for that. If fltk
+               // fixes that some day, we'll never hit this case,
+               // so this should still be compatible.
+               int patlength = pattern_p->size();
+               int bufflength = editor_p->buffer()->length();
+               where = (down_p->value() ? bufflength - patlength : 0);
+               // If pattern is longer than buffer, then no match possible.
+               // If already at pattern,
+               // we already found the end one last time.
+               if (patlength <= bufflength &&
+                               where != editor_p->insert_position() - patlength) {
+                       if (casematch_p->value()) {
+                               if (strncmp(pattern_p->value(),
+                                               editor_p->buffer()->text()
+                                               + where, patlength) == 0) {
+                                       found = 1;
+                               }
+                       }
+                       else {
+                               if (strncasecmp(pattern_p->value(),
+                                               editor_p->buffer()->text()
+                                               + where, patlength) == 0) {
+                                       found = 1;
+                               }
+                       }
+               }
+
+               if ( ! found ) {
+                       fl_alert("Cannot find \"%s\"", pattern_p->value());
+                       gray_out();
+
+                       // The main editor window should now
+                       // be made active, rather than the Find window.
+                       editor_p->take_focus();
+               }
+       }
+       if (found) {
+               editor_p->buffer()->highlight(where, where + pattern_p->size());
+               editor_p->insert_position(where + pattern_p->size());
+               editor_p->show_insert_position();
+       }
+}
+
+
+// Callback for when user clicks "Replace"
+
+CALL_BACK(Find_dialog, Replace)
+{
+       // See if we are already at the pattern to replace due to
+       // a previous Replace/Find Next
+       int start, end, isRect, rectStart, rectEnd;
+       bool at_pattern = false;
+       if (editor_p->buffer()->highlight_position(&start, &end, &isRect,
+                                               &rectStart, &rectEnd)) {
+               int place = editor_p->insert_position();
+               if (place == end && (end - start == pattern_p->size())) {
+                       if (casematch_p->value()) {
+                       
+                               at_pattern = (strncmp(pattern_p->value(),
+                                       editor_p->buffer()->text() + start,
+                                       pattern_p->size()) == 0);
+                       }
+                       else {
+                               at_pattern = (strncasecmp(pattern_p->value(),
+                                       editor_p->buffer()->text() + start,
+                                       pattern_p->size()) == 0);
+                       }
+               }
+       }
+
+       if (at_pattern) {
+               editor_p->buffer()->unhighlight();
+               editor_p->buffer()->replace(start, end, replace_with_p->value());
+       }
+
+       FindNext();
+}
+
+
+// Callback for when use clicks "Replace All"
+
+CALL_BACK(Find_dialog, ReplaceAll)
+{
+       // We want to be able to "undo" the entire "Replace All"
+       // so we make a copy of the buffer, make all the changes in the
+       // copy and then replace the original with the altered copy.
+       Fl_Text_Buffer altered_buff;
+       altered_buff.copy(editor_p->buffer(), 0, editor_p->buffer()->length(), 0);
+       int start;      // where to begin each search
+       bool found = true;      // if matching pattern found on current search
+       int where;      // offset into buffer where match occurred
+       int new_cursor_pos;
+       bool replaced_something = false;        // if any matches found at all
+
+       new_cursor_pos = editor_p->insert_position();
+       for (start = 0; found; start = where + replace_with_p->size()) {
+               if ((found = altered_buff.search_forward(
+                                               start, pattern_p->value(),
+                                               &where, casematch_p->value()))
+                                               != 0) {
+                       altered_buff.replace(where, where + pattern_p->size(),
+                                               replace_with_p->value());
+                       new_cursor_pos = where + replace_with_p->size();
+                       replaced_something = true;
+               }
+       }
+       // Kludge because pattern at very end is not found.
+       // See more complete explanation in FindNext().
+       where = altered_buff.length() - pattern_p->size();
+       if (where >= 0) {
+               found = false;
+               if (casematch_p->value()) {
+                       if (strcmp(pattern_p->value(),
+                                       altered_buff.text() + where) == 0) {
+                               found = true;
+                       }
+               }       
+               else {
+                       if (strcasecmp(pattern_p->value(),
+                                       altered_buff.text() + where) == 0) {
+                               found = true;
+                       }
+               }
+               if (found) {
+                       altered_buff.replace(where, where + pattern_p->size(),
+                                               replace_with_p->value());
+                       new_cursor_pos = where + replace_with_p->size();
+                       replaced_something = true;
+               }
+       }
+
+       if (replaced_something) {
+               editor_p->buffer()->replace(0, editor_p->buffer()->length(),
+                                               altered_buff.text());
+               editor_p->insert_position(new_cursor_pos);
+       }
+       else {
+               fl_alert("No instances of pattern to replace.");
+       }
+       replace_all_p->deactivate();
+}
+
+
+// Callback for when user clicks "Cancel" in the "Find" window.
+// Hides the window.
+
+CALL_BACK(Find_dialog, Cancel)
+{
+       editor_p->buffer()->unhighlight();
+       hide();
+}
+
+
+// If user did Find Next until no more instances found,
+// the Find Next and Replace buttons will get grayed out,
+// But if they then change search direction, we need to reactivate them
+// since the pattern might be found in that direction.
+// They must also be ungrayed if the contents of the editor buffer change,
+// since the new text might contain the pattern.
+
+CALL_BACK(Find_dialog, change)
+{
+       next_p->activate();
+       replace_p->activate();
+       replace_all_p->activate();
+}
+
+
+// Callback for when user changes pattern, to know whether to gray or ungray
+// Find Next button or not.
+
+CALL_BACK(Find_dialog, Pattern)
+{
+       if (pattern_p->size() > 0) {
+               next_p->activate();
+               replace_p->activate();
+               replace_all_p->activate();
+       }
+       else {
+               gray_out();
+       }
+}
+
+// Horrible kludge. If the "Find" or "Replace" is grayed out because there
+// are no more instances of the pattern in the current direction,
+// and then user moves the cursor somewhere else, it's possible the pattern
+// may then be findable. But modify_callback does not get called for
+// a change in cursor position. So we poll to see if the cursor position
+// changed since the last check, and if so, ungray.
+// Fortunately, we can limit this to only when the button are grayed,
+// which shouldn't be too often.
+
+void
+Find_dialog::gray_out(void)
+{
+       next_p->deactivate();
+       replace_p->deactivate();
+       replace_all_p->deactivate();
+       // Remember where we are and set up to poll for changes
+       last_cursor_position = editor_p->insert_position();
+       Fl::add_timeout(0.5, cursor_change_check, this);
+}
+
+void
+Find_dialog::cursor_change_check(void * data)
+{
+       Find_dialog * obj_p = (Find_dialog *) data;
+       if (obj_p->editor_p->insert_position() != obj_p->last_cursor_position
+                               && obj_p->pattern_p->size() > 0) {
+               obj_p->change();
+       }
+       else {
+               Fl::repeat_timeout(0.5, cursor_change_check, data);
+       }
+}
+
+
+// Class needs access to the editor; this lets it know which editor
+// instance to use, and which main window it is associated with.
+
+void
+Find_dialog::set_editor(Fl_Text_Editor * ed)
+{
+       editor_p = ed;
+}
+
+
+// Returns the current "Find" pattern entered by user,
+// or "" if they have not yet entered any such pattern
+
+const char *
+Find_dialog::get_pattern()
+{
+       if (pattern_p->value() == 0) {
+               return("");
+       }
+       else {
+               return(pattern_p->value());
+       }
+}
+
+
+//---------------- GoTo class---------------------------------------------
+
+GoTo_dialog::GoTo_dialog(void)
+       : Fl_Double_Window(225, 95, "Goto line")
+{
+       linenum_p = new Positive_Int_Input(115, 10, 60, 30, "Line Number:");
+       linenum_p->tooltip("Enter the line number of the line\n"
+                       "you want to make the current line.");
+
+       ok_p = new Fl_Return_Button(25, 50, 75, 30, "OK");
+       ok_p->when(FL_WHEN_RELEASE);
+       ok_p->callback(OK_cb, this);
+
+       cancel_p = new Fl_Button(125, 50, 75, 30, "Cancel");
+       cancel_p->shortcut(FL_Escape);
+       cancel_p->when(FL_WHEN_RELEASE);
+       cancel_p->callback(Cancel_cb, this);
+
+       // Arrange for destructor to clean up new-ed widgets
+       end();
+
+       // Arrange for window manager closes to do Cancel.
+       callback(Cancel_cb, this);
+       when(FL_WHEN_NEVER);
+}
+
+GoTo_dialog::~GoTo_dialog()
+{
+}
+
+
+// Callback for when user clicks "OK" in GoTo dialog
+
+CALL_BACK(GoTo_dialog, OK)
+{
+       // Find end of valid range
+       int last_line = editor_p->buffer()->count_lines(0,
+                       editor_p->buffer()->length());
+       // FLTK line numbers start at 0, so we have to subtract 1 from
+       // number supplied by user.
+       int desired_line = (int) strtol(linenum_p->value(), 0, 0) - 1;
+       if (desired_line < 0 || desired_line > last_line) {
+               fl_alert("Line number out of range");
+               return;
+       }
+
+       // Find appropriate new cursor position and move cursor there
+       int newposition = editor_p->buffer()->skip_lines(0, desired_line);
+       editor_p->insert_position(newposition);
+       editor_p->show_insert_position();
+       hide();
+}
+
+
+// Callback if user cancel Go To
+
+CALL_BACK(GoTo_dialog, Cancel)
+{
+       hide();
+}
+
+
+// Code that calls constructor should then call this to tell
+// us which editor instance to act upon.
+
+void
+GoTo_dialog::set_editor(Fl_Text_Editor * ed)
+{
+       editor_p = ed;
+}
+
+
+// Initialize contents on GoTo field to the current line number
+
+void
+GoTo_dialog::set_current_line()
+{
+       char num_as_string[16];
+       // fltk numbers lines from 0, so add 1 to get what user expects
+       (void) sprintf(num_as_string, "%d",
+               editor_p->buffer()->count_lines(0, editor_p->insert_position()) + 1);
+       linenum_p->value(num_as_string);
+}
+
+
+//------------------Edit class-----------------------------------------------
+// Implements the items in the Edit menu on the main toolbar
+
+
+Edit::Edit()
+{
+       find_p = 0;
+       goto_p = 0;
+       wrote_to_clipboard = false;
+}
+
+
+Edit::~Edit()
+{
+       if (find_p != 0) {
+               delete find_p;
+               find_p = 0;
+       }
+       if (goto_p != 0) {
+               delete goto_p;
+               goto_p = 0;
+       }
+}
+
+
+//---Undo menu item---------------
+
+CALL_BACK(Edit, Undo)
+{
+       buffer_p->undo();
+}
+
+
+//---Cut menu item---------------
+
+CALL_BACK(Edit, Cut)
+{
+       Fl_Text_Editor::kf_cut('x', editor_p);
+       set_can_paste();
+}
+
+
+//---Copy menu item---------------
+
+CALL_BACK(Edit, Copy)
+{
+       Fl_Text_Editor::kf_copy('c', editor_p);
+       set_can_paste();
+}
+
+
+//---Paste menu item---------------
+
+CALL_BACK(Edit, Paste)
+{
+       Fl_Text_Editor::kf_paste('v', editor_p);
+}
+
+
+//---Delete menu item---------------
+
+CALL_BACK(Edit, Delete)
+{
+       buffer_p->remove_selection();
+       buffer_p->unselect();
+}
+
+
+//---Find menu item---------------
+
+CALL_BACK(Edit, Find)
+{
+       if (find_p == 0) {
+               // First time, create widget
+               find_p = new Find_dialog();
+               find_p->set_editor(editor_p);
+       }
+       find_p->as_Find();
+       find_p->show();
+}
+
+
+//---Find Next menu item---------------
+
+CALL_BACK(Edit, FindNext)
+{
+       if (find_p == 0 || strlen(find_p->get_pattern()) == 0) {
+               // No pattern specified yet; turn into Find
+               Find();
+               return;
+       }
+       find_p->FindNext();
+}
+
+
+//---Replace menu item---------------
+
+CALL_BACK(Edit, Replace)
+{
+       if (find_p == 0) {
+               // First time, create widget
+               find_p = new Find_dialog();
+               find_p->set_editor(editor_p);
+       }
+       find_p->as_Replace();
+       find_p->show();
+}
+
+
+//---Go To menu item---------------
+
+CALL_BACK(Edit, GoTo)
+{
+       if (goto_p == 0) {
+               // First time, create widget
+               goto_p = new GoTo_dialog();
+               goto_p->set_editor(editor_p);
+       }
+       goto_p->set_current_line();
+       goto_p->show();
+}
+
+
+//---Select All menu item---------------
+
+CALL_BACK(Edit, SelectAll)
+{
+       buffer_p->select(0, buffer_p->length());
+}
+
+
+//---- Callback for when editor window is modified.
+// Grayed out find/replace should be ungrayed, because the modified
+// text might now match.
+
+void
+Edit::modify_cb(int, int, int, int, const char *, void * data)
+{
+       if ( ((Edit *)data)->find_p != 0) {
+               ((Edit *)data)->find_p->change();
+       }
+}
+
+
+// Class needs access to the editor; this lets it know which editor
+// instance to use.
+
+void
+Edit::set_editor(Fl_Text_Editor * ed)
+{
+       editor_p = ed;
+       buffer_p = editor_p->buffer();
+}
+
+// Ungray Paste button
+
+void
+Edit::set_can_paste(void)
+{
+       wrote_to_clipboard = true;
+       editor_p->buffer()->call_modify_callbacks();
+}