| 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 |
| 23 | extern String recentPath[]; |
| 24 | extern String recentLabel[]; |
| 25 | extern int []; |
| 26 | extern const char *historyFile; |
| 27 | extern const char *untitledFile; |
| 28 | |
| 29 | // in BasicEditor.cxx |
| 30 | extern Fl_Text_Display::Style_Table_Entry styletable[]; |
| 31 | extern Fl_Color defaultColor[]; |
| 32 | |
| 33 | int completionIndex = 0; |
| 34 | |
| 35 | static bool rename_active = false; |
| 36 | const char scanLabel[] = "(Refresh)" ; |
| 37 | |
| 38 | EditorWidget *get_editor() { |
| 39 | EditorWidget *result = wnd->getEditor(); |
| 40 | if (!result) { |
| 41 | result = wnd->getEditor(true); |
| 42 | } |
| 43 | return result; |
| 44 | } |
| 45 | |
| 46 | struct 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 | |
| 51 | private: |
| 52 | int orig_x, orig_y, orig_w, orig_h; |
| 53 | }; |
| 54 | |
| 55 | //--EditorWidget---------------------------------------------------------------- |
| 56 | |
| 57 | EditorWidget::(Fl_Widget *rect, Fl_Menu_Bar *) : |
| 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 | |
| 190 | EditorWidget::~EditorWidget() { |
| 191 | delete _editor; |
| 192 | } |
| 193 | |
| 194 | //--Event handler methods------------------------------------------------------- |
| 195 | |
| 196 | /** |
| 197 | * change the selected text to upper/lower/camel case |
| 198 | */ |
| 199 | void 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 | */ |
| 240 | void 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 | */ |
| 247 | void EditorWidget::cut_text(Fl_Widget *w, void *eventData) { |
| 248 | Fl_Text_Editor::kf_cut(0, _editor); |
| 249 | } |
| 250 | |
| 251 | /** |
| 252 | * delete selected text |
| 253 | */ |
| 254 | void EditorWidget::do_delete(Fl_Widget *w, void *eventData) { |
| 255 | _editor->_textbuf->remove_selection(); |
| 256 | } |
| 257 | |
| 258 | /** |
| 259 | * perform keyword completion |
| 260 | */ |
| 261 | void 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 | */ |
| 400 | void EditorWidget::find(Fl_Widget *w, void *eventData) { |
| 401 | setCommand(cmd_find); |
| 402 | } |
| 403 | |
| 404 | /** |
| 405 | * clears the console |
| 406 | */ |
| 407 | void EditorWidget::clear_console(Fl_Widget *w, void *eventData) { |
| 408 | _tty->clearScreen(); |
| 409 | } |
| 410 | |
| 411 | /** |
| 412 | * performs the current command |
| 413 | */ |
| 414 | void 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 | */ |
| 456 | void 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 | */ |
| 475 | void EditorWidget::goto_line(Fl_Widget *w, void *eventData) { |
| 476 | setCommand(cmd_goto); |
| 477 | } |
| 478 | |
| 479 | /** |
| 480 | * paste clipboard text onto the buffer |
| 481 | */ |
| 482 | void 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 | */ |
| 489 | void 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 | */ |
| 524 | void 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 | */ |
| 553 | void 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 | */ |
| 566 | void EditorWidget::scroll_lock(Fl_Widget *w, void *eventData) { |
| 567 | _tty->setScrollLock(_lockBn->value()); |
| 568 | } |
| 569 | |
| 570 | /** |
| 571 | * select all text |
| 572 | */ |
| 573 | void 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 | */ |
| 580 | void 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 | */ |
| 597 | void 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 | */ |
| 610 | void 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 | */ |
| 617 | void 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 | */ |
| 626 | bool 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 | */ |
| 652 | void 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 | */ |
| 661 | void 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 | */ |
| 711 | void 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 | */ |
| 758 | bool 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 | */ |
| 799 | int EditorWidget::getFontSize() { |
| 800 | return _editor->getFontSize(); |
| 801 | } |
| 802 | |
| 803 | /** |
| 804 | * use the input control as the INPUT basic command handler |
| 805 | */ |
| 806 | void 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 | */ |
| 826 | void 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 | */ |
| 834 | char *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 | */ |
| 854 | void EditorWidget::getSelEndRowCol(int *row, int *col) { |
| 855 | return ((BasicEditor *)_editor)->getSelEndRowCol(row, col); |
| 856 | } |
| 857 | |
| 858 | /** |
| 859 | * returns where text selection starts |
| 860 | */ |
| 861 | void 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 | */ |
| 868 | void EditorWidget::gotoLine(int line) { |
| 869 | ((BasicEditor *)_editor)->gotoLine(line); |
| 870 | } |
| 871 | |
| 872 | /** |
| 873 | * FLTK event handler |
| 874 | */ |
| 875 | int 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 | */ |
| 901 | void 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 | */ |
| 947 | bool EditorWidget::readonly() { |
| 948 | return ((BasicEditor *)_editor)->_readonly; |
| 949 | } |
| 950 | |
| 951 | /** |
| 952 | * sets the buffer readonly flag |
| 953 | */ |
| 954 | void 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 | */ |
| 968 | void 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 | */ |
| 989 | void 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 | */ |
| 1008 | void 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 | */ |
| 1025 | void 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 | */ |
| 1036 | void 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 | */ |
| 1045 | void EditorWidget::setIndentLevel(int level) { |
| 1046 | ((BasicEditor *)_editor)->_indentLevel = level; |
| 1047 | } |
| 1048 | |
| 1049 | /** |
| 1050 | * displays the row/col in the editor toolbar |
| 1051 | */ |
| 1052 | void 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 | */ |
| 1070 | void EditorWidget::showPath() { |
| 1071 | _commandChoice->tooltip(_filename); |
| 1072 | } |
| 1073 | |
| 1074 | /** |
| 1075 | * prints a status message on the tty-widget |
| 1076 | */ |
| 1077 | void 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 | */ |
| 1087 | void 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 | */ |
| 1097 | void 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 | */ |
| 1146 | void 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 * = 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 | */ |
| 1212 | void 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 | */ |
| 1228 | void 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 | */ |
| 1247 | char *EditorWidget::getSelection(Fl_Rect *rc) { |
| 1248 | return ((BasicEditor *)_editor)->getSelection(rc); |
| 1249 | } |
| 1250 | |
| 1251 | /** |
| 1252 | * returns the current file modified time |
| 1253 | */ |
| 1254 | uint32_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 | */ |
| 1266 | void 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 | */ |
| 1285 | void EditorWidget::resetList() { |
| 1286 | _funcList->clear(); |
| 1287 | createFuncList(); |
| 1288 | _funcList->add(scanLabel); |
| 1289 | } |
| 1290 | |
| 1291 | /** |
| 1292 | * resize the heights |
| 1293 | */ |
| 1294 | void 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 | */ |
| 1323 | void 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 | */ |
| 1339 | void 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 | */ |
| 1348 | int 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 | */ |
| 1386 | bool 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 | */ |
| 1409 | void 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 | */ |
| 1435 | void 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 | */ |
| 1472 | void 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 | */ |
| 1482 | void EditorWidget::showFindText(const char *text) { |
| 1483 | _editor->showFindText(text); |
| 1484 | } |
| 1485 | |
| 1486 | LineInput::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 | */ |
| 1501 | void LineInput::resize(int x, int y, int w, int h) { |
| 1502 | Fl_Input::resize(orig_x, orig_y, orig_w, orig_h); |
| 1503 | } |
| 1504 | |
| 1505 | int 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 | |