4
<H1 ALIGN="RIGHT"><A NAME="editor">4 - Designing a Simple Text Editor</A></H1>
6
<P>This chapter takes you through the design of a simple
7
FLTK-based text editor.
9
<H2>Determining the Goals of the Text Editor</H2>
11
<P>Since this will be the first big project you'll be doing with FLTK,
12
lets define what we want our text editor to do:
16
<LI>Provide a menubar/menus for all functions.</LI>
17
<LI>Edit a single text file, possibly with multiple views.</LI>
18
<LI>Load from a file.</LI>
19
<LI>Save to a file.</LI>
20
<LI>Cut/copy/delete/paste functions.</LI>
21
<LI>Search and replace functions.</LI>
22
<LI>Keep track of when the file has been changed.</LI>
28
<H2>Designing the Main Window</H2>
30
<P>Now that we've outlined the goals for our editor, we can begin with
31
the design of our GUI. Obviously the first thing that we need is a
32
window, which we'll place inside a class called <TT>EditorWindow</TT>:
35
class EditorWindow : public Fl_Double_Window {
37
EditorWindow(int w, int h, const char* t);
40
Fl_Window *replace_dlg;
41
Fl_Input *replace_find;
42
Fl_Input *replace_with;
43
Fl_Button *replace_all;
44
Fl_Return_Button *replace_next;
45
Fl_Button *replace_cancel;
47
Fl_Text_Editor *editor;
54
<P>Our text editor will need some global variables to keep track of
59
char filename[256] = "";
60
Fl_Text_Buffer *textbuf;
63
<P>The <TT>textbuf</TT> variable is the text editor buffer for
64
our window class described previously. We'll cover the other
65
variables as we build the application.</P>
67
<H2>Menubars and Menus</H2>
69
<P>The first goal requires us to use a menubar and menus that
70
define each function the editor needs to perform. The <A
71
href="Fl_Menu_Item.html"><TT>Fl_Menu_Item</TT></A> structure is
72
used to define the menus and items in a menubar:</P>
75
Fl_Menu_Item menuitems[] = {
76
{ "&File", 0, 0, 0, FL_SUBMENU },
77
{ "&New File", 0, (Fl_Callback *)new_cb },
78
{ "&Open File...", FL_CTRL + 'o', (Fl_Callback *)open_cb },
79
{ "&Insert File...", FL_CTRL + 'i', (Fl_Callback *)insert_cb, 0, FL_MENU_DIVIDER },
80
{ "&Save File", FL_CTRL + 's', (Fl_Callback *)save_cb },
81
{ "Save File &As...", FL_CTRL + FL_SHIFT + 's', (Fl_Callback *)saveas_cb, 0, FL_MENU_DIVIDER },
82
{ "New &View", FL_ALT + 'v', (Fl_Callback *)view_cb, 0 },
83
{ "&Close View", FL_CTRL + 'w', (Fl_Callback *)close_cb, 0, FL_MENU_DIVIDER },
84
{ "E&xit", FL_CTRL + 'q', (Fl_Callback *)quit_cb, 0 },
87
{ "&Edit", 0, 0, 0, FL_SUBMENU },
88
{ "&Undo", FL_CTRL + 'z', (Fl_Callback *)undo_cb, 0, FL_MENU_DIVIDER },
89
{ "Cu&t", FL_CTRL + 'x', (Fl_Callback *)cut_cb },
90
{ "&Copy", FL_CTRL + 'c', (Fl_Callback *)copy_cb },
91
{ "&Paste", FL_CTRL + 'v', (Fl_Callback *)paste_cb },
92
{ "&Delete", 0, (Fl_Callback *)delete_cb },
95
{ "&Search", 0, 0, 0, FL_SUBMENU },
96
{ "&Find...", FL_CTRL + 'f', (Fl_Callback *)find_cb },
97
{ "F&ind Again", FL_CTRL + 'g', find2_cb },
98
{ "&Replace...", FL_CTRL + 'r', replace_cb },
99
{ "Re&place Again", FL_CTRL + 't', replace2_cb },
106
<P>Once we have the menus defined we can create the
107
<TT>Fl_Menu_Bar</TT> widget and assign the menus to it with:</P>
110
Fl_Menu_Bar *m = new Fl_Menu_Bar(0, 0, 640, 30);
111
m->copy(menuitems);
114
<P>We'll define the callback functions later.
116
<H2>Editing the Text</H2>
118
<P>To keep things simple our text editor will use the
119
<A HREF="Fl_Text_Editor.html"><TT>Fl_Text_Editor</TT></A>
120
widget to edit the text:
123
w->editor = new Fl_Text_Editor(0, 30, 640, 370);
124
w->editor->buffer(textbuf);
127
<P>So that we can keep track of changes to the file, we also want to add
128
a "modify" callback:</P>
131
textbuf->add_modify_callback(changed_cb, w);
132
textbuf->call_modify_callbacks();
135
<P>Finally, we want to use a mono-spaced font like <TT>FL_COURIER</TT>:
138
w->editor->textfont(FL_COURIER);
141
<H2>The Replace Dialog</H2>
143
<P>We can use the FLTK convenience functions for many of the
144
editor's dialogs, however the replace dialog needs its own
145
custom window. To keep things simple we will have a
146
"find" string, a "replace" string, and
147
"replace all", "replace next", and
148
"cancel" buttons. The strings are just
149
<TT>Fl_Input</TT> widgets, the "replace all" and
150
"cancel" buttons are <TT>Fl_Button</TT> widgets, and
151
the "replace next " button is a
152
<TT>Fl_Return_Button</TT> widget:</P>
154
<P ALIGN="CENTER"><IMG src="editor-replace.gif" ALT="The search and replace dialog."><BR>
155
<I>Figure 4-1: The search and replace dialog.</I></P>
158
Fl_Window *replace_dlg = new Fl_Window(300, 105, "Replace");
159
Fl_Input *replace_find = new Fl_Input(70, 10, 200, 25, "Find:");
160
Fl_Input *replace_with = new Fl_Input(70, 40, 200, 25, "Replace:");
161
Fl_Button *replace_all = new Fl_Button(10, 70, 90, 25, "Replace All");
162
Fl_Button *replace_next = new Fl_Button(105, 70, 120, 25, "Replace Next");
163
Fl_Button *replace_cancel = new Fl_Button(230, 70, 60, 25, "Cancel");
168
<P>Now that we've defined the GUI components of our editor, we
169
need to define our callback functions.</P>
171
<H3>changed_cb()</H3>
173
<P>This function will be called whenever the user changes any text in the
174
<TT>editor</TT> widget:
177
void changed_cb(int, int nInserted, int nDeleted,int, const char*, void* v) {
178
if ((nInserted || nDeleted) && !loading) changed = 1;
179
EditorWindow *w = (EditorWindow *)v;
181
if (loading) w->editor->show_insert_position();
185
<P>The <TT>set_title()</TT> function is one that we will write to set
186
the changed status on the current file. We're doing it this way
187
because we want to show the changed status in the window's
192
<P>This callback function will call <A
193
href="Fl_Text_Editor.html#Fl_Text_Editor.kf_copy"><TT>kf_copy()</TT></A>
194
to copy the currently selected text to the clipboard:</P>
197
void copy_cb(Fl_Widget*, void* v) {
198
EditorWindow* e = (EditorWindow*)v;
199
Fl_Text_Editor::kf_copy(0, e->editor);
205
<P>This callback function will call <A
206
href="Fl_Text_Editor.html#Fl_Text_Editor.kf_cut"><TT>kf_cut()</TT></A>
207
to cut the currently selected text to the clipboard:</P>
210
void cut_cb(Fl_Widget*, void* v) {
211
EditorWindow* e = (EditorWindow*)v;
212
Fl_Text_Editor::kf_cut(0, e->editor);
218
<P>This callback function will call <A
219
href="Fl_Text_Buffer.html#Fl_Text_Buffer.remove_selection"><TT>remove_selection()</TT></A>
220
to delete the currently selected text to the clipboard:</P>
223
void delete_cb(Fl_Widget*, void* v) {
224
textbuf->remove_selection();
230
<P>This callback function asks for a search string using the <A
231
href="functions.html#fl_input2"><TT>fl_input()</TT></A>
232
convenience function and then calls the <TT>find2_cb()</TT>
233
function to find the string:
236
void find_cb(Fl_Widget* w, void* v) {
237
EditorWindow* e = (EditorWindow*)v;
240
val = fl_input("Search String:", e->search);
242
// User entered a string - go find it!
243
strcpy(e->search, val);
250
<P>This function will find the next occurrence of the search
251
string. If the search string is blank then we want to pop up the
255
void find2_cb(Fl_Widget* w, void* v) {
256
EditorWindow* e = (EditorWindow*)v;
257
if (e->search[0] == '\0') {
258
// Search string is blank; get a new one...
263
int pos = e->editor->insert_position();
264
int found = textbuf->search_forward(pos, e->search, &pos);
266
// Found a match; select and update the position...
267
textbuf->select(pos, pos+strlen(e->search));
268
e->editor->insert_position(pos+strlen(e->search));
269
e->editor->show_insert_position();
271
else fl_alert("No occurrences of \'%s\' found!", e->search);
275
<P>If the search string cannot be found we use the <A
276
href="functions.html#fl_alert"><TT>fl_alert()</TT></A>
277
convenience function to display a message to that effect.
280
<P>This callback function will clear the editor widget and current
281
filename. It also calls the <TT>check_save()</TT> function to give the
282
user the opportunity to save the current file first as needed:
285
void new_cb(Fl_Widget*, void*) {
286
if (!check_save()) return;
289
textbuf->select(0, textbuf->length());
290
textbuf->remove_selection();
292
textbuf->call_modify_callbacks();
298
<P>This callback function will ask the user for a filename and then load
299
the specified file into the input widget and current filename. It also
300
calls the <TT>check_save()</TT> function to give the user the
301
opportunity to save the current file first as needed:
304
void open_cb(Fl_Widget*, void*) {
305
if (!check_save()) return;
307
char *newfile = fl_file_chooser("Open File?", "*", filename);
308
if (newfile != NULL) load_file(newfile, -1);
312
<P>We call the <TT>load_file()</TT> function to actually load the file.
316
<P>This callback function will call <A
317
href="Fl_Text_Editor.html#Fl_Text_Editor.kf_paste"><TT>kf_paste()</TT></A>
318
to paste the clipboard at the current position:</P>
321
void paste_cb(Fl_Widget*, void* v) {
322
EditorWindow* e = (EditorWindow*)v;
323
Fl_Text_Editor::kf_paste(0, e->editor);
329
<P>The quit callback will first see if the current file has been
330
modified, and if so give the user a chance to save it. It then exits
334
void quit_cb(Fl_Widget*, void*) {
335
if (changed && !check_save())
342
<H3>replace_cb()</H3>
344
<P>The replace callback just shows the replace dialog:
347
void replace_cb(Fl_Widget*, void* v) {
348
EditorWindow* e = (EditorWindow*)v;
349
e->replace_dlg->show();
353
<H3>replace2_cb()</H3>
355
<P>This callback will replace the next occurence of the replacement
356
string. If nothing has been entered for the replacement string, then
357
the replace dialog is displayed instead:
360
void replace2_cb(Fl_Widget*, void* v) {
361
EditorWindow* e = (EditorWindow*)v;
362
const char *find = e->replace_find->value();
363
const char *replace = e->replace_with->value();
365
if (find[0] == '\0') {
366
// Search string is blank; get a new one...
367
e->replace_dlg->show();
371
e->replace_dlg->hide();
373
int pos = e->editor->insert_position();
374
int found = textbuf->search_forward(pos, find, &pos);
377
// Found a match; update the position and replace text...
378
textbuf->select(pos, pos+strlen(find));
379
textbuf->remove_selection();
380
textbuf->insert(pos, replace);
381
textbuf->select(pos, pos+strlen(replace));
382
e->editor->insert_position(pos+strlen(replace));
383
e->editor->show_insert_position();
385
else fl_alert("No occurrences of \'%s\' found!", find);
389
<H3>replall_cb()</H3>
391
<P>This callback will replace all occurences of the search
395
void replall_cb(Fl_Widget*, void* v) {
396
EditorWindow* e = (EditorWindow*)v;
397
const char *find = e->replace_find->value();
398
const char *replace = e->replace_with->value();
400
find = e->replace_find->value();
401
if (find[0] == '\0') {
402
// Search string is blank; get a new one...
403
e->replace_dlg->show();
407
e->replace_dlg->hide();
409
e->editor->insert_position(0);
412
// Loop through the whole string
413
for (int found = 1; found;) {
414
int pos = e->editor->insert_position();
415
found = textbuf->search_forward(pos, find, &pos);
418
// Found a match; update the position and replace text...
419
textbuf->select(pos, pos+strlen(find));
420
textbuf->remove_selection();
421
textbuf->insert(pos, replace);
422
e->editor->insert_position(pos+strlen(replace));
423
e->editor->show_insert_position();
428
if (times) fl_message("Replaced %d occurrences.", times);
429
else fl_alert("No occurrences of \'%s\' found!", find);
433
<H3>replcan_cb()</H3>
435
<P>This callback just hides the replace dialog:
438
void replcan_cb(Fl_Widget*, void* v) {
439
EditorWindow* e = (EditorWindow*)v;
440
e->replace_dlg->hide();
446
<P>This callback saves the current file. If the current filename is
447
blank it calls the "save as" callback:
451
if (filename[0] == '\0') {
452
// No filename - get one!
456
else save_file(filename);
460
<P>The <TT>save_file()</TT> function saves the current file to the
465
<P>This callback asks the user for a filename and saves the current file:
468
void saveas_cb(void) {
471
newfile = fl_file_chooser("Save File As?", "*", filename);
472
if (newfile != NULL) save_file(newfile);
476
<P>The <TT>save_file()</TT> function saves the current file to the
479
<H2>Other Functions</H2>
481
<P>Now that we've defined the callback functions, we need our support
482
functions to make it all work:
484
<H3>check_save()</H3>
486
<P>This function checks to see if the current file needs to be saved. If
487
so, it asks the user if they want to save it:
490
int check_save(void) {
491
if (!changed) return 1;
493
int r = fl_choice("The current file has not been saved.\n"
494
"Would you like to save it now?",
495
"Cancel", "Save", "Discard");
498
save_cb(); // Save the file...
502
return (r == 2) ? 1 : 0;
508
<P>This function loads the specified file into the <TT>textbuf</TT> class:
512
void load_file(char *newfile, int ipos) {
514
int insert = (ipos != -1);
516
if (!insert) strcpy(filename, "");
518
if (!insert) r = textbuf->loadfile(newfile);
519
else r = textbuf->insertfile(newfile, ipos);
521
fl_alert("Error reading from file \'%s\':\n%s.", newfile, strerror(errno));
523
if (!insert) strcpy(filename, newfile);
525
textbuf->call_modify_callbacks();
529
<P>When loading the file we use the <A
530
href="Fl_Text_Buffer.html#Fl_Text_Buffer.loadfile"><TT>loadfile()</TT></A>
531
method to "replace" the text in the buffer, or the <A
532
href="Fl_Text_Buffer.html#Fl_Text_Buffer.insertfile"><TT>insertfile()</TT></A>
533
method to insert text in the buffer from the named file.
537
<P>This function saves the current buffer to the specified file:
540
void save_file(char *newfile) {
541
if (textbuf->savefile(newfile))
542
fl_alert("Error writing to file \'%s\':\n%s.", newfile, strerror(errno));
544
strcpy(filename, newfile);
546
textbuf->call_modify_callbacks();
552
<P>This function checks the <TT>changed</TT> variable and updates the
553
window label accordingly:
555
void set_title(Fl_Window* w) {
556
if (filename[0] == '\0') strcpy(title, "Untitled");
559
slash = strrchr(filename, '/');
561
if (slash == NULL) slash = strrchr(filename, '\\');
563
if (slash != NULL) strcpy(title, slash + 1);
564
else strcpy(title, filename);
567
if (changed) strcat(title, " (modified)");
573
<H2>The main() Function</H2>
575
<P>Once we've created all of the support functions, the only thing left
576
is to tie them all together with the <TT>main()</TT> function.
577
The <TT>main()</TT> function creates a new text buffer, creates a
578
new view (window) for the text, shows the window, loads the file on
579
the command-line (if any), and then enters the FLTK event loop:
582
int main(int argc, char **argv) {
583
textbuf = new Fl_Text_Buffer;
585
Fl_Window* window = new_view();
587
window->show(1, argv);
589
if (argc > 1) load_file(argv[1], -1);
595
<H2>Compiling the Editor</H2>
597
<P>The complete source for our text editor can be found in the <TT>test/editor.cxx</TT> source file. Both the Makefile and Visual C++
598
workspace include the necessary rules to build the editor. You can
599
also compile it using a standard compiler with:
602
CC -o editor editor.cxx -lfltk -lXext -lX11 -lm
605
<P>or by using the <TT>fltk-config</TT> script with:
608
fltk-config --compile editor.cxx
611
<P>As noted in <A href="basics.html">Chapter 1</A>, you may need to
612
include compiler and linker options to tell them where to find the FLTK
613
library. Also, the <TT>CC</TT> command may also be called <TT>gcc</TT>
614
or <TT>c++</TT> on your system.
616
<P>Congratulations, you've just built your own text editor!</P>
618
<H2>The Final Product</H2>
620
The final editor window should look like the image in Figure 4-2.
622
<P ALIGN="CENTER"><IMG src="editor.gif" ALT="The completed editor window."><BR>
623
<I>Figure 4-2: The completed editor window</I></P>
625
<H2>Advanced Features</H2>
627
<P>Now that we've implemented the basic functionality, it is
628
time to show off some of the advanced features of the
629
<CODE>Fl_Text_Editor</CODE> widget.
631
<H3>Syntax Highlighting</H3>
633
<P>The <CODE>Fl_Text_Editor</CODE> widget supports highlighting
634
of text with different fonts, colors, and sizes. The
635
implementation is based on the excellent <A
636
HREF="http://www.nedit.org/">NEdit</A> text editor core, which
637
uses a parallel "style" buffer which tracks the font, color, and
638
size of the text that is drawn.
640
<P>Styles are defined using the
641
<CODE>Fl_Text_Display::Style_Table_Entry</CODE> structure
642
defined in <CODE><FL/Fl_Text_Display.H></CODE>:
645
struct Style_Table_Entry {
653
<P>The <CODE>color</CODE> member sets the color for the text,
654
the <CODE>font</CODE> member sets the FLTK font index to use,
655
and the <CODE>size</CODE> member sets the pixel size of the
656
text. The <CODE>attr</CODE> member is currently not used.
658
<P>For our text editor we'll define 7 styles for plain code,
659
comments, keywords, and preprocessor directives:
662
Fl_Text_Display::Style_Table_Entry styletable[] = { // Style table
663
{ FL_BLACK, FL_COURIER, FL_NORMAL_SIZE }, // A - Plain
664
{ FL_DARK_GREEN, FL_COURIER_ITALIC, FL_NORMAL_SIZE }, // B - Line comments
665
{ FL_DARK_GREEN, FL_COURIER_ITALIC, FL_NORMAL_SIZE }, // C - Block comments
666
{ FL_BLUE, FL_COURIER, FL_NORMAL_SIZE }, // D - Strings
667
{ FL_DARK_RED, FL_COURIER, FL_NORMAL_SIZE }, // E - Directives
668
{ FL_DARK_RED, FL_COURIER_BOLD, FL_NORMAL_SIZE }, // F - Types
669
{ FL_BLUE, FL_COURIER_BOLD, FL_NORMAL_SIZE } // G - Keywords
673
<P>You'll notice that the comments show a letter next to each
674
style - each style in the style buffer is referenced using a
675
character starting with the letter 'A'.
677
<P>You call the <CODE>highlight_data()</CODE> method to associate the
678
style data and buffer with the text editor widget:
681
Fl_Text_Buffer *stylebuf;
683
w->editor->highlight_data(stylebuf, styletable,
684
sizeof(styletable) / sizeof(styletable[0]),
685
'A', style_unfinished_cb, 0);
688
<P>Finally, you need to add a callback to the main text buffer so
689
that changes to the text buffer are mirrored in the style buffer:
692
textbuf->add_modify_callback(style_update, w->editor);
695
<P>The <CODE>style_update()</CODE> function, like the <CODE>change_cb()</CODE>
696
function described earlier, is called whenever text is added or removed from
697
the text buffer. It mirrors the changes in the style buffer and then updates
698
the style data as necessary:
702
// 'style_update()' - Update the style buffer...
706
style_update(int pos, // I - Position of update
707
int nInserted, // I - Number of inserted chars
708
int nDeleted, // I - Number of deleted chars
709
int nRestyled, // I - Number of restyled chars
710
const char *deletedText, // I - Text that was deleted
711
void *cbArg) { // I - Callback data
712
int start, // Start of text
714
char last, // Last style on line
715
*style, // Style data
719
// If this is just a selection change, just unselect the style buffer...
720
if (nInserted == 0 && nDeleted == 0) {
721
stylebuf->unselect();
725
// Track changes in the text buffer...
727
// Insert characters into the style buffer...
728
style = new char[nInserted + 1];
729
memset(style, 'A', nInserted);
730
style[nInserted] = '\0';
732
stylebuf->replace(pos, pos + nDeleted, style);
735
// Just delete characters in the style buffer...
736
stylebuf->remove(pos, pos + nDeleted);
739
// Select the area that was just updated to avoid unnecessary
741
stylebuf->select(pos, pos + nInserted - nDeleted);
743
// Re-parse the changed region; we do this by parsing from the
744
// beginning of the line of the changed region to the end of
745
// the line of the changed region... Then we check the last
746
// style character and keep updating if we have a multi-line
747
// comment character...
748
start = textbuf->line_start(pos);
749
end = textbuf->line_end(pos + nInserted - nDeleted);
750
text = textbuf->text_range(start, end);
751
style = stylebuf->text_range(start, end);
752
last = style[end - start - 1];
754
style_parse(text, style, end - start);
756
stylebuf->replace(start, end, style);
757
((Fl_Text_Editor *)cbArg)->redisplay_range(start, end);
759
if (last != style[end - start - 1]) {
760
// The last character on the line changed styles, so reparse the
761
// remainder of the buffer...
765
end = textbuf->length();
766
text = textbuf->text_range(start, end);
767
style = stylebuf->text_range(start, end);
769
style_parse(text, style, end - start);
771
stylebuf->replace(start, end, style);
772
((Fl_Text_Editor *)cbArg)->redisplay_range(start, end);
780
<P>The <CODE>style_parse()</CODE> function scans a copy of the
781
text in the buffer and generates the necessary style characters
782
for display. It assumes that parsing begins at the start of a line:
786
// 'style_parse()' - Parse text and produce style data.
790
style_parse(const char *text,
800
for (current = *style, col = 0, last = 0; length > 0; length --, text ++) {
801
if (current == 'A') {
802
// Check for directives, comments, strings, and keywords...
803
if (col == 0 && *text == '#') {
804
// Set style to directive
806
} else if (strncmp(text, "//", 2) == 0) {
808
} else if (strncmp(text, "/*", 2) == 0) {
810
} else if (strncmp(text, "\\\"", 2) == 0) {
818
} else if (*text == '\"') {
820
} else if (!last && islower(*text)) {
821
// Might be a keyword...
822
for (temp = text, bufptr = buf;
823
islower(*temp) && bufptr < (buf + sizeof(buf) - 1);
824
*bufptr++ = *temp++);
826
if (!islower(*temp)) {
831
if (bsearch(&bufptr, code_types,
832
sizeof(code_types) / sizeof(code_types[0]),
833
sizeof(code_types[0]), compare_keywords)) {
834
while (text < temp) {
845
} else if (bsearch(&bufptr, code_keywords,
846
sizeof(code_keywords) / sizeof(code_keywords[0]),
847
sizeof(code_keywords[0]), compare_keywords)) {
848
while (text < temp) {
862
} else if (current == 'C' && strncmp(text, "*/", 2) == 0) {
863
// Close a C comment...
871
} else if (current == 'D') {
872
// Continuing in string...
873
if (strncmp(text, "\\\"", 2) == 0) {
874
// Quoted end quote...
881
} else if (*text == '\"') {
890
// Copy style info...
891
if (current == 'A' && (*text == '{' || *text == '}')) *style++ = 'G';
892
else *style++ = current;
895
last = isalnum(*text) || *text == '.';
898
// Reset column and possibly reset the style
900
if (current == 'B' || current == 'E') current = 'A';