1// This file is part of SmallBASIC
2//
3// Copyright(C) 2001-2019 Chris Warren-Smith.
4//
5// This program is distributed under the terms of the GPL v2.0 or later
6// Download the GNU Public License (GPL) from www.gnu.org
7//
8
9#include <config.h>
10#include <stdint.h>
11#include <unistd.h>
12#include <errno.h>
13#include <sys/stat.h>
14#include <FL/fl_ask.H>
15#include <FL/Fl_Color_Chooser.H>
16#include <FL/Fl_Tile.H>
17#include "platform/fltk/MainWindow.h"
18#include "platform/fltk/EditorWidget.h"
19#include "platform/fltk/FileWidget.h"
20#include "common/smbas.h"
21
22// in MainWindow.cxx
23extern String recentPath[];
24extern String recentLabel[];
25extern int recentMenu[];
26extern const char *historyFile;
27extern const char *untitledFile;
28
29// in BasicEditor.cxx
30extern Fl_Text_Display::Style_Table_Entry styletable[];
31extern Fl_Color defaultColor[];
32
33int completionIndex = 0;
34
35static bool rename_active = false;
36const char scanLabel[] = "(Refresh)";
37
38EditorWidget *get_editor() {
39 EditorWidget *result = wnd->getEditor();
40 if (!result) {
41 result = wnd->getEditor(true);
42 }
43 return result;
44}
45
46struct LineInput : public Fl_Input {
47 LineInput(int x, int y, int w, int h);
48 void resize(int x, int y, int w, int h);
49 int handle(int event);
50
51private:
52 int orig_x, orig_y, orig_w, orig_h;
53};
54
55//--EditorWidget----------------------------------------------------------------
56
57EditorWidget::EditorWidget(Fl_Widget *rect, Fl_Menu_Bar *menuBar) :
58 Fl_Group(rect->x(), rect->y(), rect->w(), rect->h()),
59 _editor(NULL),
60 _tty(NULL),
61 _dirty(false),
62 _loading(false),
63 _modifiedTime(0),
64 _commandText(NULL),
65 _rowStatus(NULL),
66 _colStatus(NULL),
67 _runStatus(NULL),
68 _modStatus(NULL),
69 _funcList(NULL),
70 _funcListEvent(false),
71 _logPrintBn(NULL),
72 _lockBn(NULL),
73 _hideIdeBn(NULL),
74 _gotoLineBn(NULL),
75 _commandOpt(cmd_find),
76 _commandChoice(NULL),
77 _menuBar(menuBar) {
78 _filename[0] = 0;
79 box(FL_NO_BOX);
80 begin();
81
82 const int st_w = 40;
83 const int bn_w = 28;
84 const int st_h = STATUS_HEIGHT;
85 const int choice_w = 80;
86 const int tileHeight = rect->h() - st_h;
87 const int ttyHeight = tileHeight / 8;
88 const int browserWidth = rect->w() / 5;
89 const int editHeight = tileHeight - ttyHeight;
90 const int editWidth = rect->w() - browserWidth;
91 const int st_y = rect->y() + editHeight + ttyHeight;
92
93 Fl_Group *tile = new Fl_Tile(rect->x(), rect->y(), rect->w(), tileHeight);
94 _editor = new BasicEditor(rect->x(), rect->y(), editWidth, editHeight, this);
95 _editor->linenumber_width(LINE_NUMBER_WIDTH);
96 _editor->wrap_mode(true, 0);
97 _editor->selection_color(fl_rgb_color(190, 189, 188));
98 _editor->_textbuf->add_modify_callback(changed_cb, this);
99 _editor->box(FL_FLAT_BOX);
100 _editor->take_focus();
101
102 // sub-func jump droplist
103 _funcList = new Fl_Tree(_editor->w(), rect->y(), browserWidth, editHeight);
104 _funcList->labelfont(FL_HELVETICA);
105 _funcList->when(FL_WHEN_RELEASE);
106 _funcList->box(FL_FLAT_BOX);
107
108 Fl_Tree_Item *scan = new Fl_Tree_Item(_funcList);
109 scan->label(scanLabel);
110 _funcList->showroot(0);
111 _funcList->add(scanLabel, scan);
112
113 _tty = new TtyWidget(rect->x(), rect->y() + editHeight, rect->w(), ttyHeight, TTY_ROWS);
114 tile->end();
115
116 // editor status bar
117 _statusBar = new Fl_Group(rect->x(), st_y, rect->w(), st_h);
118 _logPrintBn = new Fl_Toggle_Button(rect->w() - bn_w, st_y + 1, bn_w, st_h - 2);
119 _lockBn = new Fl_Toggle_Button(_logPrintBn->x() - (bn_w + 2), st_y + 1, bn_w, st_h - 2);
120 _hideIdeBn = new Fl_Toggle_Button(_lockBn->x() - (bn_w + 2), st_y + 1, bn_w, st_h - 2);
121 _gotoLineBn = new Fl_Toggle_Button(_hideIdeBn->x() - (bn_w + 2), st_y + 1, bn_w, st_h - 2);
122 _colStatus = new Fl_Button(_gotoLineBn->x() - (st_w + 2), st_y, st_w, st_h);
123 _rowStatus = new Fl_Button(_colStatus->x() - (st_w + 2), st_y, st_w, st_h);
124 _runStatus = new Fl_Button(_rowStatus->x() - (st_w + 2), st_y, st_w, st_h);
125 _modStatus = new Fl_Button(_runStatus->x() - (st_w + 2), st_y, st_w, st_h);
126 _commandChoice = new Fl_Button(rect->x(), st_y, choice_w, st_h);
127 _commandText = new Fl_Input(rect->x() + choice_w + 2, st_y + 1, _modStatus->x() - choice_w - 4, st_h - 2);
128 _commandText->align(FL_ALIGN_LEFT | FL_ALIGN_CLIP);
129 _commandText->when(FL_WHEN_ENTER_KEY_ALWAYS);
130 _commandText->labelfont(FL_HELVETICA);
131
132 for (int n = 0; n < _statusBar->children(); n++) {
133 Fl_Widget *w = _statusBar->child(n);
134 w->labelfont(FL_HELVETICA);
135 w->box(FL_NO_BOX);
136 }
137 _commandText->box(FL_THIN_DOWN_BOX);
138 _logPrintBn->box(FL_THIN_UP_BOX);
139 _lockBn->box(FL_THIN_UP_BOX);
140 _hideIdeBn->box(FL_THIN_UP_BOX);
141 _gotoLineBn->box(FL_THIN_UP_BOX);
142
143 _statusBar->resizable(_commandText);
144 _statusBar->end();
145 resizable(tile);
146 end();
147
148 // command selection
149 setCommand(cmd_find);
150 runState(rs_ready);
151 setModified(false);
152
153 // button callbacks
154 _lockBn->callback(scroll_lock_cb);
155 _modStatus->callback(save_file_cb);
156 _runStatus->callback(MainWindow::run_cb);
157 _commandChoice->callback(command_cb, (void *)1);
158 _commandText->callback(command_cb, (void *)1);
159 _funcList->callback(func_list_cb, 0);
160 _logPrintBn->callback(un_select_cb, (void *)_hideIdeBn);
161 _hideIdeBn->callback(un_select_cb, (void *)_logPrintBn);
162 _colStatus->callback(goto_line_cb, 0);
163 _rowStatus->callback(goto_line_cb, 0);
164
165 // setup icons
166 _gotoLineBn->label("B"); // right arrow (goto)
167 _hideIdeBn->label("W"); // large dot
168 _lockBn->label("J"); // vertical bars
169 _logPrintBn->label("T"); // italic bold T
170
171 // setup tooltips
172 _commandText->tooltip("Press Ctrl+f or Ctrl+Shift+f to find again");
173 _rowStatus->tooltip("Cursor row position");
174 _colStatus->tooltip("Cursor column position");
175 _runStatus->tooltip("Run or BREAK");
176 _modStatus->tooltip("Save file");
177 _logPrintBn->tooltip("Display PRINT statements in the log window");
178 _lockBn->tooltip("Prevent log window auto-scrolling");
179 _hideIdeBn->tooltip("Hide the editor while program is running");
180 _gotoLineBn->tooltip("Position the cursor to the last program line after BREAK");
181 statusMsg(SB_STR_VER);
182
183 // setup defaults or restore settings
184 if (wnd && wnd->_profile) {
185 wnd->_profile->loadConfig(this);
186 }
187 take_focus();
188}
189
190EditorWidget::~EditorWidget() {
191 delete _editor;
192}
193
194//--Event handler methods-------------------------------------------------------
195
196/**
197 * change the selected text to upper/lower/camel case
198 */
199void EditorWidget::change_case(Fl_Widget *w, void *eventData) {
200 Fl_Text_Buffer *tb = _editor->_textbuf;
201 int start, end;
202 char *selection = getSelection(&start, &end);
203 int len = strlen(selection);
204 enum { up, down, mixed } curcase = isupper(selection[0]) ? up : down;
205
206 for (int i = 1; i < len; i++) {
207 if (isalpha(selection[i])) {
208 bool isup = isupper(selection[i]);
209 if ((curcase == up && isup == false) || (curcase == down && isup)) {
210 curcase = mixed;
211 break;
212 }
213 }
214 }
215
216 // transform pattern: Foo -> FOO, FOO -> foo, foo -> Foo
217 for (int i = 0; i < len; i++) {
218 selection[i] = curcase == mixed ? toupper(selection[i]) : tolower(selection[i]);
219 }
220 if (curcase == down) {
221 selection[0] = toupper(selection[0]);
222 // upcase chars following non-alpha chars
223 for (int i = 1; i < len; i++) {
224 if (isalpha(selection[i]) == false && i + 1 < len) {
225 selection[i + 1] = toupper(selection[i + 1]);
226 }
227 }
228 }
229
230 if (selection[0]) {
231 tb->replace_selection(selection);
232 tb->select(start, end);
233 }
234 free((void *)selection);
235}
236
237/**
238 * command handler
239 */
240void EditorWidget::command_opt(Fl_Widget *w, void *eventData) {
241 setCommand((CommandOpt) (intptr_t) eventData);
242}
243
244/**
245 * cut selected text to the clipboard
246 */
247void EditorWidget::cut_text(Fl_Widget *w, void *eventData) {
248 Fl_Text_Editor::kf_cut(0, _editor);
249}
250
251/**
252 * delete selected text
253 */
254void EditorWidget::do_delete(Fl_Widget *w, void *eventData) {
255 _editor->_textbuf->remove_selection();
256}
257
258/**
259 * perform keyword completion
260 */
261void EditorWidget::expand_word(Fl_Widget *w, void *eventData) {
262 int start, end;
263 const char *fullWord = 0;
264 unsigned fullWordLen = 0;
265
266 Fl_Text_Buffer *textbuf = _editor->_textbuf;
267 char *text = textbuf->text();
268
269 if (textbuf->selected()) {
270 // get word before selection
271 int pos1, pos2;
272 textbuf->selection_position(&pos1, &pos2);
273 start = textbuf->word_start(pos1 - 1);
274 end = pos1;
275 // get word from before selection to end of selection
276 fullWord = text + start;
277 fullWordLen = pos2 - start - 1;
278 } else {
279 // nothing selected - get word to left of cursor position
280 int pos = _editor->insert_position();
281 end = textbuf->word_end(pos);
282 start = textbuf->word_start(end - 1);
283 completionIndex = 0;
284 }
285
286 if (start >= end) {
287 free(text);
288 return;
289 }
290
291 const char *expandWord = text + start;
292 unsigned expandWordLen = end - start;
293 int wordPos = 0;
294
295 // scan for expandWord from within the current text buffer
296 if (completionIndex != -1 && searchBackward(text, start - 1,
297 expandWord, expandWordLen,
298 &wordPos)) {
299 int matchPos = -1;
300 if (textbuf->selected() == 0) {
301 matchPos = wordPos;
302 completionIndex = 1; // find next word on next call
303 } else {
304 // find the next word prior to the currently selected word
305 int index = 1;
306 while (wordPos > 0) {
307 if (strncasecmp(text + wordPos, fullWord, fullWordLen) != 0 ||
308 isalpha(text[wordPos + fullWordLen + 1])) {
309 // isalpha - matches fullWord but word has more chars
310 matchPos = wordPos;
311 if (completionIndex == index) {
312 completionIndex++;
313 break;
314 }
315 // count index for non-matching fullWords only
316 index++;
317 }
318
319 if (searchBackward(text, wordPos - 1, expandWord,
320 expandWordLen, &wordPos) == 0) {
321 matchPos = -1;
322 break; // no more partial matches
323 }
324 }
325 if (index == completionIndex) {
326 // end of expansion sequence
327 matchPos = -1;
328 }
329 }
330 if (matchPos != -1) {
331 char *word = textbuf->text_range(matchPos, textbuf->word_end(matchPos));
332 if (textbuf->selected()) {
333 textbuf->replace_selection(word + expandWordLen);
334 } else {
335 textbuf->insert(end, word + expandWordLen);
336 }
337 textbuf->select(end, end + strlen(word + expandWordLen));
338 _editor->insert_position(end + strlen(word + expandWordLen));
339 free((void *)word);
340 free(text);
341 return;
342 }
343 }
344
345 completionIndex = -1; // no more buffer expansions
346
347 strlib::List<String *> keywords;
348 _editor->getKeywords(keywords);
349
350 // find the next replacement
351 int firstIndex = -1;
352 int lastIndex = -1;
353 int curIndex = -1;
354 int numWords = keywords.size();
355 for (int i = 0; i < numWords; i++) {
356 const char *keyword = ((String *)keywords.get(i))->c_str();
357 if (strncasecmp(expandWord, keyword, expandWordLen) == 0) {
358 if (firstIndex == -1) {
359 firstIndex = i;
360 }
361 if (fullWordLen == 0) {
362 if (expandWordLen == strlen(keyword)) {
363 // nothing selected and word to left of cursor matches
364 curIndex = i;
365 }
366 } else if (strncasecmp(fullWord, keyword, fullWordLen) == 0) {
367 // selection+word to left of selection matches
368 curIndex = i;
369 }
370 lastIndex = i;
371 } else if (lastIndex != -1) {
372 // moved beyond matching words
373 break;
374 }
375 }
376
377 if (lastIndex != -1) {
378 if (lastIndex == curIndex || curIndex == -1) {
379 lastIndex = firstIndex; // wrap to first in subset
380 } else {
381 lastIndex = curIndex + 1;
382 }
383
384 const char *keyword = ((String *)keywords.get(lastIndex))->c_str();
385 // updated the segment of the replacement text
386 // that completes the current selection
387 if (textbuf->selected()) {
388 textbuf->replace_selection(keyword + expandWordLen);
389 } else {
390 textbuf->insert(end, keyword + expandWordLen);
391 }
392 textbuf->select(end, end + strlen(keyword + expandWordLen));
393 }
394 free(text);
395}
396
397/**
398 * handler for find text command
399 */
400void EditorWidget::find(Fl_Widget *w, void *eventData) {
401 setCommand(cmd_find);
402}
403
404/**
405 * clears the console
406 */
407void EditorWidget::clear_console(Fl_Widget *w, void *eventData) {
408 _tty->clearScreen();
409}
410
411/**
412 * performs the current command
413 */
414void EditorWidget::command(Fl_Widget *w, void *eventData) {
415 bool found = false;
416 bool forward = (intptr_t) eventData;
417 bool updatePos = (_commandOpt != cmd_find_inc);
418
419 if (Fl::event_button() == FL_RIGHT_MOUSE) {
420 // right click
421 forward = 0;
422 }
423
424 switch (_commandOpt) {
425 case cmd_find_inc:
426 case cmd_find:
427 found = _editor->findText(_commandText->value(), forward, updatePos);
428 _commandText->textcolor(found ? _commandChoice->color() : FL_RED);
429 _commandText->redraw();
430 break;
431
432 case cmd_replace:
433 _commandBuffer.clear();
434 _commandBuffer.append(_commandText->value());
435 setCommand(cmd_replace_with);
436 break;
437
438 case cmd_replace_with:
439 replace_next();
440 break;
441
442 case cmd_goto:
443 gotoLine(atoi(_commandText->value()));
444 take_focus();
445 break;
446
447 case cmd_input_text:
448 wnd->setModal(false);
449 break;
450 }
451}
452
453/**
454 * sub/func selection list handler
455 */
456void EditorWidget::func_list(Fl_Widget *w, void *eventData) {
457 if (_funcList && _funcList->callback_item()) {
458 Fl_Tree_Item *item = (Fl_Tree_Item*)_funcList->callback_item();
459 const char *label = item->label();
460 if (label) {
461 _funcListEvent = true;
462 if (strcmp(label, scanLabel) == 0) {
463 resetList();
464 } else {
465 gotoLine((int)(intptr_t)item->user_data());
466 take_focus();
467 }
468 }
469 }
470}
471
472/**
473 * goto-line command handler
474 */
475void EditorWidget::goto_line(Fl_Widget *w, void *eventData) {
476 setCommand(cmd_goto);
477}
478
479/**
480 * paste clipboard text onto the buffer
481 */
482void EditorWidget::paste_text(Fl_Widget *w, void *eventData) {
483 Fl_Text_Editor::kf_paste(0, _editor);
484}
485
486/**
487 * rename the currently selected variable
488 */
489void EditorWidget::rename_word(Fl_Widget *w, void *eventData) {
490 if (rename_active) {
491 rename_active = false;
492 } else {
493 Fl_Rect rc;
494 char *selection = getSelection(&rc);
495 if (selection) {
496 showFindText(selection);
497 begin();
498 LineInput *in = new LineInput(rc.x(), rc.y(), rc.w() + 10, rc.h());
499 end();
500
501 in->value(selection);
502 in->callback(rename_word_cb);
503 in->textfont(FL_COURIER);
504 in->textsize(getFontSize());
505
506 rename_active = true;
507 while (rename_active && in == Fl::focus()) {
508 Fl::wait();
509 }
510
511 showFindText("");
512 replaceAll(selection, in->value(), true, true);
513 remove(in);
514 take_focus();
515 delete in;
516 free((void *)selection);
517 }
518 }
519}
520
521/**
522 * replace the next find occurance
523 */
524void EditorWidget::replace_next(Fl_Widget *w, void *eventData) {
525 if (readonly()) {
526 return;
527 }
528
529 const char *find = _commandBuffer;
530 const char *replace = _commandText->value();
531
532 Fl_Text_Buffer *textbuf = _editor->_textbuf;
533 int pos = _editor->insert_position();
534 int found = textbuf->search_forward(pos, find, &pos);
535
536 if (found) {
537 // found a match; update the position and replace text
538 textbuf->select(pos, pos + strlen(find));
539 textbuf->remove_selection();
540 textbuf->insert(pos, replace);
541 textbuf->select(pos, pos + strlen(replace));
542 _editor->insert_position(pos + strlen(replace));
543 _editor->show_insert_position();
544 } else {
545 setCommand(cmd_find);
546 _editor->take_focus();
547 }
548}
549
550/**
551 * save file menu command handler
552 */
553void EditorWidget::save_file(Fl_Widget *w, void *eventData) {
554 if (_filename[0] == '\0') {
555 // no filename - get one!
556 wnd->save_file_as();
557 return;
558 } else {
559 doSaveFile(_filename);
560 }
561}
562
563/**
564 * prevent the tty window from scrolling with new data
565 */
566void EditorWidget::scroll_lock(Fl_Widget *w, void *eventData) {
567 _tty->setScrollLock(_lockBn->value());
568}
569
570/**
571 * select all text
572 */
573void EditorWidget::select_all(Fl_Widget *w, void *eventData) {
574 Fl_Text_Editor::kf_select_all(0, _editor);
575}
576
577/**
578 * set colour menu command handler
579 */
580void EditorWidget::set_color(Fl_Widget *w, void *eventData) {
581 StyleField field = (StyleField)(intptr_t)eventData;
582 uint8_t r, g, b;
583 Fl::get_color(styletable[field].color, r, g, b);
584 if (fl_color_chooser(w->label(), r, g, b)) {
585 styletable[field].color = fl_rgb_color(r, g, b);
586 _editor->styleChanged();
587 wnd->_profile->updateTheme();
588 wnd->_profile->setEditTheme(this);
589 wnd->updateConfig(this);
590 wnd->show();
591 }
592}
593
594/**
595 * replace text menu command handler
596 */
597void EditorWidget::show_replace(Fl_Widget *w, void *eventData) {
598 const char *prime = _editor->_search;
599 if (!prime || !prime[0]) {
600 // use selected text when search not available
601 prime = _editor->_textbuf->selection_text();
602 }
603 _commandText->value(prime);
604 setCommand(cmd_replace);
605}
606
607/**
608 * undo any edit changes
609 */
610void EditorWidget::undo(Fl_Widget *w, void *eventData) {
611 Fl_Text_Editor::kf_undo(0, _editor);
612}
613
614/**
615 * de-select the button specified in the eventData
616 */
617void EditorWidget::un_select(Fl_Widget *w, void *eventData) {
618 ((Fl_Button *)eventData)->value(false);
619}
620
621//--Public methods--------------------------------------------------------------
622
623/**
624 * handles saving the current buffer
625 */
626bool EditorWidget::checkSave(bool discard) {
627 if (!_dirty) {
628 // continue next operation
629 return true;
630 }
631
632 const char *msg = "The current file has not been saved\n\nWould you like to save it now?";
633 int r;
634 if (discard) {
635 r = fl_choice(msg, "Save", "Discard", "Cancel", NULL);
636 } else {
637 r = fl_choice(msg, "Save", "Cancel", NULL, NULL);
638 }
639 if (r == 0) {
640 // save selected
641 save_file();
642 return !_dirty;
643 }
644
645 // continue operation when discard selected
646 return (discard && r == 1);
647}
648
649/**
650 * copy selection text to the clipboard
651 */
652void EditorWidget::copyText() {
653 if (!_tty->copySelection()) {
654 Fl_Text_Editor::kf_copy(0, _editor);
655 }
656}
657
658/**
659 * saves the editor buffer to the given file name
660 */
661void EditorWidget::doSaveFile(const char *newfile, bool force) {
662 if (!force && !_dirty && strcmp(newfile, _filename) == 0) {
663 // neither buffer or filename have changed
664 return;
665 }
666
667 char basfile[PATH_MAX];
668 Fl_Text_Buffer *textbuf = _editor->_textbuf;
669
670 if (wnd->_profile->createBackups() && access(newfile, 0) == 0) {
671 // rename any existing file as a backup
672 strcpy(basfile, newfile);
673 strcat(basfile, "~");
674 rename(newfile, basfile);
675 }
676
677 strcpy(basfile, newfile);
678 if (strchr(basfile, '.') == 0) {
679 strcat(basfile, ".bas");
680 }
681
682 if (textbuf->savefile(basfile)) {
683 fl_alert("Error writing to file \'%s\':\n%s.", basfile, strerror(errno));
684 return;
685 }
686
687 _dirty = 0;
688 strcpy(_filename, basfile);
689 _modifiedTime = getModifiedTime();
690
691 wnd->updateEditTabName(this);
692 wnd->showEditTab(this);
693
694 // store a copy in lastedit.bas
695 if (wnd->_profile->createBackups()) {
696 getHomeDir(basfile, sizeof(basfile));
697 strlcat(basfile, "lastedit.bas", sizeof(basfile));
698 textbuf->savefile(basfile);
699 }
700
701 textbuf->call_modify_callbacks();
702 showPath();
703 fileChanged(true);
704 addHistory(_filename);
705 _editor->take_focus();
706}
707
708/**
709 * called when the buffer has changed
710 */
711void EditorWidget::fileChanged(bool loadfile) {
712 _funcList->clear();
713 if (loadfile) {
714 // update the func/sub navigator
715 createFuncList();
716 _funcList->redraw();
717
718 const char *filename = getFilename();
719 if (filename && filename[0]) {
720 // update the last used file menu
721 bool found = false;
722
723 for (int i = 0; i < NUM_RECENT_ITEMS; i++) {
724 if (recentPath[i].c_str() != NULL &&
725 strcmp(filename, recentPath[i].c_str()) == 0) {
726 found = true;
727 break;
728 }
729 }
730
731 if (found == false) {
732 const char *label = FileWidget::splitPath(filename, NULL);
733
734 // shift items downwards
735 for (int i = NUM_RECENT_ITEMS - 1; i > 0; i--) {
736 _menuBar->replace(recentMenu[i], recentLabel[i - 1]);
737 recentLabel[i].clear();
738 recentLabel[i].append(recentLabel[i - 1]);
739 recentPath[i].clear();
740 recentPath[i].append(recentPath[i - 1]);
741 }
742 // create new item in first position
743 recentLabel[0].clear();
744 recentLabel[0].append(label);
745 recentPath[0].clear();
746 recentPath[0].append(filename);
747 _menuBar->replace(recentMenu[0], recentLabel[0]);
748 }
749 }
750 }
751
752 _funcList->add(scanLabel);
753}
754
755/**
756 * keyboard shortcut handler
757 */
758bool EditorWidget::focusWidget() {
759 switch (Fl::event_key()) {
760 case 'b':
761 setBreakToLine(!isBreakToLine());
762 return true;
763
764 case 'e':
765 setCommand(cmd_find);
766 take_focus();
767 return true;
768
769 case 'f':
770 if (strlen(_commandText->value()) > 0 && _commandOpt == cmd_find) {
771 // continue search - shift -> backward else forward
772 command(0, (void *)(intptr_t)(Fl::event_state(FL_SHIFT) ? 0 : 1));
773 }
774 setCommand(cmd_find);
775 return true;
776
777 case 'i':
778 setCommand(cmd_find_inc);
779 return true;
780
781 case 't':
782 setLogPrint(!isLogPrint());
783 return true;
784
785 case 'w':
786 setHideIde(!isHideIDE());
787 return true;
788
789 case 'j':
790 setScrollLock(!isScrollLock());
791 return true;
792 }
793 return false;
794}
795
796/**
797 * returns the current font size
798 */
799int EditorWidget::getFontSize() {
800 return _editor->getFontSize();
801}
802
803/**
804 * use the input control as the INPUT basic command handler
805 */
806void EditorWidget::getInput(char *result, int size) {
807 if (result && result[0]) {
808 _commandText->value(result);
809 }
810 setCommand(cmd_input_text);
811 wnd->setModal(true);
812 while (wnd->isModal()) {
813 Fl::wait();
814 }
815 if (wnd->isBreakExec()) {
816 brun_break();
817 } else {
818 strlcpy(result, _commandText->value(), size);
819 }
820 setCommand(cmd_find);
821}
822
823/**
824 * returns the row and col position for the current cursor position
825 */
826void EditorWidget::getRowCol(int *row, int *col) {
827 return ((BasicEditor *)_editor)->getRowCol(row, col);
828}
829
830/**
831 * returns the selected text or the word around the cursor if there
832 * is no current selection. caller must free the returned value
833 */
834char *EditorWidget::getSelection(int *start, int *end) {
835 char *result = 0;
836
837 Fl_Text_Buffer *tb = _editor->_textbuf;
838 if (tb->selected()) {
839 result = tb->selection_text();
840 tb->selection_position(start, end);
841 } else {
842 int pos = _editor->insert_position();
843 *start = tb->word_start(pos);
844 *end = tb->word_end(pos);
845 result = tb->text_range(*start, *end);
846 }
847
848 return result;
849}
850
851/**
852 * returns where text selection ends
853 */
854void EditorWidget::getSelEndRowCol(int *row, int *col) {
855 return ((BasicEditor *)_editor)->getSelEndRowCol(row, col);
856}
857
858/**
859 * returns where text selection starts
860 */
861void EditorWidget::getSelStartRowCol(int *row, int *col) {
862 return ((BasicEditor *)_editor)->getSelStartRowCol(row, col);
863}
864
865/**
866 * sets the cursor to the given line number
867 */
868void EditorWidget::gotoLine(int line) {
869 ((BasicEditor *)_editor)->gotoLine(line);
870}
871
872/**
873 * FLTK event handler
874 */
875int EditorWidget::handle(int e) {
876 switch (e) {
877 case FL_SHOW:
878 case FL_FOCUS:
879 Fl::focus(_editor);
880 handleFileChange();
881 return 1;
882 case FL_KEYBOARD:
883 if (Fl::event_key() == FL_Escape) {
884 take_focus();
885 return 1;
886 }
887 break;
888 case FL_ENTER:
889 if (rename_active) {
890 // prevent drawing over the inplace editor child control
891 return 0;
892 }
893 }
894
895 return Fl_Group::handle(e);
896}
897
898/**
899 * load the given filename into the buffer
900 */
901void EditorWidget::loadFile(const char *newfile) {
902 // save the current filename
903 char oldpath[PATH_MAX];
904 strcpy(oldpath, _filename);
905
906 // convert relative path to full path
907 getcwd(_filename, sizeof(_filename));
908 strcat(_filename, "/");
909 strcat(_filename, newfile);
910
911 if (access(_filename, R_OK) != 0) {
912 // filename unreadable, try newfile
913 strcpy(_filename, newfile);
914 }
915
916 FileWidget::forwardSlash(_filename);
917
918 _loading = true;
919 if (_editor->_textbuf->loadfile(_filename)) {
920 // read failed
921 fl_alert("Error reading from file \'%s\':\n%s.", _filename, strerror(errno));
922
923 // restore previous file
924 strcpy(_filename, oldpath);
925 _editor->_textbuf->loadfile(_filename);
926 }
927
928 _dirty = false;
929 _loading = false;
930
931 _editor->_textbuf->call_modify_callbacks();
932 _editor->show_insert_position();
933 _modifiedTime = getModifiedTime();
934 readonly(false);
935
936 wnd->updateEditTabName(this);
937 wnd->showEditTab(this);
938
939 showPath();
940 fileChanged(true);
941 setRowCol(1, 1);
942}
943
944/**
945 * returns the buffer readonly flag
946 */
947bool EditorWidget::readonly() {
948 return ((BasicEditor *)_editor)->_readonly;
949}
950
951/**
952 * sets the buffer readonly flag
953 */
954void EditorWidget::readonly(bool is_readonly) {
955 if (!is_readonly && access(_filename, W_OK) != 0) {
956 // cannot set writable since file is readonly
957 is_readonly = true;
958 }
959 _modStatus->label(is_readonly ? "RO" : "@line");
960 _modStatus->redraw();
961 _editor->cursor_style(is_readonly ? Fl_Text_Display::DIM_CURSOR : Fl_Text_Display::NORMAL_CURSOR);
962 ((BasicEditor *)_editor)->_readonly = is_readonly;
963}
964
965/**
966 * displays the current run-mode flag
967 */
968void EditorWidget::runState(RunMessage runMessage) {
969 _runStatus->callback(MainWindow::run_cb);
970 const char *msg = 0;
971 switch (runMessage) {
972 case rs_err:
973 msg = "ERR";
974 break;
975 case rs_run:
976 msg = "BRK";
977 _runStatus->callback(MainWindow::run_break_cb);
978 break;
979 default:
980 msg = "RUN";
981 }
982 _runStatus->copy_label(msg);
983 _runStatus->redraw();
984}
985
986/**
987 * Saves the selected text to the given file path
988 */
989void EditorWidget::saveSelection(const char *path) {
990 FILE *fp = fopen(path, "w");
991 if (fp) {
992 Fl_Rect rc;
993 char *selection = getSelection(&rc);
994 if (selection) {
995 fwrite(selection, strlen(selection), 1, fp);
996 free((void *)selection);
997 } else {
998 // save as an empty file
999 fputc(0, fp);
1000 }
1001 fclose(fp);
1002 }
1003}
1004
1005/**
1006 * Sets the editor and editor toolbar color from the selected theme
1007 */
1008void EditorWidget::setTheme(EditTheme *theme) {
1009 _editor->color(get_color(theme->_background));
1010 _editor->linenumber_bgcolor(get_color(theme->_background));
1011 _editor->linenumber_fgcolor(get_color(theme->_number_color));
1012 _editor->cursor_color(get_color(theme->_cursor_color));
1013 _editor->selection_color(get_color(theme->_selection_background));
1014 _funcList->color(fl_color_average(get_color(theme->_background), get_color(theme->_color), .92f));
1015 _funcList->item_labelfgcolor(get_color(theme->_color));
1016 _tty->color(_editor->color());
1017 _tty->labelcolor(fl_contrast(_tty->color(), get_color(theme->_background)));
1018 _tty->selection_color(_editor->selection_color());
1019 resetList();
1020}
1021
1022/**
1023 * sets the current display font
1024 */
1025void EditorWidget::setFont(Fl_Font font) {
1026 if (font) {
1027 _editor->setFont(font);
1028 _tty->setFont(font);
1029 wnd->_profile->setFont(font);
1030 }
1031}
1032
1033/**
1034 * sets the current font size
1035 */
1036void EditorWidget::setFontSize(int size) {
1037 _editor->setFontSize(size);
1038 _tty->setFontSize(size);
1039 wnd->_profile->setFontSize(size);
1040}
1041
1042/**
1043 * sets the indent level to the given amount
1044 */
1045void EditorWidget::setIndentLevel(int level) {
1046 ((BasicEditor *)_editor)->_indentLevel = level;
1047}
1048
1049/**
1050 * displays the row/col in the editor toolbar
1051 */
1052void EditorWidget::setRowCol(int row, int col) {
1053 char rowcol[20];
1054 sprintf(rowcol, "%d", row);
1055 _rowStatus->copy_label(rowcol);
1056 _rowStatus->redraw();
1057 sprintf(rowcol, "%d", col);
1058 _colStatus->copy_label(rowcol);
1059 _colStatus->redraw();
1060 if (!_funcListEvent) {
1061 selectRowInBrowser(row);
1062 } else {
1063 _funcListEvent = false;
1064 }
1065}
1066
1067/**
1068 * display the full pathname
1069 */
1070void EditorWidget::showPath() {
1071 _commandChoice->tooltip(_filename);
1072}
1073
1074/**
1075 * prints a status message on the tty-widget
1076 */
1077void EditorWidget::statusMsg(const char *msg) {
1078 if (msg) {
1079 _tty->print(msg);
1080 _tty->print("\n");
1081 }
1082}
1083
1084/**
1085 * sets the font face, size and colour
1086 */
1087void EditorWidget::updateConfig(EditorWidget *current) {
1088 setFont(current->_editor->getFont());
1089 setFontSize(current->_editor->getFontSize());
1090}
1091
1092//--Protected methods-----------------------------------------------------------
1093
1094/**
1095 * add filename to the hiistory file
1096 */
1097void EditorWidget::addHistory(const char *filename) {
1098 FILE *fp;
1099 char buffer[PATH_MAX];
1100 char updatedfile[PATH_MAX];
1101 char path[PATH_MAX];
1102
1103 int len = strlen(filename);
1104 if (strcasecmp(filename + len - 4, ".sbx") == 0 || access(filename, R_OK) != 0) {
1105 // don't remember bas exe or invalid files
1106 return;
1107 }
1108
1109 len -= strlen(untitledFile);
1110 if (len > 0 && strcmp(filename + len, untitledFile) == 0) {
1111 // don't remember the untitled file
1112 return;
1113 }
1114 // save paths with unix path separators
1115 strcpy(updatedfile, filename);
1116 FileWidget::forwardSlash(updatedfile);
1117 filename = updatedfile;
1118
1119 // save into the history file
1120 getHomeDir(path, sizeof(path));
1121 strlcat(path, historyFile, sizeof(path));
1122
1123 fp = fopen(path, "r");
1124 if (fp) {
1125 // don't add the item if it already exists
1126 while (feof(fp) == 0) {
1127 if (fgets(buffer, sizeof(buffer), fp) && strncmp(filename, buffer, strlen(filename) - 1) == 0) {
1128 fclose(fp);
1129 return;
1130 }
1131 }
1132 fclose(fp);
1133 }
1134
1135 fp = fopen(path, "a");
1136 if (fp) {
1137 fwrite(filename, strlen(filename), 1, fp);
1138 fwrite("\n", 1, 1, fp);
1139 fclose(fp);
1140 }
1141}
1142
1143/**
1144 * creates the sub/func selection list
1145 */
1146void EditorWidget::createFuncList() {
1147 Fl_Text_Buffer *textbuf = _editor->_textbuf;
1148 char *text = textbuf->text();
1149 int len = textbuf->length();
1150 int curLine = 1;
1151 const char *keywords[] = {
1152 "sub ", "func ", "def ", "label ", "const ", "local ", "dim "
1153 };
1154 int keywords_length = sizeof(keywords) / sizeof(keywords[0]);
1155 int keywords_len[keywords_length];
1156 for (int j = 0; j < keywords_length; j++) {
1157 keywords_len[j] = strlen(keywords[j]);
1158 }
1159 Fl_Tree_Item *menuGroup = NULL;
1160
1161 for (int i = 0; i < len; i++) {
1162 // skip to the newline start
1163 while (i < len && i != 0 && text[i] != '\n') {
1164 i++;
1165 }
1166
1167 // skip any successive newlines
1168 while (i < len && text[i] == '\n') {
1169 curLine++;
1170 i++;
1171 }
1172
1173 // skip any leading whitespace
1174 while (i < len && (text[i] == ' ' || text[i] == '\t')) {
1175 i++;
1176 }
1177
1178 for (int j = 0; j < keywords_length; j++) {
1179 if (!strncasecmp(text + i, keywords[j], keywords_len[j])) {
1180 i += keywords_len[j];
1181 int i_begin = i;
1182 while (i < len && text[i] != '=' && text[i] != '\r' && text[i] != '\n') {
1183 i++;
1184 }
1185 if (i > i_begin) {
1186 String s(text + i_begin, i - i_begin);
1187 if (j < 2) {
1188 menuGroup = new Fl_Tree_Item(_funcList);
1189 menuGroup->label(s.c_str());
1190 menuGroup->user_data((void *)(intptr_t)curLine);
1191 _funcList->add(s.c_str(), menuGroup);
1192 } else if (menuGroup != NULL) {
1193 Fl_Tree_Item *leaf = new Fl_Tree_Item(_funcList);
1194 leaf->label(s.c_str());
1195 leaf->user_data((void *)(intptr_t)curLine);
1196 menuGroup->add(_funcList->prefs(), s.c_str(), leaf);
1197 }
1198 }
1199 break;
1200 }
1201 }
1202 if (text[i] == '\n') {
1203 i--; // avoid eating the entire next line
1204 }
1205 }
1206 free(text);
1207}
1208
1209/**
1210 * called when the buffer has change - sets the modified flag
1211 */
1212void EditorWidget::doChange(int inserted, int deleted) {
1213 if (!_loading) {
1214 // do nothing while file load in progress
1215 if (inserted || deleted) {
1216 _dirty = 1;
1217 }
1218
1219 if (!readonly()) {
1220 setModified(_dirty);
1221 }
1222 }
1223}
1224
1225/**
1226 * handler for the sub/func list selection event
1227 */
1228void EditorWidget::findFunc(const char *find) {
1229 char *text = _editor->_textbuf->text();
1230 int findLen = strlen(find);
1231 int len = _editor->_textbuf->length();
1232 int lineNo = 1;
1233 for (int i = 0; i < len; i++) {
1234 if (strncasecmp(text + i, find, findLen) == 0) {
1235 gotoLine(lineNo);
1236 break;
1237 } else if (text[i] == '\n') {
1238 lineNo++;
1239 }
1240 }
1241 free(text);
1242}
1243
1244/**
1245 * returns the current selection text
1246 */
1247char *EditorWidget::getSelection(Fl_Rect *rc) {
1248 return ((BasicEditor *)_editor)->getSelection(rc);
1249}
1250
1251/**
1252 * returns the current file modified time
1253 */
1254uint32_t EditorWidget::getModifiedTime() {
1255 struct stat st_file;
1256 uint32_t modified = 0;
1257 if (_filename[0] && !stat(_filename, &st_file)) {
1258 modified = st_file.st_mtime;
1259 }
1260 return modified;
1261}
1262
1263/**
1264 * handler for the external file change event
1265 */
1266void EditorWidget::handleFileChange() {
1267 // handle outside changes to the file
1268 if (_filename[0] && _modifiedTime != 0 && _modifiedTime != getModifiedTime()) {
1269 String st;
1270 st.append("File")
1271 .append(_filename)
1272 .append("has changed on disk.\n\n")
1273 .append("Do you want to reload the file?");
1274 if (fl_choice(st.c_str(), "Yes", "No", NULL, NULL) == 0) {
1275 reloadFile();
1276 } else {
1277 _modifiedTime = 0;
1278 }
1279 }
1280}
1281
1282/**
1283 * reset the function list
1284 */
1285void EditorWidget::resetList() {
1286 _funcList->clear();
1287 createFuncList();
1288 _funcList->add(scanLabel);
1289}
1290
1291/**
1292 * resize the heights
1293 */
1294void EditorWidget::resize(int x, int y, int w, int h) {
1295 Fl_Group *tile = _editor->parent();
1296 int edit_scale_w = 1 + (_editor->w() * 100 / tile->w());
1297 int edit_scale_h = 1 + (_editor->h() * 100 / tile->h());
1298 edit_scale_w = MIN(80, edit_scale_w);
1299 edit_scale_h = MIN(80, edit_scale_h);
1300
1301 tile->resizable(_editor);
1302 Fl_Group::resize(x, y, w, h);
1303 tile->resize(tile->x(), y, w, h - STATUS_HEIGHT);
1304 tile->resizable(NULL);
1305
1306 int status_y = y + h - STATUS_HEIGHT;
1307 int edit_w = tile->w() * edit_scale_w / 100;
1308 int edit_h = tile->h() * edit_scale_h / 100;
1309 int tty_y = tile->y() + edit_h;
1310 int tty_h = status_y - tty_y;
1311 int func_x = tile->x() + edit_w;
1312 int func_w = tile->w() - edit_w;
1313
1314 _editor->resize(_editor->x(), _editor->y(), edit_w, edit_h);
1315 _funcList->resize(func_x, _funcList->y(), func_w, _editor->h());
1316 _tty->resize(_tty->x(), tty_y, _tty->w(), tty_h);
1317 _statusBar->resize(_statusBar->x(), status_y, _statusBar->w(), STATUS_HEIGHT);
1318}
1319
1320/**
1321 * create a new editor buffer
1322 */
1323void EditorWidget::newFile() {
1324 if (!readonly() && checkSave(true)) {
1325 Fl_Text_Buffer *textbuf = _editor->_textbuf;
1326 _filename[0] = '\0';
1327 textbuf->select(0, textbuf->length());
1328 textbuf->remove_selection();
1329 _dirty = 0;
1330 textbuf->call_modify_callbacks();
1331 fileChanged(false);
1332 _modifiedTime = 0;
1333 }
1334}
1335
1336/**
1337 * reload the editor buffer
1338 */
1339void EditorWidget::reloadFile() {
1340 char buffer[PATH_MAX];
1341 strcpy(buffer, _filename);
1342 loadFile(buffer);
1343}
1344
1345/**
1346 * replace all occurances of the given text
1347 */
1348int EditorWidget::replaceAll(const char *find, const char *replace, bool restorePos, bool matchWord) {
1349 int times = 0;
1350
1351 if (strcmp(find, replace) != 0) {
1352 Fl_Text_Buffer *textbuf = _editor->_textbuf;
1353 int prevPos = _editor->insert_position();
1354
1355 // loop through the whole string
1356 int pos = 0;
1357 _editor->insert_position(pos);
1358
1359 while (textbuf->search_forward(pos, find, &pos)) {
1360 // found a match; update the position and replace text
1361 if (!matchWord ||
1362 ((pos == 0 || !isvar(textbuf->char_at(pos - 1))) &&
1363 !isvar(textbuf->char_at(pos + strlen(find))))) {
1364 textbuf->select(pos, pos + strlen(find));
1365 textbuf->remove_selection();
1366 textbuf->insert(pos, replace);
1367 }
1368 // advance beyond replace string
1369 pos += strlen(replace);
1370 _editor->insert_position(pos);
1371 times++;
1372 }
1373
1374 if (restorePos) {
1375 _editor->insert_position(prevPos);
1376 }
1377 _editor->show_insert_position();
1378 }
1379
1380 return times;
1381}
1382
1383/**
1384 * handler for searching backwards
1385 */
1386bool EditorWidget::searchBackward(const char *text, int startPos,
1387 const char *find, int findLen, int *foundPos) {
1388 int matchIndex = findLen - 1;
1389 for (int i = startPos; i >= 0; i--) {
1390 bool equals = toupper(text[i]) == toupper(find[matchIndex]);
1391 if (equals == false && matchIndex < findLen - 1) {
1392 // partial match now fails - reset search at current index
1393 matchIndex = findLen - 1;
1394 equals = toupper(text[i]) == toupper(find[matchIndex]);
1395 }
1396 matchIndex = (equals ? matchIndex - 1 : findLen - 1);
1397 if (matchIndex == -1 && (i == 0 || isalpha(text[i - 1]) == 0)) {
1398 // char prior to word is non-alpha
1399 *foundPos = i;
1400 return true;
1401 }
1402 }
1403 return false;
1404}
1405
1406/**
1407 * sync the browser widget selection
1408 */
1409void EditorWidget::selectRowInBrowser(int row) {
1410 Fl_Tree_Item *root = _funcList->root();
1411 int len = root->children() - 1;
1412 bool found = false;
1413 for (int i = 0; i < len; i++) {
1414 int line = (int)(intptr_t)root->child(i)->user_data();
1415 int nextLine = (int)(intptr_t)root->child(i + 1)->user_data();
1416 if (row >= line && (i == len - 1 || row < nextLine)) {
1417 int y = _funcList->vposition() + root->child(i)->y();
1418 int bottom = _funcList->y() + _funcList->h();
1419 int pos = bottom - y - (_funcList->h() / 2);
1420 _funcList->select_only(root->child(i), 0);
1421 _funcList->vposition(-pos);
1422 found = true;
1423 break;
1424 }
1425 }
1426 if (!found) {
1427 _funcList->select_only(root->child(0), 0);
1428 _funcList->vposition(0);
1429 }
1430}
1431
1432/**
1433 * sets the current command
1434 */
1435void EditorWidget::setCommand(CommandOpt command) {
1436 if (_commandOpt == cmd_input_text) {
1437 wnd->setModal(false);
1438 }
1439
1440 _commandOpt = command;
1441 switch (command) {
1442 case cmd_find:
1443 _commandChoice->label("@search Find:");
1444 break;
1445 case cmd_find_inc:
1446 _commandChoice->label("Inc Find:");
1447 break;
1448 case cmd_replace:
1449 _commandChoice->label("Replace:");
1450 break;
1451 case cmd_replace_with:
1452 _commandChoice->label("With:");
1453 break;
1454 case cmd_goto:
1455 _commandChoice->label("Goto:");
1456 break;
1457 case cmd_input_text:
1458 _commandChoice->label("INPUT:");
1459 break;
1460 }
1461 _commandChoice->redraw();
1462
1463 _commandText->color(_commandChoice->color());
1464 _commandText->redraw();
1465 _commandText->take_focus();
1466 _commandText->when(_commandOpt == cmd_find_inc ? FL_WHEN_CHANGED : FL_WHEN_ENTER_KEY_ALWAYS);
1467}
1468
1469/**
1470 * display the toolbar modified flag
1471 */
1472void EditorWidget::setModified(bool dirty) {
1473 _dirty = dirty;
1474 _modStatus->when(dirty ? FL_WHEN_CHANGED : FL_WHEN_NEVER);
1475 _modStatus->label(dirty ? "MOD" : "@line");
1476 _modStatus->redraw();
1477}
1478
1479/**
1480 * highlight the given search text
1481 */
1482void EditorWidget::showFindText(const char *text) {
1483 _editor->showFindText(text);
1484}
1485
1486LineInput::LineInput(int x, int y, int w, int h) :
1487 Fl_Input(x, y, w, h),
1488 orig_x(x),
1489 orig_y(y),
1490 orig_w(w),
1491 orig_h(h) {
1492 when(FL_WHEN_ENTER_KEY);
1493 box(FL_BORDER_BOX);
1494 fl_color(fl_rgb_color(220, 220, 220));
1495 take_focus();
1496}
1497
1498/**
1499 * veto the layout changes
1500 */
1501void LineInput::resize(int x, int y, int w, int h) {
1502 Fl_Input::resize(orig_x, orig_y, orig_w, orig_h);
1503}
1504
1505int LineInput::handle(int event) {
1506 int result;
1507 if (event == FL_KEYBOARD) {
1508 if (Fl::event_state(FL_CTRL) && Fl::event_key() == 'b') {
1509 if (!wnd->isEdit()) {
1510 wnd->setBreak();
1511 }
1512 } else if (Fl::event_key(FL_Escape)) {
1513 do_callback();
1514 } else {
1515 // grow the input box width as text is entered
1516 const char *text = value();
1517 int strw = fl_width(text) + fl_width(value()) + 4;
1518 if (strw > w()) {
1519 w(strw);
1520 orig_w = strw;
1521 redraw();
1522 }
1523 }
1524 result = Fl_Input::handle(event);
1525 } else {
1526 result = Fl_Input::handle(event);
1527 }
1528 return result;
1529}
1530