1 // Written in the D programming language. 2 3 /** 4 This module contains implementation of editors. 5 6 7 EditLine - single line editor. 8 9 EditBox - multiline editor 10 11 LogWidget - readonly text box for showing logs 12 13 Synopsis: 14 15 ---- 16 import dlangui.widgets.editors; 17 18 ---- 19 20 Copyright: Vadim Lopatin, 2014 21 License: Boost License 1.0 22 Authors: Vadim Lopatin, coolreader.org@gmail.com 23 */ 24 module dlangui.widgets.editors; 25 26 import dlangui.widgets.widget; 27 import dlangui.widgets.controls; 28 import dlangui.widgets.scroll; 29 import dlangui.widgets.layouts; 30 import dlangui.core.signals; 31 import dlangui.core.collections; 32 import dlangui.core.linestream; 33 import dlangui.platforms.common.platform; 34 import dlangui.widgets.menu; 35 import dlangui.widgets.popup; 36 import dlangui.graphics.colors; 37 public import dlangui.core.editable; 38 39 import std.algorithm; 40 import std.conv : to; 41 import std.typecons : Yes, No; 42 import dlangui.core.streams; 43 44 /// Modified state change listener 45 interface ModifiedStateListener { 46 void onModifiedStateChange(Widget source, bool modified); 47 } 48 49 /// Modified content listener 50 interface EditableContentChangeListener { 51 void onEditableContentChanged(EditableContent source); 52 } 53 54 /// editor state to display in status line 55 struct EditorStateInfo { 56 /// editor mode: true if replace mode, false if insert mode 57 bool replaceMode; 58 /// cursor position column (1-based) 59 int col; 60 /// cursor position line (1-based) 61 int line; 62 /// character under cursor 63 dchar character; 64 /// returns true if editor is in active state 65 @property bool active() { return col > 0 && line > 0; } 66 } 67 68 interface EditorStateListener { 69 void onEditorStateUpdate(Widget source, ref EditorStateInfo editorState); 70 } 71 72 /// Flags used for search / replace / text highlight 73 enum TextSearchFlag { 74 CaseSensitive = 1, 75 WholeWords = 2, 76 SelectionOnly = 4, 77 } 78 79 /// Editor action codes 80 enum EditorActions : int { 81 None = 0, 82 /// move cursor one char left 83 Left = 1000, 84 /// move cursor one char left with selection 85 SelectLeft, 86 /// move cursor one char right 87 Right, 88 /// move cursor one char right with selection 89 SelectRight, 90 /// move cursor one line up 91 Up, 92 /// move cursor one line up with selection 93 SelectUp, 94 /// move cursor one line down 95 Down, 96 /// move cursor one line down with selection 97 SelectDown, 98 /// move cursor one word left 99 WordLeft, 100 /// move cursor one word left with selection 101 SelectWordLeft, 102 /// move cursor one word right 103 WordRight, 104 /// move cursor one word right with selection 105 SelectWordRight, 106 /// move cursor one page up 107 PageUp, 108 /// move cursor one page up with selection 109 SelectPageUp, 110 /// move cursor one page down 111 PageDown, 112 /// move cursor one page down with selection 113 SelectPageDown, 114 /// move cursor to the beginning of page 115 PageBegin, 116 /// move cursor to the beginning of page with selection 117 SelectPageBegin, 118 /// move cursor to the end of page 119 PageEnd, 120 /// move cursor to the end of page with selection 121 SelectPageEnd, 122 /// move cursor to the beginning of line 123 LineBegin, 124 /// move cursor to the beginning of line with selection 125 SelectLineBegin, 126 /// move cursor to the end of line 127 LineEnd, 128 /// move cursor to the end of line with selection 129 SelectLineEnd, 130 /// move cursor to the beginning of document 131 DocumentBegin, 132 /// move cursor to the beginning of document with selection 133 SelectDocumentBegin, 134 /// move cursor to the end of document 135 DocumentEnd, 136 /// move cursor to the end of document with selection 137 SelectDocumentEnd, 138 /// delete char before cursor (backspace) 139 DelPrevChar, 140 /// delete char after cursor (del key) 141 DelNextChar, 142 /// delete word before cursor (ctrl + backspace) 143 DelPrevWord, 144 /// delete char after cursor (ctrl + del key) 145 DelNextWord, 146 147 /// insert new line (Enter) 148 InsertNewLine, 149 /// insert new line before current position (Ctrl+Enter) 150 PrependNewLine, 151 /// insert new line after current position (Ctrl+Enter) 152 AppendNewLine, 153 154 /// Turn On/Off replace mode 155 ToggleReplaceMode, 156 157 /// Copy selection to clipboard 158 Copy, 159 /// Cut selection to clipboard 160 Cut, 161 /// Paste selection from clipboard 162 Paste, 163 /// Undo last change 164 Undo, 165 /// Redo last undoed change 166 Redo, 167 168 /// Tab (e.g., Tab key to insert tab character or indent text) 169 Tab, 170 /// Tab (unindent text, or remove whitespace before cursor, usually Shift+Tab) 171 BackTab, 172 /// Indent text block or single line 173 Indent, 174 /// Unindent text 175 Unindent, 176 177 /// Select whole content (usually, Ctrl+A) 178 SelectAll, 179 180 // Scroll operations 181 182 /// Scroll one line up (not changing cursor) 183 ScrollLineUp, 184 /// Scroll one line down (not changing cursor) 185 ScrollLineDown, 186 /// Scroll one page up (not changing cursor) 187 ScrollPageUp, 188 /// Scroll one page down (not changing cursor) 189 ScrollPageDown, 190 /// Scroll window left 191 ScrollLeft, 192 /// Scroll window right 193 ScrollRight, 194 195 /// Zoom in editor font 196 ZoomIn, 197 /// Zoom out editor font 198 ZoomOut, 199 200 /// Togle line comment 201 ToggleLineComment, 202 /// Toggle block comment 203 ToggleBlockComment, 204 /// Delete current line 205 DeleteLine, 206 /// Insert line 207 InsertLine, 208 209 /// Toggle bookmark in current line 210 ToggleBookmark, 211 /// move cursor to next bookmark 212 GoToNextBookmark, 213 /// move cursor to previous bookmark 214 GoToPreviousBookmark, 215 216 /// Find text 217 Find, 218 /// Replace text 219 Replace, 220 221 /// Find next occurence - continue search forward 222 FindNext, 223 /// Find previous occurence - continue search backward 224 FindPrev, 225 } 226 227 228 void initStandardEditorActions() { 229 // register editor action names and ids 230 registerActionEnum!EditorActions(); 231 } 232 233 const Action ACTION_EDITOR_COPY = (new Action(EditorActions.Copy, "MENU_EDIT_COPY"c, null, KeyCode.KEY_C, KeyFlag.Control)).addAccelerator(KeyCode.INS, KeyFlag.Control).disableByDefault(); 234 const Action ACTION_EDITOR_PASTE = (new Action(EditorActions.Paste, "MENU_EDIT_PASTE"c, null, KeyCode.KEY_V, KeyFlag.Control)).addAccelerator(KeyCode.INS, KeyFlag.Shift).disableByDefault(); 235 const Action ACTION_EDITOR_CUT = (new Action(EditorActions.Cut, "MENU_EDIT_CUT"c, null, KeyCode.KEY_X, KeyFlag.Control)).addAccelerator(KeyCode.DEL, KeyFlag.Shift).disableByDefault(); 236 const Action ACTION_EDITOR_UNDO = (new Action(EditorActions.Undo, "MENU_EDIT_UNDO"c, null, KeyCode.KEY_Z, KeyFlag.Control)).disableByDefault(); 237 const Action ACTION_EDITOR_REDO = (new Action(EditorActions.Redo, "MENU_EDIT_REDO"c, null, KeyCode.KEY_Y, KeyFlag.Control)).addAccelerator(KeyCode.KEY_Z, KeyFlag.Control|KeyFlag.Shift).disableByDefault(); 238 239 const Action ACTION_EDITOR_INSERT_NEW_LINE = (new Action(EditorActions.InsertNewLine, KeyCode.RETURN, 0, ActionStateUpdateFlag.never)).addAccelerator(KeyCode.RETURN, KeyFlag.Shift); 240 const Action ACTION_EDITOR_PREPEND_NEW_LINE = (new Action(EditorActions.PrependNewLine, KeyCode.RETURN, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never)); 241 const Action ACTION_EDITOR_APPEND_NEW_LINE = (new Action(EditorActions.AppendNewLine, KeyCode.RETURN, KeyFlag.Control, ActionStateUpdateFlag.never)); 242 const Action ACTION_EDITOR_DELETE_LINE = (new Action(EditorActions.DeleteLine, KeyCode.KEY_D, KeyFlag.Control, ActionStateUpdateFlag.never)).addAccelerator(KeyCode.KEY_L, KeyFlag.Control); 243 const Action ACTION_EDITOR_TOGGLE_REPLACE_MODE = (new Action(EditorActions.ToggleReplaceMode, KeyCode.INS, 0, ActionStateUpdateFlag.never)); 244 const Action ACTION_EDITOR_SELECT_ALL = (new Action(EditorActions.SelectAll, KeyCode.KEY_A, KeyFlag.Control, ActionStateUpdateFlag.never)); 245 const Action ACTION_EDITOR_TOGGLE_LINE_COMMENT = (new Action(EditorActions.ToggleLineComment, KeyCode.KEY_DIVIDE, KeyFlag.Control)); 246 const Action ACTION_EDITOR_TOGGLE_BLOCK_COMMENT = (new Action(EditorActions.ToggleBlockComment, KeyCode.KEY_DIVIDE, KeyFlag.Control | KeyFlag.Shift)); 247 const Action ACTION_EDITOR_TOGGLE_BOOKMARK = (new Action(EditorActions.ToggleBookmark, "ACTION_EDITOR_TOGGLE_BOOKMARK"c, null, KeyCode.KEY_B, KeyFlag.Control | KeyFlag.Shift)); 248 const Action ACTION_EDITOR_GOTO_NEXT_BOOKMARK = (new Action(EditorActions.GoToNextBookmark, "ACTION_EDITOR_GOTO_NEXT_BOOKMARK"c, null, KeyCode.DOWN, KeyFlag.Control | KeyFlag.Shift | KeyFlag.Alt)); 249 const Action ACTION_EDITOR_GOTO_PREVIOUS_BOOKMARK = (new Action(EditorActions.GoToPreviousBookmark, "ACTION_EDITOR_GOTO_PREVIOUS_BOOKMARK"c, null, KeyCode.UP, KeyFlag.Control | KeyFlag.Shift | KeyFlag.Alt)); 250 const Action ACTION_EDITOR_FIND = (new Action(EditorActions.Find, "ACTION_EDITOR_FIND"c, null, KeyCode.KEY_F, KeyFlag.Control)); 251 const Action ACTION_EDITOR_FIND_NEXT = (new Action(EditorActions.FindNext, "ACTION_EDITOR_FIND_NEXT"c, null, KeyCode.F3, 0)); 252 const Action ACTION_EDITOR_FIND_PREV = (new Action(EditorActions.FindPrev, "ACTION_EDITOR_FIND_PREV"c, null, KeyCode.F3, KeyFlag.Shift)); 253 const Action ACTION_EDITOR_REPLACE = (new Action(EditorActions.Replace, "ACTION_EDITOR_REPLACE"c, null, KeyCode.KEY_H, KeyFlag.Control)); 254 255 const Action[] STD_EDITOR_ACTIONS = [ACTION_EDITOR_INSERT_NEW_LINE, ACTION_EDITOR_PREPEND_NEW_LINE, 256 ACTION_EDITOR_APPEND_NEW_LINE, ACTION_EDITOR_DELETE_LINE, ACTION_EDITOR_TOGGLE_REPLACE_MODE, 257 ACTION_EDITOR_SELECT_ALL, ACTION_EDITOR_TOGGLE_LINE_COMMENT, ACTION_EDITOR_TOGGLE_BLOCK_COMMENT, 258 ACTION_EDITOR_TOGGLE_BOOKMARK, ACTION_EDITOR_GOTO_NEXT_BOOKMARK, ACTION_EDITOR_GOTO_PREVIOUS_BOOKMARK, 259 ACTION_EDITOR_FIND, ACTION_EDITOR_REPLACE, 260 ACTION_EDITOR_FIND_NEXT, ACTION_EDITOR_FIND_PREV, 261 ACTION_EDITOR_COPY, ACTION_EDITOR_PASTE, ACTION_EDITOR_CUT, 262 ACTION_EDITOR_UNDO, ACTION_EDITOR_REDO 263 ]; 264 265 /// base for all editor widgets 266 class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemActionHandler { 267 protected EditableContent _content; 268 269 protected int _lineHeight; 270 protected Point _scrollPos; 271 protected bool _fixedFont; 272 protected int _spaceWidth; 273 protected int _leftPaneWidth; // left pane - can be used to show line numbers, collapse controls, bookmarks, breakpoints, custom icons 274 275 protected int _minFontSize = -1; // disable zooming 276 protected int _maxFontSize = -1; // disable zooming 277 278 protected bool _wantTabs = true; 279 protected bool _showLineNumbers = false; // show line numbers in left pane 280 protected bool _showModificationMarks = false; // show modification marks in left pane 281 protected bool _showIcons = false; // show icons in left pane 282 protected bool _showFolding = false; // show folding controls in left pane 283 protected int _lineNumbersWidth = 0; 284 protected int _modificationMarksWidth = 0; 285 protected int _iconsWidth = 0; 286 protected int _foldingWidth = 0; 287 288 protected bool _selectAllWhenFocusedWithTab = false; 289 protected bool _deselectAllWhenUnfocused = false; 290 291 protected bool _replaceMode; 292 293 protected uint _selectionColorFocused = 0xB060A0FF; 294 protected uint _selectionColorNormal = 0xD060A0FF; 295 protected uint _searchHighlightColorCurrent = 0x808080FF; 296 protected uint _searchHighlightColorOther = 0xC08080FF; 297 protected uint _leftPaneBackgroundColor = 0xF4F4F4; 298 protected uint _leftPaneBackgroundColor2 = 0xFFFFFF; 299 protected uint _leftPaneBackgroundColor3 = 0xF8F8F8; 300 protected uint _leftPaneLineNumberColor = 0x4060D0; 301 protected uint _leftPaneLineNumberColorEdited = 0xC0C000; 302 protected uint _leftPaneLineNumberColorSaved = 0x00C000; 303 protected uint _leftPaneLineNumberColorCurrentLine = 0xFFFFFFFF; 304 protected uint _leftPaneLineNumberBackgroundColorCurrentLine = 0xC08080FF; 305 protected uint _leftPaneLineNumberBackgroundColor = 0xF4F4F4; 306 protected uint _colorIconBreakpoint = 0xFF0000; 307 protected uint _colorIconBookmark = 0x0000FF; 308 protected uint _colorIconError = 0x80FF0000; 309 310 protected uint _caretColor = 0x000000; 311 protected uint _caretColorReplace = 0x808080FF; 312 protected uint _matchingBracketHightlightColor = 0x60FFE0B0; 313 314 protected uint _iconsPaneWidth = WIDGET_STYLE_CONSOLE ? 1 : 16; 315 protected uint _foldingPaneWidth = WIDGET_STYLE_CONSOLE ? 1 : 12; 316 protected uint _modificationMarksPaneWidth = WIDGET_STYLE_CONSOLE ? 1 : 4; 317 /// when true, call measureVisibileText on next layout 318 protected bool _contentChanged = true; 319 320 protected bool _copyCurrentLineWhenNoSelection = true; 321 /// when true allows copy / cut whole current line if there is no selection 322 @property bool copyCurrentLineWhenNoSelection() { return _copyCurrentLineWhenNoSelection; } 323 @property EditWidgetBase copyCurrentLineWhenNoSelection(bool flg) { _copyCurrentLineWhenNoSelection = flg; return this; } 324 325 protected bool _showTabPositionMarks = false; 326 /// when true shows mark on tab positions in beginning of line 327 @property bool showTabPositionMarks() { return _showTabPositionMarks; } 328 @property EditWidgetBase showTabPositionMarks(bool flg) { 329 if (flg != _showTabPositionMarks) { 330 _showTabPositionMarks = flg; 331 invalidate(); 332 } 333 return this; 334 } 335 336 /// Modified state change listener (e.g. content has been saved, or first time modified after save) 337 Signal!ModifiedStateListener modifiedStateChange; 338 339 /// Signal to emit when editor content is changed 340 Signal!EditableContentChangeListener contentChange; 341 342 /// Signal to emit when editor cursor position or Insert/Replace mode is changed. 343 Signal!EditorStateListener editorStateChange; 344 345 /// sets focus to this widget or suitable focusable child, returns previously focused widget 346 override Widget setFocus(FocusReason reason = FocusReason.Unspecified) { 347 Widget res = super.setFocus(reason); 348 if (focused) 349 handleEditorStateChange(); 350 return res; 351 } 352 353 /// updates editorStateChange with recent position 354 protected void handleEditorStateChange() { 355 if (!editorStateChange.assigned) 356 return; 357 EditorStateInfo info; 358 if (visible) { 359 info.replaceMode = _replaceMode; 360 info.line = _caretPos.line + 1; 361 info.col = _caretPos.pos + 1; 362 if (_caretPos.line >= 0 && _caretPos.line < _content.length) { 363 dstring line = _content.line(_caretPos.line); 364 if (_caretPos.pos >= 0 && _caretPos.pos < line.length) 365 info.character = line[_caretPos.pos]; 366 else 367 info.character = '\n'; 368 } 369 } 370 editorStateChange(this, info); 371 } 372 373 /// override to support modification of client rect after change, e.g. apply offset 374 override protected void handleClientRectLayout(ref Rect rc) { 375 updateLeftPaneWidth(); 376 rc.left += _leftPaneWidth; 377 } 378 379 /// override for multiline editors 380 protected int lineCount() { 381 return 1; 382 } 383 384 /// Override for EditBox 385 void wordWrapRefresh(){return;} 386 387 /// To hold _scrollpos.x toggling between normal and word wrap mode 388 int previousXScrollPos; 389 390 protected bool _wordWrap; 391 /// true if word wrap mode is set 392 @property bool wordWrap() { 393 return _wordWrap; 394 } 395 /// true if word wrap mode is set 396 @property EditWidgetBase wordWrap(bool v) { 397 _wordWrap = v; 398 //Horizontal scrollbar should not be visible in word wrap mode 399 if (v) 400 { 401 _hscrollbar.visibility(Visibility.Invisible); 402 previousXScrollPos = _scrollPos.x; 403 _scrollPos.x = 0; 404 wordWrapRefresh(); 405 } 406 else 407 { 408 _hscrollbar.visibility(Visibility.Visible); 409 _scrollPos.x = previousXScrollPos; 410 } 411 invalidate(); 412 return this; 413 } 414 415 /// Characters at which content is split for word wrap mode 416 dchar[] splitChars = [' ', '-', '\t']; 417 418 /// Divides up a string for word wrapping, sets info in _span 419 dstring[] wrapLine(dstring str, int lineNumber) { 420 FontRef font = font(); 421 dstring[] words = explode(str, splitChars); 422 int curLineLength = 0; 423 dchar[] buildingStr; 424 dstring[] buildingStrArr; 425 WrapPoint[] wrapPoints; 426 int wrappedLineCount = 0; 427 int curLineWidth = 0; 428 int maxWidth = _clientRect.width; 429 for (int i = 0; i < words.length; i++) 430 { 431 dstring word = words[i]; 432 if (curLineWidth + measureWrappedText(word) > maxWidth) 433 { 434 if (curLineWidth > 0) 435 { 436 buildingStrArr ~= to!dstring(buildingStr); 437 wrappedLineCount++; 438 wrapPoints ~= WrapPoint(curLineLength, curLineWidth); 439 curLineLength = 0; 440 curLineWidth = 0; 441 buildingStr = []; 442 } 443 while (measureWrappedText(word) > maxWidth) 444 { 445 //For when string still too long 446 int wrapPoint = findWrapPoint(word); 447 wrapPoints ~= WrapPoint(wrapPoint, measureWrappedText(word[0..wrapPoint])); 448 buildingStr ~= word[0 .. wrapPoint]; 449 word = word[wrapPoint .. $]; 450 buildingStrArr ~= to!dstring(buildingStr); 451 buildingStr = []; 452 wrappedLineCount++; 453 } 454 } 455 buildingStr ~= word; 456 curLineLength += to!int(word.length); 457 curLineWidth += measureWrappedText(word); 458 } 459 wrapPoints ~= WrapPoint(curLineLength, curLineWidth); 460 buildingStrArr ~= to!dstring(buildingStr); 461 _span ~= LineSpan(lineNumber, wrappedLineCount + 1, wrapPoints, buildingStrArr); 462 return buildingStrArr; 463 } 464 465 /// Divide (and conquer) text into words 466 dstring[] explode(dstring str, dchar[] splitChars) 467 { 468 dstring[] parts; 469 int startIndex = 0; 470 import std.string:indexOfAny; 471 while (true) 472 { 473 int index = to!int(str.indexOfAny(splitChars, startIndex)); 474 475 if (index == -1) 476 { 477 parts ~= str[startIndex .. $]; 478 //Log.d("Explode output: ", parts); 479 return parts; 480 } 481 482 dstring word = str[startIndex .. index]; 483 dchar nextChar = (str[index .. index + 1])[0]; 484 485 import std.ascii:isWhite; 486 if (isWhite(nextChar)) 487 { 488 parts ~= word; 489 parts ~= to!dstring(nextChar); 490 } 491 else 492 { 493 parts ~= word ~ nextChar; 494 } 495 startIndex = index + 1; 496 } 497 } 498 499 /// information about line span into several lines - in word wrap mode 500 protected LineSpan[] _span; 501 protected LineSpan[] _spanCache; 502 503 /// Finds good visual wrapping point for string 504 int findWrapPoint(dstring text) 505 { 506 int maxWidth = _clientRect.width; 507 int wrapPoint = 0; 508 while (true) 509 { 510 if (measureWrappedText(text[0 .. wrapPoint]) < maxWidth) 511 { 512 wrapPoint++; 513 } 514 else 515 { 516 return wrapPoint; 517 } 518 } 519 } 520 521 /// Calls measureText for word wrap 522 int measureWrappedText(dstring text) 523 { 524 FontRef font = font(); 525 int[] measuredWidths; 526 measuredWidths.length = text.length; 527 //DO NOT REMOVE THIS 528 int boggle = font.measureText(text, measuredWidths); 529 if (measuredWidths.length > 0) 530 return measuredWidths[$-1]; 531 return 0; 532 } 533 534 /// Returns number of visible wraps up to a line (not including the first wrapLines themselves) 535 int wrapsUpTo(int line) 536 { 537 int sum; 538 lineSpanIterate(delegate(LineSpan curSpan) 539 { 540 if (curSpan.start < line) 541 sum += curSpan.len - 1; 542 }); 543 return sum; 544 } 545 546 /// Returns LineSpan for line based on actual line number 547 LineSpan getSpan(int lineNumber) 548 { 549 LineSpan lineSpan = LineSpan(lineNumber, 0, [WrapPoint(0,0)], []); 550 lineSpanIterate(delegate(LineSpan curSpan) 551 { 552 if (curSpan.start == lineNumber) 553 lineSpan = curSpan; 554 }); 555 return lineSpan; 556 } 557 558 /// Based on a TextPosition, finds which wrapLine it is on for its current line 559 int findWrapLine(TextPosition textPos) 560 { 561 int curWrapLine = 0; 562 int curPosition = textPos.pos; 563 LineSpan curSpan = getSpan(textPos.line); 564 while (true) 565 { 566 if (curWrapLine == curSpan.wrapPoints.length - 1) 567 return curWrapLine; 568 curPosition -= curSpan.wrapPoints[curWrapLine].wrapPos; 569 if (curPosition < 0) 570 { 571 return curWrapLine; 572 } 573 curWrapLine++; 574 } 575 } 576 577 /// Simple way of iterating through _span 578 void lineSpanIterate(void delegate(LineSpan curSpan) iterator) 579 { 580 //TODO: Rename iterator to iteration? 581 foreach (currentSpan; _span) 582 iterator(currentSpan); 583 } 584 585 /// override to add custom items on left panel 586 protected void updateLeftPaneWidth() { 587 import std.conv : to; 588 _iconsWidth = _showIcons ? _iconsPaneWidth : 0; 589 _foldingWidth = _showFolding ? _foldingPaneWidth : 0; 590 _modificationMarksWidth = _showModificationMarks && (BACKEND_GUI || !_showLineNumbers) ? _modificationMarksPaneWidth : 0; 591 _lineNumbersWidth = 0; 592 if (_showLineNumbers) { 593 dchar[] s = to!(dchar[])(lineCount + 1); 594 foreach(ref ch; s) 595 ch = '9'; 596 FontRef fnt = font; 597 Point sz = fnt.textSize(cast(immutable)s); 598 _lineNumbersWidth = sz.x; 599 } 600 _leftPaneWidth = _lineNumbersWidth + _modificationMarksWidth + _foldingWidth + _iconsWidth; 601 if (_leftPaneWidth) 602 _leftPaneWidth += WIDGET_STYLE_CONSOLE ? 1 : 3; 603 } 604 605 protected void drawLeftPaneFolding(DrawBuf buf, Rect rc, int line) { 606 buf.fillRect(rc, _leftPaneBackgroundColor2); 607 } 608 609 protected void drawLeftPaneIcon(DrawBuf buf, Rect rc, LineIcon icon) { 610 if (!icon) 611 return; 612 if (icon.type == LineIconType.error) { 613 buf.fillRect(rc, _colorIconError); 614 } else if (icon.type == LineIconType.bookmark) { 615 int dh = rc.height / 4; 616 rc.top += dh; 617 rc.bottom -= dh; 618 buf.fillRect(rc, _colorIconBookmark); 619 } else if (icon.type == LineIconType.breakpoint) { 620 if (rc.height > rc.width) { 621 int delta = rc.height - rc.width; 622 rc.top += delta / 2; 623 rc.bottom -= (delta + 1) / 2; 624 } else { 625 int delta = rc.width - rc.height; 626 rc.left += delta / 2; 627 rc.right -= (delta + 1) / 2; 628 } 629 int dh = rc.height / 5; 630 rc.top += dh; 631 rc.bottom -= dh; 632 int dw = rc.width / 5; 633 rc.left += dw; 634 rc.right -= dw; 635 buf.fillRect(rc, _colorIconBreakpoint); 636 } 637 } 638 639 protected void drawLeftPaneIcons(DrawBuf buf, Rect rc, int line) { 640 buf.fillRect(rc, _leftPaneBackgroundColor3); 641 drawLeftPaneIcon(buf, rc, content.lineIcons.findByLineAndType(line, LineIconType.error)); 642 drawLeftPaneIcon(buf, rc, content.lineIcons.findByLineAndType(line, LineIconType.bookmark)); 643 drawLeftPaneIcon(buf, rc, content.lineIcons.findByLineAndType(line, LineIconType.breakpoint)); 644 } 645 646 protected void drawLeftPaneModificationMarks(DrawBuf buf, Rect rc, int line) { 647 if (line >= 0 && line < content.length) { 648 EditStateMark m = content.editMark(line); 649 if (m == EditStateMark.changed) { 650 // modified, not saved 651 buf.fillRect(rc, 0xFFD040); 652 } else if (m == EditStateMark.saved) { 653 // modified, saved 654 buf.fillRect(rc, 0x20C020); 655 } 656 } 657 } 658 659 protected void drawLeftPaneLineNumbers(DrawBuf buf, Rect rc, int line) { 660 import std.conv : to; 661 uint bgcolor = _leftPaneLineNumberBackgroundColor; 662 if (line == _caretPos.line && !isFullyTransparentColor(_leftPaneLineNumberBackgroundColorCurrentLine)) 663 bgcolor = _leftPaneLineNumberBackgroundColorCurrentLine; 664 buf.fillRect(rc, bgcolor); 665 if (line < 0) 666 return; 667 dstring s = to!dstring(line + 1); 668 FontRef fnt = font; 669 Point sz = fnt.textSize(s); 670 int x = rc.right - sz.x; 671 int y = rc.top + (rc.height - sz.y) / 2; 672 uint color = _leftPaneLineNumberColor; 673 if (line == _caretPos.line && !isFullyTransparentColor(_leftPaneLineNumberColorCurrentLine)) 674 color = _leftPaneLineNumberColorCurrentLine; 675 if (line >= 0 && line < content.length) { 676 EditStateMark m = content.editMark(line); 677 if (m == EditStateMark.changed) { 678 // modified, not saved 679 color = _leftPaneLineNumberColorEdited; 680 } else if (m == EditStateMark.saved) { 681 // modified, saved 682 color = _leftPaneLineNumberColorSaved; 683 } 684 } 685 fnt.drawText(buf, x, y, s, color); 686 } 687 688 protected bool onLeftPaneMouseClick(MouseEvent event) { 689 return false; 690 } 691 692 protected bool handleLeftPaneFoldingMouseClick(MouseEvent event, Rect rc, int line) { 693 return true; 694 } 695 696 protected bool handleLeftPaneModificationMarksMouseClick(MouseEvent event, Rect rc, int line) { 697 return true; 698 } 699 700 protected bool handleLeftPaneLineNumbersMouseClick(MouseEvent event, Rect rc, int line) { 701 return true; 702 } 703 704 protected MenuItem getLeftPaneIconsPopupMenu(int line) { 705 return null; 706 } 707 708 protected bool handleLeftPaneIconsMouseClick(MouseEvent event, Rect rc, int line) { 709 if (event.button == MouseButton.Right) { 710 MenuItem menu = getLeftPaneIconsPopupMenu(line); 711 if (menu) { 712 if (menu.openingSubmenu.assigned) 713 if (!menu.openingSubmenu(_popupMenu)) 714 return true; 715 menu.updateActionState(this); 716 PopupMenu popupMenu = new PopupMenu(menu); 717 popupMenu.menuItemAction = this; 718 PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, event.x, event.y); 719 popup.flags = PopupFlags.CloseOnClickOutside; 720 } 721 return true; 722 } 723 return true; 724 } 725 726 protected bool handleLeftPaneMouseClick(MouseEvent event, Rect rc, int line) { 727 rc.right -= 3; 728 if (_foldingWidth) { 729 Rect rc2 = rc; 730 rc.right = rc2.left = rc2.right - _foldingWidth; 731 if (event.x >= rc2.left && event.x < rc2.right) 732 return handleLeftPaneFoldingMouseClick(event, rc2, line); 733 } 734 if (_modificationMarksWidth) { 735 Rect rc2 = rc; 736 rc.right = rc2.left = rc2.right - _modificationMarksWidth; 737 if (event.x >= rc2.left && event.x < rc2.right) 738 return handleLeftPaneModificationMarksMouseClick(event, rc2, line); 739 } 740 if (_lineNumbersWidth) { 741 Rect rc2 = rc; 742 rc.right = rc2.left = rc2.right - _lineNumbersWidth; 743 if (event.x >= rc2.left && event.x < rc2.right) 744 return handleLeftPaneLineNumbersMouseClick(event, rc2, line); 745 } 746 if (_iconsWidth) { 747 Rect rc2 = rc; 748 rc.right = rc2.left = rc2.right - _iconsWidth; 749 if (event.x >= rc2.left && event.x < rc2.right) 750 return handleLeftPaneIconsMouseClick(event, rc2, line); 751 } 752 return true; 753 } 754 755 protected void drawLeftPane(DrawBuf buf, Rect rc, int line) { 756 // override for custom drawn left pane 757 buf.fillRect(rc, _leftPaneBackgroundColor); 758 //buf.fillRect(Rect(rc.right - 2, rc.top, rc.right - 1, rc.bottom), _leftPaneBackgroundColor2); 759 //buf.fillRect(Rect(rc.right - 1, rc.top, rc.right - 0, rc.bottom), _leftPaneBackgroundColor3); 760 rc.right -= WIDGET_STYLE_CONSOLE ? 1 : 3; 761 if (_foldingWidth) { 762 Rect rc2 = rc; 763 rc.right = rc2.left = rc2.right - _foldingWidth; 764 drawLeftPaneFolding(buf, rc2, line); 765 } 766 if (_modificationMarksWidth) { 767 Rect rc2 = rc; 768 rc.right = rc2.left = rc2.right - _modificationMarksWidth; 769 drawLeftPaneModificationMarks(buf, rc2, line); 770 } 771 if (_lineNumbersWidth) { 772 Rect rc2 = rc; 773 rc.right = rc2.left = rc2.right - _lineNumbersWidth; 774 drawLeftPaneLineNumbers(buf, rc2, line); 775 } 776 if (_iconsWidth) { 777 Rect rc2 = rc; 778 rc.right = rc2.left = rc2.right - _iconsWidth; 779 drawLeftPaneIcons(buf, rc2, line); 780 } 781 } 782 783 this(string ID, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) { 784 super(ID, hscrollbarMode, vscrollbarMode); 785 focusable = true; 786 acceleratorMap.add( [ 787 new Action(EditorActions.Up, KeyCode.UP, 0, ActionStateUpdateFlag.never), 788 new Action(EditorActions.SelectUp, KeyCode.UP, KeyFlag.Shift, ActionStateUpdateFlag.never), 789 new Action(EditorActions.SelectUp, KeyCode.UP, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 790 new Action(EditorActions.Down, KeyCode.DOWN, 0, ActionStateUpdateFlag.never), 791 new Action(EditorActions.SelectDown, KeyCode.DOWN, KeyFlag.Shift, ActionStateUpdateFlag.never), 792 new Action(EditorActions.SelectDown, KeyCode.DOWN, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 793 new Action(EditorActions.Left, KeyCode.LEFT, 0, ActionStateUpdateFlag.never), 794 new Action(EditorActions.SelectLeft, KeyCode.LEFT, KeyFlag.Shift, ActionStateUpdateFlag.never), 795 new Action(EditorActions.Right, KeyCode.RIGHT, 0, ActionStateUpdateFlag.never), 796 new Action(EditorActions.SelectRight, KeyCode.RIGHT, KeyFlag.Shift, ActionStateUpdateFlag.never), 797 new Action(EditorActions.WordLeft, KeyCode.LEFT, KeyFlag.Control, ActionStateUpdateFlag.never), 798 new Action(EditorActions.SelectWordLeft, KeyCode.LEFT, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 799 new Action(EditorActions.WordRight, KeyCode.RIGHT, KeyFlag.Control, ActionStateUpdateFlag.never), 800 new Action(EditorActions.SelectWordRight, KeyCode.RIGHT, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 801 new Action(EditorActions.PageUp, KeyCode.PAGEUP, 0, ActionStateUpdateFlag.never), 802 new Action(EditorActions.SelectPageUp, KeyCode.PAGEUP, KeyFlag.Shift, ActionStateUpdateFlag.never), 803 new Action(EditorActions.PageDown, KeyCode.PAGEDOWN, 0, ActionStateUpdateFlag.never), 804 new Action(EditorActions.SelectPageDown, KeyCode.PAGEDOWN, KeyFlag.Shift, ActionStateUpdateFlag.never), 805 new Action(EditorActions.PageBegin, KeyCode.PAGEUP, KeyFlag.Control, ActionStateUpdateFlag.never), 806 new Action(EditorActions.SelectPageBegin, KeyCode.PAGEUP, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 807 new Action(EditorActions.PageEnd, KeyCode.PAGEDOWN, KeyFlag.Control, ActionStateUpdateFlag.never), 808 new Action(EditorActions.SelectPageEnd, KeyCode.PAGEDOWN, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 809 new Action(EditorActions.LineBegin, KeyCode.HOME, 0, ActionStateUpdateFlag.never), 810 new Action(EditorActions.SelectLineBegin, KeyCode.HOME, KeyFlag.Shift, ActionStateUpdateFlag.never), 811 new Action(EditorActions.LineEnd, KeyCode.END, 0, ActionStateUpdateFlag.never), 812 new Action(EditorActions.SelectLineEnd, KeyCode.END, KeyFlag.Shift, ActionStateUpdateFlag.never), 813 new Action(EditorActions.DocumentBegin, KeyCode.HOME, KeyFlag.Control, ActionStateUpdateFlag.never), 814 new Action(EditorActions.SelectDocumentBegin, KeyCode.HOME, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 815 new Action(EditorActions.DocumentEnd, KeyCode.END, KeyFlag.Control, ActionStateUpdateFlag.never), 816 new Action(EditorActions.SelectDocumentEnd, KeyCode.END, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 817 818 new Action(EditorActions.ScrollLineUp, KeyCode.UP, KeyFlag.Control, ActionStateUpdateFlag.never), 819 new Action(EditorActions.ScrollLineDown, KeyCode.DOWN, KeyFlag.Control, ActionStateUpdateFlag.never), 820 821 // Backspace/Del 822 new Action(EditorActions.DelPrevChar, KeyCode.BACK, 0, ActionStateUpdateFlag.never), 823 new Action(EditorActions.DelNextChar, KeyCode.DEL, 0, ActionStateUpdateFlag.never), 824 new Action(EditorActions.DelPrevWord, KeyCode.BACK, KeyFlag.Control, ActionStateUpdateFlag.never), 825 new Action(EditorActions.DelNextWord, KeyCode.DEL, KeyFlag.Control, ActionStateUpdateFlag.never), 826 827 // Copy/Paste 828 new Action(EditorActions.Copy, KeyCode.KEY_C, KeyFlag.Control), 829 new Action(EditorActions.Copy, KeyCode.KEY_C, KeyFlag.Control | KeyFlag.Shift), 830 new Action(EditorActions.Copy, KeyCode.INS, KeyFlag.Control), 831 new Action(EditorActions.Cut, KeyCode.KEY_X, KeyFlag.Control), 832 new Action(EditorActions.Cut, KeyCode.KEY_X, KeyFlag.Control | KeyFlag.Shift), 833 new Action(EditorActions.Cut, KeyCode.DEL, KeyFlag.Shift), 834 new Action(EditorActions.Paste, KeyCode.KEY_V, KeyFlag.Control), 835 new Action(EditorActions.Paste, KeyCode.KEY_V, KeyFlag.Control | KeyFlag.Shift), 836 new Action(EditorActions.Paste, KeyCode.INS, KeyFlag.Shift), 837 838 // Undo/Redo 839 new Action(EditorActions.Undo, KeyCode.KEY_Z, KeyFlag.Control), 840 new Action(EditorActions.Redo, KeyCode.KEY_Y, KeyFlag.Control), 841 new Action(EditorActions.Redo, KeyCode.KEY_Z, KeyFlag.Control | KeyFlag.Shift), 842 843 new Action(EditorActions.Tab, KeyCode.TAB, 0, ActionStateUpdateFlag.never), 844 new Action(EditorActions.BackTab, KeyCode.TAB, KeyFlag.Shift, ActionStateUpdateFlag.never), 845 846 new Action(EditorActions.Find, KeyCode.KEY_F, KeyFlag.Control), 847 new Action(EditorActions.Replace, KeyCode.KEY_H, KeyFlag.Control), 848 ]); 849 acceleratorMap.add(STD_EDITOR_ACTIONS); 850 acceleratorMap.add([ACTION_EDITOR_FIND_NEXT, ACTION_EDITOR_FIND_PREV]); 851 } 852 853 /// 854 override bool onMenuItemAction(const Action action) { 855 return dispatchAction(action); 856 } 857 858 /// returns true if widget can show popup (e.g. by mouse right click at point x,y) 859 override bool canShowPopupMenu(int x, int y) { 860 if (_popupMenu is null) 861 return false; 862 if (_popupMenu.openingSubmenu.assigned) 863 if (!_popupMenu.openingSubmenu(_popupMenu)) 864 return false; 865 return true; 866 } 867 868 /// returns true if widget is focusable and visible and enabled 869 override @property bool canFocus() { 870 // allow to focus even if not enabled 871 return focusable && visible; 872 } 873 874 /// override to change popup menu items state 875 override bool isActionEnabled(const Action action) { 876 switch (action.id) with(EditorActions) 877 { 878 case Tab: 879 case BackTab: 880 case Indent: 881 case Unindent: 882 return enabled; 883 case Copy: 884 return _copyCurrentLineWhenNoSelection || !_selectionRange.empty; 885 case Cut: 886 return enabled && (_copyCurrentLineWhenNoSelection || !_selectionRange.empty); 887 case Paste: 888 return enabled && Platform.instance.hasClipboardText(); 889 case Undo: 890 return enabled && _content.hasUndo; 891 case Redo: 892 return enabled && _content.hasRedo; 893 case ToggleBookmark: 894 return _content.multiline; 895 case GoToNextBookmark: 896 return _content.multiline && _content.lineIcons.hasBookmarks; 897 case GoToPreviousBookmark: 898 return _content.multiline && _content.lineIcons.hasBookmarks; 899 case Replace: 900 return _content.multiline && !readOnly; 901 case Find: 902 case FindNext: 903 case FindPrev: 904 return _content.multiline; 905 default: 906 return super.isActionEnabled(action); 907 } 908 } 909 910 /// shows popup at (x,y) 911 override void showPopupMenu(int x, int y) { 912 /// if preparation signal handler assigned, call it; don't show popup if false is returned from handler 913 if (_popupMenu.openingSubmenu.assigned) 914 if (!_popupMenu.openingSubmenu(_popupMenu)) 915 return; 916 _popupMenu.updateActionState(this); 917 PopupMenu popupMenu = new PopupMenu(_popupMenu); 918 popupMenu.menuItemAction = this; 919 PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, x, y); 920 popup.flags = PopupFlags.CloseOnClickOutside; 921 } 922 923 void onPopupMenuItem(MenuItem item) { 924 // TODO 925 } 926 927 /// returns mouse cursor type for widget 928 override uint getCursorType(int x, int y) { 929 return x < _pos.left + _leftPaneWidth ? CursorType.Arrow : CursorType.IBeam; 930 } 931 932 /// set bool property value, for ML loaders 933 mixin(generatePropertySettersMethodOverride("setBoolProperty", "bool", 934 "wantTabs", "showIcons", "showFolding", "showModificationMarks", "showLineNumbers", "readOnly", "replaceMode", "useSpacesForTabs", "copyCurrentLineWhenNoSelection", "showTabPositionMarks")); 935 936 /// set int property value, for ML loaders 937 mixin(generatePropertySettersMethodOverride("setIntProperty", "int", 938 "tabSize")); 939 940 /// when true, Tab / Shift+Tab presses are processed internally in widget (e.g. insert tab character) instead of focus change navigation. 941 @property bool wantTabs() { 942 return _wantTabs; 943 } 944 945 /// ditto 946 @property EditWidgetBase wantTabs(bool wantTabs) { 947 _wantTabs = wantTabs; 948 return this; 949 } 950 951 /// when true, show icons like bookmarks or breakpoints at the left 952 @property bool showIcons() { 953 return _showIcons; 954 } 955 956 /// when true, show icons like bookmarks or breakpoints at the left 957 @property EditWidgetBase showIcons(bool flg) { 958 if (_showIcons != flg) { 959 _showIcons = flg; 960 updateLeftPaneWidth(); 961 requestLayout(); 962 } 963 return this; 964 } 965 966 /// when true, show folding controls at the left 967 @property bool showFolding() { 968 return _showFolding; 969 } 970 971 /// when true, show folding controls at the left 972 @property EditWidgetBase showFolding(bool flg) { 973 if (_showFolding != flg) { 974 _showFolding = flg; 975 updateLeftPaneWidth(); 976 requestLayout(); 977 } 978 return this; 979 } 980 981 /// when true, show modification marks for lines (whether line is unchanged/modified/modified_saved) 982 @property bool showModificationMarks() { 983 return _showModificationMarks; 984 } 985 986 /// when true, show modification marks for lines (whether line is unchanged/modified/modified_saved) 987 @property EditWidgetBase showModificationMarks(bool flg) { 988 if (_showModificationMarks != flg) { 989 _showModificationMarks = flg; 990 updateLeftPaneWidth(); 991 requestLayout(); 992 } 993 return this; 994 } 995 996 /// when true, line numbers are shown 997 @property bool showLineNumbers() { 998 return _showLineNumbers; 999 } 1000 1001 /// when true, line numbers are shown 1002 @property EditWidgetBase showLineNumbers(bool flg) { 1003 if (_showLineNumbers != flg) { 1004 _showLineNumbers = flg; 1005 updateLeftPaneWidth(); 1006 requestLayout(); 1007 } 1008 return this; 1009 } 1010 1011 /// readonly flag (when true, user cannot change content of editor) 1012 @property bool readOnly() { 1013 return !enabled || _content.readOnly; 1014 } 1015 1016 /// sets readonly flag 1017 @property EditWidgetBase readOnly(bool readOnly) { 1018 enabled = !readOnly; 1019 invalidate(); 1020 return this; 1021 } 1022 1023 /// replace mode flag (when true, entered character replaces character under cursor) 1024 @property bool replaceMode() { 1025 return _replaceMode; 1026 } 1027 1028 /// sets replace mode flag 1029 @property EditWidgetBase replaceMode(bool replaceMode) { 1030 _replaceMode = replaceMode; 1031 handleEditorStateChange(); 1032 invalidate(); 1033 return this; 1034 } 1035 1036 /// when true, spaces will be inserted instead of tabs 1037 @property bool useSpacesForTabs() { 1038 return _content.useSpacesForTabs; 1039 } 1040 1041 /// set new Tab key behavior flag: when true, spaces will be inserted instead of tabs 1042 @property EditWidgetBase useSpacesForTabs(bool useSpacesForTabs) { 1043 _content.useSpacesForTabs = useSpacesForTabs; 1044 return this; 1045 } 1046 1047 /// returns tab size (in number of spaces) 1048 @property int tabSize() { 1049 return _content.tabSize; 1050 } 1051 1052 /// sets tab size (in number of spaces) 1053 @property EditWidgetBase tabSize(int newTabSize) { 1054 if (newTabSize < 1) 1055 newTabSize = 1; 1056 else if (newTabSize > 16) 1057 newTabSize = 16; 1058 if (newTabSize != tabSize) { 1059 _content.tabSize = newTabSize; 1060 requestLayout(); 1061 } 1062 return this; 1063 } 1064 1065 /// true if smart indents are supported 1066 @property bool supportsSmartIndents() { return _content.supportsSmartIndents; } 1067 /// true if smart indents are enabled 1068 @property bool smartIndents() { return _content.smartIndents; } 1069 /// set smart indents enabled flag 1070 @property EditWidgetBase smartIndents(bool enabled) { _content.smartIndents = enabled; return this; } 1071 1072 /// true if smart indents are enabled 1073 @property bool smartIndentsAfterPaste() { return _content.smartIndentsAfterPaste; } 1074 /// set smart indents enabled flag 1075 @property EditWidgetBase smartIndentsAfterPaste(bool enabled) { _content.smartIndentsAfterPaste = enabled; return this; } 1076 1077 1078 /// editor content object 1079 @property EditableContent content() { 1080 return _content; 1081 } 1082 1083 /// when _ownContent is false, _content should not be destroyed in editor destructor 1084 protected bool _ownContent = true; 1085 /// set content object 1086 @property EditWidgetBase content(EditableContent content) { 1087 if (_content is content) 1088 return this; // not changed 1089 if (_content !is null) { 1090 // disconnect old content 1091 _content.contentChanged.disconnect(this); 1092 if (_ownContent) { 1093 destroy(_content); 1094 } 1095 } 1096 _content = content; 1097 _ownContent = false; 1098 _content.contentChanged.connect(this); 1099 if (_content.readOnly) 1100 enabled = false; 1101 return this; 1102 } 1103 1104 /// free resources 1105 ~this() { 1106 if (_ownContent) { 1107 destroy(_content); 1108 _content = null; 1109 } 1110 } 1111 1112 protected void updateMaxLineWidth() { 1113 } 1114 1115 protected void processSmartIndent(EditOperation operation) { 1116 if (!supportsSmartIndents) 1117 return; 1118 if (!smartIndents && !smartIndentsAfterPaste) 1119 return; 1120 _content.syntaxSupport.applySmartIndent(operation, this); 1121 } 1122 1123 override void onContentChange(EditableContent content, EditOperation operation, ref TextRange rangeBefore, ref TextRange rangeAfter, Object source) { 1124 //Log.d("onContentChange rangeBefore=", rangeBefore, " rangeAfter=", rangeAfter, " text=", operation.content); 1125 _contentChanged = true; 1126 if (source is this) { 1127 if (operation.action == EditAction.ReplaceContent) { 1128 // fully replaced, e.g., loaded from file or text property is assigned 1129 _caretPos = rangeAfter.end; 1130 _selectionRange.start = _caretPos; 1131 _selectionRange.end = _caretPos; 1132 updateMaxLineWidth(); 1133 measureVisibleText(); 1134 ensureCaretVisible(); 1135 correctCaretPos(); 1136 requestLayout(); 1137 requestActionsUpdate(); 1138 } else if (operation.action == EditAction.SaveContent) { 1139 // saved 1140 } else { 1141 // modified 1142 _caretPos = rangeAfter.end; 1143 _selectionRange.start = _caretPos; 1144 _selectionRange.end = _caretPos; 1145 updateMaxLineWidth(); 1146 measureVisibleText(); 1147 ensureCaretVisible(); 1148 requestActionsUpdate(); 1149 processSmartIndent(operation); 1150 } 1151 } else { 1152 updateMaxLineWidth(); 1153 measureVisibleText(); 1154 correctCaretPos(); 1155 requestLayout(); 1156 requestActionsUpdate(); 1157 } 1158 invalidate(); 1159 if (modifiedStateChange.assigned) { 1160 if (_lastReportedModifiedState != content.modified) { 1161 _lastReportedModifiedState = content.modified; 1162 modifiedStateChange(this, content.modified); 1163 requestActionsUpdate(); 1164 } 1165 } 1166 if (contentChange.assigned) { 1167 contentChange(_content); 1168 } 1169 handleEditorStateChange(); 1170 return; 1171 } 1172 protected bool _lastReportedModifiedState; 1173 1174 /// get widget text 1175 override @property dstring text() const { return _content.text; } 1176 1177 /// set text 1178 override @property Widget text(dstring s) { 1179 _content.text = s; 1180 requestLayout(); 1181 return this; 1182 } 1183 1184 /// set text 1185 override @property Widget text(UIString s) { 1186 _content.text = s; 1187 requestLayout(); 1188 return this; 1189 } 1190 1191 protected TextPosition _caretPos; 1192 protected TextRange _selectionRange; 1193 1194 abstract protected Rect textPosToClient(TextPosition p); 1195 1196 abstract protected TextPosition clientToTextPos(Point pt); 1197 1198 abstract protected void ensureCaretVisible(bool center = false); 1199 1200 abstract protected Point measureVisibleText(); 1201 1202 protected int _caretBlingingInterval = 800; 1203 protected ulong _caretTimerId; 1204 protected bool _caretBlinkingPhase; 1205 protected long _lastBlinkStartTs; 1206 protected bool _caretBlinks = true; 1207 1208 /// when true, enables caret blinking, otherwise it's always visible 1209 @property void showCaretBlinking(bool blinks) { 1210 _caretBlinks = blinks; 1211 } 1212 /// when true, enables caret blinking, otherwise it's always visible 1213 @property bool showCaretBlinking() { 1214 return _caretBlinks; 1215 } 1216 1217 protected void startCaretBlinking() { 1218 if (window) { 1219 static if (WIDGET_STYLE_CONSOLE) { 1220 window.caretRect = caretRect; 1221 window.caretReplace = _replaceMode; 1222 } else { 1223 long ts = currentTimeMillis; 1224 if (_caretTimerId) { 1225 if (_lastBlinkStartTs + _caretBlingingInterval / 4 > ts) 1226 return; // don't update timer too frequently 1227 cancelTimer(_caretTimerId); 1228 } 1229 _caretTimerId = setTimer(_caretBlingingInterval / 2); 1230 _lastBlinkStartTs = ts; 1231 _caretBlinkingPhase = false; 1232 invalidate(); 1233 } 1234 } 1235 } 1236 1237 protected void stopCaretBlinking() { 1238 if (window) { 1239 static if (WIDGET_STYLE_CONSOLE) { 1240 window.caretRect = Rect.init; 1241 } else { 1242 if (_caretTimerId) { 1243 cancelTimer(_caretTimerId); 1244 _caretTimerId = 0; 1245 } 1246 } 1247 } 1248 } 1249 1250 /// handle timer; return true to repeat timer event after next interval, false cancel timer 1251 override bool onTimer(ulong id) { 1252 if (id == _caretTimerId) { 1253 _caretBlinkingPhase = !_caretBlinkingPhase; 1254 if (!_caretBlinkingPhase) 1255 _lastBlinkStartTs = currentTimeMillis; 1256 invalidate(); 1257 //window.update(true); 1258 bool res = focused; 1259 if (!res) 1260 _caretTimerId = 0; 1261 return res; 1262 } 1263 if (id == _hoverTimer) { 1264 cancelHoverTimer(); 1265 onHoverTimeout(_hoverMousePosition, _hoverTextPosition); 1266 return false; 1267 } 1268 return super.onTimer(id); 1269 } 1270 1271 /// override to handle focus changes 1272 override protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) { 1273 if (focused) 1274 startCaretBlinking(); 1275 else { 1276 stopCaretBlinking(); 1277 cancelHoverTimer(); 1278 1279 if(_deselectAllWhenUnfocused) { 1280 _selectionRange.start = _caretPos; 1281 _selectionRange.end = _caretPos; 1282 } 1283 } 1284 if(focused && _selectAllWhenFocusedWithTab && receivedFocusFromKeyboard) 1285 handleAction(ACTION_EDITOR_SELECT_ALL); 1286 super.handleFocusChange(focused); 1287 } 1288 1289 //In word wrap mode, set by caretRect so ensureCaretVisible will know when to scroll 1290 protected int caretHeightOffset; 1291 1292 /// returns cursor rectangle 1293 protected Rect caretRect() { 1294 Rect caretRc = textPosToClient(_caretPos); 1295 if (_replaceMode) { 1296 dstring s = _content[_caretPos.line]; 1297 if (_caretPos.pos < s.length) { 1298 TextPosition nextPos = _caretPos; 1299 nextPos.pos++; 1300 Rect nextRect = textPosToClient(nextPos); 1301 caretRc.right = nextRect.right; 1302 } else { 1303 caretRc.right += _spaceWidth; 1304 } 1305 } 1306 if (_wordWrap) 1307 { 1308 _scrollPos.x = 0; 1309 int wrapLine = findWrapLine(_caretPos); 1310 int xOffset; 1311 if (wrapLine > 0) 1312 { 1313 LineSpan curSpan = getSpan(_caretPos.line); 1314 xOffset = curSpan.accumulation(wrapLine, LineSpan.WrapPointInfo.Width); 1315 } 1316 auto yOffset = -1 * _lineHeight * (wrapsUpTo(_caretPos.line) + wrapLine); 1317 caretHeightOffset = yOffset; 1318 caretRc.offset(_clientRect.left - xOffset, _clientRect.top - yOffset); 1319 } 1320 else 1321 caretRc.offset(_clientRect.left, _clientRect.top); 1322 return caretRc; 1323 } 1324 1325 /// handle theme change: e.g. reload some themed resources 1326 override void onThemeChanged() { 1327 super.onThemeChanged(); 1328 _caretColor = style.customColor("edit_caret"); 1329 _caretColorReplace = style.customColor("edit_caret_replace"); 1330 _selectionColorFocused = style.customColor("editor_selection_focused"); 1331 _selectionColorNormal = style.customColor("editor_selection_normal"); 1332 _searchHighlightColorCurrent = style.customColor("editor_search_highlight_current"); 1333 _searchHighlightColorOther = style.customColor("editor_search_highlight_other"); 1334 _leftPaneBackgroundColor = style.customColor("editor_left_pane_background"); 1335 _leftPaneBackgroundColor2 = style.customColor("editor_left_pane_background2"); 1336 _leftPaneBackgroundColor3 = style.customColor("editor_left_pane_background3"); 1337 _leftPaneLineNumberColor = style.customColor("editor_left_pane_line_number_text"); 1338 _leftPaneLineNumberColorEdited = style.customColor("editor_left_pane_line_number_text_edited", 0xC0C000); 1339 _leftPaneLineNumberColorSaved = style.customColor("editor_left_pane_line_number_text_saved", 0x00C000); 1340 _leftPaneLineNumberColorCurrentLine = style.customColor("editor_left_pane_line_number_text_current_line", 0xFFFFFFFF); 1341 _leftPaneLineNumberBackgroundColorCurrentLine = style.customColor("editor_left_pane_line_number_background_current_line", 0xC08080FF); 1342 _leftPaneLineNumberBackgroundColor = style.customColor("editor_left_pane_line_number_background"); 1343 _colorIconBreakpoint = style.customColor("editor_left_pane_line_icon_color_breakpoint", 0xFF0000); 1344 _colorIconBookmark = style.customColor("editor_left_pane_line_icon_color_bookmark", 0x0000FF); 1345 _colorIconError = style.customColor("editor_left_pane_line_icon_color_error", 0x80FF0000); 1346 _matchingBracketHightlightColor = style.customColor("editor_matching_bracket_highlight"); 1347 } 1348 1349 /// draws caret 1350 protected void drawCaret(DrawBuf buf) { 1351 if (focused) { 1352 if (_caretBlinkingPhase && _caretBlinks) { 1353 return; 1354 } 1355 // draw caret 1356 Rect caretRc = caretRect(); 1357 if (caretRc.intersects(_clientRect)) { 1358 //caretRc.left++; 1359 if (_replaceMode && BACKEND_GUI) 1360 buf.fillRect(caretRc, _caretColorReplace); 1361 //buf.drawLine(Point(caretRc.left, caretRc.bottom), Point(caretRc.left, caretRc.top), _caretColor); 1362 buf.fillRect(Rect(caretRc.left, caretRc.top, caretRc.left + 1, caretRc.bottom), _caretColor); 1363 } 1364 } 1365 } 1366 1367 protected void updateFontProps() { 1368 FontRef font = font(); 1369 _fixedFont = font.isFixed; 1370 _spaceWidth = font.spaceWidth; 1371 _lineHeight = font.height; 1372 } 1373 1374 /// when cursor position or selection is out of content bounds, fix it to nearest valid position 1375 protected void correctCaretPos() { 1376 _content.correctPosition(_caretPos); 1377 _content.correctPosition(_selectionRange.start); 1378 _content.correctPosition(_selectionRange.end); 1379 if (_selectionRange.empty) 1380 _selectionRange = TextRange(_caretPos, _caretPos); 1381 handleEditorStateChange(); 1382 } 1383 1384 1385 private int[] _lineWidthBuf; 1386 protected int calcLineWidth(dstring s) { 1387 int w = 0; 1388 if (_fixedFont) { 1389 int tabw = tabSize * _spaceWidth; 1390 // version optimized for fixed font 1391 for (int i = 0; i < s.length; i++) { 1392 if (s[i] == '\t') { 1393 w += _spaceWidth; 1394 w = (w + tabw - 1) / tabw * tabw; 1395 } else { 1396 w += _spaceWidth; 1397 } 1398 } 1399 } else { 1400 // variable pitch font 1401 if (_lineWidthBuf.length < s.length) 1402 _lineWidthBuf.length = s.length; 1403 int charsMeasured = font.measureText(s, _lineWidthBuf, int.max); 1404 if (charsMeasured > 0) 1405 w = _lineWidthBuf[charsMeasured - 1]; 1406 } 1407 return w; 1408 } 1409 1410 protected void updateSelectionAfterCursorMovement(TextPosition oldCaretPos, bool selecting) { 1411 if (selecting) { 1412 if (oldCaretPos == _selectionRange.start) { 1413 if (_caretPos >= _selectionRange.end) { 1414 _selectionRange.start = _selectionRange.end; 1415 _selectionRange.end = _caretPos; 1416 } else { 1417 _selectionRange.start = _caretPos; 1418 } 1419 } else if (oldCaretPos == _selectionRange.end) { 1420 if (_caretPos < _selectionRange.start) { 1421 _selectionRange.end = _selectionRange.start; 1422 _selectionRange.start = _caretPos; 1423 } else { 1424 _selectionRange.end = _caretPos; 1425 } 1426 } else { 1427 if (oldCaretPos < _caretPos) { 1428 // start selection forward 1429 _selectionRange.start = oldCaretPos; 1430 _selectionRange.end = _caretPos; 1431 } else { 1432 // start selection backward 1433 _selectionRange.start = _caretPos; 1434 _selectionRange.end = oldCaretPos; 1435 } 1436 } 1437 } else { 1438 _selectionRange.start = _caretPos; 1439 _selectionRange.end = _caretPos; 1440 } 1441 invalidate(); 1442 requestActionsUpdate(); 1443 handleEditorStateChange(); 1444 } 1445 1446 protected dstring _textToHighlight; 1447 protected uint _textToHighlightOptions; 1448 1449 /// text pattern to highlight - e.g. for search 1450 @property dstring textToHighlight() { 1451 return _textToHighlight; 1452 } 1453 /// set text to highlight -- e.g. for search 1454 void setTextToHighlight(dstring pattern, uint textToHighlightOptions) { 1455 _textToHighlight = pattern; 1456 _textToHighlightOptions = textToHighlightOptions; 1457 invalidate(); 1458 } 1459 1460 /// Used instead of using clientToTextPos for mouse input when in word wrap mode 1461 protected TextPosition wordWrapMouseOffset(int x, int y) 1462 { 1463 if(_span.length == 0) 1464 return clientToTextPos(Point(x,y)); 1465 int selectedVisibleLine = y / _lineHeight; 1466 1467 LineSpan _curSpan; 1468 1469 int wrapLine = 0; 1470 int curLine = 0; 1471 bool foundWrap = false; 1472 int accumulativeWidths = 0; 1473 int curWrapOfSpan = 0; 1474 1475 lineSpanIterate(delegate(LineSpan curSpan){ 1476 while (!foundWrap) 1477 { 1478 if (wrapLine == selectedVisibleLine) 1479 { 1480 foundWrap = true; 1481 break; 1482 } 1483 accumulativeWidths += curSpan.wrapPoints[curWrapOfSpan].wrapWidth; 1484 wrapLine++; 1485 curWrapOfSpan++; 1486 if (curWrapOfSpan >= curSpan.len) 1487 { 1488 break; 1489 } 1490 } 1491 if (!foundWrap) 1492 { 1493 accumulativeWidths = 0; 1494 curLine++; 1495 } 1496 curWrapOfSpan = 0; 1497 }); 1498 1499 int fakeLineHeight = curLine * _lineHeight; 1500 return clientToTextPos(Point(x + accumulativeWidths,fakeLineHeight)); 1501 } 1502 1503 protected void selectWordByMouse(int x, int y) { 1504 TextPosition oldCaretPos = _caretPos; 1505 TextPosition newPos = _wordWrap ? wordWrapMouseOffset(x,y) : clientToTextPos(Point(x,y)); 1506 TextRange r = content.wordBounds(newPos); 1507 if (r.start < r.end) { 1508 _selectionRange = r; 1509 _caretPos = r.end; 1510 invalidate(); 1511 requestActionsUpdate(); 1512 } else { 1513 _caretPos = newPos; 1514 updateSelectionAfterCursorMovement(oldCaretPos, false); 1515 } 1516 handleEditorStateChange(); 1517 } 1518 1519 protected void selectLineByMouse(int x, int y, bool onSameLineOnly = true) { 1520 TextPosition oldCaretPos = _caretPos; 1521 TextPosition newPos = _wordWrap ? wordWrapMouseOffset(x,y) : clientToTextPos(Point(x,y)); 1522 if (onSameLineOnly && newPos.line != oldCaretPos.line) 1523 return; // different lines 1524 TextRange r = content.lineRange(newPos.line); 1525 if (r.start < r.end) { 1526 _selectionRange = r; 1527 _caretPos = r.end; 1528 invalidate(); 1529 requestActionsUpdate(); 1530 } else { 1531 _caretPos = newPos; 1532 updateSelectionAfterCursorMovement(oldCaretPos, false); 1533 } 1534 handleEditorStateChange(); 1535 } 1536 1537 protected void updateCaretPositionByMouse(int x, int y, bool selecting) { 1538 TextPosition oldCaretPos = _caretPos; 1539 TextPosition newPos = _wordWrap ? wordWrapMouseOffset(x,y) : clientToTextPos(Point(x,y)); 1540 if (newPos != _caretPos) { 1541 _caretPos = newPos; 1542 updateSelectionAfterCursorMovement(oldCaretPos, selecting); 1543 invalidate(); 1544 } 1545 handleEditorStateChange(); 1546 } 1547 1548 /// generate string of spaces, to reach next tab position 1549 protected dstring spacesForTab(int currentPos) { 1550 int newPos = (currentPos + tabSize + 1) / tabSize * tabSize; 1551 return " "d[0..(newPos - currentPos)]; 1552 } 1553 1554 /// returns true if one or more lines selected fully 1555 protected bool multipleLinesSelected() { 1556 return _selectionRange.end.line > _selectionRange.start.line; 1557 } 1558 1559 protected bool _camelCasePartsAsWords = true; 1560 1561 void replaceSelectionText(dstring newText) { 1562 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [newText]); 1563 _content.performOperation(op, this); 1564 ensureCaretVisible(); 1565 } 1566 1567 protected bool removeSelectionTextIfSelected() { 1568 if (_selectionRange.empty) 1569 return false; 1570 // clear selection 1571 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d]); 1572 _content.performOperation(op, this); 1573 ensureCaretVisible(); 1574 return true; 1575 } 1576 1577 /// returns current selection text (joined with LF when span over multiple lines) 1578 public dstring getSelectedText() { 1579 return getRangeText(_selectionRange); 1580 } 1581 1582 /// returns text for specified range (joined with LF when span over multiple lines) 1583 public dstring getRangeText(TextRange range) { 1584 dstring selectionText = concatDStrings(_content.rangeText(range)); 1585 return selectionText; 1586 } 1587 1588 /// returns range for line with cursor 1589 @property public TextRange currentLineRange() { 1590 return _content.lineRange(_caretPos.line); 1591 } 1592 1593 /// clears selection (don't change text, just unselect) 1594 void clearSelection() { 1595 _selectionRange = TextRange(_caretPos, _caretPos); 1596 invalidate(); 1597 } 1598 1599 protected bool removeRangeText(TextRange range) { 1600 if (range.empty) 1601 return false; 1602 _selectionRange = range; 1603 _caretPos = _selectionRange.start; 1604 EditOperation op = new EditOperation(EditAction.Replace, range, [""d]); 1605 _content.performOperation(op, this); 1606 //_selectionRange.start = _caretPos; 1607 //_selectionRange.end = _caretPos; 1608 ensureCaretVisible(); 1609 handleEditorStateChange(); 1610 return true; 1611 } 1612 1613 /// returns current selection range 1614 @property TextRange selectionRange() { 1615 return _selectionRange; 1616 } 1617 /// sets current selection range 1618 @property void selectionRange(TextRange range) { 1619 if (range.empty) 1620 return; 1621 _selectionRange = range; 1622 _caretPos = range.end; 1623 handleEditorStateChange(); 1624 } 1625 1626 /// override to handle specific actions state (e.g. change enabled state for supported actions) 1627 override bool handleActionStateRequest(const Action a) { 1628 switch (a.id) with(EditorActions) 1629 { 1630 case ToggleBlockComment: 1631 if (!_content.syntaxSupport || !_content.syntaxSupport.supportsToggleBlockComment) 1632 a.state = ACTION_STATE_INVISIBLE; 1633 else if (enabled && _content.syntaxSupport.canToggleBlockComment(_selectionRange)) 1634 a.state = ACTION_STATE_ENABLED; 1635 else 1636 a.state = ACTION_STATE_DISABLE; 1637 return true; 1638 case ToggleLineComment: 1639 if (!_content.syntaxSupport || !_content.syntaxSupport.supportsToggleLineComment) 1640 a.state = ACTION_STATE_INVISIBLE; 1641 else if (enabled && _content.syntaxSupport.canToggleLineComment(_selectionRange)) 1642 a.state = ACTION_STATE_ENABLED; 1643 else 1644 a.state = ACTION_STATE_DISABLE; 1645 return true; 1646 case Copy: 1647 case Cut: 1648 case Paste: 1649 case Undo: 1650 case Redo: 1651 case Tab: 1652 case BackTab: 1653 case Indent: 1654 case Unindent: 1655 if (isActionEnabled(a)) 1656 a.state = ACTION_STATE_ENABLED; 1657 else 1658 a.state = ACTION_STATE_DISABLE; 1659 return true; 1660 default: 1661 return super.handleActionStateRequest(a); 1662 } 1663 } 1664 1665 override protected bool handleAction(const Action a) { 1666 TextPosition oldCaretPos = _caretPos; 1667 dstring currentLine = _content[_caretPos.line]; 1668 switch (a.id) with(EditorActions) 1669 { 1670 case Left: 1671 case SelectLeft: 1672 correctCaretPos(); 1673 if (_caretPos.pos > 0) { 1674 _caretPos.pos--; 1675 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1676 ensureCaretVisible(); 1677 } else if (_caretPos.line > 0) { 1678 _caretPos = _content.lineEnd(_caretPos.line - 1); 1679 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1680 ensureCaretVisible(); 1681 } 1682 return true; 1683 case Right: 1684 case SelectRight: 1685 correctCaretPos(); 1686 if (_caretPos.pos < currentLine.length) { 1687 _caretPos.pos++; 1688 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1689 ensureCaretVisible(); 1690 } else if (_caretPos.line < _content.length - 1 && _content.multiline) { 1691 _caretPos.pos = 0; 1692 _caretPos.line++; 1693 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1694 ensureCaretVisible(); 1695 } 1696 return true; 1697 case WordLeft: 1698 case SelectWordLeft: 1699 { 1700 TextPosition newpos = _content.moveByWord(_caretPos, -1, _camelCasePartsAsWords); 1701 if (newpos != _caretPos) { 1702 _caretPos = newpos; 1703 updateSelectionAfterCursorMovement(oldCaretPos, a.id == EditorActions.SelectWordLeft); 1704 ensureCaretVisible(); 1705 } 1706 } 1707 return true; 1708 case WordRight: 1709 case SelectWordRight: 1710 { 1711 TextPosition newpos = _content.moveByWord(_caretPos, 1, _camelCasePartsAsWords); 1712 if (newpos != _caretPos) { 1713 _caretPos = newpos; 1714 updateSelectionAfterCursorMovement(oldCaretPos, a.id == EditorActions.SelectWordRight); 1715 ensureCaretVisible(); 1716 } 1717 } 1718 return true; 1719 case DocumentBegin: 1720 case SelectDocumentBegin: 1721 if (_caretPos.pos > 0 || _caretPos.line > 0) { 1722 _caretPos.line = 0; 1723 _caretPos.pos = 0; 1724 ensureCaretVisible(); 1725 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1726 } 1727 return true; 1728 case LineBegin: 1729 case SelectLineBegin: 1730 auto space = _content.getLineWhiteSpace(_caretPos.line); 1731 if (_caretPos.pos > 0) { 1732 if (_caretPos.pos > space.firstNonSpaceIndex && space.firstNonSpaceIndex > 0) 1733 _caretPos.pos = space.firstNonSpaceIndex; 1734 else 1735 _caretPos.pos = 0; 1736 ensureCaretVisible(); 1737 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1738 } else { 1739 // caret pos is 0 1740 if (space.firstNonSpaceIndex > 0) 1741 _caretPos.pos = space.firstNonSpaceIndex; 1742 ensureCaretVisible(); 1743 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1744 if (a.id == EditorActions.LineBegin && _caretPos == oldCaretPos) { 1745 clearSelection(); 1746 } 1747 } 1748 return true; 1749 case DocumentEnd: 1750 case SelectDocumentEnd: 1751 if (_caretPos.line < _content.length - 1 || _caretPos.pos < _content[_content.length - 1].length) { 1752 _caretPos.line = _content.length - 1; 1753 _caretPos.pos = cast(int)_content[_content.length - 1].length; 1754 ensureCaretVisible(); 1755 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1756 } 1757 return true; 1758 case LineEnd: 1759 case SelectLineEnd: 1760 if (_caretPos.pos < currentLine.length) { 1761 _caretPos.pos = cast(int)currentLine.length; 1762 ensureCaretVisible(); 1763 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1764 } else if (a.id == EditorActions.LineEnd) { 1765 clearSelection(); 1766 } 1767 return true; 1768 case DelPrevWord: 1769 if (readOnly) 1770 return true; 1771 correctCaretPos(); 1772 if (removeSelectionTextIfSelected()) // clear selection 1773 return true; 1774 TextPosition newpos = _content.moveByWord(_caretPos, -1, _camelCasePartsAsWords); 1775 if (newpos < _caretPos) 1776 removeRangeText(TextRange(newpos, _caretPos)); 1777 return true; 1778 case DelNextWord: 1779 if (readOnly) 1780 return true; 1781 correctCaretPos(); 1782 if (removeSelectionTextIfSelected()) // clear selection 1783 return true; 1784 TextPosition newpos = _content.moveByWord(_caretPos, 1, _camelCasePartsAsWords); 1785 if (newpos > _caretPos) 1786 removeRangeText(TextRange(_caretPos, newpos)); 1787 return true; 1788 case DelPrevChar: 1789 if (readOnly) 1790 return true; 1791 correctCaretPos(); 1792 if (removeSelectionTextIfSelected()) // clear selection 1793 return true; 1794 if (_caretPos.pos > 0) { 1795 // delete prev char in current line 1796 TextRange range = TextRange(_caretPos, _caretPos); 1797 range.start.pos--; 1798 removeRangeText(range); 1799 } else if (_caretPos.line > 0) { 1800 // merge with previous line 1801 TextRange range = TextRange(_caretPos, _caretPos); 1802 range.start = _content.lineEnd(range.start.line - 1); 1803 removeRangeText(range); 1804 } 1805 return true; 1806 case DelNextChar: 1807 if (readOnly) 1808 return true; 1809 correctCaretPos(); 1810 if (removeSelectionTextIfSelected()) // clear selection 1811 return true; 1812 if (_caretPos.pos < currentLine.length) { 1813 // delete char in current line 1814 TextRange range = TextRange(_caretPos, _caretPos); 1815 range.end.pos++; 1816 removeRangeText(range); 1817 } else if (_caretPos.line < _content.length - 1) { 1818 // merge with next line 1819 TextRange range = TextRange(_caretPos, _caretPos); 1820 range.end.line++; 1821 range.end.pos = 0; 1822 removeRangeText(range); 1823 } 1824 return true; 1825 case Copy: 1826 case Cut: 1827 TextRange range = _selectionRange; 1828 if (range.empty && _copyCurrentLineWhenNoSelection) { 1829 range = currentLineRange; 1830 } 1831 if (!range.empty) { 1832 dstring selectionText = getRangeText(range); 1833 platform.setClipboardText(selectionText); 1834 if (!readOnly && a.id == Cut) { 1835 EditOperation op = new EditOperation(EditAction.Replace, range, [""d]); 1836 _content.performOperation(op, this); 1837 } 1838 } 1839 return true; 1840 case Paste: 1841 { 1842 if (readOnly) 1843 return true; 1844 dstring selectionText = platform.getClipboardText(); 1845 dstring[] lines; 1846 if (_content.multiline) { 1847 lines = splitDString(selectionText); 1848 } else { 1849 lines = [replaceEolsWithSpaces(selectionText)]; 1850 } 1851 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, lines); 1852 _content.performOperation(op, this); 1853 } 1854 return true; 1855 case Undo: 1856 { 1857 if (readOnly) 1858 return true; 1859 _content.undo(this); 1860 } 1861 return true; 1862 case Redo: 1863 { 1864 if (readOnly) 1865 return true; 1866 _content.redo(this); 1867 } 1868 return true; 1869 case Indent: 1870 indentRange(false); 1871 return true; 1872 case Unindent: 1873 indentRange(true); 1874 return true; 1875 case Tab: 1876 { 1877 if (readOnly) 1878 return true; 1879 if (_selectionRange.empty) { 1880 if (useSpacesForTabs) { 1881 // insert one or more spaces to 1882 EditOperation op = new EditOperation(EditAction.Replace, TextRange(_caretPos, _caretPos), [spacesForTab(_caretPos.pos)]); 1883 _content.performOperation(op, this); 1884 } else { 1885 // just insert tab character 1886 EditOperation op = new EditOperation(EditAction.Replace, TextRange(_caretPos, _caretPos), ["\t"d]); 1887 _content.performOperation(op, this); 1888 } 1889 } else { 1890 if (multipleLinesSelected()) { 1891 // indent range 1892 return handleAction(new Action(EditorActions.Indent)); 1893 } else { 1894 // insert tab 1895 if (useSpacesForTabs) { 1896 // insert one or more spaces to 1897 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [spacesForTab(_selectionRange.start.pos)]); 1898 _content.performOperation(op, this); 1899 } else { 1900 // just insert tab character 1901 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, ["\t"d]); 1902 _content.performOperation(op, this); 1903 } 1904 } 1905 1906 } 1907 } 1908 return true; 1909 case BackTab: 1910 { 1911 if (readOnly) 1912 return true; 1913 if (_selectionRange.empty) { 1914 // remove spaces before caret 1915 TextRange r = spaceBefore(_caretPos); 1916 if (!r.empty) { 1917 EditOperation op = new EditOperation(EditAction.Replace, r, [""d]); 1918 _content.performOperation(op, this); 1919 } 1920 } else { 1921 if (multipleLinesSelected()) { 1922 // unindent range 1923 return handleAction(new Action(EditorActions.Unindent)); 1924 } else { 1925 // remove space before selection 1926 TextRange r = spaceBefore(_selectionRange.start); 1927 if (!r.empty) { 1928 int nchars = r.end.pos - r.start.pos; 1929 TextRange saveRange = _selectionRange; 1930 TextPosition saveCursor = _caretPos; 1931 EditOperation op = new EditOperation(EditAction.Replace, r, [""d]); 1932 _content.performOperation(op, this); 1933 if (saveCursor.line == saveRange.start.line) 1934 saveCursor.pos -= nchars; 1935 if (saveRange.end.line == saveRange.start.line) 1936 saveRange.end.pos -= nchars; 1937 saveRange.start.pos -= nchars; 1938 _selectionRange = saveRange; 1939 _caretPos = saveCursor; 1940 ensureCaretVisible(); 1941 } 1942 } 1943 } 1944 } 1945 return true; 1946 case ToggleReplaceMode: 1947 replaceMode = !replaceMode; 1948 return true; 1949 case SelectAll: 1950 selectAll(); 1951 ensureCaretVisible(); 1952 return true; 1953 case ToggleBookmark: 1954 if (_content.multiline) { 1955 int line = a.longParam >= 0 ? cast(int)a.longParam : _caretPos.line; 1956 _content.lineIcons.toggleBookmark(line); 1957 return true; 1958 } 1959 return false; 1960 case GoToNextBookmark: 1961 case GoToPreviousBookmark: 1962 if (_content.multiline) { 1963 LineIcon mark = _content.lineIcons.findNext(LineIconType.bookmark, _selectionRange.end.line, a.id == EditorActions.GoToNextBookmark ? 1 : -1); 1964 if (mark) { 1965 setCaretPos(mark.line, 0, true); 1966 return true; 1967 } 1968 } 1969 return false; 1970 default: 1971 break; 1972 } 1973 return super.handleAction(a); 1974 } 1975 1976 /// Select whole text 1977 void selectAll() { 1978 _selectionRange.start.line = 0; 1979 _selectionRange.start.pos = 0; 1980 _selectionRange.end = _content.lineEnd(_content.length - 1); 1981 _caretPos = _selectionRange.end; 1982 requestActionsUpdate(); 1983 } 1984 1985 protected TextRange spaceBefore(TextPosition pos) { 1986 TextRange res = TextRange(pos, pos); 1987 dstring s = _content[pos.line]; 1988 int x = 0; 1989 int start = -1; 1990 for (int i = 0; i < pos.pos; i++) { 1991 dchar ch = s[i]; 1992 if (ch == ' ') { 1993 if (start == -1 || (x % tabSize) == 0) 1994 start = i; 1995 x++; 1996 } else if (ch == '\t') { 1997 if (start == -1 || (x % tabSize) == 0) 1998 start = i; 1999 x = (x + tabSize + 1) / tabSize * tabSize; 2000 } else { 2001 x++; 2002 start = -1; 2003 } 2004 } 2005 if (start != -1) { 2006 res.start.pos = start; 2007 } 2008 return res; 2009 } 2010 2011 /// change line indent 2012 protected dstring indentLine(dstring src, bool back, TextPosition * cursorPos) { 2013 int firstNonSpace = -1; 2014 int x = 0; 2015 int unindentPos = -1; 2016 int cursor = cursorPos ? cursorPos.pos : 0; 2017 for (int i = 0; i < src.length; i++) { 2018 dchar ch = src[i]; 2019 if (ch == ' ') { 2020 x++; 2021 } else if (ch == '\t') { 2022 x = (x + tabSize + 1) / tabSize * tabSize; 2023 } else { 2024 firstNonSpace = i; 2025 break; 2026 } 2027 if (x <= tabSize) 2028 unindentPos = i + 1; 2029 } 2030 if (firstNonSpace == -1) // only spaces or empty line -- do not change it 2031 return src; 2032 if (back) { 2033 // unindent 2034 if (unindentPos == -1) 2035 return src; // no change 2036 if (unindentPos == src.length) { 2037 if (cursorPos) 2038 cursorPos.pos = 0; 2039 return ""d; 2040 } 2041 if (cursor >= unindentPos) 2042 cursorPos.pos -= unindentPos; 2043 return src[unindentPos .. $].dup; 2044 } else { 2045 // indent 2046 if (useSpacesForTabs) { 2047 if (cursor > 0) 2048 cursorPos.pos += tabSize; 2049 return spacesForTab(0) ~ src; 2050 } else { 2051 if (cursor > 0) 2052 cursorPos.pos++; 2053 return "\t"d ~ src; 2054 } 2055 } 2056 } 2057 2058 /// indent / unindent range 2059 protected void indentRange(bool back) { 2060 TextRange r = _selectionRange; 2061 r.start.pos = 0; 2062 if (r.end.pos > 0) 2063 r.end = _content.lineBegin(r.end.line + 1); 2064 if (r.end.line <= r.start.line) 2065 r = TextRange(_content.lineBegin(_caretPos.line), _content.lineBegin(_caretPos.line + 1)); 2066 int lineCount = r.end.line - r.start.line; 2067 if (r.end.pos > 0) 2068 lineCount++; 2069 dstring[] newContent = new dstring[lineCount + 1]; 2070 bool changed = false; 2071 for (int i = 0; i < lineCount; i++) { 2072 dstring srcline = _content.line(r.start.line + i); 2073 dstring dstline = indentLine(srcline, back, r.start.line + i == _caretPos.line ? &_caretPos : null); 2074 newContent[i] = dstline; 2075 if (dstline.length != srcline.length) 2076 changed = true; 2077 } 2078 if (changed) { 2079 TextRange saveRange = r; 2080 TextPosition saveCursor = _caretPos; 2081 EditOperation op = new EditOperation(EditAction.Replace, r, newContent); 2082 _content.performOperation(op, this); 2083 _selectionRange = saveRange; 2084 _caretPos = saveCursor; 2085 ensureCaretVisible(); 2086 } 2087 } 2088 2089 /// map key to action 2090 override protected Action findKeyAction(uint keyCode, uint flags) { 2091 // don't handle tabs when disabled 2092 if (keyCode == KeyCode.TAB && (flags == 0 || flags == KeyFlag.Shift) && (!_wantTabs || readOnly)) 2093 return null; 2094 return super.findKeyAction(keyCode, flags); 2095 } 2096 2097 static bool isAZaz(dchar ch) { 2098 return (ch >= 'a' && ch <='z') || (ch >= 'A' && ch <='Z'); 2099 } 2100 2101 /// handle keys 2102 override bool onKeyEvent(KeyEvent event) { 2103 //Log.d("onKeyEvent ", event.action, " ", event.keyCode, " flags ", event.flags); 2104 if (focused) startCaretBlinking(); 2105 cancelHoverTimer(); 2106 bool ctrlOrAltPressed = !!(event.flags & KeyFlag.Control); //(event.flags & (KeyFlag.Control /* | KeyFlag.Alt */)); 2107 //if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.SPACE && (event.flags & KeyFlag.Control)) { 2108 // Log.d("Ctrl+Space pressed"); 2109 //} 2110 if (event.action == KeyAction.Text && event.text.length && !ctrlOrAltPressed) { 2111 //Log.d("text entered: ", event.text); 2112 if (readOnly) 2113 return true; 2114 if (!(!!(event.flags & KeyFlag.Alt) && event.text.length == 1 && isAZaz(event.text[0]))) { // filter out Alt+A..Z 2115 if (replaceMode && _selectionRange.empty && _content[_caretPos.line].length >= _caretPos.pos + event.text.length) { 2116 // replace next char(s) 2117 TextRange range = _selectionRange; 2118 range.end.pos += cast(int)event.text.length; 2119 EditOperation op = new EditOperation(EditAction.Replace, range, [event.text]); 2120 _content.performOperation(op, this); 2121 } else { 2122 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [event.text]); 2123 _content.performOperation(op, this); 2124 } 2125 return true; 2126 } 2127 } 2128 //if (event.keyCode == KeyCode.SPACE && !readOnly) { 2129 // return true; 2130 //} 2131 //if (event.keyCode == KeyCode.RETURN && !readOnly && !_content.multiline) { 2132 // return true; 2133 //} 2134 return super.onKeyEvent(event); 2135 } 2136 2137 /// Handle Ctrl + Left mouse click on text 2138 protected void onControlClick() { 2139 // override to do something useful on Ctrl + Left mouse click in text 2140 } 2141 2142 protected TextPosition _hoverTextPosition; 2143 protected Point _hoverMousePosition; 2144 protected ulong _hoverTimer; 2145 protected long _hoverTimeoutMillis = 800; 2146 2147 /// override to handle mouse hover timeout in text 2148 protected void onHoverTimeout(Point pt, TextPosition pos) { 2149 // override to do something useful on hover timeout 2150 } 2151 2152 protected void onHover(Point pos) { 2153 if (_hoverMousePosition == pos) 2154 return; 2155 //Log.d("onHover ", pos); 2156 int x = pos.x - left - _leftPaneWidth; 2157 int y = pos.y - top; 2158 _hoverMousePosition = pos; 2159 _hoverTextPosition = clientToTextPos(Point(x, y)); 2160 cancelHoverTimer(); 2161 Rect reversePos = textPosToClient(_hoverTextPosition); 2162 if (x < reversePos.left + 10.pointsToPixels) 2163 _hoverTimer = setTimer(_hoverTimeoutMillis); 2164 } 2165 2166 protected void cancelHoverTimer() { 2167 if (_hoverTimer) { 2168 cancelTimer(_hoverTimer); 2169 _hoverTimer = 0; 2170 } 2171 } 2172 2173 /// process mouse event; return true if event is processed by widget. 2174 override bool onMouseEvent(MouseEvent event) { 2175 //Log.d("onMouseEvent ", id, " ", event.action, " (", event.x, ",", event.y, ")"); 2176 // support onClick 2177 bool insideLeftPane = event.x < _clientRect.left && event.x >= _clientRect.left - _leftPaneWidth; 2178 if (event.action == MouseAction.ButtonDown && insideLeftPane) { 2179 setFocus(); 2180 cancelHoverTimer(); 2181 if (onLeftPaneMouseClick(event)) 2182 return true; 2183 } 2184 if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) { 2185 setFocus(); 2186 cancelHoverTimer(); 2187 if (event.tripleClick) { 2188 selectLineByMouse(event.x - _clientRect.left, event.y - _clientRect.top); 2189 } else if (event.doubleClick) { 2190 selectWordByMouse(event.x - _clientRect.left, event.y - _clientRect.top); 2191 } else { 2192 auto doSelect = cast(bool)(event.keyFlags & MouseFlag.Shift); 2193 updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, doSelect); 2194 2195 if (event.keyFlags == MouseFlag.Control) 2196 onControlClick(); 2197 } 2198 startCaretBlinking(); 2199 invalidate(); 2200 return true; 2201 } 2202 if (event.action == MouseAction.Move && (event.flags & MouseButton.Left) != 0) { 2203 updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, true); 2204 return true; 2205 } 2206 if (event.action == MouseAction.Move && event.flags == 0) { 2207 // hover 2208 if (focused && !insideLeftPane) { 2209 onHover(event.pos); 2210 } else { 2211 cancelHoverTimer(); 2212 } 2213 return true; 2214 } 2215 if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) { 2216 cancelHoverTimer(); 2217 return true; 2218 } 2219 if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) { 2220 cancelHoverTimer(); 2221 return true; 2222 } 2223 if (event.action == MouseAction.FocusIn) { 2224 cancelHoverTimer(); 2225 return true; 2226 } 2227 if (event.action == MouseAction.Wheel) { 2228 cancelHoverTimer(); 2229 uint keyFlags = event.flags & (MouseFlag.Shift | MouseFlag.Control | MouseFlag.Alt); 2230 if (event.wheelDelta < 0) { 2231 if (keyFlags == MouseFlag.Shift) 2232 return handleAction(new Action(EditorActions.ScrollRight)); 2233 if (keyFlags == MouseFlag.Control) 2234 return handleAction(new Action(EditorActions.ZoomOut)); 2235 return handleAction(new Action(EditorActions.ScrollLineDown)); 2236 } else if (event.wheelDelta > 0) { 2237 if (keyFlags == MouseFlag.Shift) 2238 return handleAction(new Action(EditorActions.ScrollLeft)); 2239 if (keyFlags == MouseFlag.Control) 2240 return handleAction(new Action(EditorActions.ZoomIn)); 2241 return handleAction(new Action(EditorActions.ScrollLineUp)); 2242 } 2243 } 2244 cancelHoverTimer(); 2245 return super.onMouseEvent(event); 2246 } 2247 2248 /// returns caret position 2249 @property TextPosition caretPos() { 2250 return _caretPos; 2251 } 2252 2253 /// change caret position and ensure it is visible 2254 void setCaretPos(int line, int column, bool makeVisible = true, bool center = false) 2255 { 2256 _caretPos = TextPosition(line,column); 2257 correctCaretPos(); 2258 invalidate(); 2259 if (makeVisible) 2260 ensureCaretVisible(center); 2261 handleEditorStateChange(); 2262 } 2263 } 2264 2265 interface EditorActionHandler { 2266 bool onEditorAction(const Action action); 2267 } 2268 2269 interface EnterKeyHandler { 2270 bool onEnterKey(EditWidgetBase editor); 2271 } 2272 2273 /// single line editor 2274 class EditLine : EditWidgetBase { 2275 2276 Signal!EditorActionHandler editorAction; 2277 /// handle Enter key press inside line editor 2278 Signal!EnterKeyHandler enterKey; 2279 2280 /// empty parameter list constructor - for usage by factory 2281 this() { 2282 this(null); 2283 } 2284 /// create with ID parameter 2285 this(string ID, dstring initialContent = null) { 2286 super(ID, ScrollBarMode.Invisible, ScrollBarMode.Invisible); 2287 _content = new EditableContent(false); 2288 _content.contentChanged = this; 2289 _selectAllWhenFocusedWithTab = true; 2290 _deselectAllWhenUnfocused = true; 2291 wantTabs = false; 2292 styleId = STYLE_EDIT_LINE; 2293 text = initialContent; 2294 onThemeChanged(); 2295 } 2296 2297 /// sets default popup menu with copy/paste/cut/undo/redo 2298 EditLine setDefaultPopupMenu() { 2299 MenuItem items = new MenuItem(); 2300 items.add(ACTION_EDITOR_COPY, ACTION_EDITOR_PASTE, ACTION_EDITOR_CUT, 2301 ACTION_EDITOR_UNDO, ACTION_EDITOR_REDO); 2302 popupMenu = items; 2303 return this; 2304 } 2305 2306 protected dstring _measuredText; 2307 protected int[] _measuredTextWidths; 2308 protected Point _measuredTextSize; 2309 2310 protected Point _measuredTextToSetWidgetSize; 2311 protected dstring _textToSetWidgetSize = "aaaaa"d; 2312 2313 @property void textToSetWidgetSize(dstring newText) { 2314 _textToSetWidgetSize = newText; 2315 requestLayout(); 2316 } 2317 2318 @property dstring textToSetWidgetSize() { 2319 return _textToSetWidgetSize; 2320 } 2321 2322 protected int[] _measuredTextToSetWidgetSizeWidths; 2323 2324 protected dchar _passwordChar = 0; 2325 /// password character - 0 for normal editor, some character, e.g. '*' to hide text by replacing all characters with this char 2326 @property dchar passwordChar() { return _passwordChar; } 2327 @property EditLine passwordChar(dchar ch) { 2328 if (_passwordChar != ch) { 2329 _passwordChar = ch; 2330 requestLayout(); 2331 } 2332 return this; 2333 } 2334 2335 override protected Rect textPosToClient(TextPosition p) { 2336 Rect res; 2337 res.bottom = _clientRect.height; 2338 if (p.pos == 0) 2339 res.left = 0; 2340 else if (p.pos >= _measuredText.length) 2341 res.left = _measuredTextSize.x; 2342 else 2343 res.left = _measuredTextWidths[p.pos - 1]; 2344 res.left -= _scrollPos.x; 2345 res.right = res.left + 1; 2346 return res; 2347 } 2348 2349 override protected TextPosition clientToTextPos(Point pt) { 2350 pt.x += _scrollPos.x; 2351 TextPosition res; 2352 for (int i = 0; i < _measuredText.length; i++) { 2353 int x0 = i > 0 ? _measuredTextWidths[i - 1] : 0; 2354 int x1 = _measuredTextWidths[i]; 2355 int mx = (x0 + x1) >> 1; 2356 if (pt.x <= mx) { 2357 res.pos = i; 2358 return res; 2359 } 2360 } 2361 res.pos = cast(int)_measuredText.length; 2362 return res; 2363 } 2364 2365 override protected void ensureCaretVisible(bool center = false) { 2366 //_scrollPos 2367 Rect rc = textPosToClient(_caretPos); 2368 if (rc.left < 0) { 2369 // scroll left 2370 _scrollPos.x -= -rc.left + _clientRect.width / 10; 2371 if (_scrollPos.x < 0) 2372 _scrollPos.x = 0; 2373 invalidate(); 2374 } else if (rc.left >= _clientRect.width - 10) { 2375 // scroll right 2376 _scrollPos.x += (rc.left - _clientRect.width) + _spaceWidth * 4; 2377 invalidate(); 2378 } 2379 updateScrollBars(); 2380 handleEditorStateChange(); 2381 } 2382 2383 protected dstring applyPasswordChar(dstring s) { 2384 if (!_passwordChar || s.length == 0) 2385 return s; 2386 dchar[] ss = s.dup; 2387 foreach(ref ch; ss) 2388 ch = _passwordChar; 2389 return cast(dstring)ss; 2390 } 2391 2392 override protected Point measureVisibleText() { 2393 FontRef font = font(); 2394 //Point sz = font.textSize(text); 2395 _measuredText = applyPasswordChar(text); 2396 _measuredTextWidths.length = _measuredText.length; 2397 int charsMeasured = font.measureText(_measuredText, _measuredTextWidths, MAX_WIDTH_UNSPECIFIED, tabSize); 2398 _measuredTextSize.x = charsMeasured > 0 ? _measuredTextWidths[charsMeasured - 1]: 0; 2399 _measuredTextSize.y = font.height; 2400 return _measuredTextSize; 2401 } 2402 2403 protected Point measureTextToSetWidgetSize() { 2404 FontRef font = font(); 2405 _measuredTextToSetWidgetSizeWidths.length = _textToSetWidgetSize.length; 2406 int charsMeasured = font.measureText(_textToSetWidgetSize, _measuredTextToSetWidgetSizeWidths, MAX_WIDTH_UNSPECIFIED, tabSize); 2407 _measuredTextToSetWidgetSize.x = charsMeasured > 0 ? _measuredTextToSetWidgetSizeWidths[charsMeasured - 1]: 0; 2408 _measuredTextToSetWidgetSize.y = font.height; 2409 return _measuredTextToSetWidgetSize; 2410 } 2411 2412 /// measure 2413 override void measure(int parentWidth, int parentHeight) { 2414 if (visibility == Visibility.Gone) 2415 return; 2416 2417 updateFontProps(); 2418 measureVisibleText(); 2419 measureTextToSetWidgetSize(); 2420 measuredContent(parentWidth, parentHeight, _measuredTextToSetWidgetSize.x + _leftPaneWidth, _measuredTextToSetWidgetSize.y); 2421 } 2422 2423 override bool handleAction(const Action a) { 2424 switch (a.id) with(EditorActions) 2425 { 2426 case InsertNewLine: 2427 case PrependNewLine: 2428 case AppendNewLine: 2429 if (editorAction.assigned) { 2430 return editorAction(a); 2431 } 2432 break; 2433 case Up: 2434 break; 2435 case Down: 2436 break; 2437 case PageUp: 2438 break; 2439 case PageDown: 2440 break; 2441 default: 2442 break; 2443 } 2444 return super.handleAction(a); 2445 } 2446 2447 2448 /// handle keys 2449 override bool onKeyEvent(KeyEvent event) { 2450 if (enterKey.assigned) { 2451 if (event.keyCode == KeyCode.RETURN && event.modifiers == 0) { 2452 if (event.action == KeyAction.KeyDown) 2453 return true; 2454 if (event.action == KeyAction.KeyUp) { 2455 if (enterKey(this)) 2456 return true; 2457 } 2458 } 2459 } 2460 return super.onKeyEvent(event); 2461 } 2462 2463 /// process mouse event; return true if event is processed by widget. 2464 override bool onMouseEvent(MouseEvent event) { 2465 return super.onMouseEvent(event); 2466 } 2467 2468 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 2469 override void layout(Rect rc) { 2470 if (visibility == Visibility.Gone) { 2471 return; 2472 } 2473 _needLayout = false; 2474 Point sz = Point(rc.width, measuredHeight); 2475 applyAlign(rc, sz); 2476 _pos = rc; 2477 _clientRect = rc; 2478 applyMargins(_clientRect); 2479 applyPadding(_clientRect); 2480 if (_contentChanged) { 2481 measureVisibleText(); 2482 _contentChanged = false; 2483 } 2484 } 2485 2486 2487 /// override to custom highlight of line background 2488 protected void drawLineBackground(DrawBuf buf, Rect lineRect, Rect visibleRect) { 2489 if (!_selectionRange.empty) { 2490 // line inside selection 2491 Rect startrc = textPosToClient(_selectionRange.start); 2492 Rect endrc = textPosToClient(_selectionRange.end); 2493 Rect rc = lineRect; 2494 rc.left = startrc.left + _clientRect.left; 2495 rc.right = endrc.left + _clientRect.left; 2496 if (!rc.empty) { 2497 // draw selection rect for line 2498 buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal); 2499 } 2500 if (_leftPaneWidth > 0) { 2501 Rect leftPaneRect = visibleRect; 2502 leftPaneRect.right = leftPaneRect.left; 2503 leftPaneRect.left -= _leftPaneWidth; 2504 drawLeftPane(buf, leftPaneRect, 0); 2505 } 2506 } 2507 } 2508 2509 /// draw content 2510 override void onDraw(DrawBuf buf) { 2511 if (visibility != Visibility.Visible) 2512 return; 2513 super.onDraw(buf); 2514 Rect rc = _pos; 2515 applyMargins(rc); 2516 applyPadding(rc); 2517 auto saver = ClipRectSaver(buf, rc, alpha); 2518 2519 FontRef font = font(); 2520 dstring txt = applyPasswordChar(text); 2521 2522 drawLineBackground(buf, _clientRect, _clientRect); 2523 font.drawText(buf, rc.left - _scrollPos.x, rc.top, txt, textColor, tabSize); 2524 2525 drawCaret(buf); 2526 } 2527 } 2528 2529 2530 2531 /// multiline editor 2532 class EditBox : EditWidgetBase { 2533 /// empty parameter list constructor - for usage by factory 2534 this() { 2535 this(null); 2536 } 2537 /// create with ID parameter 2538 this(string ID, dstring initialContent = null, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) { 2539 super(ID, hscrollbarMode, vscrollbarMode); 2540 _content = new EditableContent(true); // multiline 2541 _content.contentChanged = this; 2542 styleId = STYLE_EDIT_BOX; 2543 text = initialContent; 2544 acceleratorMap.add( [ 2545 // zoom 2546 new Action(EditorActions.ZoomIn, KeyCode.ADD, KeyFlag.Control), 2547 new Action(EditorActions.ZoomOut, KeyCode.SUB, KeyFlag.Control), 2548 ]); 2549 onThemeChanged(); 2550 } 2551 2552 ~this() { 2553 if (_findPanel) { 2554 destroy(_findPanel); 2555 _findPanel = null; 2556 } 2557 } 2558 2559 protected int _firstVisibleLine; 2560 2561 protected int _maxLineWidth; 2562 protected int _numVisibleLines; // number of lines visible in client area 2563 protected dstring[] _visibleLines; // text for visible lines 2564 protected int[][] _visibleLinesMeasurement; // char positions for visible lines 2565 protected int[] _visibleLinesWidths; // width (in pixels) of visible lines 2566 protected CustomCharProps[][] _visibleLinesHighlights; 2567 protected CustomCharProps[][] _visibleLinesHighlightsBuf; 2568 2569 protected Point _measuredTextToSetWidgetSize; 2570 protected dstring _textToSetWidgetSize = "aaaaa/naaaaa"d; 2571 protected int[] _measuredTextToSetWidgetSizeWidths; 2572 2573 /// Set _needRewrap to true; 2574 override void wordWrapRefresh() 2575 { 2576 _needRewrap = true; 2577 } 2578 2579 override @property int fontSize() const { return super.fontSize(); } 2580 override @property Widget fontSize(int size) { 2581 // Need to rewrap if fontSize changed 2582 _needRewrap = true; 2583 return super.fontSize(size); 2584 } 2585 2586 override protected int lineCount() { 2587 return _content.length; 2588 } 2589 2590 override protected void updateMaxLineWidth() { 2591 // find max line width. TODO: optimize!!! 2592 int maxw; 2593 int[] buf; 2594 for (int i = 0; i < _content.length; i++) { 2595 dstring s = _content[i]; 2596 int w = calcLineWidth(s); 2597 if (maxw < w) 2598 maxw = w; 2599 } 2600 _maxLineWidth = maxw; 2601 } 2602 2603 @property int minFontSize() { 2604 return _minFontSize; 2605 } 2606 @property EditBox minFontSize(int size) { 2607 _minFontSize = size; 2608 return this; 2609 } 2610 2611 @property int maxFontSize() { 2612 return _maxFontSize; 2613 } 2614 2615 @property EditBox maxFontSize(int size) { 2616 _maxFontSize = size; 2617 return this; 2618 } 2619 2620 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 2621 override void layout(Rect rc) { 2622 if (visibility == Visibility.Gone) 2623 return; 2624 2625 if (rc != _pos) 2626 _contentChanged = true; 2627 Rect contentRc = rc; 2628 int findPanelHeight; 2629 if (_findPanel && _findPanel.visibility != Visibility.Gone) { 2630 _findPanel.measure(rc.width, rc.height); 2631 findPanelHeight = _findPanel.measuredHeight; 2632 _findPanel.layout(Rect(rc.left, rc.bottom - findPanelHeight, rc.right, rc.bottom)); 2633 contentRc.bottom -= findPanelHeight; 2634 } 2635 2636 super.layout(contentRc); 2637 if (_contentChanged) { 2638 measureVisibleText(); 2639 _needRewrap = true; 2640 _contentChanged = false; 2641 } 2642 2643 _pos = rc; 2644 } 2645 2646 override protected Point measureVisibleText() { 2647 Point sz; 2648 FontRef font = font(); 2649 _lineHeight = font.height; 2650 _numVisibleLines = (_clientRect.height + _lineHeight - 1) / _lineHeight; 2651 if (_firstVisibleLine >= _content.length) { 2652 _firstVisibleLine = _content.length - _numVisibleLines + 1; 2653 if (_firstVisibleLine < 0) 2654 _firstVisibleLine = 0; 2655 _caretPos.line = _content.length - 1; 2656 _caretPos.pos = 0; 2657 } 2658 if (_numVisibleLines < 1) 2659 _numVisibleLines = 1; 2660 if (_firstVisibleLine + _numVisibleLines > _content.length) 2661 _numVisibleLines = _content.length - _firstVisibleLine; 2662 if (_numVisibleLines < 1) 2663 _numVisibleLines = 1; 2664 _visibleLines.length = _numVisibleLines; 2665 if (_visibleLinesMeasurement.length < _numVisibleLines) 2666 _visibleLinesMeasurement.length = _numVisibleLines; 2667 if (_visibleLinesWidths.length < _numVisibleLines) 2668 _visibleLinesWidths.length = _numVisibleLines; 2669 if (_visibleLinesHighlights.length < _numVisibleLines) { 2670 _visibleLinesHighlights.length = _numVisibleLines; 2671 _visibleLinesHighlightsBuf.length = _numVisibleLines; 2672 } 2673 for (int i = 0; i < _numVisibleLines; i++) { 2674 _visibleLines[i] = _content[_firstVisibleLine + i]; 2675 size_t len = _visibleLines[i].length; 2676 if (_visibleLinesMeasurement[i].length < len) 2677 _visibleLinesMeasurement[i].length = len; 2678 if (_visibleLinesHighlightsBuf[i].length < len) 2679 _visibleLinesHighlightsBuf[i].length = len; 2680 _visibleLinesHighlights[i] = handleCustomLineHighlight(_firstVisibleLine + i, _visibleLines[i], _visibleLinesHighlightsBuf[i]); 2681 int charsMeasured = font.measureText(_visibleLines[i], _visibleLinesMeasurement[i], int.max, tabSize); 2682 _visibleLinesWidths[i] = charsMeasured > 0 ? _visibleLinesMeasurement[i][charsMeasured - 1] : 0; 2683 if (sz.x < _visibleLinesWidths[i]) 2684 sz.x = _visibleLinesWidths[i]; // width - max from visible lines 2685 } 2686 sz.x = _maxLineWidth; 2687 sz.y = _lineHeight * _content.length; // height - for all lines 2688 return sz; 2689 } 2690 2691 protected bool _extendRightScrollBound = true; 2692 /// override to determine if scrollbars are needed or not 2693 override protected void checkIfScrollbarsNeeded(ref bool needHScroll, ref bool needVScroll) { 2694 needHScroll = _hscrollbar && (_hscrollbarMode == ScrollBarMode.Visible || _hscrollbarMode == ScrollBarMode.Auto); 2695 needVScroll = _vscrollbar && (_vscrollbarMode == ScrollBarMode.Visible || _vscrollbarMode == ScrollBarMode.Auto); 2696 if (!needHScroll && !needVScroll) 2697 return; // not needed 2698 if (_hscrollbarMode != ScrollBarMode.Auto && _vscrollbarMode != ScrollBarMode.Auto) 2699 return; // no auto scrollbars 2700 // either h or v scrollbar is in auto mode 2701 2702 int hsbHeight = _hscrollbar.measuredHeight; 2703 int vsbWidth = _hscrollbar.measuredWidth; 2704 2705 int visibleLines = _lineHeight > 0 ? (_clientRect.height / _lineHeight) : 1; // fully visible lines 2706 if (visibleLines < 1) 2707 visibleLines = 1; 2708 int visibleLinesWithScrollbar = _lineHeight > 0 ? ((_clientRect.height - hsbHeight) / _lineHeight) : 1; // fully visible lines 2709 if (visibleLinesWithScrollbar < 1) 2710 visibleLinesWithScrollbar = 1; 2711 2712 // either h or v scrollbar is in auto mode 2713 //Point contentSize = fullContentSize(); 2714 int contentWidth = _maxLineWidth + (_extendRightScrollBound ? _clientRect.width / 16 : 0); 2715 int contentHeight = _content.length; 2716 2717 int clientWidth = _clientRect.width; 2718 int clientHeight = visibleLines; 2719 2720 int clientWidthWithScrollbar = clientWidth - vsbWidth; 2721 int clientHeightWithScrollbar = visibleLinesWithScrollbar; 2722 2723 if (_hscrollbarMode == ScrollBarMode.Auto && _vscrollbarMode == ScrollBarMode.Auto) { 2724 // both scrollbars in auto mode 2725 bool xFits = contentWidth <= clientWidth; 2726 bool yFits = contentHeight <= clientHeight; 2727 if (!xFits && !yFits) { 2728 // none fits, need both scrollbars 2729 } else if (xFits && yFits) { 2730 // everything fits! 2731 needHScroll = false; 2732 needVScroll = false; 2733 } else if (xFits) { 2734 // only X fits 2735 if (contentWidth <= clientWidthWithScrollbar) 2736 needHScroll = false; // disable hscroll 2737 } else { // yFits 2738 // only Y fits 2739 if (contentHeight <= clientHeightWithScrollbar) 2740 needVScroll = false; // disable vscroll 2741 } 2742 } else if (_hscrollbarMode == ScrollBarMode.Auto) { 2743 // only hscroll is in auto mode 2744 if (needVScroll) 2745 clientWidth = clientWidthWithScrollbar; 2746 needHScroll = contentWidth > clientWidth; 2747 } else { 2748 // only vscroll is in auto mode 2749 if (needHScroll) 2750 clientHeight = clientHeightWithScrollbar; 2751 needVScroll = contentHeight > clientHeight; 2752 } 2753 } 2754 2755 /// update horizontal scrollbar widget position 2756 override protected void updateHScrollBar() { 2757 _hscrollbar.setRange(0, _maxLineWidth + (_extendRightScrollBound ? _clientRect.width / 16 : 0)); 2758 _hscrollbar.pageSize = _clientRect.width; 2759 _hscrollbar.position = _scrollPos.x; 2760 } 2761 2762 /// update verticat scrollbar widget position 2763 override protected void updateVScrollBar() { 2764 int visibleLines = _lineHeight ? _clientRect.height / _lineHeight : 1; // fully visible lines 2765 if (visibleLines < 1) 2766 visibleLines = 1; 2767 _vscrollbar.setRange(0, _content.length); 2768 _vscrollbar.pageSize = visibleLines; 2769 _vscrollbar.position = _firstVisibleLine; 2770 } 2771 2772 /// process horizontal scrollbar event 2773 override bool onHScroll(ScrollEvent event) { 2774 if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) { 2775 if (_scrollPos.x != event.position) { 2776 _scrollPos.x = event.position; 2777 invalidate(); 2778 } 2779 } else if (event.action == ScrollAction.PageUp) { 2780 dispatchAction(new Action(EditorActions.ScrollLeft)); 2781 } else if (event.action == ScrollAction.PageDown) { 2782 dispatchAction(new Action(EditorActions.ScrollRight)); 2783 } else if (event.action == ScrollAction.LineUp) { 2784 dispatchAction(new Action(EditorActions.ScrollLeft)); 2785 } else if (event.action == ScrollAction.LineDown) { 2786 dispatchAction(new Action(EditorActions.ScrollRight)); 2787 } 2788 return true; 2789 } 2790 2791 /// process vertical scrollbar event 2792 override bool onVScroll(ScrollEvent event) { 2793 if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) { 2794 if (_firstVisibleLine != event.position) { 2795 _firstVisibleLine = event.position; 2796 measureVisibleText(); 2797 invalidate(); 2798 } 2799 } else if (event.action == ScrollAction.PageUp) { 2800 dispatchAction(new Action(EditorActions.ScrollPageUp)); 2801 } else if (event.action == ScrollAction.PageDown) { 2802 dispatchAction(new Action(EditorActions.ScrollPageDown)); 2803 } else if (event.action == ScrollAction.LineUp) { 2804 dispatchAction(new Action(EditorActions.ScrollLineUp)); 2805 } else if (event.action == ScrollAction.LineDown) { 2806 dispatchAction(new Action(EditorActions.ScrollLineDown)); 2807 } 2808 return true; 2809 } 2810 2811 protected bool _enableScrollAfterText = true; 2812 override protected void ensureCaretVisible(bool center = false) { 2813 if (_caretPos.line >= _content.length) 2814 _caretPos.line = _content.length - 1; 2815 if (_caretPos.line < 0) 2816 _caretPos.line = 0; 2817 int visibleLines = _lineHeight > 0 ? _clientRect.height / _lineHeight : 1; // fully visible lines 2818 if (visibleLines < 1) 2819 visibleLines = 1; 2820 int maxFirstVisibleLine = _content.length - 1; 2821 if (!_enableScrollAfterText) 2822 maxFirstVisibleLine = _content.length - visibleLines; 2823 if (maxFirstVisibleLine < 0) 2824 maxFirstVisibleLine = 0; 2825 2826 if (_caretPos.line < _firstVisibleLine) { 2827 _firstVisibleLine = _caretPos.line; 2828 if (center) { 2829 _firstVisibleLine -= visibleLines / 2; 2830 if (_firstVisibleLine < 0) 2831 _firstVisibleLine = 0; 2832 } 2833 if (_firstVisibleLine > maxFirstVisibleLine) 2834 _firstVisibleLine = maxFirstVisibleLine; 2835 measureVisibleText(); 2836 invalidate(); 2837 } else if(_wordWrap && !(_firstVisibleLine > maxFirstVisibleLine)) { 2838 //For wordwrap mode, move down sooner 2839 int offsetLines = -1 * caretHeightOffset / _lineHeight; 2840 //Log.d("offsetLines: ", offsetLines); 2841 if (_caretPos.line >= _firstVisibleLine + visibleLines - offsetLines) 2842 { 2843 _firstVisibleLine = _caretPos.line - visibleLines + 1 + offsetLines; 2844 if (center) 2845 _firstVisibleLine += visibleLines / 2; 2846 if (_firstVisibleLine > maxFirstVisibleLine) 2847 _firstVisibleLine = maxFirstVisibleLine; 2848 if (_firstVisibleLine < 0) 2849 _firstVisibleLine = 0; 2850 measureVisibleText(); 2851 invalidate(); 2852 } 2853 } else if (_caretPos.line >= _firstVisibleLine + visibleLines) { 2854 _firstVisibleLine = _caretPos.line - visibleLines + 1; 2855 if (center) 2856 _firstVisibleLine += visibleLines / 2; 2857 if (_firstVisibleLine > maxFirstVisibleLine) 2858 _firstVisibleLine = maxFirstVisibleLine; 2859 if (_firstVisibleLine < 0) 2860 _firstVisibleLine = 0; 2861 measureVisibleText(); 2862 invalidate(); 2863 } else if (_firstVisibleLine > maxFirstVisibleLine) { 2864 _firstVisibleLine = maxFirstVisibleLine; 2865 if (_firstVisibleLine < 0) 2866 _firstVisibleLine = 0; 2867 measureVisibleText(); 2868 invalidate(); 2869 } 2870 //_scrollPos 2871 Rect rc = textPosToClient(_caretPos); 2872 if (rc.left < 0) { 2873 // scroll left 2874 _scrollPos.x -= -rc.left + _clientRect.width / 4; 2875 if (_scrollPos.x < 0) 2876 _scrollPos.x = 0; 2877 invalidate(); 2878 } else if (rc.left >= _clientRect.width - 10) { 2879 // scroll right 2880 if (!_wordWrap) 2881 _scrollPos.x += (rc.left - _clientRect.width) + _clientRect.width / 4; 2882 invalidate(); 2883 } 2884 updateScrollBars(); 2885 handleEditorStateChange(); 2886 } 2887 2888 override protected Rect textPosToClient(TextPosition p) { 2889 Rect res; 2890 int lineIndex = p.line - _firstVisibleLine; 2891 res.top = lineIndex * _lineHeight; 2892 res.bottom = res.top + _lineHeight; 2893 // if visible 2894 if (lineIndex >= 0 && lineIndex < _visibleLines.length) { 2895 if (p.pos == 0) 2896 res.left = 0; 2897 else if (p.pos >= _visibleLinesMeasurement[lineIndex].length) 2898 res.left = _visibleLinesWidths[lineIndex]; 2899 else 2900 res.left = _visibleLinesMeasurement[lineIndex][p.pos - 1]; 2901 } 2902 res.left -= _scrollPos.x; 2903 res.right = res.left + 1; 2904 return res; 2905 } 2906 2907 override protected TextPosition clientToTextPos(Point pt) { 2908 TextPosition res; 2909 pt.x += _scrollPos.x; 2910 int lineIndex = pt.y / _lineHeight; 2911 if (lineIndex < 0) 2912 lineIndex = 0; 2913 if (lineIndex < _visibleLines.length) { 2914 res.line = lineIndex + _firstVisibleLine; 2915 int len = cast(int)_visibleLines[lineIndex].length; 2916 for (int i = 0; i < len; i++) { 2917 int x0 = i > 0 ? _visibleLinesMeasurement[lineIndex][i - 1] : 0; 2918 int x1 = _visibleLinesMeasurement[lineIndex][i]; 2919 int mx = (x0 + x1) >> 1; 2920 if (pt.x <= mx) { 2921 res.pos = i; 2922 return res; 2923 } 2924 } 2925 res.pos = cast(int)_visibleLines[lineIndex].length; 2926 } else if (_visibleLines.length > 0) { 2927 res.line = _firstVisibleLine + cast(int)_visibleLines.length - 1; 2928 res.pos = cast(int)_visibleLines[$ - 1].length; 2929 } else { 2930 res.line = 0; 2931 res.pos = 0; 2932 } 2933 return res; 2934 } 2935 2936 override protected bool handleAction(const Action a) { 2937 TextPosition oldCaretPos = _caretPos; 2938 dstring currentLine = _content[_caretPos.line]; 2939 switch (a.id) with(EditorActions) 2940 { 2941 case PrependNewLine: 2942 if (!readOnly) { 2943 correctCaretPos(); 2944 _caretPos.pos = 0; 2945 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]); 2946 _content.performOperation(op, this); 2947 } 2948 return true; 2949 case InsertNewLine: 2950 if (!readOnly) { 2951 correctCaretPos(); 2952 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]); 2953 _content.performOperation(op, this); 2954 } 2955 return true; 2956 case Up: 2957 case SelectUp: 2958 if ((_caretPos.line > 0) | wordWrap) { 2959 if (_wordWrap) 2960 { 2961 LineSpan curSpan = getSpan(_caretPos.line); 2962 int curWrap = findWrapLine(_caretPos); 2963 if (curWrap > 0) 2964 { 2965 _caretPos.pos-= curSpan.wrapPoints[curWrap - 1].wrapPos; 2966 } 2967 else 2968 { 2969 int previousPos = _caretPos.pos; 2970 curSpan = getSpan(_caretPos.line - 1); 2971 curWrap = curSpan.len - 1; 2972 if (curWrap > 0) 2973 { 2974 int accumulativePoint = curSpan.accumulation(curSpan.len - 1, LineSpan.WrapPointInfo.Position); 2975 _caretPos.line--; 2976 _caretPos.pos = accumulativePoint + previousPos; 2977 } 2978 else 2979 { 2980 _caretPos.line--; 2981 } 2982 } 2983 } 2984 else if(_caretPos.line > 0) 2985 _caretPos.line--; 2986 correctCaretPos(); 2987 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 2988 ensureCaretVisible(); 2989 } 2990 return true; 2991 case Down: 2992 case SelectDown: 2993 if (_caretPos.line < _content.length - 1) { 2994 if (_wordWrap) 2995 { 2996 LineSpan curSpan = getSpan(_caretPos.line); 2997 int curWrap = findWrapLine(_caretPos); 2998 if (curWrap < curSpan.len - 1) 2999 { 3000 int previousPos = _caretPos.pos; 3001 _caretPos.pos+= curSpan.wrapPoints[curWrap].wrapPos; 3002 correctCaretPos(); 3003 if (_caretPos.pos == previousPos) 3004 { 3005 _caretPos.pos = 0; 3006 _caretPos.line++; 3007 } 3008 } 3009 else if (curSpan.len > 1) 3010 { 3011 int previousPos = _caretPos.pos; 3012 int previousAccumulatedPosition = curSpan.accumulation(curSpan.len - 1, LineSpan.WrapPointInfo.Position); 3013 _caretPos.line++; 3014 _caretPos.pos = previousPos - previousAccumulatedPosition; 3015 } 3016 else 3017 { 3018 _caretPos.line++; 3019 } 3020 } 3021 else 3022 { 3023 _caretPos.line++; 3024 } 3025 correctCaretPos(); 3026 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3027 ensureCaretVisible(); 3028 } 3029 return true; 3030 case PageBegin: 3031 case SelectPageBegin: 3032 { 3033 ensureCaretVisible(); 3034 _caretPos.line = _firstVisibleLine; 3035 correctCaretPos(); 3036 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3037 } 3038 return true; 3039 case PageEnd: 3040 case SelectPageEnd: 3041 { 3042 ensureCaretVisible(); 3043 int fullLines = _clientRect.height / _lineHeight; 3044 int newpos = _firstVisibleLine + fullLines - 1; 3045 if (newpos >= _content.length) 3046 newpos = _content.length - 1; 3047 _caretPos.line = newpos; 3048 correctCaretPos(); 3049 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3050 } 3051 return true; 3052 case PageUp: 3053 case SelectPageUp: 3054 { 3055 ensureCaretVisible(); 3056 int fullLines = _clientRect.height / _lineHeight; 3057 int newpos = _firstVisibleLine - fullLines; 3058 if (newpos < 0) { 3059 _firstVisibleLine = 0; 3060 _caretPos.line = 0; 3061 } else { 3062 int delta = _firstVisibleLine - newpos; 3063 _firstVisibleLine = newpos; 3064 _caretPos.line -= delta; 3065 } 3066 correctCaretPos(); 3067 measureVisibleText(); 3068 updateScrollBars(); 3069 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3070 } 3071 return true; 3072 case PageDown: 3073 case SelectPageDown: 3074 { 3075 ensureCaretVisible(); 3076 int fullLines = _clientRect.height / _lineHeight; 3077 int newpos = _firstVisibleLine + fullLines; 3078 if (newpos >= _content.length) { 3079 _caretPos.line = _content.length - 1; 3080 } else { 3081 int delta = newpos - _firstVisibleLine; 3082 _firstVisibleLine = newpos; 3083 _caretPos.line += delta; 3084 } 3085 correctCaretPos(); 3086 measureVisibleText(); 3087 updateScrollBars(); 3088 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3089 } 3090 return true; 3091 case ScrollLeft: 3092 { 3093 if (_scrollPos.x > 0) { 3094 int newpos = _scrollPos.x - _spaceWidth * 4; 3095 if (newpos < 0) 3096 newpos = 0; 3097 _scrollPos.x = newpos; 3098 updateScrollBars(); 3099 invalidate(); 3100 } 3101 } 3102 return true; 3103 case ScrollRight: 3104 { 3105 if (_scrollPos.x < _maxLineWidth - _clientRect.width) { 3106 int newpos = _scrollPos.x + _spaceWidth * 4; 3107 if (newpos > _maxLineWidth - _clientRect.width) 3108 newpos = _maxLineWidth - _clientRect.width; 3109 _scrollPos.x = newpos; 3110 updateScrollBars(); 3111 invalidate(); 3112 } 3113 } 3114 return true; 3115 case ScrollLineUp: 3116 { 3117 if (_firstVisibleLine > 0) { 3118 _firstVisibleLine -= 3; 3119 if (_firstVisibleLine < 0) 3120 _firstVisibleLine = 0; 3121 measureVisibleText(); 3122 updateScrollBars(); 3123 invalidate(); 3124 } 3125 } 3126 return true; 3127 case ScrollPageUp: 3128 { 3129 int fullLines = _clientRect.height / _lineHeight; 3130 if (_firstVisibleLine > 0) { 3131 _firstVisibleLine -= fullLines * 3 / 4; 3132 if (_firstVisibleLine < 0) 3133 _firstVisibleLine = 0; 3134 measureVisibleText(); 3135 updateScrollBars(); 3136 invalidate(); 3137 } 3138 } 3139 return true; 3140 case ScrollLineDown: 3141 { 3142 int fullLines = _clientRect.height / _lineHeight; 3143 if (_firstVisibleLine + fullLines < _content.length) { 3144 _firstVisibleLine += 3; 3145 if (_firstVisibleLine > _content.length - fullLines) 3146 _firstVisibleLine = _content.length - fullLines; 3147 if (_firstVisibleLine < 0) 3148 _firstVisibleLine = 0; 3149 measureVisibleText(); 3150 updateScrollBars(); 3151 invalidate(); 3152 } 3153 } 3154 return true; 3155 case ScrollPageDown: 3156 { 3157 int fullLines = _clientRect.height / _lineHeight; 3158 if (_firstVisibleLine + fullLines < _content.length) { 3159 _firstVisibleLine += fullLines * 3 / 4; 3160 if (_firstVisibleLine > _content.length - fullLines) 3161 _firstVisibleLine = _content.length - fullLines; 3162 if (_firstVisibleLine < 0) 3163 _firstVisibleLine = 0; 3164 measureVisibleText(); 3165 updateScrollBars(); 3166 invalidate(); 3167 } 3168 } 3169 return true; 3170 case ZoomOut: 3171 case ZoomIn: 3172 { 3173 int dir = a.id == ZoomIn ? 1 : -1; 3174 if (_minFontSize < _maxFontSize && _minFontSize > 0 && _maxFontSize > 0) { 3175 int currentFontSize = fontSize; 3176 int increment = currentFontSize >= 30 ? 2 : 1; 3177 int newFontSize = currentFontSize + increment * dir; //* 110 / 100; 3178 if (newFontSize > 30) 3179 newFontSize &= 0xFFFE; 3180 if (currentFontSize != newFontSize && newFontSize <= _maxFontSize && newFontSize >= _minFontSize) { 3181 Log.i("Font size in editor ", id, " zoomed to ", newFontSize); 3182 fontSize = cast(ushort)newFontSize; 3183 updateFontProps(); 3184 _needRewrap = true; 3185 measureVisibleText(); 3186 updateScrollBars(); 3187 invalidate(); 3188 } 3189 } 3190 } 3191 return true; 3192 case ToggleBlockComment: 3193 if (!readOnly && _content.syntaxSupport && _content.syntaxSupport.supportsToggleBlockComment && _content.syntaxSupport.canToggleBlockComment(_selectionRange)) 3194 _content.syntaxSupport.toggleBlockComment(_selectionRange, this); 3195 return true; 3196 case ToggleLineComment: 3197 if (!readOnly && _content.syntaxSupport && _content.syntaxSupport.supportsToggleLineComment && _content.syntaxSupport.canToggleLineComment(_selectionRange)) 3198 _content.syntaxSupport.toggleLineComment(_selectionRange, this); 3199 return true; 3200 case AppendNewLine: 3201 if (!readOnly) { 3202 correctCaretPos(); 3203 TextPosition p = _content.lineEnd(_caretPos.line); 3204 TextRange r = TextRange(p, p); 3205 EditOperation op = new EditOperation(EditAction.Replace, r, [""d, ""d]); 3206 _content.performOperation(op, this); 3207 _caretPos = oldCaretPos; 3208 handleEditorStateChange(); 3209 } 3210 return true; 3211 case DeleteLine: 3212 if (!readOnly) { 3213 correctCaretPos(); 3214 EditOperation op = new EditOperation(EditAction.Replace, _content.lineRange(_caretPos.line), [""d]); 3215 _content.performOperation(op, this); 3216 } 3217 return true; 3218 case Find: 3219 openFindPanel(); 3220 return true; 3221 case FindNext: 3222 findNext(false); 3223 return true; 3224 case FindPrev: 3225 findNext(true); 3226 return true; 3227 case Replace: 3228 openReplacePanel(); 3229 return true; 3230 default: 3231 break; 3232 } 3233 return super.handleAction(a); 3234 } 3235 3236 /// calculate full content size in pixels 3237 override Point fullContentSize() { 3238 Point textSz; 3239 textSz.y = _lineHeight * _content.length; 3240 textSz.x = _maxLineWidth; 3241 //int maxy = _lineHeight * 5; // limit measured height 3242 //if (textSz.y > maxy) 3243 // textSz.y = maxy; 3244 return textSz; 3245 } 3246 3247 // override to set minimum scrollwidget size - default 100x100 3248 override protected Point minimumVisibleContentSize() { 3249 FontRef font = font(); 3250 _measuredTextToSetWidgetSizeWidths.length = _textToSetWidgetSize.length; 3251 int charsMeasured = font.measureText(_textToSetWidgetSize, _measuredTextToSetWidgetSizeWidths, MAX_WIDTH_UNSPECIFIED, tabSize); 3252 _measuredTextToSetWidgetSize.x = charsMeasured > 0 ? _measuredTextToSetWidgetSizeWidths[charsMeasured - 1]: 0; 3253 _measuredTextToSetWidgetSize.y = font.height; 3254 return _measuredTextToSetWidgetSize; 3255 } 3256 3257 /// measure 3258 override void measure(int parentWidth, int parentHeight) { 3259 if (visibility == Visibility.Gone) 3260 return; 3261 3262 updateFontProps(); 3263 updateMaxLineWidth(); 3264 int findPanelHeight; 3265 if (_findPanel) { 3266 _findPanel.measure(parentWidth, parentHeight); 3267 findPanelHeight = _findPanel.measuredHeight; 3268 if (parentHeight != SIZE_UNSPECIFIED) 3269 parentHeight -= findPanelHeight; 3270 } 3271 3272 super.measure(parentWidth, parentHeight); 3273 } 3274 3275 3276 protected void highlightTextPattern(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) { 3277 dstring pattern = _textToHighlight; 3278 uint options = _textToHighlightOptions; 3279 if (!pattern.length) { 3280 // support highlighting selection text - if whole word is selected 3281 if (_selectionRange.empty || !_selectionRange.singleLine) 3282 return; 3283 if (_selectionRange.start.line >= _content.length) 3284 return; 3285 dstring selLine = _content.line(_selectionRange.start.line); 3286 int start = _selectionRange.start.pos; 3287 int end = _selectionRange.end.pos; 3288 if (start >= selLine.length) 3289 return; 3290 pattern = selLine[start .. end]; 3291 if (!isWordChar(pattern[0]) || !isWordChar(pattern[$-1])) 3292 return; 3293 if (!isWholeWord(selLine, start, end)) 3294 return; 3295 // whole word is selected - enable highlight for it 3296 options = TextSearchFlag.CaseSensitive | TextSearchFlag.WholeWords; 3297 } 3298 if (!pattern.length) 3299 return; 3300 dstring lineText = _content.line(lineIndex); 3301 if (lineText.length < pattern.length) 3302 return; 3303 ptrdiff_t start = 0; 3304 import std.string : indexOf, CaseSensitive; 3305 import std.typecons : Flag; 3306 bool caseSensitive = (options & TextSearchFlag.CaseSensitive) != 0; 3307 bool wholeWords = (options & TextSearchFlag.WholeWords) != 0; 3308 bool selectionOnly = (options & TextSearchFlag.SelectionOnly) != 0; 3309 for (;;) { 3310 ptrdiff_t pos = lineText[start .. $].indexOf(pattern, caseSensitive ? Yes.caseSensitive : No.caseSensitive); 3311 if (pos < 0) 3312 break; 3313 // found text to highlight 3314 start += pos; 3315 if (!wholeWords || isWholeWord(lineText, start, start + pattern.length)) { 3316 TextRange r = TextRange(TextPosition(lineIndex, cast(int)start), TextPosition(lineIndex, cast(int)(start + pattern.length))); 3317 uint color = r.isInsideOrNext(caretPos) ? _searchHighlightColorCurrent : _searchHighlightColorOther; 3318 highlightLineRange(buf, lineRect, color, r); 3319 } 3320 start += pattern.length; 3321 } 3322 } 3323 3324 static bool isWordChar(dchar ch) { 3325 if (ch >= 'a' && ch <= 'z') 3326 return true; 3327 if (ch >= 'A' && ch <= 'Z') 3328 return true; 3329 if (ch == '_') 3330 return true; 3331 return false; 3332 } 3333 static bool isValidWordBound(dchar innerChar, dchar outerChar) { 3334 return !isWordChar(innerChar) || !isWordChar(outerChar); 3335 } 3336 /// returns true if selected range of string is whole word 3337 static bool isWholeWord(dstring lineText, size_t start, size_t end) { 3338 if (start >= lineText.length || start >= end) 3339 return false; 3340 if (start > 0 && !isValidWordBound(lineText[start], lineText[start - 1])) 3341 return false; 3342 if (end > 0 && end < lineText.length && !isValidWordBound(lineText[end - 1], lineText[end])) 3343 return false; 3344 return true; 3345 } 3346 3347 /// find all occurences of text pattern in content; options = bitset of TextSearchFlag 3348 TextRange[] findAll(dstring pattern, uint options) { 3349 TextRange[] res; 3350 res.assumeSafeAppend(); 3351 if (!pattern.length) 3352 return res; 3353 import std.string : indexOf, CaseSensitive; 3354 bool caseSensitive = (options & TextSearchFlag.CaseSensitive) != 0; 3355 bool wholeWords = (options & TextSearchFlag.WholeWords) != 0; 3356 bool selectionOnly = (options & TextSearchFlag.SelectionOnly) != 0; 3357 for (int i = 0; i < _content.length; i++) { 3358 dstring lineText = _content.line(i); 3359 if (lineText.length < pattern.length) 3360 continue; 3361 ptrdiff_t start = 0; 3362 for (;;) { 3363 ptrdiff_t pos = lineText[start .. $].indexOf(pattern, caseSensitive ? Yes.caseSensitive : No.caseSensitive); 3364 if (pos < 0) 3365 break; 3366 // found text to highlight 3367 start += pos; 3368 if (!wholeWords || isWholeWord(lineText, start, start + pattern.length)) { 3369 TextRange r = TextRange(TextPosition(i, cast(int)start), TextPosition(i, cast(int)(start + pattern.length))); 3370 res ~= r; 3371 } 3372 start += _textToHighlight.length; 3373 } 3374 } 3375 return res; 3376 } 3377 3378 /// find next occurence of text pattern in content, returns true if found 3379 bool findNextPattern(ref TextPosition pos, dstring pattern, uint searchOptions, int direction) { 3380 TextRange[] all = findAll(pattern, searchOptions); 3381 if (!all.length) 3382 return false; 3383 int currentIndex = -1; 3384 int nearestIndex = cast(int)all.length; 3385 for (int i = 0; i < all.length; i++) { 3386 if (all[i].isInsideOrNext(pos)) { 3387 currentIndex = i; 3388 break; 3389 } 3390 } 3391 for (int i = 0; i < all.length; i++) { 3392 if (pos < all[i].start) { 3393 nearestIndex = i; 3394 break; 3395 } 3396 if (pos > all[i].end) { 3397 nearestIndex = i + 1; 3398 } 3399 } 3400 if (currentIndex >= 0) { 3401 if (all.length < 2 && direction != 0) 3402 return false; 3403 currentIndex += direction; 3404 if (currentIndex < 0) 3405 currentIndex = cast(int)all.length - 1; 3406 else if (currentIndex >= all.length) 3407 currentIndex = 0; 3408 pos = all[currentIndex].start; 3409 return true; 3410 } 3411 if (direction < 0) 3412 nearestIndex--; 3413 if (nearestIndex < 0) 3414 nearestIndex = cast(int)all.length - 1; 3415 else if (nearestIndex >= all.length) 3416 nearestIndex = 0; 3417 pos = all[nearestIndex].start; 3418 return true; 3419 } 3420 3421 protected void highlightLineRange(DrawBuf buf, Rect lineRect, uint color, TextRange r) { 3422 Rect startrc = textPosToClient(r.start); 3423 Rect endrc = textPosToClient(r.end); 3424 Rect rc = lineRect; 3425 rc.left = _clientRect.left + startrc.left; 3426 rc.right = _clientRect.left + endrc.right; 3427 if (_wordWrap && !rc.empty) 3428 { 3429 wordWrapFillRect(buf, r.start.line, rc, color); 3430 } 3431 else if (!rc.empty) { 3432 // draw selection rect for matching bracket 3433 buf.fillRect(rc, color); 3434 } 3435 } 3436 3437 /// Used in place of directly calling buf.fillRect in word wrap mode 3438 void wordWrapFillRect(DrawBuf buf, int line, Rect lineToDivide, uint color) 3439 { 3440 Rect rc = lineToDivide; 3441 auto limitNumber = (int num, int limit) => num > limit ? limit : num; 3442 LineSpan curSpan = getSpan(line); 3443 int yOffset = _lineHeight * (wrapsUpTo(line)); 3444 rc.offset(0, yOffset); 3445 Rect[] wrappedSelection; 3446 wrappedSelection.length = curSpan.len; 3447 foreach (int i, wrapLineRect; wrappedSelection) 3448 { 3449 int startingDifference = rc.left - _clientRect.left; 3450 wrapLineRect = rc; 3451 wrapLineRect.offset(-1 * curSpan.accumulation(i, LineSpan.WrapPointInfo.Width), i * _lineHeight); 3452 wrapLineRect.right = limitNumber(wrapLineRect.right,(rc.left + curSpan.wrapPoints[i].wrapWidth) - startingDifference); 3453 buf.fillRect(wrapLineRect, color); 3454 } 3455 } 3456 3457 /// override to custom highlight of line background 3458 protected void drawLineBackground(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) { 3459 // highlight odd lines 3460 //if ((lineIndex & 1)) 3461 // buf.fillRect(visibleRect, 0xF4808080); 3462 3463 if (!_selectionRange.empty && _selectionRange.start.line <= lineIndex && _selectionRange.end.line >= lineIndex) { 3464 // line inside selection 3465 Rect startrc = textPosToClient(_selectionRange.start); 3466 Rect endrc = textPosToClient(_selectionRange.end); 3467 int startx = lineIndex == _selectionRange.start.line ? startrc.left + _clientRect.left : lineRect.left; 3468 int endx = lineIndex == _selectionRange.end.line ? endrc.left + _clientRect.left : lineRect.right + _spaceWidth; 3469 Rect rc = lineRect; 3470 rc.left = startx; 3471 rc.right = endx; 3472 if (!rc.empty && _wordWrap) 3473 { 3474 wordWrapFillRect(buf, lineIndex, rc, focused ? _selectionColorFocused : _selectionColorNormal); 3475 } 3476 else if (!rc.empty) { 3477 // draw selection rect for line 3478 buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal); 3479 } 3480 } 3481 3482 highlightTextPattern(buf, lineIndex, lineRect, visibleRect); 3483 3484 if (_matchingBraces.start.line == lineIndex) { 3485 TextRange r = TextRange(_matchingBraces.start, _matchingBraces.start.offset(1)); 3486 highlightLineRange(buf, lineRect, _matchingBracketHightlightColor, r); 3487 } 3488 if (_matchingBraces.end.line == lineIndex) { 3489 TextRange r = TextRange(_matchingBraces.end, _matchingBraces.end.offset(1)); 3490 highlightLineRange(buf, lineRect, _matchingBracketHightlightColor, r); 3491 } 3492 3493 // frame around current line 3494 if (focused && lineIndex == _caretPos.line && _selectionRange.singleLine && _selectionRange.start.line == _caretPos.line) { 3495 //TODO: Figure out why a little slow to catch up 3496 if (_wordWrap) 3497 visibleRect.offset(0, -caretHeightOffset); 3498 buf.drawFrame(visibleRect, 0xA0808080, Rect(1,1,1,1)); 3499 } 3500 3501 } 3502 3503 override protected void drawExtendedArea(DrawBuf buf) { 3504 if (_leftPaneWidth <= 0) 3505 return; 3506 Rect rc = _clientRect; 3507 3508 FontRef font = font(); 3509 int i = _firstVisibleLine; 3510 int lc = lineCount; 3511 for (;;) { 3512 Rect lineRect = rc; 3513 lineRect.left = _clientRect.left - _leftPaneWidth; 3514 lineRect.right = _clientRect.left; 3515 lineRect.bottom = lineRect.top + _lineHeight; 3516 if (lineRect.top >= _clientRect.bottom) 3517 break; 3518 drawLeftPane(buf, lineRect, i < lc ? i : -1); 3519 rc.top += _lineHeight; 3520 if (_wordWrap) 3521 { 3522 int currentWrap = 1; 3523 for (;;) 3524 { 3525 LineSpan curSpan = getSpan(i); 3526 if (currentWrap > curSpan.len - 1) 3527 break; 3528 Rect lineRect2 = rc; 3529 lineRect2.left = _clientRect.left - _leftPaneWidth; 3530 lineRect2.right = _clientRect.left; 3531 lineRect2.bottom = lineRect.top + _lineHeight; 3532 if (lineRect2.top >= _clientRect.bottom) 3533 break; 3534 drawLeftPane(buf, lineRect2, -1); 3535 rc.top += _lineHeight; 3536 3537 currentWrap++; 3538 } 3539 } 3540 i++; 3541 } 3542 } 3543 3544 3545 protected CustomCharProps[ubyte] _tokenHighlightColors; 3546 3547 /// set highlight options for particular token category 3548 void setTokenHightlightColor(ubyte tokenCategory, uint color, bool underline = false, bool strikeThrough = false) { 3549 _tokenHighlightColors[tokenCategory] = CustomCharProps(color, underline, strikeThrough); 3550 } 3551 /// clear highlight colors 3552 void clearTokenHightlightColors() { 3553 destroy(_tokenHighlightColors); 3554 } 3555 3556 /** 3557 Custom text color and style highlight (using text highlight) support. 3558 3559 Return null if no syntax highlight required for line. 3560 */ 3561 protected CustomCharProps[] handleCustomLineHighlight(int line, dstring txt, ref CustomCharProps[] buf) { 3562 if (!_tokenHighlightColors) 3563 return null; // no highlight colors set 3564 TokenPropString tokenProps = _content.lineTokenProps(line); 3565 if (tokenProps.length > 0) { 3566 bool hasNonzeroTokens = false; 3567 foreach(t; tokenProps) 3568 if (t) { 3569 hasNonzeroTokens = true; 3570 break; 3571 } 3572 if (!hasNonzeroTokens) 3573 return null; // all characters are of unknown token type (or white space) 3574 if (buf.length < tokenProps.length) 3575 buf.length = tokenProps.length; 3576 CustomCharProps[] colors = buf[0..tokenProps.length]; //new CustomCharProps[tokenProps.length]; 3577 for (int i = 0; i < tokenProps.length; i++) { 3578 ubyte p = tokenProps[i]; 3579 if (p in _tokenHighlightColors) 3580 colors[i] = _tokenHighlightColors[p]; 3581 else if ((p & TOKEN_CATEGORY_MASK) in _tokenHighlightColors) 3582 colors[i] = _tokenHighlightColors[(p & TOKEN_CATEGORY_MASK)]; 3583 else 3584 colors[i].color = textColor; 3585 if (isFullyTransparentColor(colors[i].color)) 3586 colors[i].color = textColor; 3587 } 3588 return colors; 3589 } 3590 return null; 3591 } 3592 3593 TextRange _matchingBraces; 3594 3595 bool _showWhiteSpaceMarks; 3596 /// when true, show marks for tabs and spaces at beginning and end of line, and tabs inside line 3597 @property bool showWhiteSpaceMarks() const { return _showWhiteSpaceMarks; } 3598 @property void showWhiteSpaceMarks(bool show) { 3599 if (_showWhiteSpaceMarks != show) { 3600 _showWhiteSpaceMarks = show; 3601 invalidate(); 3602 } 3603 } 3604 3605 /// find max tab mark column position for line 3606 protected int findMaxTabMarkColumn(int lineIndex) { 3607 if (lineIndex < 0 || lineIndex >= content.length) 3608 return -1; 3609 int maxSpace = -1; 3610 auto space = content.getLineWhiteSpace(lineIndex); 3611 maxSpace = space.firstNonSpaceColumn; 3612 if (maxSpace >= 0) 3613 return maxSpace; 3614 for(int i = lineIndex - 1; i >= 0; i--) { 3615 space = content.getLineWhiteSpace(i); 3616 if (!space.empty) { 3617 maxSpace = space.firstNonSpaceColumn; 3618 break; 3619 } 3620 } 3621 for(int i = lineIndex + 1; i < content.length; i++) { 3622 space = content.getLineWhiteSpace(i); 3623 if (!space.empty) { 3624 if (maxSpace < 0 || maxSpace < space.firstNonSpaceColumn) 3625 maxSpace = space.firstNonSpaceColumn; 3626 break; 3627 } 3628 } 3629 return maxSpace; 3630 } 3631 3632 void drawTabPositionMarks(DrawBuf buf, ref FontRef font, int lineIndex, Rect lineRect) { 3633 int maxCol = findMaxTabMarkColumn(lineIndex); 3634 if (maxCol > 0) { 3635 int spaceWidth = font.charWidth(' '); 3636 Rect rc = lineRect; 3637 uint color = addAlpha(textColor, 0xC0); 3638 for (int i = 0; i < maxCol; i += tabSize) { 3639 rc.left = lineRect.left + i * spaceWidth; 3640 rc.right = rc.left + 1; 3641 buf.fillRectPattern(rc, color, PatternType.dotted); 3642 } 3643 } 3644 } 3645 3646 void drawWhiteSpaceMarks(DrawBuf buf, ref FontRef font, dstring txt, int tabSize, Rect lineRect, Rect visibleRect) { 3647 // _showTabPositionMarks 3648 // _showWhiteSpaceMarks 3649 int firstNonSpace = -1; 3650 int lastNonSpace = -1; 3651 bool hasTabs = false; 3652 for(int i = 0; i < txt.length; i++) { 3653 if (txt[i] == '\t') { 3654 hasTabs = true; 3655 } else if (txt[i] != ' ') { 3656 if (firstNonSpace == -1) 3657 firstNonSpace = i; 3658 lastNonSpace = i + 1; 3659 } 3660 } 3661 bool spacesOnly = txt.length > 0 && firstNonSpace < 0; 3662 if (firstNonSpace <= 0 && lastNonSpace >= txt.length && !hasTabs && !spacesOnly) 3663 return; 3664 uint color = addAlpha(textColor, 0xC0); 3665 static int[] textSizeBuffer; 3666 int charsMeasured = font.measureText(txt, textSizeBuffer, MAX_WIDTH_UNSPECIFIED, tabSize, 0, 0); 3667 int ts = tabSize; 3668 if (ts < 1) 3669 ts = 1; 3670 if (ts > 8) 3671 ts = 8; 3672 int spaceIndex = 0; 3673 for (int i = 0; i < txt.length && i < charsMeasured; i++) { 3674 dchar ch = txt[i]; 3675 bool outsideText = (i < firstNonSpace || i >= lastNonSpace || spacesOnly); 3676 if ((ch == ' ' && outsideText) || ch == '\t') { 3677 Rect rc = lineRect; 3678 rc.left = lineRect.left + (i > 0 ? textSizeBuffer[i - 1] : 0); 3679 rc.right = lineRect.left + textSizeBuffer[i]; 3680 int h = rc.height; 3681 if (rc.intersects(visibleRect)) { 3682 // draw space mark 3683 if (ch == ' ') { 3684 // space 3685 int sz = h / 6; 3686 if (sz < 1) 3687 sz = 1; 3688 rc.top += h / 2 - sz / 2; 3689 rc.bottom = rc.top + sz; 3690 rc.left += rc.width / 2 - sz / 2; 3691 rc.right = rc.left + sz; 3692 buf.fillRect(rc, color); 3693 } else if (ch == '\t') { 3694 // tab 3695 Point p1 = Point(rc.left + 1, rc.top + h / 2); 3696 Point p2 = p1; 3697 p2.x = rc.right - 1; 3698 int sz = h / 4; 3699 if (sz < 2) 3700 sz = 2; 3701 if (sz > p2.x - p1.x) 3702 sz = p2.x - p1.x; 3703 buf.drawLine(p1, p2, color); 3704 buf.drawLine(p2, Point(p2.x - sz, p2.y - sz), color); 3705 buf.drawLine(p2, Point(p2.x - sz, p2.y + sz), color); 3706 } 3707 } 3708 } 3709 } 3710 } 3711 3712 /// Clear _span 3713 void resetVisibleSpans() 3714 { 3715 //TODO: Don't erase spans which have not been modified, cache them 3716 _span = []; 3717 } 3718 3719 private bool _needRewrap = true; 3720 private int lastStartingLine; 3721 3722 override protected void drawClient(DrawBuf buf) { 3723 // update matched braces 3724 if (!content.findMatchedBraces(_caretPos, _matchingBraces)) { 3725 _matchingBraces.start.line = -1; 3726 _matchingBraces.end.line = -1; 3727 } 3728 3729 Rect rc = _clientRect; 3730 3731 if (_contentChanged) 3732 _needRewrap = true; 3733 if (lastStartingLine != _firstVisibleLine) 3734 { 3735 _needRewrap = true; 3736 lastStartingLine = _firstVisibleLine; 3737 } 3738 if (rc.width <= 0 && _wordWrap) 3739 { 3740 //Prevent drawClient from getting stuck in loop 3741 return; 3742 } 3743 bool doRewrap = false; 3744 if (_needRewrap && _wordWrap) 3745 { 3746 resetVisibleSpans(); 3747 _needRewrap = false; 3748 doRewrap = true; 3749 } 3750 3751 FontRef font = font(); 3752 int previousWraps; 3753 for (int i = 0; i < _visibleLines.length; i++) { 3754 dstring txt = _visibleLines[i]; 3755 Rect lineRect; 3756 lineRect.left = _clientRect.left - _scrollPos.x; 3757 lineRect.right = lineRect.left + calcLineWidth(_content[_firstVisibleLine + i]); 3758 lineRect.top = _clientRect.top + i * _lineHeight; 3759 lineRect.bottom = lineRect.top + _lineHeight; 3760 Rect visibleRect = lineRect; 3761 visibleRect.left = _clientRect.left; 3762 visibleRect.right = _clientRect.right; 3763 drawLineBackground(buf, _firstVisibleLine + i, lineRect, visibleRect); 3764 if (_showTabPositionMarks) 3765 drawTabPositionMarks(buf, font, _firstVisibleLine + i, lineRect); 3766 if (!txt.length && !_wordWrap) 3767 continue; 3768 if (_showWhiteSpaceMarks) 3769 { 3770 Rect whiteSpaceRc = lineRect; 3771 Rect whiteSpaceRcVisible = visibleRect; 3772 for(int z; z < previousWraps; z++) 3773 { 3774 whiteSpaceRc.offset(0, _lineHeight); 3775 whiteSpaceRcVisible.offset(0, _lineHeight); 3776 } 3777 drawWhiteSpaceMarks(buf, font, txt, tabSize, whiteSpaceRc, whiteSpaceRcVisible); 3778 } 3779 if (_leftPaneWidth > 0) { 3780 Rect leftPaneRect = visibleRect; 3781 leftPaneRect.right = leftPaneRect.left; 3782 leftPaneRect.left -= _leftPaneWidth; 3783 drawLeftPane(buf, leftPaneRect, 0); 3784 } 3785 if (txt.length > 0 || _wordWrap) { 3786 CustomCharProps[] highlight = _visibleLinesHighlights[i]; 3787 if (_wordWrap) 3788 { 3789 dstring[] wrappedLine; 3790 if (doRewrap) 3791 wrappedLine = wrapLine(txt, _firstVisibleLine + i); 3792 else 3793 if (i < _span.length) 3794 wrappedLine = _span[i].wrappedContent; 3795 int accumulativeLength; 3796 CustomCharProps[] wrapProps; 3797 foreach (int q, curWrap; wrappedLine) 3798 { 3799 auto lineOffset = q + i + wrapsUpTo(i + _firstVisibleLine); 3800 if (highlight) 3801 { 3802 wrapProps = highlight[accumulativeLength .. $]; 3803 accumulativeLength += curWrap.length; 3804 font.drawColoredText(buf, rc.left - _scrollPos.x, rc.top + lineOffset * _lineHeight, curWrap, wrapProps, tabSize); 3805 } 3806 else 3807 font.drawText(buf, rc.left - _scrollPos.x, rc.top + lineOffset * _lineHeight, curWrap, textColor, tabSize); 3808 3809 } 3810 previousWraps += to!int(wrappedLine.length - 1); 3811 } 3812 else 3813 { 3814 if (highlight) 3815 font.drawColoredText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, highlight, tabSize); 3816 else 3817 font.drawText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, textColor, tabSize); 3818 } 3819 } 3820 } 3821 3822 drawCaret(buf); 3823 } 3824 3825 protected override bool onLeftPaneMouseClick(MouseEvent event) { 3826 if (_leftPaneWidth <= 0) 3827 return false; 3828 Rect rc = _clientRect; 3829 FontRef font = font(); 3830 int i = _firstVisibleLine; 3831 int lc = lineCount; 3832 for (;;) { 3833 Rect lineRect = rc; 3834 lineRect.left = _clientRect.left - _leftPaneWidth; 3835 lineRect.right = _clientRect.left; 3836 lineRect.bottom = lineRect.top + _lineHeight; 3837 if (lineRect.top >= _clientRect.bottom) 3838 break; 3839 if (event.y >= lineRect.top && event.y < lineRect.bottom) { 3840 return handleLeftPaneMouseClick(event, lineRect, i); 3841 } 3842 i++; 3843 rc.top += _lineHeight; 3844 } 3845 return false; 3846 } 3847 3848 override protected MenuItem getLeftPaneIconsPopupMenu(int line) { 3849 MenuItem menu = new MenuItem(); 3850 Action toggleBookmarkAction = ACTION_EDITOR_TOGGLE_BOOKMARK.clone(); 3851 toggleBookmarkAction.longParam = line; 3852 toggleBookmarkAction.objectParam = this; 3853 MenuItem item = menu.add(toggleBookmarkAction); 3854 return menu; 3855 } 3856 3857 protected FindPanel _findPanel; 3858 3859 dstring selectionText(bool singleLineOnly = false) { 3860 TextRange range = _selectionRange; 3861 if (range.empty) { 3862 return null; 3863 } 3864 dstring res = getRangeText(range); 3865 if (singleLineOnly) { 3866 for (int i = 0; i < res.length; i++) { 3867 if (res[i] == '\n') { 3868 res = res[0 .. i]; 3869 break; 3870 } 3871 } 3872 } 3873 return res; 3874 } 3875 3876 protected void findNext(bool backward) { 3877 createFindPanel(false, false); 3878 _findPanel.findNext(backward); 3879 // don't change replace mode 3880 } 3881 3882 protected void openFindPanel() { 3883 createFindPanel(false, false); 3884 _findPanel.replaceMode = false; 3885 _findPanel.activate(); 3886 } 3887 3888 protected void openReplacePanel() { 3889 createFindPanel(false, true); 3890 _findPanel.replaceMode = true; 3891 _findPanel.activate(); 3892 } 3893 3894 /// create find panel; returns true if panel was not yet visible 3895 protected bool createFindPanel(bool selectionOnly, bool replaceMode) { 3896 bool res = false; 3897 dstring txt = selectionText(true); 3898 if (!_findPanel) { 3899 _findPanel = new FindPanel(this, selectionOnly, replaceMode, txt); 3900 addChild(_findPanel); 3901 res = true; 3902 } else { 3903 if (_findPanel.visibility != Visibility.Visible) { 3904 _findPanel.visibility = Visibility.Visible; 3905 if (txt.length) 3906 _findPanel.searchText = txt; 3907 res = true; 3908 } 3909 } 3910 if (!pos.empty) 3911 layout(pos); 3912 requestLayout(); 3913 return res; 3914 } 3915 3916 /// close find panel 3917 protected void closeFindPanel(bool hideOnly = true) { 3918 if (_findPanel) { 3919 setFocus(); 3920 if (hideOnly) { 3921 _findPanel.visibility = Visibility.Gone; 3922 } else { 3923 removeChild(_findPanel); 3924 destroy(_findPanel); 3925 _findPanel = null; 3926 requestLayout(); 3927 } 3928 } 3929 } 3930 3931 /// Draw widget at its position to buffer 3932 override void onDraw(DrawBuf buf) { 3933 if (visibility != Visibility.Visible) 3934 return; 3935 super.onDraw(buf); 3936 if (_findPanel && _findPanel.visibility == Visibility.Visible) { 3937 _findPanel.onDraw(buf); 3938 } 3939 } 3940 } 3941 3942 /// Read only edit box for displaying logs with lines append operation 3943 class LogWidget : EditBox { 3944 3945 protected int _maxLines; 3946 /// max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit 3947 @property int maxLines() { return _maxLines; } 3948 /// set max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit 3949 @property void maxLines(int n) { _maxLines = n; } 3950 3951 protected bool _scrollLock; 3952 /// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction) 3953 @property bool scrollLock() { return _scrollLock; } 3954 /// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction) 3955 @property void scrollLock(bool flg) { _scrollLock = flg; } 3956 3957 this() { 3958 this(null); 3959 } 3960 3961 this(string ID) { 3962 super(ID); 3963 styleId = STYLE_LOG_WIDGET; 3964 _scrollLock = true; 3965 _enableScrollAfterText = false; 3966 enabled = false; 3967 minFontSize(pointsToPixels(6)).maxFontSize(pointsToPixels(32)); // allow font zoom with Ctrl + MouseWheel 3968 onThemeChanged(); 3969 } 3970 3971 /// append lines to the end of text 3972 void appendText(dstring text) { 3973 import std.array : split; 3974 if (text.length == 0) 3975 return; 3976 dstring[] lines = text.split("\n"); 3977 //lines ~= ""d; // append new line after last line 3978 content.appendLines(lines); 3979 if (_maxLines > 0 && lineCount > _maxLines) { 3980 TextRange range; 3981 range.end.line = lineCount - _maxLines; 3982 EditOperation op = new EditOperation(EditAction.Replace, range, [""d]); 3983 _content.performOperation(op, this); 3984 _contentChanged = true; 3985 } 3986 updateScrollBars(); 3987 if (_scrollLock) { 3988 _caretPos = lastLineBegin(); 3989 ensureCaretVisible(); 3990 } 3991 } 3992 3993 TextPosition lastLineBegin() { 3994 TextPosition res; 3995 if (_content.length == 0) 3996 return res; 3997 if (_content.lineLength(_content.length - 1) == 0 && _content.length > 1) 3998 res.line = _content.length - 2; 3999 else 4000 res.line = _content.length - 1; 4001 return res; 4002 } 4003 4004 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 4005 override void layout(Rect rc) { 4006 if (visibility == Visibility.Gone) 4007 return; 4008 4009 super.layout(rc); 4010 if (_scrollLock) { 4011 measureVisibleText(); 4012 _caretPos = lastLineBegin(); 4013 ensureCaretVisible(); 4014 } 4015 } 4016 4017 } 4018 4019 class FindPanel : HorizontalLayout { 4020 protected EditBox _editor; 4021 protected EditLine _edFind; 4022 protected EditLine _edReplace; 4023 protected ImageCheckButton _cbCaseSensitive; 4024 protected ImageCheckButton _cbWholeWords; 4025 protected CheckBox _cbSelection; 4026 protected Button _btnFindNext; 4027 protected Button _btnFindPrev; 4028 protected Button _btnReplace; 4029 protected Button _btnReplaceAndFind; 4030 protected Button _btnReplaceAll; 4031 protected ImageButton _btnClose; 4032 protected bool _replaceMode; 4033 /// returns true if panel is working in replace mode 4034 @property bool replaceMode() { return _replaceMode; } 4035 @property FindPanel replaceMode(bool newMode) { 4036 if (newMode != _replaceMode) { 4037 _replaceMode = newMode; 4038 childById("replace").visibility = newMode ? Visibility.Visible : Visibility.Gone; 4039 } 4040 return this; 4041 } 4042 4043 @property dstring searchText() { 4044 return _edFind.text; 4045 } 4046 4047 @property FindPanel searchText(dstring newText) { 4048 _edFind.text = newText; 4049 return this; 4050 } 4051 4052 this(EditBox editor, bool selectionOnly, bool replace, dstring initialText = ""d) { 4053 _replaceMode = replace; 4054 import dlangui.dml.parser; 4055 try { 4056 parseML(q{ 4057 { 4058 layoutWidth: fill 4059 VerticalLayout { 4060 layoutWidth: fill 4061 HorizontalLayout { 4062 layoutWidth: fill 4063 EditLine { id: edFind; layoutWidth: fill; alignment: vcenter } 4064 Button { id: btnFindNext; text: EDIT_FIND_NEXT } 4065 Button { id: btnFindPrev; text: EDIT_FIND_PREV } 4066 VerticalLayout { 4067 VSpacer {} 4068 HorizontalLayout { 4069 ImageCheckButton { id: cbCaseSensitive; drawableId: "find_case_sensitive"; tooltipText: EDIT_FIND_CASE_SENSITIVE; styleId: TOOLBAR_BUTTON; alignment: vcenter } 4070 ImageCheckButton { id: cbWholeWords; drawableId: "find_whole_words"; tooltipText: EDIT_FIND_WHOLE_WORDS; styleId: TOOLBAR_BUTTON; alignment: vcenter } 4071 CheckBox { id: cbSelection; text: "Sel" } 4072 } 4073 VSpacer {} 4074 } 4075 } 4076 HorizontalLayout { 4077 id: replace 4078 layoutWidth: fill; 4079 EditLine { id: edReplace; layoutWidth: fill; alignment: vcenter } 4080 Button { id: btnReplace; text: EDIT_REPLACE_NEXT } 4081 Button { id: btnReplaceAndFind; text: EDIT_REPLACE_AND_FIND } 4082 Button { id: btnReplaceAll; text: EDIT_REPLACE_ALL } 4083 } 4084 } 4085 VerticalLayout { 4086 VSpacer {} 4087 ImageButton { id: btnClose; drawableId: close; styleId: BUTTON_TRANSPARENT } 4088 VSpacer {} 4089 } 4090 } 4091 }, null, this); 4092 } catch (Exception e) { 4093 Log.e("Exception while parsing DML: ", e); 4094 } 4095 _editor = editor; 4096 _edFind = childById!EditLine("edFind"); 4097 _edReplace = childById!EditLine("edReplace"); 4098 4099 if (initialText.length) { 4100 _edFind.text = initialText; 4101 _edReplace.text = initialText; 4102 } 4103 4104 _edFind.editorAction.connect(&onFindEditorAction); 4105 _edFind.contentChange.connect(&onFindTextChange); 4106 4107 //_edFind.keyEvent = &onEditorKeyEvent; 4108 //_edReplace.keyEvent = &onEditorKeyEvent; 4109 4110 _btnFindNext = childById!Button("btnFindNext"); 4111 _btnFindNext.click = &onButtonClick; 4112 _btnFindPrev = childById!Button("btnFindPrev"); 4113 _btnFindPrev.click = &onButtonClick; 4114 _btnReplace = childById!Button("btnReplace"); 4115 _btnReplace.click = &onButtonClick; 4116 _btnReplaceAndFind = childById!Button("btnReplaceAndFind"); 4117 _btnReplaceAndFind.click = &onButtonClick; 4118 _btnReplaceAll = childById!Button("btnReplaceAll"); 4119 _btnReplaceAll.click = &onButtonClick; 4120 _btnClose = childById!ImageButton("btnClose"); 4121 _btnClose.click = &onButtonClick; 4122 _cbCaseSensitive = childById!ImageCheckButton("cbCaseSensitive"); 4123 _cbWholeWords = childById!ImageCheckButton("cbWholeWords"); 4124 _cbSelection = childById!CheckBox("cbSelection"); 4125 _cbCaseSensitive.checkChange = &onCaseSensitiveCheckChange; 4126 _cbWholeWords.checkChange = &onCaseSensitiveCheckChange; 4127 _cbSelection.checkChange = &onCaseSensitiveCheckChange; 4128 focusGroup = true; 4129 if (!replace) 4130 childById("replace").visibility = Visibility.Gone; 4131 //_edFind = new EditLine("edFind" 4132 dstring currentText = _edFind.text; 4133 Log.d("currentText=", currentText); 4134 setDirection(false); 4135 updateHighlight(); 4136 } 4137 void activate() { 4138 _edFind.setFocus(); 4139 dstring currentText = _edFind.text; 4140 Log.d("activate.currentText=", currentText); 4141 _edFind.setCaretPos(0, cast(int)currentText.length, true); 4142 } 4143 4144 bool onButtonClick(Widget source) { 4145 switch (source.id) { 4146 case "btnFindNext": 4147 findNext(false); 4148 return true; 4149 case "btnFindPrev": 4150 findNext(true); 4151 return true; 4152 case "btnClose": 4153 close(); 4154 return true; 4155 case "btnReplace": 4156 replaceOne(); 4157 return true; 4158 case "btnReplaceAndFind": 4159 replaceOne(); 4160 findNext(_backDirection); 4161 return true; 4162 case "btnReplaceAll": 4163 replaceAll(); 4164 return true; 4165 default: 4166 return true; 4167 } 4168 } 4169 4170 void close() { 4171 _editor.setTextToHighlight(null, 0); 4172 _editor.closeFindPanel(); 4173 } 4174 4175 override bool onKeyEvent(KeyEvent event) { 4176 if (event.keyCode == KeyCode.TAB) 4177 return super.onKeyEvent(event); 4178 if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.ESCAPE) { 4179 close(); 4180 return true; 4181 } 4182 return true; 4183 } 4184 4185 /// override to handle specific actions 4186 override bool handleAction(const Action a) { 4187 switch (a.id) { 4188 case EditorActions.FindNext: 4189 findNext(false); 4190 return true; 4191 case EditorActions.FindPrev: 4192 findNext(true); 4193 return true; 4194 default: 4195 return false; 4196 } 4197 } 4198 4199 protected bool _backDirection; 4200 void setDirection(bool back) { 4201 _backDirection = back; 4202 if (back) { 4203 _btnFindNext.resetState(State.Default); 4204 _btnFindPrev.setState(State.Default); 4205 } else { 4206 _btnFindNext.setState(State.Default); 4207 _btnFindPrev.resetState(State.Default); 4208 } 4209 } 4210 4211 uint makeSearchFlags() { 4212 uint res = 0; 4213 if (_cbCaseSensitive.checked) 4214 res |= TextSearchFlag.CaseSensitive; 4215 if (_cbWholeWords.checked) 4216 res |= TextSearchFlag.WholeWords; 4217 if (_cbSelection.checked) 4218 res |= TextSearchFlag.SelectionOnly; 4219 return res; 4220 } 4221 bool findNext(bool back) { 4222 setDirection(back); 4223 dstring currentText = _edFind.text; 4224 Log.d("findNext text=", currentText, " back=", back); 4225 if (!currentText.length) 4226 return false; 4227 _editor.setTextToHighlight(currentText, makeSearchFlags); 4228 TextPosition pos = _editor.caretPos; 4229 bool res = _editor.findNextPattern(pos, currentText, makeSearchFlags, back ? -1 : 1); 4230 if (res) { 4231 _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)currentText.length)); 4232 _editor.ensureCaretVisible(); 4233 //_editor.setCaretPos(pos.line, pos.pos, true); 4234 } 4235 return res; 4236 } 4237 4238 bool replaceOne() { 4239 dstring currentText = _edFind.text; 4240 dstring newText = _edReplace.text; 4241 Log.d("replaceOne text=", currentText, " back=", _backDirection, " newText=", newText); 4242 if (!currentText.length) 4243 return false; 4244 _editor.setTextToHighlight(currentText, makeSearchFlags); 4245 TextPosition pos = _editor.caretPos; 4246 bool res = _editor.findNextPattern(pos, currentText, makeSearchFlags, 0); 4247 if (res) { 4248 _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)currentText.length)); 4249 _editor.replaceSelectionText(newText); 4250 _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)newText.length)); 4251 _editor.ensureCaretVisible(); 4252 //_editor.setCaretPos(pos.line, pos.pos, true); 4253 } 4254 return res; 4255 } 4256 4257 int replaceAll() { 4258 int count = 0; 4259 for(int i = 0; ; i++) { 4260 debug Log.d("replaceAll - calling replaceOne, iteration ", i); 4261 if (!replaceOne()) 4262 break; 4263 count++; 4264 TextPosition initialPosition = _editor.caretPos; 4265 debug Log.d("replaceAll - position is ", initialPosition); 4266 if (!findNext(_backDirection)) 4267 break; 4268 TextPosition newPosition = _editor.caretPos; 4269 debug Log.d("replaceAll - next position is ", newPosition); 4270 if (_backDirection && newPosition >= initialPosition) 4271 break; 4272 if (!_backDirection && newPosition <= initialPosition) 4273 break; 4274 } 4275 debug Log.d("replaceAll - done, replace count = ", count); 4276 _editor.ensureCaretVisible(); 4277 return count; 4278 } 4279 4280 void updateHighlight() { 4281 dstring currentText = _edFind.text; 4282 Log.d("onFindTextChange.currentText=", currentText); 4283 _editor.setTextToHighlight(currentText, makeSearchFlags); 4284 } 4285 4286 void onFindTextChange(EditableContent source) { 4287 Log.d("onFindTextChange"); 4288 updateHighlight(); 4289 } 4290 4291 bool onCaseSensitiveCheckChange(Widget source, bool checkValue) { 4292 updateHighlight(); 4293 return true; 4294 } 4295 4296 bool onFindEditorAction(const Action action) { 4297 Log.d("onFindEditorAction ", action); 4298 if (action.id == EditorActions.InsertNewLine) { 4299 findNext(_backDirection); 4300 return true; 4301 } 4302 return false; 4303 } 4304 } 4305 4306 //import dlangui.widgets.metadata; 4307 //mixin(registerWidgets!(EditLine, EditBox, LogWidget)());