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 showSoftKeyboard(); 350 handleEditorStateChange(); 351 } 352 return res; 353 } 354 355 /// updates editorStateChange with recent position 356 protected void handleEditorStateChange() { 357 if (!editorStateChange.assigned) 358 return; 359 EditorStateInfo info; 360 if (visible) { 361 info.replaceMode = _replaceMode; 362 info.line = _caretPos.line + 1; 363 info.col = _caretPos.pos + 1; 364 if (_caretPos.line >= 0 && _caretPos.line < _content.length) { 365 dstring line = _content.line(_caretPos.line); 366 if (_caretPos.pos >= 0 && _caretPos.pos < line.length) 367 info.character = line[_caretPos.pos]; 368 else 369 info.character = '\n'; 370 } 371 } 372 editorStateChange(this, info); 373 } 374 375 /// override to support modification of client rect after change, e.g. apply offset 376 override protected void handleClientRectLayout(ref Rect rc) { 377 updateLeftPaneWidth(); 378 rc.left += _leftPaneWidth; 379 } 380 381 /// override for multiline editors 382 protected int lineCount() { 383 return 1; 384 } 385 386 /// Override for EditBox 387 void wordWrapRefresh(){return;} 388 389 /// To hold _scrollpos.x toggling between normal and word wrap mode 390 int previousXScrollPos; 391 392 protected bool _wordWrap; 393 /// true if word wrap mode is set 394 @property bool wordWrap() { 395 return _wordWrap; 396 } 397 /// true if word wrap mode is set 398 @property EditWidgetBase wordWrap(bool v) { 399 _wordWrap = v; 400 //Horizontal scrollbar should not be visible in word wrap mode 401 if (v) 402 { 403 _hscrollbar.visibility(Visibility.Invisible); 404 previousXScrollPos = _scrollPos.x; 405 _scrollPos.x = 0; 406 wordWrapRefresh(); 407 } 408 else 409 { 410 _hscrollbar.visibility(Visibility.Visible); 411 _scrollPos.x = previousXScrollPos; 412 } 413 invalidate(); 414 return this; 415 } 416 417 /// Characters at which content is split for word wrap mode 418 dchar[] splitChars = [' ', '-', '\t']; 419 420 /// Divides up a string for word wrapping, sets info in _span 421 dstring[] wrapLine(dstring str, int lineNumber) { 422 FontRef font = font(); 423 dstring[] words = explode(str, splitChars); 424 int curLineLength = 0; 425 dchar[] buildingStr; 426 dstring[] buildingStrArr; 427 WrapPoint[] wrapPoints; 428 int wrappedLineCount = 0; 429 int curLineWidth = 0; 430 int maxWidth = _clientRect.width; 431 for (int i = 0; i < words.length; i++) 432 { 433 dstring word = words[i]; 434 if (curLineWidth + measureWrappedText(word) > maxWidth) 435 { 436 if (curLineWidth > 0) 437 { 438 buildingStrArr ~= to!dstring(buildingStr); 439 wrappedLineCount++; 440 wrapPoints ~= WrapPoint(curLineLength, curLineWidth); 441 curLineLength = 0; 442 curLineWidth = 0; 443 buildingStr = []; 444 } 445 while (measureWrappedText(word) > maxWidth) 446 { 447 //For when string still too long 448 int wrapPoint = findWrapPoint(word); 449 wrapPoints ~= WrapPoint(wrapPoint, measureWrappedText(word[0..wrapPoint])); 450 buildingStr ~= word[0 .. wrapPoint]; 451 word = word[wrapPoint .. $]; 452 buildingStrArr ~= to!dstring(buildingStr); 453 buildingStr = []; 454 wrappedLineCount++; 455 } 456 } 457 buildingStr ~= word; 458 curLineLength += to!int(word.length); 459 curLineWidth += measureWrappedText(word); 460 } 461 wrapPoints ~= WrapPoint(curLineLength, curLineWidth); 462 buildingStrArr ~= to!dstring(buildingStr); 463 _span ~= LineSpan(lineNumber, wrappedLineCount + 1, wrapPoints, buildingStrArr); 464 return buildingStrArr; 465 } 466 467 /// Divide (and conquer) text into words 468 dstring[] explode(dstring str, dchar[] splitChars) 469 { 470 dstring[] parts; 471 int startIndex = 0; 472 import std.string:indexOfAny; 473 while (true) 474 { 475 int index = to!int(str.indexOfAny(splitChars, startIndex)); 476 477 if (index == -1) 478 { 479 parts ~= str[startIndex .. $]; 480 //Log.d("Explode output: ", parts); 481 return parts; 482 } 483 484 dstring word = str[startIndex .. index]; 485 dchar nextChar = (str[index .. index + 1])[0]; 486 487 import std.ascii:isWhite; 488 if (isWhite(nextChar)) 489 { 490 parts ~= word; 491 parts ~= to!dstring(nextChar); 492 } 493 else 494 { 495 parts ~= word ~ nextChar; 496 } 497 startIndex = index + 1; 498 } 499 } 500 501 /// information about line span into several lines - in word wrap mode 502 protected LineSpan[] _span; 503 protected LineSpan[] _spanCache; 504 505 /// Finds good visual wrapping point for string 506 int findWrapPoint(dstring text) 507 { 508 int maxWidth = _clientRect.width; 509 int wrapPoint = 0; 510 while (true) 511 { 512 if (measureWrappedText(text[0 .. wrapPoint]) < maxWidth) 513 { 514 wrapPoint++; 515 } 516 else 517 { 518 return wrapPoint; 519 } 520 } 521 } 522 523 /// Calls measureText for word wrap 524 int measureWrappedText(dstring text) 525 { 526 FontRef font = font(); 527 int[] measuredWidths; 528 measuredWidths.length = text.length; 529 //DO NOT REMOVE THIS 530 int boggle = font.measureText(text, measuredWidths); 531 if (measuredWidths.length > 0) 532 return measuredWidths[$-1]; 533 return 0; 534 } 535 536 /// Returns number of visible wraps up to a line (not including the first wrapLines themselves) 537 int wrapsUpTo(int line) 538 { 539 int sum; 540 lineSpanIterate(delegate(LineSpan curSpan) 541 { 542 if (curSpan.start < line) 543 sum += curSpan.len - 1; 544 }); 545 return sum; 546 } 547 548 /// Returns LineSpan for line based on actual line number 549 LineSpan getSpan(int lineNumber) 550 { 551 LineSpan lineSpan = LineSpan(lineNumber, 0, [WrapPoint(0,0)], []); 552 lineSpanIterate(delegate(LineSpan curSpan) 553 { 554 if (curSpan.start == lineNumber) 555 lineSpan = curSpan; 556 }); 557 return lineSpan; 558 } 559 560 /// Based on a TextPosition, finds which wrapLine it is on for its current line 561 int findWrapLine(TextPosition textPos) 562 { 563 int curWrapLine = 0; 564 int curPosition = textPos.pos; 565 LineSpan curSpan = getSpan(textPos.line); 566 while (true) 567 { 568 if (curWrapLine == curSpan.wrapPoints.length - 1) 569 return curWrapLine; 570 curPosition -= curSpan.wrapPoints[curWrapLine].wrapPos; 571 if (curPosition < 0) 572 { 573 return curWrapLine; 574 } 575 curWrapLine++; 576 } 577 } 578 579 /// Simple way of iterating through _span 580 void lineSpanIterate(void delegate(LineSpan curSpan) iterator) 581 { 582 //TODO: Rename iterator to iteration? 583 foreach (currentSpan; _span) 584 iterator(currentSpan); 585 } 586 587 /// override to add custom items on left panel 588 protected void updateLeftPaneWidth() { 589 import std.conv : to; 590 _iconsWidth = _showIcons ? _iconsPaneWidth : 0; 591 _foldingWidth = _showFolding ? _foldingPaneWidth : 0; 592 _modificationMarksWidth = _showModificationMarks && (BACKEND_GUI || !_showLineNumbers) ? _modificationMarksPaneWidth : 0; 593 _lineNumbersWidth = 0; 594 if (_showLineNumbers) { 595 dchar[] s = to!(dchar[])(lineCount + 1); 596 foreach(ref ch; s) 597 ch = '9'; 598 FontRef fnt = font; 599 Point sz = fnt.textSize(cast(immutable)s); 600 _lineNumbersWidth = sz.x; 601 } 602 _leftPaneWidth = _lineNumbersWidth + _modificationMarksWidth + _foldingWidth + _iconsWidth; 603 if (_leftPaneWidth) 604 _leftPaneWidth += WIDGET_STYLE_CONSOLE ? 1 : 3; 605 } 606 607 protected void drawLeftPaneFolding(DrawBuf buf, Rect rc, int line) { 608 buf.fillRect(rc, _leftPaneBackgroundColor2); 609 } 610 611 protected void drawLeftPaneIcon(DrawBuf buf, Rect rc, LineIcon icon) { 612 if (!icon) 613 return; 614 if (icon.type == LineIconType.error) { 615 buf.fillRect(rc, _colorIconError); 616 } else if (icon.type == LineIconType.bookmark) { 617 int dh = rc.height / 4; 618 rc.top += dh; 619 rc.bottom -= dh; 620 buf.fillRect(rc, _colorIconBookmark); 621 } else if (icon.type == LineIconType.breakpoint) { 622 if (rc.height > rc.width) { 623 int delta = rc.height - rc.width; 624 rc.top += delta / 2; 625 rc.bottom -= (delta + 1) / 2; 626 } else { 627 int delta = rc.width - rc.height; 628 rc.left += delta / 2; 629 rc.right -= (delta + 1) / 2; 630 } 631 int dh = rc.height / 5; 632 rc.top += dh; 633 rc.bottom -= dh; 634 int dw = rc.width / 5; 635 rc.left += dw; 636 rc.right -= dw; 637 buf.fillRect(rc, _colorIconBreakpoint); 638 } 639 } 640 641 protected void drawLeftPaneIcons(DrawBuf buf, Rect rc, int line) { 642 buf.fillRect(rc, _leftPaneBackgroundColor3); 643 drawLeftPaneIcon(buf, rc, content.lineIcons.findByLineAndType(line, LineIconType.error)); 644 drawLeftPaneIcon(buf, rc, content.lineIcons.findByLineAndType(line, LineIconType.bookmark)); 645 drawLeftPaneIcon(buf, rc, content.lineIcons.findByLineAndType(line, LineIconType.breakpoint)); 646 } 647 648 protected void drawLeftPaneModificationMarks(DrawBuf buf, Rect rc, int line) { 649 if (line >= 0 && line < content.length) { 650 EditStateMark m = content.editMark(line); 651 if (m == EditStateMark.changed) { 652 // modified, not saved 653 buf.fillRect(rc, 0xFFD040); 654 } else if (m == EditStateMark.saved) { 655 // modified, saved 656 buf.fillRect(rc, 0x20C020); 657 } 658 } 659 } 660 661 protected void drawLeftPaneLineNumbers(DrawBuf buf, Rect rc, int line) { 662 import std.conv : to; 663 uint bgcolor = _leftPaneLineNumberBackgroundColor; 664 if (line == _caretPos.line && !isFullyTransparentColor(_leftPaneLineNumberBackgroundColorCurrentLine)) 665 bgcolor = _leftPaneLineNumberBackgroundColorCurrentLine; 666 buf.fillRect(rc, bgcolor); 667 if (line < 0) 668 return; 669 dstring s = to!dstring(line + 1); 670 FontRef fnt = font; 671 Point sz = fnt.textSize(s); 672 int x = rc.right - sz.x; 673 int y = rc.top + (rc.height - sz.y) / 2; 674 uint color = _leftPaneLineNumberColor; 675 if (line == _caretPos.line && !isFullyTransparentColor(_leftPaneLineNumberColorCurrentLine)) 676 color = _leftPaneLineNumberColorCurrentLine; 677 if (line >= 0 && line < content.length) { 678 EditStateMark m = content.editMark(line); 679 if (m == EditStateMark.changed) { 680 // modified, not saved 681 color = _leftPaneLineNumberColorEdited; 682 } else if (m == EditStateMark.saved) { 683 // modified, saved 684 color = _leftPaneLineNumberColorSaved; 685 } 686 } 687 fnt.drawText(buf, x, y, s, color); 688 } 689 690 protected bool onLeftPaneMouseClick(MouseEvent event) { 691 return false; 692 } 693 694 protected bool handleLeftPaneFoldingMouseClick(MouseEvent event, Rect rc, int line) { 695 return true; 696 } 697 698 protected bool handleLeftPaneModificationMarksMouseClick(MouseEvent event, Rect rc, int line) { 699 return true; 700 } 701 702 protected bool handleLeftPaneLineNumbersMouseClick(MouseEvent event, Rect rc, int line) { 703 return true; 704 } 705 706 protected MenuItem getLeftPaneIconsPopupMenu(int line) { 707 return null; 708 } 709 710 protected bool handleLeftPaneIconsMouseClick(MouseEvent event, Rect rc, int line) { 711 if (event.button == MouseButton.Right) { 712 MenuItem menu = getLeftPaneIconsPopupMenu(line); 713 if (menu) { 714 if (menu.openingSubmenu.assigned) 715 if (!menu.openingSubmenu(_popupMenu)) 716 return true; 717 menu.updateActionState(this); 718 PopupMenu popupMenu = new PopupMenu(menu); 719 popupMenu.menuItemAction = this; 720 PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, event.x, event.y); 721 popup.flags = PopupFlags.CloseOnClickOutside; 722 } 723 return true; 724 } 725 return true; 726 } 727 728 protected bool handleLeftPaneMouseClick(MouseEvent event, Rect rc, int line) { 729 rc.right -= 3; 730 if (_foldingWidth) { 731 Rect rc2 = rc; 732 rc.right = rc2.left = rc2.right - _foldingWidth; 733 if (event.x >= rc2.left && event.x < rc2.right) 734 return handleLeftPaneFoldingMouseClick(event, rc2, line); 735 } 736 if (_modificationMarksWidth) { 737 Rect rc2 = rc; 738 rc.right = rc2.left = rc2.right - _modificationMarksWidth; 739 if (event.x >= rc2.left && event.x < rc2.right) 740 return handleLeftPaneModificationMarksMouseClick(event, rc2, line); 741 } 742 if (_lineNumbersWidth) { 743 Rect rc2 = rc; 744 rc.right = rc2.left = rc2.right - _lineNumbersWidth; 745 if (event.x >= rc2.left && event.x < rc2.right) 746 return handleLeftPaneLineNumbersMouseClick(event, rc2, line); 747 } 748 if (_iconsWidth) { 749 Rect rc2 = rc; 750 rc.right = rc2.left = rc2.right - _iconsWidth; 751 if (event.x >= rc2.left && event.x < rc2.right) 752 return handleLeftPaneIconsMouseClick(event, rc2, line); 753 } 754 return true; 755 } 756 757 protected void drawLeftPane(DrawBuf buf, Rect rc, int line) { 758 // override for custom drawn left pane 759 buf.fillRect(rc, _leftPaneBackgroundColor); 760 //buf.fillRect(Rect(rc.right - 2, rc.top, rc.right - 1, rc.bottom), _leftPaneBackgroundColor2); 761 //buf.fillRect(Rect(rc.right - 1, rc.top, rc.right - 0, rc.bottom), _leftPaneBackgroundColor3); 762 rc.right -= WIDGET_STYLE_CONSOLE ? 1 : 3; 763 if (_foldingWidth) { 764 Rect rc2 = rc; 765 rc.right = rc2.left = rc2.right - _foldingWidth; 766 drawLeftPaneFolding(buf, rc2, line); 767 } 768 if (_modificationMarksWidth) { 769 Rect rc2 = rc; 770 rc.right = rc2.left = rc2.right - _modificationMarksWidth; 771 drawLeftPaneModificationMarks(buf, rc2, line); 772 } 773 if (_lineNumbersWidth) { 774 Rect rc2 = rc; 775 rc.right = rc2.left = rc2.right - _lineNumbersWidth; 776 drawLeftPaneLineNumbers(buf, rc2, line); 777 } 778 if (_iconsWidth) { 779 Rect rc2 = rc; 780 rc.right = rc2.left = rc2.right - _iconsWidth; 781 drawLeftPaneIcons(buf, rc2, line); 782 } 783 } 784 785 this(string ID, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) { 786 super(ID, hscrollbarMode, vscrollbarMode); 787 focusable = true; 788 acceleratorMap.add( [ 789 new Action(EditorActions.Up, KeyCode.UP, 0, ActionStateUpdateFlag.never), 790 new Action(EditorActions.SelectUp, KeyCode.UP, KeyFlag.Shift, ActionStateUpdateFlag.never), 791 new Action(EditorActions.SelectUp, KeyCode.UP, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 792 new Action(EditorActions.Down, KeyCode.DOWN, 0, ActionStateUpdateFlag.never), 793 new Action(EditorActions.SelectDown, KeyCode.DOWN, KeyFlag.Shift, ActionStateUpdateFlag.never), 794 new Action(EditorActions.SelectDown, KeyCode.DOWN, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 795 new Action(EditorActions.Left, KeyCode.LEFT, 0, ActionStateUpdateFlag.never), 796 new Action(EditorActions.SelectLeft, KeyCode.LEFT, KeyFlag.Shift, ActionStateUpdateFlag.never), 797 new Action(EditorActions.Right, KeyCode.RIGHT, 0, ActionStateUpdateFlag.never), 798 new Action(EditorActions.SelectRight, KeyCode.RIGHT, KeyFlag.Shift, ActionStateUpdateFlag.never), 799 new Action(EditorActions.WordLeft, KeyCode.LEFT, KeyFlag.Control, ActionStateUpdateFlag.never), 800 new Action(EditorActions.SelectWordLeft, KeyCode.LEFT, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 801 new Action(EditorActions.WordRight, KeyCode.RIGHT, KeyFlag.Control, ActionStateUpdateFlag.never), 802 new Action(EditorActions.SelectWordRight, KeyCode.RIGHT, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 803 new Action(EditorActions.PageUp, KeyCode.PAGEUP, 0, ActionStateUpdateFlag.never), 804 new Action(EditorActions.SelectPageUp, KeyCode.PAGEUP, KeyFlag.Shift, ActionStateUpdateFlag.never), 805 new Action(EditorActions.PageDown, KeyCode.PAGEDOWN, 0, ActionStateUpdateFlag.never), 806 new Action(EditorActions.SelectPageDown, KeyCode.PAGEDOWN, KeyFlag.Shift, ActionStateUpdateFlag.never), 807 new Action(EditorActions.PageBegin, KeyCode.PAGEUP, KeyFlag.Control, ActionStateUpdateFlag.never), 808 new Action(EditorActions.SelectPageBegin, KeyCode.PAGEUP, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 809 new Action(EditorActions.PageEnd, KeyCode.PAGEDOWN, KeyFlag.Control, ActionStateUpdateFlag.never), 810 new Action(EditorActions.SelectPageEnd, KeyCode.PAGEDOWN, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 811 new Action(EditorActions.LineBegin, KeyCode.HOME, 0, ActionStateUpdateFlag.never), 812 new Action(EditorActions.SelectLineBegin, KeyCode.HOME, KeyFlag.Shift, ActionStateUpdateFlag.never), 813 new Action(EditorActions.LineEnd, KeyCode.END, 0, ActionStateUpdateFlag.never), 814 new Action(EditorActions.SelectLineEnd, KeyCode.END, KeyFlag.Shift, ActionStateUpdateFlag.never), 815 new Action(EditorActions.DocumentBegin, KeyCode.HOME, KeyFlag.Control, ActionStateUpdateFlag.never), 816 new Action(EditorActions.SelectDocumentBegin, KeyCode.HOME, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 817 new Action(EditorActions.DocumentEnd, KeyCode.END, KeyFlag.Control, ActionStateUpdateFlag.never), 818 new Action(EditorActions.SelectDocumentEnd, KeyCode.END, KeyFlag.Control | KeyFlag.Shift, ActionStateUpdateFlag.never), 819 820 new Action(EditorActions.ScrollLineUp, KeyCode.UP, KeyFlag.Control, ActionStateUpdateFlag.never), 821 new Action(EditorActions.ScrollLineDown, KeyCode.DOWN, KeyFlag.Control, ActionStateUpdateFlag.never), 822 823 // Backspace/Del 824 new Action(EditorActions.DelPrevChar, KeyCode.BACK, 0, ActionStateUpdateFlag.never), 825 new Action(EditorActions.DelNextChar, KeyCode.DEL, 0, ActionStateUpdateFlag.never), 826 new Action(EditorActions.DelPrevWord, KeyCode.BACK, KeyFlag.Control, ActionStateUpdateFlag.never), 827 new Action(EditorActions.DelNextWord, KeyCode.DEL, KeyFlag.Control, ActionStateUpdateFlag.never), 828 829 // Copy/Paste 830 new Action(EditorActions.Copy, KeyCode.KEY_C, KeyFlag.Control), 831 new Action(EditorActions.Copy, KeyCode.KEY_C, KeyFlag.Control | KeyFlag.Shift), 832 new Action(EditorActions.Copy, KeyCode.INS, KeyFlag.Control), 833 new Action(EditorActions.Cut, KeyCode.KEY_X, KeyFlag.Control), 834 new Action(EditorActions.Cut, KeyCode.KEY_X, KeyFlag.Control | KeyFlag.Shift), 835 new Action(EditorActions.Cut, KeyCode.DEL, KeyFlag.Shift), 836 new Action(EditorActions.Paste, KeyCode.KEY_V, KeyFlag.Control), 837 new Action(EditorActions.Paste, KeyCode.KEY_V, KeyFlag.Control | KeyFlag.Shift), 838 new Action(EditorActions.Paste, KeyCode.INS, KeyFlag.Shift), 839 840 // Undo/Redo 841 new Action(EditorActions.Undo, KeyCode.KEY_Z, KeyFlag.Control), 842 new Action(EditorActions.Redo, KeyCode.KEY_Y, KeyFlag.Control), 843 new Action(EditorActions.Redo, KeyCode.KEY_Z, KeyFlag.Control | KeyFlag.Shift), 844 845 new Action(EditorActions.Tab, KeyCode.TAB, 0, ActionStateUpdateFlag.never), 846 new Action(EditorActions.BackTab, KeyCode.TAB, KeyFlag.Shift, ActionStateUpdateFlag.never), 847 848 new Action(EditorActions.Find, KeyCode.KEY_F, KeyFlag.Control), 849 new Action(EditorActions.Replace, KeyCode.KEY_H, KeyFlag.Control), 850 ]); 851 acceleratorMap.add(STD_EDITOR_ACTIONS); 852 acceleratorMap.add([ACTION_EDITOR_FIND_NEXT, ACTION_EDITOR_FIND_PREV]); 853 } 854 855 /// 856 override bool onMenuItemAction(const Action action) { 857 return dispatchAction(action); 858 } 859 860 /// returns true if widget can show popup (e.g. by mouse right click at point x,y) 861 override bool canShowPopupMenu(int x, int y) { 862 if (_popupMenu is null) 863 return false; 864 if (_popupMenu.openingSubmenu.assigned) 865 if (!_popupMenu.openingSubmenu(_popupMenu)) 866 return false; 867 return true; 868 } 869 870 /// returns true if widget is focusable and visible and enabled 871 override @property bool canFocus() { 872 // allow to focus even if not enabled 873 return focusable && visible; 874 } 875 876 /// override to change popup menu items state 877 override bool isActionEnabled(const Action action) { 878 switch (action.id) with(EditorActions) 879 { 880 case Tab: 881 case BackTab: 882 case Indent: 883 case Unindent: 884 return enabled; 885 case Copy: 886 return _copyCurrentLineWhenNoSelection || !_selectionRange.empty; 887 case Cut: 888 return enabled && (_copyCurrentLineWhenNoSelection || !_selectionRange.empty); 889 case Paste: 890 return enabled && Platform.instance.hasClipboardText(); 891 case Undo: 892 return enabled && _content.hasUndo; 893 case Redo: 894 return enabled && _content.hasRedo; 895 case ToggleBookmark: 896 return _content.multiline; 897 case GoToNextBookmark: 898 return _content.multiline && _content.lineIcons.hasBookmarks; 899 case GoToPreviousBookmark: 900 return _content.multiline && _content.lineIcons.hasBookmarks; 901 case Replace: 902 return _content.multiline && !readOnly; 903 case Find: 904 case FindNext: 905 case FindPrev: 906 return _content.multiline; 907 default: 908 return super.isActionEnabled(action); 909 } 910 } 911 912 /// shows popup at (x,y) 913 override void showPopupMenu(int x, int y) { 914 /// if preparation signal handler assigned, call it; don't show popup if false is returned from handler 915 if (_popupMenu.openingSubmenu.assigned) 916 if (!_popupMenu.openingSubmenu(_popupMenu)) 917 return; 918 _popupMenu.updateActionState(this); 919 PopupMenu popupMenu = new PopupMenu(_popupMenu); 920 popupMenu.menuItemAction = this; 921 PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, x, y); 922 popup.flags = PopupFlags.CloseOnClickOutside; 923 } 924 925 void onPopupMenuItem(MenuItem item) { 926 // TODO 927 } 928 929 /// returns mouse cursor type for widget 930 override uint getCursorType(int x, int y) { 931 return x < _pos.left + _leftPaneWidth ? CursorType.Arrow : CursorType.IBeam; 932 } 933 934 /// set bool property value, for ML loaders 935 mixin(generatePropertySettersMethodOverride("setBoolProperty", "bool", 936 "wantTabs", "showIcons", "showFolding", "showModificationMarks", "showLineNumbers", "readOnly", "replaceMode", "useSpacesForTabs", "copyCurrentLineWhenNoSelection", "showTabPositionMarks")); 937 938 /// set int property value, for ML loaders 939 mixin(generatePropertySettersMethodOverride("setIntProperty", "int", 940 "tabSize")); 941 942 /// when true, Tab / Shift+Tab presses are processed internally in widget (e.g. insert tab character) instead of focus change navigation. 943 @property bool wantTabs() { 944 return _wantTabs; 945 } 946 947 /// ditto 948 @property EditWidgetBase wantTabs(bool wantTabs) { 949 _wantTabs = wantTabs; 950 return this; 951 } 952 953 /// when true, show icons like bookmarks or breakpoints at the left 954 @property bool showIcons() { 955 return _showIcons; 956 } 957 958 /// when true, show icons like bookmarks or breakpoints at the left 959 @property EditWidgetBase showIcons(bool flg) { 960 if (_showIcons != flg) { 961 _showIcons = flg; 962 updateLeftPaneWidth(); 963 requestLayout(); 964 } 965 return this; 966 } 967 968 /// when true, show folding controls at the left 969 @property bool showFolding() { 970 return _showFolding; 971 } 972 973 /// when true, show folding controls at the left 974 @property EditWidgetBase showFolding(bool flg) { 975 if (_showFolding != flg) { 976 _showFolding = flg; 977 updateLeftPaneWidth(); 978 requestLayout(); 979 } 980 return this; 981 } 982 983 /// when true, show modification marks for lines (whether line is unchanged/modified/modified_saved) 984 @property bool showModificationMarks() { 985 return _showModificationMarks; 986 } 987 988 /// when true, show modification marks for lines (whether line is unchanged/modified/modified_saved) 989 @property EditWidgetBase showModificationMarks(bool flg) { 990 if (_showModificationMarks != flg) { 991 _showModificationMarks = flg; 992 updateLeftPaneWidth(); 993 requestLayout(); 994 } 995 return this; 996 } 997 998 /// when true, line numbers are shown 999 @property bool showLineNumbers() { 1000 return _showLineNumbers; 1001 } 1002 1003 /// when true, line numbers are shown 1004 @property EditWidgetBase showLineNumbers(bool flg) { 1005 if (_showLineNumbers != flg) { 1006 _showLineNumbers = flg; 1007 updateLeftPaneWidth(); 1008 requestLayout(); 1009 } 1010 return this; 1011 } 1012 1013 /// readonly flag (when true, user cannot change content of editor) 1014 @property bool readOnly() { 1015 return !enabled || _content.readOnly; 1016 } 1017 1018 /// sets readonly flag 1019 @property EditWidgetBase readOnly(bool readOnly) { 1020 enabled = !readOnly; 1021 invalidate(); 1022 return this; 1023 } 1024 1025 /// replace mode flag (when true, entered character replaces character under cursor) 1026 @property bool replaceMode() { 1027 return _replaceMode; 1028 } 1029 1030 /// sets replace mode flag 1031 @property EditWidgetBase replaceMode(bool replaceMode) { 1032 _replaceMode = replaceMode; 1033 handleEditorStateChange(); 1034 invalidate(); 1035 return this; 1036 } 1037 1038 /// when true, spaces will be inserted instead of tabs 1039 @property bool useSpacesForTabs() { 1040 return _content.useSpacesForTabs; 1041 } 1042 1043 /// set new Tab key behavior flag: when true, spaces will be inserted instead of tabs 1044 @property EditWidgetBase useSpacesForTabs(bool useSpacesForTabs) { 1045 _content.useSpacesForTabs = useSpacesForTabs; 1046 return this; 1047 } 1048 1049 /// returns tab size (in number of spaces) 1050 @property int tabSize() { 1051 return _content.tabSize; 1052 } 1053 1054 /// sets tab size (in number of spaces) 1055 @property EditWidgetBase tabSize(int newTabSize) { 1056 if (newTabSize < 1) 1057 newTabSize = 1; 1058 else if (newTabSize > 16) 1059 newTabSize = 16; 1060 if (newTabSize != tabSize) { 1061 _content.tabSize = newTabSize; 1062 requestLayout(); 1063 } 1064 return this; 1065 } 1066 1067 /// true if smart indents are supported 1068 @property bool supportsSmartIndents() { return _content.supportsSmartIndents; } 1069 /// true if smart indents are enabled 1070 @property bool smartIndents() { return _content.smartIndents; } 1071 /// set smart indents enabled flag 1072 @property EditWidgetBase smartIndents(bool enabled) { _content.smartIndents = enabled; return this; } 1073 1074 /// true if smart indents are enabled 1075 @property bool smartIndentsAfterPaste() { return _content.smartIndentsAfterPaste; } 1076 /// set smart indents enabled flag 1077 @property EditWidgetBase smartIndentsAfterPaste(bool enabled) { _content.smartIndentsAfterPaste = enabled; return this; } 1078 1079 1080 /// editor content object 1081 @property EditableContent content() { 1082 return _content; 1083 } 1084 1085 /// when _ownContent is false, _content should not be destroyed in editor destructor 1086 protected bool _ownContent = true; 1087 /// set content object 1088 @property EditWidgetBase content(EditableContent content) { 1089 if (_content is content) 1090 return this; // not changed 1091 if (_content !is null) { 1092 // disconnect old content 1093 _content.contentChanged.disconnect(this); 1094 if (_ownContent) { 1095 destroy(_content); 1096 } 1097 } 1098 _content = content; 1099 _ownContent = false; 1100 _content.contentChanged.connect(this); 1101 if (_content.readOnly) 1102 enabled = false; 1103 return this; 1104 } 1105 1106 /// free resources 1107 ~this() { 1108 if (_ownContent) { 1109 destroy(_content); 1110 _content = null; 1111 } 1112 } 1113 1114 protected void updateMaxLineWidth() { 1115 } 1116 1117 protected void processSmartIndent(EditOperation operation) { 1118 if (!supportsSmartIndents) 1119 return; 1120 if (!smartIndents && !smartIndentsAfterPaste) 1121 return; 1122 _content.syntaxSupport.applySmartIndent(operation, this); 1123 } 1124 1125 override void onContentChange(EditableContent content, EditOperation operation, ref TextRange rangeBefore, ref TextRange rangeAfter, Object source) { 1126 //Log.d("onContentChange rangeBefore=", rangeBefore, " rangeAfter=", rangeAfter, " text=", operation.content); 1127 _contentChanged = true; 1128 if (source is this) { 1129 if (operation.action == EditAction.ReplaceContent) { 1130 // fully replaced, e.g., loaded from file or text property is assigned 1131 _caretPos = rangeAfter.end; 1132 _selectionRange.start = _caretPos; 1133 _selectionRange.end = _caretPos; 1134 updateMaxLineWidth(); 1135 measureVisibleText(); 1136 ensureCaretVisible(); 1137 correctCaretPos(); 1138 requestLayout(); 1139 requestActionsUpdate(); 1140 } else if (operation.action == EditAction.SaveContent) { 1141 // saved 1142 } else { 1143 // modified 1144 _caretPos = rangeAfter.end; 1145 _selectionRange.start = _caretPos; 1146 _selectionRange.end = _caretPos; 1147 updateMaxLineWidth(); 1148 measureVisibleText(); 1149 ensureCaretVisible(); 1150 requestActionsUpdate(); 1151 processSmartIndent(operation); 1152 } 1153 } else { 1154 updateMaxLineWidth(); 1155 measureVisibleText(); 1156 correctCaretPos(); 1157 requestLayout(); 1158 requestActionsUpdate(); 1159 } 1160 invalidate(); 1161 if (modifiedStateChange.assigned) { 1162 if (_lastReportedModifiedState != content.modified) { 1163 _lastReportedModifiedState = content.modified; 1164 modifiedStateChange(this, content.modified); 1165 requestActionsUpdate(); 1166 } 1167 } 1168 if (contentChange.assigned) { 1169 contentChange(_content); 1170 } 1171 handleEditorStateChange(); 1172 return; 1173 } 1174 protected bool _lastReportedModifiedState; 1175 1176 /// get widget text 1177 override @property dstring text() const { return _content.text; } 1178 1179 /// set text 1180 override @property Widget text(dstring s) { 1181 _content.text = s; 1182 requestLayout(); 1183 return this; 1184 } 1185 1186 /// set text 1187 override @property Widget text(UIString s) { 1188 _content.text = s; 1189 requestLayout(); 1190 return this; 1191 } 1192 1193 protected TextPosition _caretPos; 1194 protected TextRange _selectionRange; 1195 1196 abstract protected Rect textPosToClient(TextPosition p); 1197 1198 abstract protected TextPosition clientToTextPos(Point pt); 1199 1200 abstract protected void ensureCaretVisible(bool center = false); 1201 1202 abstract protected Point measureVisibleText(); 1203 1204 protected int _caretBlingingInterval = 800; 1205 protected ulong _caretTimerId; 1206 protected bool _caretBlinkingPhase; 1207 protected long _lastBlinkStartTs; 1208 protected bool _caretBlinks = true; 1209 1210 /// when true, enables caret blinking, otherwise it's always visible 1211 @property void showCaretBlinking(bool blinks) { 1212 _caretBlinks = blinks; 1213 } 1214 /// when true, enables caret blinking, otherwise it's always visible 1215 @property bool showCaretBlinking() { 1216 return _caretBlinks; 1217 } 1218 1219 protected void startCaretBlinking() { 1220 if (window) { 1221 static if (WIDGET_STYLE_CONSOLE) { 1222 window.caretRect = caretRect; 1223 window.caretReplace = _replaceMode; 1224 } else { 1225 long ts = currentTimeMillis; 1226 if (_caretTimerId) { 1227 if (_lastBlinkStartTs + _caretBlingingInterval / 4 > ts) 1228 return; // don't update timer too frequently 1229 cancelTimer(_caretTimerId); 1230 } 1231 _caretTimerId = setTimer(_caretBlingingInterval / 2); 1232 _lastBlinkStartTs = ts; 1233 _caretBlinkingPhase = false; 1234 invalidate(); 1235 } 1236 } 1237 } 1238 1239 protected void stopCaretBlinking() { 1240 if (window) { 1241 static if (WIDGET_STYLE_CONSOLE) { 1242 window.caretRect = Rect.init; 1243 } else { 1244 if (_caretTimerId) { 1245 cancelTimer(_caretTimerId); 1246 _caretTimerId = 0; 1247 } 1248 } 1249 } 1250 } 1251 1252 /// handle timer; return true to repeat timer event after next interval, false cancel timer 1253 override bool onTimer(ulong id) { 1254 if (id == _caretTimerId) { 1255 _caretBlinkingPhase = !_caretBlinkingPhase; 1256 if (!_caretBlinkingPhase) 1257 _lastBlinkStartTs = currentTimeMillis; 1258 invalidate(); 1259 //window.update(true); 1260 bool res = focused; 1261 if (!res) 1262 _caretTimerId = 0; 1263 return res; 1264 } 1265 if (id == _hoverTimer) { 1266 cancelHoverTimer(); 1267 onHoverTimeout(_hoverMousePosition, _hoverTextPosition); 1268 return false; 1269 } 1270 return super.onTimer(id); 1271 } 1272 1273 /// override to handle focus changes 1274 override protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) { 1275 if (focused) 1276 startCaretBlinking(); 1277 else { 1278 stopCaretBlinking(); 1279 cancelHoverTimer(); 1280 1281 if(_deselectAllWhenUnfocused) { 1282 _selectionRange.start = _caretPos; 1283 _selectionRange.end = _caretPos; 1284 } 1285 } 1286 if(focused && _selectAllWhenFocusedWithTab && receivedFocusFromKeyboard) 1287 handleAction(ACTION_EDITOR_SELECT_ALL); 1288 super.handleFocusChange(focused); 1289 } 1290 1291 //In word wrap mode, set by caretRect so ensureCaretVisible will know when to scroll 1292 protected int caretHeightOffset; 1293 1294 /// returns cursor rectangle 1295 protected Rect caretRect() { 1296 Rect caretRc = textPosToClient(_caretPos); 1297 if (_replaceMode) { 1298 dstring s = _content[_caretPos.line]; 1299 if (_caretPos.pos < s.length) { 1300 TextPosition nextPos = _caretPos; 1301 nextPos.pos++; 1302 Rect nextRect = textPosToClient(nextPos); 1303 caretRc.right = nextRect.right; 1304 } else { 1305 caretRc.right += _spaceWidth; 1306 } 1307 } 1308 if (_wordWrap) 1309 { 1310 _scrollPos.x = 0; 1311 int wrapLine = findWrapLine(_caretPos); 1312 int xOffset; 1313 if (wrapLine > 0) 1314 { 1315 LineSpan curSpan = getSpan(_caretPos.line); 1316 xOffset = curSpan.accumulation(wrapLine, LineSpan.WrapPointInfo.Width); 1317 } 1318 auto yOffset = -1 * _lineHeight * (wrapsUpTo(_caretPos.line) + wrapLine); 1319 caretHeightOffset = yOffset; 1320 caretRc.offset(_clientRect.left - xOffset, _clientRect.top - yOffset); 1321 } 1322 else 1323 caretRc.offset(_clientRect.left, _clientRect.top); 1324 return caretRc; 1325 } 1326 1327 /// handle theme change: e.g. reload some themed resources 1328 override void onThemeChanged() { 1329 super.onThemeChanged(); 1330 _caretColor = style.customColor("edit_caret"); 1331 _caretColorReplace = style.customColor("edit_caret_replace"); 1332 _selectionColorFocused = style.customColor("editor_selection_focused"); 1333 _selectionColorNormal = style.customColor("editor_selection_normal"); 1334 _searchHighlightColorCurrent = style.customColor("editor_search_highlight_current"); 1335 _searchHighlightColorOther = style.customColor("editor_search_highlight_other"); 1336 _leftPaneBackgroundColor = style.customColor("editor_left_pane_background"); 1337 _leftPaneBackgroundColor2 = style.customColor("editor_left_pane_background2"); 1338 _leftPaneBackgroundColor3 = style.customColor("editor_left_pane_background3"); 1339 _leftPaneLineNumberColor = style.customColor("editor_left_pane_line_number_text"); 1340 _leftPaneLineNumberColorEdited = style.customColor("editor_left_pane_line_number_text_edited", 0xC0C000); 1341 _leftPaneLineNumberColorSaved = style.customColor("editor_left_pane_line_number_text_saved", 0x00C000); 1342 _leftPaneLineNumberColorCurrentLine = style.customColor("editor_left_pane_line_number_text_current_line", 0xFFFFFFFF); 1343 _leftPaneLineNumberBackgroundColorCurrentLine = style.customColor("editor_left_pane_line_number_background_current_line", 0xC08080FF); 1344 _leftPaneLineNumberBackgroundColor = style.customColor("editor_left_pane_line_number_background"); 1345 _colorIconBreakpoint = style.customColor("editor_left_pane_line_icon_color_breakpoint", 0xFF0000); 1346 _colorIconBookmark = style.customColor("editor_left_pane_line_icon_color_bookmark", 0x0000FF); 1347 _colorIconError = style.customColor("editor_left_pane_line_icon_color_error", 0x80FF0000); 1348 _matchingBracketHightlightColor = style.customColor("editor_matching_bracket_highlight"); 1349 } 1350 1351 /// draws caret 1352 protected void drawCaret(DrawBuf buf) { 1353 if (focused) { 1354 if (_caretBlinkingPhase && _caretBlinks) { 1355 return; 1356 } 1357 // draw caret 1358 Rect caretRc = caretRect(); 1359 if (caretRc.intersects(_clientRect)) { 1360 //caretRc.left++; 1361 if (_replaceMode && BACKEND_GUI) 1362 buf.fillRect(caretRc, _caretColorReplace); 1363 //buf.drawLine(Point(caretRc.left, caretRc.bottom), Point(caretRc.left, caretRc.top), _caretColor); 1364 buf.fillRect(Rect(caretRc.left, caretRc.top, caretRc.left + 1, caretRc.bottom), _caretColor); 1365 } 1366 } 1367 } 1368 1369 protected void updateFontProps() { 1370 FontRef font = font(); 1371 _fixedFont = font.isFixed; 1372 _spaceWidth = font.spaceWidth; 1373 _lineHeight = font.height; 1374 } 1375 1376 /// when cursor position or selection is out of content bounds, fix it to nearest valid position 1377 protected void correctCaretPos() { 1378 _content.correctPosition(_caretPos); 1379 _content.correctPosition(_selectionRange.start); 1380 _content.correctPosition(_selectionRange.end); 1381 if (_selectionRange.empty) 1382 _selectionRange = TextRange(_caretPos, _caretPos); 1383 handleEditorStateChange(); 1384 } 1385 1386 1387 private int[] _lineWidthBuf; 1388 protected int calcLineWidth(dstring s) { 1389 int w = 0; 1390 if (_fixedFont) { 1391 int tabw = tabSize * _spaceWidth; 1392 // version optimized for fixed font 1393 for (int i = 0; i < s.length; i++) { 1394 if (s[i] == '\t') { 1395 w += _spaceWidth; 1396 w = (w + tabw - 1) / tabw * tabw; 1397 } else { 1398 w += _spaceWidth; 1399 } 1400 } 1401 } else { 1402 // variable pitch font 1403 if (_lineWidthBuf.length < s.length) 1404 _lineWidthBuf.length = s.length; 1405 int charsMeasured = font.measureText(s, _lineWidthBuf, int.max); 1406 if (charsMeasured > 0) 1407 w = _lineWidthBuf[charsMeasured - 1]; 1408 } 1409 return w; 1410 } 1411 1412 protected void updateSelectionAfterCursorMovement(TextPosition oldCaretPos, bool selecting) { 1413 if (selecting) { 1414 if (oldCaretPos == _selectionRange.start) { 1415 if (_caretPos >= _selectionRange.end) { 1416 _selectionRange.start = _selectionRange.end; 1417 _selectionRange.end = _caretPos; 1418 } else { 1419 _selectionRange.start = _caretPos; 1420 } 1421 } else if (oldCaretPos == _selectionRange.end) { 1422 if (_caretPos < _selectionRange.start) { 1423 _selectionRange.end = _selectionRange.start; 1424 _selectionRange.start = _caretPos; 1425 } else { 1426 _selectionRange.end = _caretPos; 1427 } 1428 } else { 1429 if (oldCaretPos < _caretPos) { 1430 // start selection forward 1431 _selectionRange.start = oldCaretPos; 1432 _selectionRange.end = _caretPos; 1433 } else { 1434 // start selection backward 1435 _selectionRange.start = _caretPos; 1436 _selectionRange.end = oldCaretPos; 1437 } 1438 } 1439 } else { 1440 _selectionRange.start = _caretPos; 1441 _selectionRange.end = _caretPos; 1442 } 1443 invalidate(); 1444 requestActionsUpdate(); 1445 handleEditorStateChange(); 1446 } 1447 1448 protected dstring _textToHighlight; 1449 protected uint _textToHighlightOptions; 1450 1451 /// text pattern to highlight - e.g. for search 1452 @property dstring textToHighlight() { 1453 return _textToHighlight; 1454 } 1455 /// set text to highlight -- e.g. for search 1456 void setTextToHighlight(dstring pattern, uint textToHighlightOptions) { 1457 _textToHighlight = pattern; 1458 _textToHighlightOptions = textToHighlightOptions; 1459 invalidate(); 1460 } 1461 1462 /// Used instead of using clientToTextPos for mouse input when in word wrap mode 1463 protected TextPosition wordWrapMouseOffset(int x, int y) { 1464 if (y < 0 || _span.length == 0) 1465 return clientToTextPos(Point(x,y)); 1466 1467 int selectedVisibleLine = y / _lineHeight; 1468 LineSpan _curSpan; 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(super.onKeyEvent(event)) 2105 return true; 2106 if (focused) startCaretBlinking(); 2107 cancelHoverTimer(); 2108 bool ctrlOrAltPressed = !!(event.flags & KeyFlag.Control); //(event.flags & (KeyFlag.Control /* | KeyFlag.Alt */)); 2109 //if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.SPACE && (event.flags & KeyFlag.Control)) { 2110 // Log.d("Ctrl+Space pressed"); 2111 //} 2112 if (event.action == KeyAction.Text && event.text.length && !ctrlOrAltPressed) { 2113 //Log.d("text entered: ", event.text); 2114 if (readOnly) 2115 return true; 2116 if (!(!!(event.flags & KeyFlag.Alt) && event.text.length == 1 && isAZaz(event.text[0]))) { // filter out Alt+A..Z 2117 if (replaceMode && _selectionRange.empty && _content[_caretPos.line].length >= _caretPos.pos + event.text.length) { 2118 // replace next char(s) 2119 TextRange range = _selectionRange; 2120 range.end.pos += cast(int)event.text.length; 2121 EditOperation op = new EditOperation(EditAction.Replace, range, [event.text]); 2122 _content.performOperation(op, this); 2123 } else { 2124 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [event.text]); 2125 _content.performOperation(op, this); 2126 } 2127 return true; 2128 } 2129 } 2130 //if (event.keyCode == KeyCode.SPACE && !readOnly) { 2131 // return true; 2132 //} 2133 //if (event.keyCode == KeyCode.RETURN && !readOnly && !_content.multiline) { 2134 // return true; 2135 //} 2136 return false; 2137 } 2138 2139 /// Handle Ctrl + Left mouse click on text 2140 protected void onControlClick() { 2141 // override to do something useful on Ctrl + Left mouse click in text 2142 } 2143 2144 protected TextPosition _hoverTextPosition; 2145 protected Point _hoverMousePosition; 2146 protected ulong _hoverTimer; 2147 protected long _hoverTimeoutMillis = 800; 2148 2149 /// override to handle mouse hover timeout in text 2150 protected void onHoverTimeout(Point pt, TextPosition pos) { 2151 // override to do something useful on hover timeout 2152 } 2153 2154 protected void onHover(Point pos) { 2155 if (_hoverMousePosition == pos) 2156 return; 2157 //Log.d("onHover ", pos); 2158 int x = pos.x - left - _leftPaneWidth; 2159 int y = pos.y - top; 2160 _hoverMousePosition = pos; 2161 _hoverTextPosition = clientToTextPos(Point(x, y)); 2162 cancelHoverTimer(); 2163 Rect reversePos = textPosToClient(_hoverTextPosition); 2164 if (x < reversePos.left + 10.pointsToPixels) 2165 _hoverTimer = setTimer(_hoverTimeoutMillis); 2166 } 2167 2168 protected void cancelHoverTimer() { 2169 if (_hoverTimer) { 2170 cancelTimer(_hoverTimer); 2171 _hoverTimer = 0; 2172 } 2173 } 2174 2175 /// process mouse event; return true if event is processed by widget. 2176 override bool onMouseEvent(MouseEvent event) { 2177 //Log.d("onMouseEvent ", id, " ", event.action, " (", event.x, ",", event.y, ")"); 2178 // support onClick 2179 bool insideLeftPane = event.x < _clientRect.left && event.x >= _clientRect.left - _leftPaneWidth; 2180 if (event.action == MouseAction.ButtonDown && insideLeftPane) { 2181 setFocus(); 2182 cancelHoverTimer(); 2183 if (onLeftPaneMouseClick(event)) 2184 return true; 2185 } 2186 if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) { 2187 setFocus(); 2188 cancelHoverTimer(); 2189 if (event.tripleClick) { 2190 selectLineByMouse(event.x - _clientRect.left, event.y - _clientRect.top); 2191 } else if (event.doubleClick) { 2192 selectWordByMouse(event.x - _clientRect.left, event.y - _clientRect.top); 2193 } else { 2194 auto doSelect = cast(bool)(event.keyFlags & MouseFlag.Shift); 2195 updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, doSelect); 2196 2197 if (event.keyFlags == MouseFlag.Control) 2198 onControlClick(); 2199 } 2200 startCaretBlinking(); 2201 invalidate(); 2202 return true; 2203 } 2204 if (event.action == MouseAction.Move && (event.flags & MouseButton.Left) != 0) { 2205 updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, true); 2206 ensureCaretVisible(); 2207 return true; 2208 } 2209 if (event.action == MouseAction.Move && event.flags == 0) { 2210 // hover 2211 if (focused && !insideLeftPane) { 2212 onHover(event.pos); 2213 } else { 2214 cancelHoverTimer(); 2215 } 2216 return true; 2217 } 2218 if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) { 2219 cancelHoverTimer(); 2220 return true; 2221 } 2222 if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) { 2223 cancelHoverTimer(); 2224 return true; 2225 } 2226 if (event.action == MouseAction.FocusIn) { 2227 cancelHoverTimer(); 2228 return true; 2229 } 2230 if (event.action == MouseAction.Wheel) { 2231 cancelHoverTimer(); 2232 uint keyFlags = event.flags & (MouseFlag.Shift | MouseFlag.Control | MouseFlag.Alt); 2233 if (event.wheelDelta < 0) { 2234 if (keyFlags == MouseFlag.Shift) 2235 return handleAction(new Action(EditorActions.ScrollRight)); 2236 if (keyFlags == MouseFlag.Control) 2237 return handleAction(new Action(EditorActions.ZoomOut)); 2238 return handleAction(new Action(EditorActions.ScrollLineDown)); 2239 } else if (event.wheelDelta > 0) { 2240 if (keyFlags == MouseFlag.Shift) 2241 return handleAction(new Action(EditorActions.ScrollLeft)); 2242 if (keyFlags == MouseFlag.Control) 2243 return handleAction(new Action(EditorActions.ZoomIn)); 2244 return handleAction(new Action(EditorActions.ScrollLineUp)); 2245 } 2246 } 2247 cancelHoverTimer(); 2248 return super.onMouseEvent(event); 2249 } 2250 2251 /// returns caret position 2252 @property TextPosition caretPos() { 2253 return _caretPos; 2254 } 2255 2256 /// change caret position and ensure it is visible 2257 void setCaretPos(int line, int column, bool makeVisible = true, bool center = false) 2258 { 2259 _caretPos = TextPosition(line,column); 2260 correctCaretPos(); 2261 invalidate(); 2262 if (makeVisible) 2263 ensureCaretVisible(center); 2264 handleEditorStateChange(); 2265 } 2266 } 2267 2268 interface EditorActionHandler { 2269 bool onEditorAction(const Action action); 2270 } 2271 2272 interface EnterKeyHandler { 2273 bool onEnterKey(EditWidgetBase editor); 2274 } 2275 2276 /// single line editor 2277 class EditLine : EditWidgetBase { 2278 2279 Signal!EditorActionHandler editorAction; 2280 /// handle Enter key press inside line editor 2281 Signal!EnterKeyHandler enterKey; 2282 2283 /// empty parameter list constructor - for usage by factory 2284 this() { 2285 this(null); 2286 } 2287 /// create with ID parameter 2288 this(string ID, dstring initialContent = null) { 2289 super(ID, ScrollBarMode.Invisible, ScrollBarMode.Invisible); 2290 _content = new EditableContent(false); 2291 _content.contentChanged = this; 2292 _selectAllWhenFocusedWithTab = true; 2293 _deselectAllWhenUnfocused = true; 2294 wantTabs = false; 2295 styleId = STYLE_EDIT_LINE; 2296 text = initialContent; 2297 onThemeChanged(); 2298 } 2299 2300 /// sets default popup menu with copy/paste/cut/undo/redo 2301 EditLine setDefaultPopupMenu() { 2302 MenuItem items = new MenuItem(); 2303 items.add(ACTION_EDITOR_COPY, ACTION_EDITOR_PASTE, ACTION_EDITOR_CUT, 2304 ACTION_EDITOR_UNDO, ACTION_EDITOR_REDO); 2305 popupMenu = items; 2306 return this; 2307 } 2308 2309 protected dstring _measuredText; 2310 protected int[] _measuredTextWidths; 2311 protected Point _measuredTextSize; 2312 2313 protected Point _measuredTextToSetWidgetSize; 2314 protected dstring _textToSetWidgetSize = "aaaaa"d; 2315 2316 @property void textToSetWidgetSize(dstring newText) { 2317 _textToSetWidgetSize = newText; 2318 requestLayout(); 2319 } 2320 2321 @property dstring textToSetWidgetSize() { 2322 return _textToSetWidgetSize; 2323 } 2324 2325 protected int[] _measuredTextToSetWidgetSizeWidths; 2326 2327 protected dchar _passwordChar = 0; 2328 /// password character - 0 for normal editor, some character, e.g. '*' to hide text by replacing all characters with this char 2329 @property dchar passwordChar() { return _passwordChar; } 2330 @property EditLine passwordChar(dchar ch) { 2331 if (_passwordChar != ch) { 2332 _passwordChar = ch; 2333 requestLayout(); 2334 } 2335 return this; 2336 } 2337 2338 override protected Rect textPosToClient(TextPosition p) { 2339 Rect res; 2340 res.bottom = _clientRect.height; 2341 if (p.pos == 0) 2342 res.left = 0; 2343 else if (p.pos >= _measuredText.length) 2344 res.left = _measuredTextSize.x; 2345 else 2346 res.left = _measuredTextWidths[p.pos - 1]; 2347 res.left -= _scrollPos.x; 2348 res.right = res.left + 1; 2349 return res; 2350 } 2351 2352 override protected TextPosition clientToTextPos(Point pt) { 2353 pt.x += _scrollPos.x; 2354 TextPosition res; 2355 for (int i = 0; i < _measuredText.length; i++) { 2356 int x0 = i > 0 ? _measuredTextWidths[i - 1] : 0; 2357 int x1 = _measuredTextWidths[i]; 2358 int mx = (x0 + x1) >> 1; 2359 if (pt.x <= mx) { 2360 res.pos = i; 2361 return res; 2362 } 2363 } 2364 res.pos = cast(int)_measuredText.length; 2365 return res; 2366 } 2367 2368 override protected void ensureCaretVisible(bool center = false) { 2369 //_scrollPos 2370 Rect rc = textPosToClient(_caretPos); 2371 if (rc.left < 0) { 2372 // scroll left 2373 _scrollPos.x += rc.left - _spaceWidth * 4; 2374 if (_scrollPos.x < 0) 2375 _scrollPos.x = 0; 2376 invalidate(); 2377 } else if (rc.left >= _clientRect.width - _spaceWidth * 4) { 2378 // scroll right 2379 _scrollPos.x += rc.left - _clientRect.width + _spaceWidth * 4; 2380 invalidate(); 2381 } 2382 updateScrollBars(); 2383 handleEditorStateChange(); 2384 } 2385 2386 protected dstring applyPasswordChar(dstring s) { 2387 if (!_passwordChar || s.length == 0) 2388 return s; 2389 dchar[] ss = s.dup; 2390 foreach(ref ch; ss) 2391 ch = _passwordChar; 2392 return cast(dstring)ss; 2393 } 2394 2395 override protected Point measureVisibleText() { 2396 FontRef font = font(); 2397 //Point sz = font.textSize(text); 2398 _measuredText = applyPasswordChar(text); 2399 _measuredTextWidths.length = _measuredText.length; 2400 int charsMeasured = font.measureText(_measuredText, _measuredTextWidths, MAX_WIDTH_UNSPECIFIED, tabSize); 2401 _measuredTextSize.x = charsMeasured > 0 ? _measuredTextWidths[charsMeasured - 1]: 0; 2402 _measuredTextSize.y = font.height; 2403 return _measuredTextSize; 2404 } 2405 2406 protected Point measureTextToSetWidgetSize() { 2407 FontRef font = font(); 2408 _measuredTextToSetWidgetSizeWidths.length = _textToSetWidgetSize.length; 2409 int charsMeasured = font.measureText(_textToSetWidgetSize, _measuredTextToSetWidgetSizeWidths, MAX_WIDTH_UNSPECIFIED, tabSize); 2410 _measuredTextToSetWidgetSize.x = charsMeasured > 0 ? _measuredTextToSetWidgetSizeWidths[charsMeasured - 1]: 0; 2411 _measuredTextToSetWidgetSize.y = font.height; 2412 return _measuredTextToSetWidgetSize; 2413 } 2414 2415 /// measure 2416 override void measure(int parentWidth, int parentHeight) { 2417 if (visibility == Visibility.Gone) 2418 return; 2419 2420 updateFontProps(); 2421 measureVisibleText(); 2422 measureTextToSetWidgetSize(); 2423 measuredContent(parentWidth, parentHeight, _measuredTextToSetWidgetSize.x + _leftPaneWidth, _measuredTextToSetWidgetSize.y); 2424 } 2425 2426 override bool handleAction(const Action a) { 2427 switch (a.id) with(EditorActions) 2428 { 2429 case InsertNewLine: 2430 case PrependNewLine: 2431 case AppendNewLine: 2432 if (editorAction.assigned) { 2433 return editorAction(a); 2434 } 2435 break; 2436 case Up: 2437 break; 2438 case Down: 2439 break; 2440 case PageUp: 2441 break; 2442 case PageDown: 2443 break; 2444 default: 2445 break; 2446 } 2447 return super.handleAction(a); 2448 } 2449 2450 2451 /// handle keys 2452 override bool onKeyEvent(KeyEvent event) { 2453 if(super.onKeyEvent(event)) 2454 return true; 2455 if (enterKey.assigned) { 2456 if (event.keyCode == KeyCode.RETURN && event.modifiers == 0) { 2457 if (event.action == KeyAction.KeyDown) 2458 return true; 2459 if (event.action == KeyAction.KeyUp) { 2460 if (enterKey(this)) 2461 return true; 2462 } 2463 } 2464 } 2465 return true; 2466 } 2467 2468 /// process mouse event; return true if event is processed by widget. 2469 override bool onMouseEvent(MouseEvent event) { 2470 return super.onMouseEvent(event); 2471 } 2472 2473 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 2474 override void layout(Rect rc) { 2475 if (visibility == Visibility.Gone) { 2476 return; 2477 } 2478 _needLayout = false; 2479 Point sz = Point(rc.width, measuredHeight); 2480 applyAlign(rc, sz); 2481 _pos = rc; 2482 _clientRect = rc; 2483 applyMargins(_clientRect); 2484 applyPadding(_clientRect); 2485 if (_contentChanged) { 2486 measureVisibleText(); 2487 _contentChanged = false; 2488 } 2489 } 2490 2491 2492 /// override to custom highlight of line background 2493 protected void drawLineBackground(DrawBuf buf, Rect lineRect, Rect visibleRect) { 2494 if (!_selectionRange.empty) { 2495 // line inside selection 2496 Rect startrc = textPosToClient(_selectionRange.start); 2497 Rect endrc = textPosToClient(_selectionRange.end); 2498 Rect rc = lineRect; 2499 rc.left = startrc.left + _clientRect.left; 2500 rc.right = endrc.left + _clientRect.left; 2501 if (!rc.empty) { 2502 // draw selection rect for line 2503 buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal); 2504 } 2505 if (_leftPaneWidth > 0) { 2506 Rect leftPaneRect = visibleRect; 2507 leftPaneRect.right = leftPaneRect.left; 2508 leftPaneRect.left -= _leftPaneWidth; 2509 drawLeftPane(buf, leftPaneRect, 0); 2510 } 2511 } 2512 } 2513 2514 /// draw content 2515 override void onDraw(DrawBuf buf) { 2516 if (visibility != Visibility.Visible) 2517 return; 2518 super.onDraw(buf); 2519 Rect rc = _pos; 2520 applyMargins(rc); 2521 applyPadding(rc); 2522 auto saver = ClipRectSaver(buf, rc, alpha); 2523 2524 FontRef font = font(); 2525 dstring txt = applyPasswordChar(text); 2526 2527 drawLineBackground(buf, _clientRect, _clientRect); 2528 font.drawText(buf, rc.left - _scrollPos.x, rc.top, txt, textColor, tabSize); 2529 2530 drawCaret(buf); 2531 } 2532 } 2533 2534 // SpinCtrl 2535 private { 2536 import std.ascii; 2537 } 2538 2539 class SpinCtrl : HorizontalLayout { 2540 2541 TextWidget label; 2542 int min, max; 2543 2544 private EditLine linEdit; 2545 private Button butUp, butDown; 2546 2547 2548 @property int value() { return linEdit.text.to!int; } 2549 @property void value(int val) { 2550 linEdit.text = val.to!dstring; 2551 } 2552 2553 override @property bool enabled() { return linEdit.enabled; } 2554 alias enabled = Widget.enabled; 2555 @property void enabled(bool status) { 2556 linEdit.enabled = status; 2557 butUp.enabled = status; 2558 butDown.enabled = status; 2559 } 2560 2561 this(int min, int max, int initialVal = 0, dstring labelText = null){ 2562 this.min = min; 2563 this.max = max; 2564 2565 if(labelText !is null){ 2566 label = new TextWidget("label", labelText); 2567 addChild(label); 2568 } 2569 2570 linEdit = new class EditLine { 2571 this(){super("linEdit", "0"d);} 2572 override bool onKeyEvent(KeyEvent event) { 2573 if (( KeyAction.Text == event.action && event.text[0].isDigit) 2574 || event.keyCode == KeyCode.BACK 2575 || event.keyCode == KeyCode.DEL 2576 || event.keyCode == KeyCode.LEFT 2577 || event.keyCode == KeyCode.RIGHT 2578 || event.keyCode == KeyCode.TAB 2579 ){ 2580 return super.onKeyEvent(event); 2581 } 2582 return false; 2583 } 2584 2585 override bool onMouseEvent(MouseEvent event) { 2586 if(enabled && event.action == MouseAction.Wheel){ 2587 if((event.wheelDelta == 1) && (value < max)) 2588 value = value + event.wheelDelta; 2589 if((event.wheelDelta == -1) && (value > min)) 2590 value = value + event.wheelDelta; 2591 return true; 2592 } 2593 return super.onMouseEvent(event); 2594 } 2595 }; 2596 2597 linEdit.addOnFocusChangeListener((w, t){ 2598 if(linEdit.text == "") 2599 linEdit.text = "0"; 2600 if(linEdit.text.to!int > max) 2601 value = max; 2602 if(linEdit.text.to!int < min) 2603 value = min; 2604 return true; 2605 }); 2606 2607 linEdit.minHeight = 35; 2608 if(initialVal != 0) 2609 value = initialVal; 2610 addChild(linEdit); 2611 2612 2613 auto butContainer = new VerticalLayout(); 2614 butContainer.maxHeight = linEdit.minHeight; 2615 2616 butUp = new Button("butUp", "+"d); 2617 butUp.margins(Rect(1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels)); 2618 2619 butDown = new Button("butDown", "-"d); 2620 butDown.margins(Rect(1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels)); 2621 2622 butContainer.addChild(butUp); 2623 butContainer.addChild(butDown); 2624 2625 addChild(butContainer); 2626 2627 butUp.click = delegate(Widget w) { 2628 immutable val = linEdit.text.to!int; 2629 if(val < max ) 2630 linEdit.text = (val + 1).to!dstring; 2631 return true; 2632 }; 2633 2634 butDown.click = delegate(Widget w) { 2635 immutable val = linEdit.text.to!int; 2636 if(val > min ) 2637 linEdit.text = (val - 1).to!dstring; 2638 return true; 2639 }; 2640 2641 enabled = true; 2642 } 2643 2644 } 2645 2646 /// multiline editor 2647 class EditBox : EditWidgetBase { 2648 /// empty parameter list constructor - for usage by factory 2649 this() { 2650 this(null); 2651 } 2652 /// create with ID parameter 2653 this(string ID, dstring initialContent = null, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) { 2654 super(ID, hscrollbarMode, vscrollbarMode); 2655 _content = new EditableContent(true); // multiline 2656 _content.contentChanged = this; 2657 styleId = STYLE_EDIT_BOX; 2658 text = initialContent; 2659 acceleratorMap.add( [ 2660 // zoom 2661 new Action(EditorActions.ZoomIn, KeyCode.ADD, KeyFlag.Control), 2662 new Action(EditorActions.ZoomOut, KeyCode.SUB, KeyFlag.Control), 2663 ]); 2664 onThemeChanged(); 2665 } 2666 2667 ~this() { 2668 if (_findPanel) { 2669 destroy(_findPanel); 2670 _findPanel = null; 2671 } 2672 } 2673 2674 protected int _firstVisibleLine; 2675 2676 protected int _maxLineWidth; 2677 protected int _numVisibleLines; // number of lines visible in client area 2678 protected dstring[] _visibleLines; // text for visible lines 2679 protected int[][] _visibleLinesMeasurement; // char positions for visible lines 2680 protected int[] _visibleLinesWidths; // width (in pixels) of visible lines 2681 protected CustomCharProps[][] _visibleLinesHighlights; 2682 protected CustomCharProps[][] _visibleLinesHighlightsBuf; 2683 2684 protected Point _measuredTextToSetWidgetSize; 2685 protected dstring _textToSetWidgetSize = "aaaaa/naaaaa"d; 2686 protected int[] _measuredTextToSetWidgetSizeWidths; 2687 2688 /// Set _needRewrap to true; 2689 override void wordWrapRefresh() 2690 { 2691 _needRewrap = true; 2692 } 2693 2694 override @property int fontSize() const { return super.fontSize(); } 2695 override @property Widget fontSize(int size) { 2696 // Need to rewrap if fontSize changed 2697 _needRewrap = true; 2698 return super.fontSize(size); 2699 } 2700 2701 override protected int lineCount() { 2702 return _content.length; 2703 } 2704 2705 override protected void updateMaxLineWidth() { 2706 // find max line width. TODO: optimize!!! 2707 int maxw; 2708 int[] buf; 2709 for (int i = 0; i < _content.length; i++) { 2710 dstring s = _content[i]; 2711 int w = calcLineWidth(s); 2712 if (maxw < w) 2713 maxw = w; 2714 } 2715 _maxLineWidth = maxw; 2716 } 2717 2718 @property int minFontSize() { 2719 return _minFontSize; 2720 } 2721 @property EditBox minFontSize(int size) { 2722 _minFontSize = size; 2723 return this; 2724 } 2725 2726 @property int maxFontSize() { 2727 return _maxFontSize; 2728 } 2729 2730 @property EditBox maxFontSize(int size) { 2731 _maxFontSize = size; 2732 return this; 2733 } 2734 2735 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 2736 override void layout(Rect rc) { 2737 if (visibility == Visibility.Gone) 2738 return; 2739 2740 if (rc != _pos) 2741 _contentChanged = true; 2742 Rect contentRc = rc; 2743 int findPanelHeight; 2744 if (_findPanel && _findPanel.visibility != Visibility.Gone) { 2745 _findPanel.measure(rc.width, rc.height); 2746 findPanelHeight = _findPanel.measuredHeight; 2747 _findPanel.layout(Rect(rc.left, rc.bottom - findPanelHeight, rc.right, rc.bottom)); 2748 contentRc.bottom -= findPanelHeight; 2749 } 2750 2751 super.layout(contentRc); 2752 if (_contentChanged) { 2753 measureVisibleText(); 2754 _needRewrap = true; 2755 _contentChanged = false; 2756 } 2757 2758 _pos = rc; 2759 } 2760 2761 override protected Point measureVisibleText() { 2762 Point sz; 2763 FontRef font = font(); 2764 _lineHeight = font.height; 2765 _numVisibleLines = (_clientRect.height + _lineHeight - 1) / _lineHeight; 2766 if (_firstVisibleLine >= _content.length) { 2767 _firstVisibleLine = _content.length - _numVisibleLines + 1; 2768 if (_firstVisibleLine < 0) 2769 _firstVisibleLine = 0; 2770 _caretPos.line = _content.length - 1; 2771 _caretPos.pos = 0; 2772 } 2773 if (_numVisibleLines < 1) 2774 _numVisibleLines = 1; 2775 if (_firstVisibleLine + _numVisibleLines > _content.length) 2776 _numVisibleLines = _content.length - _firstVisibleLine; 2777 if (_numVisibleLines < 1) 2778 _numVisibleLines = 1; 2779 _visibleLines.length = _numVisibleLines; 2780 if (_visibleLinesMeasurement.length < _numVisibleLines) 2781 _visibleLinesMeasurement.length = _numVisibleLines; 2782 if (_visibleLinesWidths.length < _numVisibleLines) 2783 _visibleLinesWidths.length = _numVisibleLines; 2784 if (_visibleLinesHighlights.length < _numVisibleLines) { 2785 _visibleLinesHighlights.length = _numVisibleLines; 2786 _visibleLinesHighlightsBuf.length = _numVisibleLines; 2787 } 2788 for (int i = 0; i < _numVisibleLines; i++) { 2789 _visibleLines[i] = _content[_firstVisibleLine + i]; 2790 size_t len = _visibleLines[i].length; 2791 if (_visibleLinesMeasurement[i].length < len) 2792 _visibleLinesMeasurement[i].length = len; 2793 if (_visibleLinesHighlightsBuf[i].length < len) 2794 _visibleLinesHighlightsBuf[i].length = len; 2795 _visibleLinesHighlights[i] = handleCustomLineHighlight(_firstVisibleLine + i, _visibleLines[i], _visibleLinesHighlightsBuf[i]); 2796 int charsMeasured = font.measureText(_visibleLines[i], _visibleLinesMeasurement[i], int.max, tabSize); 2797 _visibleLinesWidths[i] = charsMeasured > 0 ? _visibleLinesMeasurement[i][charsMeasured - 1] : 0; 2798 if (sz.x < _visibleLinesWidths[i]) 2799 sz.x = _visibleLinesWidths[i]; // width - max from visible lines 2800 } 2801 sz.x = _maxLineWidth; 2802 sz.y = _lineHeight * _content.length; // height - for all lines 2803 return sz; 2804 } 2805 2806 protected bool _extendRightScrollBound = true; 2807 /// override to determine if scrollbars are needed or not 2808 override protected void checkIfScrollbarsNeeded(ref bool needHScroll, ref bool needVScroll) { 2809 needHScroll = _hscrollbar && (_hscrollbarMode == ScrollBarMode.Visible || _hscrollbarMode == ScrollBarMode.Auto); 2810 needVScroll = _vscrollbar && (_vscrollbarMode == ScrollBarMode.Visible || _vscrollbarMode == ScrollBarMode.Auto); 2811 if (!needHScroll && !needVScroll) 2812 return; // not needed 2813 if (_hscrollbarMode != ScrollBarMode.Auto && _vscrollbarMode != ScrollBarMode.Auto) 2814 return; // no auto scrollbars 2815 // either h or v scrollbar is in auto mode 2816 2817 int hsbHeight = _hscrollbar.measuredHeight; 2818 int vsbWidth = _hscrollbar.measuredWidth; 2819 2820 int visibleLines = _lineHeight > 0 ? (_clientRect.height / _lineHeight) : 1; // fully visible lines 2821 if (visibleLines < 1) 2822 visibleLines = 1; 2823 int visibleLinesWithScrollbar = _lineHeight > 0 ? ((_clientRect.height - hsbHeight) / _lineHeight) : 1; // fully visible lines 2824 if (visibleLinesWithScrollbar < 1) 2825 visibleLinesWithScrollbar = 1; 2826 2827 // either h or v scrollbar is in auto mode 2828 //Point contentSize = fullContentSize(); 2829 int contentWidth = _maxLineWidth + (_extendRightScrollBound ? _clientRect.width / 16 : 0); 2830 int contentHeight = _content.length; 2831 2832 int clientWidth = _clientRect.width; 2833 int clientHeight = visibleLines; 2834 2835 int clientWidthWithScrollbar = clientWidth - vsbWidth; 2836 int clientHeightWithScrollbar = visibleLinesWithScrollbar; 2837 2838 if (_hscrollbarMode == ScrollBarMode.Auto && _vscrollbarMode == ScrollBarMode.Auto) { 2839 // both scrollbars in auto mode 2840 bool xFits = contentWidth <= clientWidth; 2841 bool yFits = contentHeight <= clientHeight; 2842 if (!xFits && !yFits) { 2843 // none fits, need both scrollbars 2844 } else if (xFits && yFits) { 2845 // everything fits! 2846 needHScroll = false; 2847 needVScroll = false; 2848 } else if (xFits) { 2849 // only X fits 2850 if (contentWidth <= clientWidthWithScrollbar) 2851 needHScroll = false; // disable hscroll 2852 } else { // yFits 2853 // only Y fits 2854 if (contentHeight <= clientHeightWithScrollbar) 2855 needVScroll = false; // disable vscroll 2856 } 2857 } else if (_hscrollbarMode == ScrollBarMode.Auto) { 2858 // only hscroll is in auto mode 2859 if (needVScroll) 2860 clientWidth = clientWidthWithScrollbar; 2861 needHScroll = contentWidth > clientWidth; 2862 } else { 2863 // only vscroll is in auto mode 2864 if (needHScroll) 2865 clientHeight = clientHeightWithScrollbar; 2866 needVScroll = contentHeight > clientHeight; 2867 } 2868 } 2869 2870 /// update horizontal scrollbar widget position 2871 override protected void updateHScrollBar() { 2872 _hscrollbar.setRange(0, _maxLineWidth + (_extendRightScrollBound ? _clientRect.width / 16 : 0)); 2873 _hscrollbar.pageSize = _clientRect.width; 2874 _hscrollbar.position = _scrollPos.x; 2875 } 2876 2877 /// update verticat scrollbar widget position 2878 override protected void updateVScrollBar() { 2879 int visibleLines = _lineHeight ? _clientRect.height / _lineHeight : 1; // fully visible lines 2880 if (visibleLines < 1) 2881 visibleLines = 1; 2882 _vscrollbar.setRange(0, _content.length); 2883 _vscrollbar.pageSize = visibleLines; 2884 _vscrollbar.position = _firstVisibleLine; 2885 } 2886 2887 /// process horizontal scrollbar event 2888 override bool onHScroll(ScrollEvent event) { 2889 if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) { 2890 if (_scrollPos.x != event.position) { 2891 _scrollPos.x = event.position; 2892 invalidate(); 2893 } 2894 } else if (event.action == ScrollAction.PageUp) { 2895 dispatchAction(new Action(EditorActions.ScrollLeft)); 2896 } else if (event.action == ScrollAction.PageDown) { 2897 dispatchAction(new Action(EditorActions.ScrollRight)); 2898 } else if (event.action == ScrollAction.LineUp) { 2899 dispatchAction(new Action(EditorActions.ScrollLeft)); 2900 } else if (event.action == ScrollAction.LineDown) { 2901 dispatchAction(new Action(EditorActions.ScrollRight)); 2902 } 2903 return true; 2904 } 2905 2906 /// process vertical scrollbar event 2907 override bool onVScroll(ScrollEvent event) { 2908 if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) { 2909 if (_firstVisibleLine != event.position) { 2910 _firstVisibleLine = event.position; 2911 measureVisibleText(); 2912 invalidate(); 2913 } 2914 } else if (event.action == ScrollAction.PageUp) { 2915 dispatchAction(new Action(EditorActions.ScrollPageUp)); 2916 } else if (event.action == ScrollAction.PageDown) { 2917 dispatchAction(new Action(EditorActions.ScrollPageDown)); 2918 } else if (event.action == ScrollAction.LineUp) { 2919 dispatchAction(new Action(EditorActions.ScrollLineUp)); 2920 } else if (event.action == ScrollAction.LineDown) { 2921 dispatchAction(new Action(EditorActions.ScrollLineDown)); 2922 } 2923 return true; 2924 } 2925 2926 protected bool _enableScrollAfterText = true; 2927 override protected void ensureCaretVisible(bool center = false) { 2928 if (_caretPos.line >= _content.length) 2929 _caretPos.line = _content.length - 1; 2930 if (_caretPos.line < 0) 2931 _caretPos.line = 0; 2932 int visibleLines = _lineHeight > 0 ? _clientRect.height / _lineHeight : 1; // fully visible lines 2933 if (visibleLines < 1) 2934 visibleLines = 1; 2935 int maxFirstVisibleLine = _content.length - 1; 2936 if (!_enableScrollAfterText) 2937 maxFirstVisibleLine = _content.length - visibleLines; 2938 if (maxFirstVisibleLine < 0) 2939 maxFirstVisibleLine = 0; 2940 2941 if (_caretPos.line < _firstVisibleLine) { 2942 _firstVisibleLine = _caretPos.line; 2943 if (center) { 2944 _firstVisibleLine -= visibleLines / 2; 2945 if (_firstVisibleLine < 0) 2946 _firstVisibleLine = 0; 2947 } 2948 if (_firstVisibleLine > maxFirstVisibleLine) 2949 _firstVisibleLine = maxFirstVisibleLine; 2950 measureVisibleText(); 2951 invalidate(); 2952 } else if(_wordWrap && !(_firstVisibleLine > maxFirstVisibleLine)) { 2953 //For wordwrap mode, move down sooner 2954 int offsetLines = -1 * caretHeightOffset / _lineHeight; 2955 //Log.d("offsetLines: ", offsetLines); 2956 if (_caretPos.line >= _firstVisibleLine + visibleLines - offsetLines) 2957 { 2958 _firstVisibleLine = _caretPos.line - visibleLines + 1 + offsetLines; 2959 if (center) 2960 _firstVisibleLine += visibleLines / 2; 2961 if (_firstVisibleLine > maxFirstVisibleLine) 2962 _firstVisibleLine = maxFirstVisibleLine; 2963 if (_firstVisibleLine < 0) 2964 _firstVisibleLine = 0; 2965 measureVisibleText(); 2966 invalidate(); 2967 } 2968 } else if (_caretPos.line >= _firstVisibleLine + visibleLines) { 2969 _firstVisibleLine = _caretPos.line - visibleLines + 1; 2970 if (center) 2971 _firstVisibleLine += visibleLines / 2; 2972 if (_firstVisibleLine > maxFirstVisibleLine) 2973 _firstVisibleLine = maxFirstVisibleLine; 2974 if (_firstVisibleLine < 0) 2975 _firstVisibleLine = 0; 2976 measureVisibleText(); 2977 invalidate(); 2978 } else if (_firstVisibleLine > maxFirstVisibleLine) { 2979 _firstVisibleLine = maxFirstVisibleLine; 2980 if (_firstVisibleLine < 0) 2981 _firstVisibleLine = 0; 2982 measureVisibleText(); 2983 invalidate(); 2984 } 2985 //_scrollPos 2986 Rect rc = textPosToClient(_caretPos); 2987 if (rc.left < 0) { 2988 // scroll left 2989 _scrollPos.x += rc.left - _spaceWidth * 4; 2990 if (_scrollPos.x < 0) 2991 _scrollPos.x = 0; 2992 invalidate(); 2993 } else if (rc.left >= _clientRect.width - _spaceWidth * 4) { 2994 // scroll right 2995 if (!_wordWrap) 2996 _scrollPos.x += rc.left - _clientRect.width + _spaceWidth * 4; 2997 invalidate(); 2998 } 2999 updateScrollBars(); 3000 handleEditorStateChange(); 3001 } 3002 3003 override protected Rect textPosToClient(TextPosition p) { 3004 Rect res; 3005 int lineIndex = p.line - _firstVisibleLine; 3006 res.top = lineIndex * _lineHeight; 3007 res.bottom = res.top + _lineHeight; 3008 // if visible 3009 if (lineIndex >= 0 && lineIndex < _visibleLines.length) { 3010 if (p.pos == 0) 3011 res.left = 0; 3012 else if (p.pos >= _visibleLinesMeasurement[lineIndex].length) 3013 res.left = _visibleLinesWidths[lineIndex]; 3014 else 3015 res.left = _visibleLinesMeasurement[lineIndex][p.pos - 1]; 3016 } 3017 res.left -= _scrollPos.x; 3018 res.right = res.left + 1; 3019 return res; 3020 } 3021 3022 override protected TextPosition clientToTextPos(Point pt) { 3023 TextPosition res; 3024 pt.x += _scrollPos.x; 3025 // if the point is above the first visible line 3026 if (pt.y < 0) { 3027 res.line = _firstVisibleLine > 0 ? _firstVisibleLine - 1 : _firstVisibleLine; 3028 res.pos = 0; 3029 return res; 3030 } 3031 int lineIndex = pt.y / _lineHeight; 3032 if (lineIndex < _visibleLines.length) { 3033 res.line = lineIndex + _firstVisibleLine; 3034 int len = cast(int)_visibleLines[lineIndex].length; 3035 for (int i = 0; i < len; i++) { 3036 int x0 = i > 0 ? _visibleLinesMeasurement[lineIndex][i - 1] : 0; 3037 int x1 = _visibleLinesMeasurement[lineIndex][i]; 3038 int mx = (x0 + x1) >> 1; 3039 if (pt.x <= mx) { 3040 res.pos = i; 3041 return res; 3042 } 3043 } 3044 res.pos = cast(int)_visibleLines[lineIndex].length; 3045 } else if (_visibleLines.length > 0) { 3046 res.line = _firstVisibleLine + cast(int)_visibleLines.length - 1; 3047 res.pos = cast(int)_visibleLines[$ - 1].length; 3048 } else { 3049 res.line = 0; 3050 res.pos = 0; 3051 } 3052 return res; 3053 } 3054 3055 override protected bool handleAction(const Action a) { 3056 TextPosition oldCaretPos = _caretPos; 3057 dstring currentLine = _content[_caretPos.line]; 3058 switch (a.id) with(EditorActions) 3059 { 3060 case PrependNewLine: 3061 if (!readOnly) { 3062 correctCaretPos(); 3063 _caretPos.pos = 0; 3064 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]); 3065 _content.performOperation(op, this); 3066 } 3067 return true; 3068 case InsertNewLine: 3069 if (!readOnly) { 3070 correctCaretPos(); 3071 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]); 3072 _content.performOperation(op, this); 3073 } 3074 return true; 3075 case Up: 3076 case SelectUp: 3077 if ((_caretPos.line > 0) | wordWrap) { 3078 if (_wordWrap) 3079 { 3080 LineSpan curSpan = getSpan(_caretPos.line); 3081 int curWrap = findWrapLine(_caretPos); 3082 if (curWrap > 0) 3083 { 3084 _caretPos.pos-= curSpan.wrapPoints[curWrap - 1].wrapPos; 3085 } 3086 else 3087 { 3088 int previousPos = _caretPos.pos; 3089 curSpan = getSpan(_caretPos.line - 1); 3090 curWrap = curSpan.len - 1; 3091 if (curWrap > 0) 3092 { 3093 int accumulativePoint = curSpan.accumulation(curSpan.len - 1, LineSpan.WrapPointInfo.Position); 3094 _caretPos.line--; 3095 _caretPos.pos = accumulativePoint + previousPos; 3096 } 3097 else 3098 { 3099 _caretPos.line--; 3100 } 3101 } 3102 } 3103 else if(_caretPos.line > 0) 3104 _caretPos.line--; 3105 correctCaretPos(); 3106 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3107 ensureCaretVisible(); 3108 } 3109 return true; 3110 case Down: 3111 case SelectDown: 3112 if (_caretPos.line < _content.length - 1) { 3113 if (_wordWrap) 3114 { 3115 LineSpan curSpan = getSpan(_caretPos.line); 3116 int curWrap = findWrapLine(_caretPos); 3117 if (curWrap < curSpan.len - 1) 3118 { 3119 int previousPos = _caretPos.pos; 3120 _caretPos.pos+= curSpan.wrapPoints[curWrap].wrapPos; 3121 correctCaretPos(); 3122 if (_caretPos.pos == previousPos) 3123 { 3124 _caretPos.pos = 0; 3125 _caretPos.line++; 3126 } 3127 } 3128 else if (curSpan.len > 1) 3129 { 3130 int previousPos = _caretPos.pos; 3131 int previousAccumulatedPosition = curSpan.accumulation(curSpan.len - 1, LineSpan.WrapPointInfo.Position); 3132 _caretPos.line++; 3133 _caretPos.pos = previousPos - previousAccumulatedPosition; 3134 } 3135 else 3136 { 3137 _caretPos.line++; 3138 } 3139 } 3140 else 3141 { 3142 _caretPos.line++; 3143 } 3144 correctCaretPos(); 3145 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3146 ensureCaretVisible(); 3147 } 3148 return true; 3149 case PageBegin: 3150 case SelectPageBegin: 3151 { 3152 ensureCaretVisible(); 3153 _caretPos.line = _firstVisibleLine; 3154 correctCaretPos(); 3155 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3156 } 3157 return true; 3158 case PageEnd: 3159 case SelectPageEnd: 3160 { 3161 ensureCaretVisible(); 3162 int fullLines = _clientRect.height / _lineHeight; 3163 int newpos = _firstVisibleLine + fullLines - 1; 3164 if (newpos >= _content.length) 3165 newpos = _content.length - 1; 3166 _caretPos.line = newpos; 3167 correctCaretPos(); 3168 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3169 } 3170 return true; 3171 case PageUp: 3172 case SelectPageUp: 3173 { 3174 ensureCaretVisible(); 3175 int fullLines = _clientRect.height / _lineHeight; 3176 int newpos = _firstVisibleLine - fullLines; 3177 if (newpos < 0) { 3178 _firstVisibleLine = 0; 3179 _caretPos.line = 0; 3180 } else { 3181 int delta = _firstVisibleLine - newpos; 3182 _firstVisibleLine = newpos; 3183 _caretPos.line -= delta; 3184 } 3185 correctCaretPos(); 3186 measureVisibleText(); 3187 updateScrollBars(); 3188 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3189 } 3190 return true; 3191 case PageDown: 3192 case SelectPageDown: 3193 { 3194 ensureCaretVisible(); 3195 int fullLines = _clientRect.height / _lineHeight; 3196 int newpos = _firstVisibleLine + fullLines; 3197 if (newpos >= _content.length) { 3198 _caretPos.line = _content.length - 1; 3199 } else { 3200 int delta = newpos - _firstVisibleLine; 3201 _firstVisibleLine = newpos; 3202 _caretPos.line += delta; 3203 } 3204 correctCaretPos(); 3205 measureVisibleText(); 3206 updateScrollBars(); 3207 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3208 } 3209 return true; 3210 case ScrollLeft: 3211 { 3212 if (_scrollPos.x > 0) { 3213 int newpos = _scrollPos.x - _spaceWidth * 4; 3214 if (newpos < 0) 3215 newpos = 0; 3216 _scrollPos.x = newpos; 3217 updateScrollBars(); 3218 invalidate(); 3219 } 3220 } 3221 return true; 3222 case ScrollRight: 3223 { 3224 if (_scrollPos.x < _maxLineWidth - _clientRect.width) { 3225 int newpos = _scrollPos.x + _spaceWidth * 4; 3226 if (newpos > _maxLineWidth - _clientRect.width) 3227 newpos = _maxLineWidth - _clientRect.width; 3228 _scrollPos.x = newpos; 3229 updateScrollBars(); 3230 invalidate(); 3231 } 3232 } 3233 return true; 3234 case ScrollLineUp: 3235 { 3236 if (_firstVisibleLine > 0) { 3237 _firstVisibleLine -= 3; 3238 if (_firstVisibleLine < 0) 3239 _firstVisibleLine = 0; 3240 measureVisibleText(); 3241 updateScrollBars(); 3242 invalidate(); 3243 } 3244 } 3245 return true; 3246 case ScrollPageUp: 3247 { 3248 int fullLines = _clientRect.height / _lineHeight; 3249 if (_firstVisibleLine > 0) { 3250 _firstVisibleLine -= fullLines * 3 / 4; 3251 if (_firstVisibleLine < 0) 3252 _firstVisibleLine = 0; 3253 measureVisibleText(); 3254 updateScrollBars(); 3255 invalidate(); 3256 } 3257 } 3258 return true; 3259 case ScrollLineDown: 3260 { 3261 int fullLines = _clientRect.height / _lineHeight; 3262 if (_firstVisibleLine + fullLines < _content.length) { 3263 _firstVisibleLine += 3; 3264 if (_firstVisibleLine > _content.length - fullLines) 3265 _firstVisibleLine = _content.length - fullLines; 3266 if (_firstVisibleLine < 0) 3267 _firstVisibleLine = 0; 3268 measureVisibleText(); 3269 updateScrollBars(); 3270 invalidate(); 3271 } 3272 } 3273 return true; 3274 case ScrollPageDown: 3275 { 3276 int fullLines = _clientRect.height / _lineHeight; 3277 if (_firstVisibleLine + fullLines < _content.length) { 3278 _firstVisibleLine += fullLines * 3 / 4; 3279 if (_firstVisibleLine > _content.length - fullLines) 3280 _firstVisibleLine = _content.length - fullLines; 3281 if (_firstVisibleLine < 0) 3282 _firstVisibleLine = 0; 3283 measureVisibleText(); 3284 updateScrollBars(); 3285 invalidate(); 3286 } 3287 } 3288 return true; 3289 case ZoomOut: 3290 case ZoomIn: 3291 { 3292 int dir = a.id == ZoomIn ? 1 : -1; 3293 if (_minFontSize < _maxFontSize && _minFontSize > 0 && _maxFontSize > 0) { 3294 int currentFontSize = fontSize; 3295 int increment = currentFontSize >= 30 ? 2 : 1; 3296 int newFontSize = currentFontSize + increment * dir; //* 110 / 100; 3297 if (newFontSize > 30) 3298 newFontSize &= 0xFFFE; 3299 if (currentFontSize != newFontSize && newFontSize <= _maxFontSize && newFontSize >= _minFontSize) { 3300 Log.i("Font size in editor ", id, " zoomed to ", newFontSize); 3301 fontSize = cast(ushort)newFontSize; 3302 updateFontProps(); 3303 _needRewrap = true; 3304 measureVisibleText(); 3305 updateScrollBars(); 3306 invalidate(); 3307 } 3308 } 3309 } 3310 return true; 3311 case ToggleBlockComment: 3312 if (!readOnly && _content.syntaxSupport && _content.syntaxSupport.supportsToggleBlockComment && _content.syntaxSupport.canToggleBlockComment(_selectionRange)) 3313 _content.syntaxSupport.toggleBlockComment(_selectionRange, this); 3314 return true; 3315 case ToggleLineComment: 3316 if (!readOnly && _content.syntaxSupport && _content.syntaxSupport.supportsToggleLineComment && _content.syntaxSupport.canToggleLineComment(_selectionRange)) 3317 _content.syntaxSupport.toggleLineComment(_selectionRange, this); 3318 return true; 3319 case AppendNewLine: 3320 if (!readOnly) { 3321 correctCaretPos(); 3322 TextPosition p = _content.lineEnd(_caretPos.line); 3323 TextRange r = TextRange(p, p); 3324 EditOperation op = new EditOperation(EditAction.Replace, r, [""d, ""d]); 3325 _content.performOperation(op, this); 3326 _caretPos = oldCaretPos; 3327 handleEditorStateChange(); 3328 } 3329 return true; 3330 case DeleteLine: 3331 if (!readOnly) { 3332 correctCaretPos(); 3333 EditOperation op = new EditOperation(EditAction.Replace, _content.lineRange(_caretPos.line), [""d]); 3334 _content.performOperation(op, this); 3335 } 3336 return true; 3337 case Find: 3338 openFindPanel(); 3339 return true; 3340 case FindNext: 3341 findNext(false); 3342 return true; 3343 case FindPrev: 3344 findNext(true); 3345 return true; 3346 case Replace: 3347 openReplacePanel(); 3348 return true; 3349 default: 3350 break; 3351 } 3352 return super.handleAction(a); 3353 } 3354 3355 /// calculate full content size in pixels 3356 override Point fullContentSize() { 3357 Point textSz; 3358 textSz.y = _lineHeight * _content.length; 3359 textSz.x = _maxLineWidth; 3360 //int maxy = _lineHeight * 5; // limit measured height 3361 //if (textSz.y > maxy) 3362 // textSz.y = maxy; 3363 return textSz; 3364 } 3365 3366 // override to set minimum scrollwidget size - default 100x100 3367 override protected Point minimumVisibleContentSize() { 3368 FontRef font = font(); 3369 _measuredTextToSetWidgetSizeWidths.length = _textToSetWidgetSize.length; 3370 int charsMeasured = font.measureText(_textToSetWidgetSize, _measuredTextToSetWidgetSizeWidths, MAX_WIDTH_UNSPECIFIED, tabSize); 3371 _measuredTextToSetWidgetSize.x = charsMeasured > 0 ? _measuredTextToSetWidgetSizeWidths[charsMeasured - 1]: 0; 3372 _measuredTextToSetWidgetSize.y = font.height; 3373 return _measuredTextToSetWidgetSize; 3374 } 3375 3376 /// measure 3377 override void measure(int parentWidth, int parentHeight) { 3378 if (visibility == Visibility.Gone) 3379 return; 3380 3381 updateFontProps(); 3382 updateMaxLineWidth(); 3383 int findPanelHeight; 3384 if (_findPanel) { 3385 _findPanel.measure(parentWidth, parentHeight); 3386 findPanelHeight = _findPanel.measuredHeight; 3387 if (parentHeight != SIZE_UNSPECIFIED) 3388 parentHeight -= findPanelHeight; 3389 } 3390 3391 super.measure(parentWidth, parentHeight); 3392 } 3393 3394 3395 protected void highlightTextPattern(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) { 3396 dstring pattern = _textToHighlight; 3397 uint options = _textToHighlightOptions; 3398 if (!pattern.length) { 3399 // support highlighting selection text - if whole word is selected 3400 if (_selectionRange.empty || !_selectionRange.singleLine) 3401 return; 3402 if (_selectionRange.start.line >= _content.length) 3403 return; 3404 dstring selLine = _content.line(_selectionRange.start.line); 3405 int start = _selectionRange.start.pos; 3406 int end = _selectionRange.end.pos; 3407 if (start >= selLine.length) 3408 return; 3409 pattern = selLine[start .. end]; 3410 if (!isWordChar(pattern[0]) || !isWordChar(pattern[$-1])) 3411 return; 3412 if (!isWholeWord(selLine, start, end)) 3413 return; 3414 // whole word is selected - enable highlight for it 3415 options = TextSearchFlag.CaseSensitive | TextSearchFlag.WholeWords; 3416 } 3417 if (!pattern.length) 3418 return; 3419 dstring lineText = _content.line(lineIndex); 3420 if (lineText.length < pattern.length) 3421 return; 3422 ptrdiff_t start = 0; 3423 import std.string : indexOf, CaseSensitive; 3424 import std.typecons : Flag; 3425 bool caseSensitive = (options & TextSearchFlag.CaseSensitive) != 0; 3426 bool wholeWords = (options & TextSearchFlag.WholeWords) != 0; 3427 bool selectionOnly = (options & TextSearchFlag.SelectionOnly) != 0; 3428 for (;;) { 3429 ptrdiff_t pos = lineText[start .. $].indexOf(pattern, caseSensitive ? Yes.caseSensitive : No.caseSensitive); 3430 if (pos < 0) 3431 break; 3432 // found text to highlight 3433 start += pos; 3434 if (!wholeWords || isWholeWord(lineText, start, start + pattern.length)) { 3435 TextRange r = TextRange(TextPosition(lineIndex, cast(int)start), TextPosition(lineIndex, cast(int)(start + pattern.length))); 3436 uint color = r.isInsideOrNext(caretPos) ? _searchHighlightColorCurrent : _searchHighlightColorOther; 3437 highlightLineRange(buf, lineRect, color, r); 3438 } 3439 start += pattern.length; 3440 } 3441 } 3442 3443 static bool isWordChar(dchar ch) { 3444 if (ch >= 'a' && ch <= 'z') 3445 return true; 3446 if (ch >= 'A' && ch <= 'Z') 3447 return true; 3448 if (ch == '_') 3449 return true; 3450 return false; 3451 } 3452 static bool isValidWordBound(dchar innerChar, dchar outerChar) { 3453 return !isWordChar(innerChar) || !isWordChar(outerChar); 3454 } 3455 /// returns true if selected range of string is whole word 3456 static bool isWholeWord(dstring lineText, size_t start, size_t end) { 3457 if (start >= lineText.length || start >= end) 3458 return false; 3459 if (start > 0 && !isValidWordBound(lineText[start], lineText[start - 1])) 3460 return false; 3461 if (end > 0 && end < lineText.length && !isValidWordBound(lineText[end - 1], lineText[end])) 3462 return false; 3463 return true; 3464 } 3465 3466 /// find all occurences of text pattern in content; options = bitset of TextSearchFlag 3467 TextRange[] findAll(dstring pattern, uint options) { 3468 TextRange[] res; 3469 res.assumeSafeAppend(); 3470 if (!pattern.length) 3471 return res; 3472 import std.string : indexOf, CaseSensitive; 3473 bool caseSensitive = (options & TextSearchFlag.CaseSensitive) != 0; 3474 bool wholeWords = (options & TextSearchFlag.WholeWords) != 0; 3475 bool selectionOnly = (options & TextSearchFlag.SelectionOnly) != 0; 3476 for (int i = 0; i < _content.length; i++) { 3477 dstring lineText = _content.line(i); 3478 if (lineText.length < pattern.length) 3479 continue; 3480 ptrdiff_t start = 0; 3481 for (;;) { 3482 ptrdiff_t pos = lineText[start .. $].indexOf(pattern, caseSensitive ? Yes.caseSensitive : No.caseSensitive); 3483 if (pos < 0) 3484 break; 3485 // found text to highlight 3486 start += pos; 3487 if (!wholeWords || isWholeWord(lineText, start, start + pattern.length)) { 3488 TextRange r = TextRange(TextPosition(i, cast(int)start), TextPosition(i, cast(int)(start + pattern.length))); 3489 res ~= r; 3490 } 3491 start += _textToHighlight.length; 3492 } 3493 } 3494 return res; 3495 } 3496 3497 /// find next occurence of text pattern in content, returns true if found 3498 bool findNextPattern(ref TextPosition pos, dstring pattern, uint searchOptions, int direction) { 3499 TextRange[] all = findAll(pattern, searchOptions); 3500 if (!all.length) 3501 return false; 3502 int currentIndex = -1; 3503 int nearestIndex = cast(int)all.length; 3504 for (int i = 0; i < all.length; i++) { 3505 if (all[i].isInsideOrNext(pos)) { 3506 currentIndex = i; 3507 break; 3508 } 3509 } 3510 for (int i = 0; i < all.length; i++) { 3511 if (pos < all[i].start) { 3512 nearestIndex = i; 3513 break; 3514 } 3515 if (pos > all[i].end) { 3516 nearestIndex = i + 1; 3517 } 3518 } 3519 if (currentIndex >= 0) { 3520 if (all.length < 2 && direction != 0) 3521 return false; 3522 currentIndex += direction; 3523 if (currentIndex < 0) 3524 currentIndex = cast(int)all.length - 1; 3525 else if (currentIndex >= all.length) 3526 currentIndex = 0; 3527 pos = all[currentIndex].start; 3528 return true; 3529 } 3530 if (direction < 0) 3531 nearestIndex--; 3532 if (nearestIndex < 0) 3533 nearestIndex = cast(int)all.length - 1; 3534 else if (nearestIndex >= all.length) 3535 nearestIndex = 0; 3536 pos = all[nearestIndex].start; 3537 return true; 3538 } 3539 3540 protected void highlightLineRange(DrawBuf buf, Rect lineRect, uint color, TextRange r) { 3541 Rect startrc = textPosToClient(r.start); 3542 Rect endrc = textPosToClient(r.end); 3543 Rect rc = lineRect; 3544 rc.left = _clientRect.left + startrc.left; 3545 rc.right = _clientRect.left + endrc.right; 3546 if (_wordWrap && !rc.empty) 3547 { 3548 wordWrapFillRect(buf, r.start.line, rc, color); 3549 } 3550 else if (!rc.empty) { 3551 // draw selection rect for matching bracket 3552 buf.fillRect(rc, color); 3553 } 3554 } 3555 3556 /// Used in place of directly calling buf.fillRect in word wrap mode 3557 void wordWrapFillRect(DrawBuf buf, int line, Rect lineToDivide, uint color) 3558 { 3559 Rect rc = lineToDivide; 3560 auto limitNumber = (int num, int limit) => num > limit ? limit : num; 3561 LineSpan curSpan = getSpan(line); 3562 int yOffset = _lineHeight * (wrapsUpTo(line)); 3563 rc.offset(0, yOffset); 3564 Rect[] wrappedSelection; 3565 wrappedSelection.length = curSpan.len; 3566 foreach (size_t i_, wrapLineRect; wrappedSelection) 3567 { 3568 int i = cast(int)i_; 3569 int startingDifference = rc.left - _clientRect.left; 3570 wrapLineRect = rc; 3571 wrapLineRect.offset(-1 * curSpan.accumulation(cast(int)i, LineSpan.WrapPointInfo.Width), cast(int)i * _lineHeight); 3572 wrapLineRect.right = limitNumber(wrapLineRect.right,(rc.left + curSpan.wrapPoints[i].wrapWidth) - startingDifference); 3573 buf.fillRect(wrapLineRect, color); 3574 } 3575 } 3576 3577 /// override to custom highlight of line background 3578 protected void drawLineBackground(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) { 3579 // highlight odd lines 3580 //if ((lineIndex & 1)) 3581 // buf.fillRect(visibleRect, 0xF4808080); 3582 3583 if (!_selectionRange.empty && _selectionRange.start.line <= lineIndex && _selectionRange.end.line >= lineIndex) { 3584 // line inside selection 3585 Rect startrc = textPosToClient(_selectionRange.start); 3586 Rect endrc = textPosToClient(_selectionRange.end); 3587 int startx = lineIndex == _selectionRange.start.line ? startrc.left + _clientRect.left : lineRect.left; 3588 int endx = lineIndex == _selectionRange.end.line ? endrc.left + _clientRect.left : lineRect.right + _spaceWidth; 3589 Rect rc = lineRect; 3590 rc.left = startx; 3591 rc.right = endx; 3592 if (!rc.empty && _wordWrap) 3593 { 3594 wordWrapFillRect(buf, lineIndex, rc, focused ? _selectionColorFocused : _selectionColorNormal); 3595 } 3596 else if (!rc.empty) { 3597 // draw selection rect for line 3598 buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal); 3599 } 3600 } 3601 3602 highlightTextPattern(buf, lineIndex, lineRect, visibleRect); 3603 3604 if (_matchingBraces.start.line == lineIndex) { 3605 TextRange r = TextRange(_matchingBraces.start, _matchingBraces.start.offset(1)); 3606 highlightLineRange(buf, lineRect, _matchingBracketHightlightColor, r); 3607 } 3608 if (_matchingBraces.end.line == lineIndex) { 3609 TextRange r = TextRange(_matchingBraces.end, _matchingBraces.end.offset(1)); 3610 highlightLineRange(buf, lineRect, _matchingBracketHightlightColor, r); 3611 } 3612 3613 // frame around current line 3614 if (focused && lineIndex == _caretPos.line && _selectionRange.singleLine && _selectionRange.start.line == _caretPos.line) { 3615 //TODO: Figure out why a little slow to catch up 3616 if (_wordWrap) 3617 visibleRect.offset(0, -caretHeightOffset); 3618 buf.drawFrame(visibleRect, 0xA0808080, Rect(1,1,1,1)); 3619 } 3620 3621 } 3622 3623 override protected void drawExtendedArea(DrawBuf buf) { 3624 if (_leftPaneWidth <= 0) 3625 return; 3626 Rect rc = _clientRect; 3627 3628 FontRef font = font(); 3629 int i = _firstVisibleLine; 3630 int lc = lineCount; 3631 for (;;) { 3632 Rect lineRect = rc; 3633 lineRect.left = _clientRect.left - _leftPaneWidth; 3634 lineRect.right = _clientRect.left; 3635 lineRect.bottom = lineRect.top + _lineHeight; 3636 if (lineRect.top >= _clientRect.bottom) 3637 break; 3638 drawLeftPane(buf, lineRect, i < lc ? i : -1); 3639 rc.top += _lineHeight; 3640 if (_wordWrap) 3641 { 3642 int currentWrap = 1; 3643 for (;;) 3644 { 3645 LineSpan curSpan = getSpan(i); 3646 if (currentWrap > curSpan.len - 1) 3647 break; 3648 Rect lineRect2 = rc; 3649 lineRect2.left = _clientRect.left - _leftPaneWidth; 3650 lineRect2.right = _clientRect.left; 3651 lineRect2.bottom = lineRect.top + _lineHeight; 3652 if (lineRect2.top >= _clientRect.bottom) 3653 break; 3654 drawLeftPane(buf, lineRect2, -1); 3655 rc.top += _lineHeight; 3656 3657 currentWrap++; 3658 } 3659 } 3660 i++; 3661 } 3662 } 3663 3664 3665 protected CustomCharProps[ubyte] _tokenHighlightColors; 3666 3667 /// set highlight options for particular token category 3668 void setTokenHightlightColor(ubyte tokenCategory, uint color, bool underline = false, bool strikeThrough = false) { 3669 _tokenHighlightColors[tokenCategory] = CustomCharProps(color, underline, strikeThrough); 3670 } 3671 /// clear highlight colors 3672 void clearTokenHightlightColors() { 3673 destroy(_tokenHighlightColors); 3674 } 3675 3676 /** 3677 Custom text color and style highlight (using text highlight) support. 3678 3679 Return null if no syntax highlight required for line. 3680 */ 3681 protected CustomCharProps[] handleCustomLineHighlight(int line, dstring txt, ref CustomCharProps[] buf) { 3682 if (!_tokenHighlightColors) 3683 return null; // no highlight colors set 3684 TokenPropString tokenProps = _content.lineTokenProps(line); 3685 if (tokenProps.length > 0) { 3686 bool hasNonzeroTokens = false; 3687 foreach(t; tokenProps) 3688 if (t) { 3689 hasNonzeroTokens = true; 3690 break; 3691 } 3692 if (!hasNonzeroTokens) 3693 return null; // all characters are of unknown token type (or white space) 3694 if (buf.length < tokenProps.length) 3695 buf.length = tokenProps.length; 3696 CustomCharProps[] colors = buf[0..tokenProps.length]; //new CustomCharProps[tokenProps.length]; 3697 for (int i = 0; i < tokenProps.length; i++) { 3698 ubyte p = tokenProps[i]; 3699 if (p in _tokenHighlightColors) 3700 colors[i] = _tokenHighlightColors[p]; 3701 else if ((p & TOKEN_CATEGORY_MASK) in _tokenHighlightColors) 3702 colors[i] = _tokenHighlightColors[(p & TOKEN_CATEGORY_MASK)]; 3703 else 3704 colors[i].color = textColor; 3705 if (isFullyTransparentColor(colors[i].color)) 3706 colors[i].color = textColor; 3707 } 3708 return colors; 3709 } 3710 return null; 3711 } 3712 3713 TextRange _matchingBraces; 3714 3715 bool _showWhiteSpaceMarks; 3716 /// when true, show marks for tabs and spaces at beginning and end of line, and tabs inside line 3717 @property bool showWhiteSpaceMarks() const { return _showWhiteSpaceMarks; } 3718 @property void showWhiteSpaceMarks(bool show) { 3719 if (_showWhiteSpaceMarks != show) { 3720 _showWhiteSpaceMarks = show; 3721 invalidate(); 3722 } 3723 } 3724 3725 /// find max tab mark column position for line 3726 protected int findMaxTabMarkColumn(int lineIndex) { 3727 if (lineIndex < 0 || lineIndex >= content.length) 3728 return -1; 3729 int maxSpace = -1; 3730 auto space = content.getLineWhiteSpace(lineIndex); 3731 maxSpace = space.firstNonSpaceColumn; 3732 if (maxSpace >= 0) 3733 return maxSpace; 3734 for(int i = lineIndex - 1; i >= 0; i--) { 3735 space = content.getLineWhiteSpace(i); 3736 if (!space.empty) { 3737 maxSpace = space.firstNonSpaceColumn; 3738 break; 3739 } 3740 } 3741 for(int i = lineIndex + 1; i < content.length; i++) { 3742 space = content.getLineWhiteSpace(i); 3743 if (!space.empty) { 3744 if (maxSpace < 0 || maxSpace < space.firstNonSpaceColumn) 3745 maxSpace = space.firstNonSpaceColumn; 3746 break; 3747 } 3748 } 3749 return maxSpace; 3750 } 3751 3752 void drawTabPositionMarks(DrawBuf buf, ref FontRef font, int lineIndex, Rect lineRect) { 3753 int maxCol = findMaxTabMarkColumn(lineIndex); 3754 if (maxCol > 0) { 3755 int spaceWidth = font.charWidth(' '); 3756 Rect rc = lineRect; 3757 uint color = addAlpha(textColor, 0xC0); 3758 for (int i = 0; i < maxCol; i += tabSize) { 3759 rc.left = lineRect.left + i * spaceWidth; 3760 rc.right = rc.left + 1; 3761 buf.fillRectPattern(rc, color, PatternType.dotted); 3762 } 3763 } 3764 } 3765 3766 void drawWhiteSpaceMarks(DrawBuf buf, ref FontRef font, dstring txt, int tabSize, Rect lineRect, Rect visibleRect) { 3767 // _showTabPositionMarks 3768 // _showWhiteSpaceMarks 3769 int firstNonSpace = -1; 3770 int lastNonSpace = -1; 3771 bool hasTabs = false; 3772 for(int i = 0; i < txt.length; i++) { 3773 if (txt[i] == '\t') { 3774 hasTabs = true; 3775 } else if (txt[i] != ' ') { 3776 if (firstNonSpace == -1) 3777 firstNonSpace = i; 3778 lastNonSpace = i + 1; 3779 } 3780 } 3781 bool spacesOnly = txt.length > 0 && firstNonSpace < 0; 3782 if (firstNonSpace <= 0 && lastNonSpace >= txt.length && !hasTabs && !spacesOnly) 3783 return; 3784 uint color = addAlpha(textColor, 0xC0); 3785 static int[] textSizeBuffer; 3786 int charsMeasured = font.measureText(txt, textSizeBuffer, MAX_WIDTH_UNSPECIFIED, tabSize, 0, 0); 3787 int ts = tabSize; 3788 if (ts < 1) 3789 ts = 1; 3790 if (ts > 8) 3791 ts = 8; 3792 int spaceIndex = 0; 3793 for (int i = 0; i < txt.length && i < charsMeasured; i++) { 3794 dchar ch = txt[i]; 3795 bool outsideText = (i < firstNonSpace || i >= lastNonSpace || spacesOnly); 3796 if ((ch == ' ' && outsideText) || ch == '\t') { 3797 Rect rc = lineRect; 3798 rc.left = lineRect.left + (i > 0 ? textSizeBuffer[i - 1] : 0); 3799 rc.right = lineRect.left + textSizeBuffer[i]; 3800 int h = rc.height; 3801 if (rc.intersects(visibleRect)) { 3802 // draw space mark 3803 if (ch == ' ') { 3804 // space 3805 int sz = h / 6; 3806 if (sz < 1) 3807 sz = 1; 3808 rc.top += h / 2 - sz / 2; 3809 rc.bottom = rc.top + sz; 3810 rc.left += rc.width / 2 - sz / 2; 3811 rc.right = rc.left + sz; 3812 buf.fillRect(rc, color); 3813 } else if (ch == '\t') { 3814 // tab 3815 Point p1 = Point(rc.left + 1, rc.top + h / 2); 3816 Point p2 = p1; 3817 p2.x = rc.right - 1; 3818 int sz = h / 4; 3819 if (sz < 2) 3820 sz = 2; 3821 if (sz > p2.x - p1.x) 3822 sz = p2.x - p1.x; 3823 buf.drawLine(p1, p2, color); 3824 buf.drawLine(p2, Point(p2.x - sz, p2.y - sz), color); 3825 buf.drawLine(p2, Point(p2.x - sz, p2.y + sz), color); 3826 } 3827 } 3828 } 3829 } 3830 } 3831 3832 /// Clear _span 3833 void resetVisibleSpans() 3834 { 3835 //TODO: Don't erase spans which have not been modified, cache them 3836 _span = []; 3837 } 3838 3839 private bool _needRewrap = true; 3840 private int lastStartingLine; 3841 3842 override protected void drawClient(DrawBuf buf) { 3843 // update matched braces 3844 if (!content.findMatchedBraces(_caretPos, _matchingBraces)) { 3845 _matchingBraces.start.line = -1; 3846 _matchingBraces.end.line = -1; 3847 } 3848 3849 Rect rc = _clientRect; 3850 3851 if (_contentChanged) 3852 _needRewrap = true; 3853 if (lastStartingLine != _firstVisibleLine) 3854 { 3855 _needRewrap = true; 3856 lastStartingLine = _firstVisibleLine; 3857 } 3858 if (rc.width <= 0 && _wordWrap) 3859 { 3860 //Prevent drawClient from getting stuck in loop 3861 return; 3862 } 3863 bool doRewrap = false; 3864 if (_needRewrap && _wordWrap) 3865 { 3866 resetVisibleSpans(); 3867 _needRewrap = false; 3868 doRewrap = true; 3869 } 3870 3871 FontRef font = font(); 3872 int previousWraps; 3873 for (int i = 0; i < _visibleLines.length; i++) { 3874 dstring txt = _visibleLines[i]; 3875 Rect lineRect; 3876 lineRect.left = _clientRect.left - _scrollPos.x; 3877 lineRect.right = lineRect.left + calcLineWidth(_content[_firstVisibleLine + i]); 3878 lineRect.top = _clientRect.top + i * _lineHeight; 3879 lineRect.bottom = lineRect.top + _lineHeight; 3880 Rect visibleRect = lineRect; 3881 visibleRect.left = _clientRect.left; 3882 visibleRect.right = _clientRect.right; 3883 drawLineBackground(buf, _firstVisibleLine + i, lineRect, visibleRect); 3884 if (_showTabPositionMarks) 3885 drawTabPositionMarks(buf, font, _firstVisibleLine + i, lineRect); 3886 if (!txt.length && !_wordWrap) 3887 continue; 3888 if (_showWhiteSpaceMarks) 3889 { 3890 Rect whiteSpaceRc = lineRect; 3891 Rect whiteSpaceRcVisible = visibleRect; 3892 for(int z; z < previousWraps; z++) 3893 { 3894 whiteSpaceRc.offset(0, _lineHeight); 3895 whiteSpaceRcVisible.offset(0, _lineHeight); 3896 } 3897 drawWhiteSpaceMarks(buf, font, txt, tabSize, whiteSpaceRc, whiteSpaceRcVisible); 3898 } 3899 if (_leftPaneWidth > 0) { 3900 Rect leftPaneRect = visibleRect; 3901 leftPaneRect.right = leftPaneRect.left; 3902 leftPaneRect.left -= _leftPaneWidth; 3903 drawLeftPane(buf, leftPaneRect, 0); 3904 } 3905 if (txt.length > 0 || _wordWrap) { 3906 CustomCharProps[] highlight = _visibleLinesHighlights[i]; 3907 if (_wordWrap) 3908 { 3909 dstring[] wrappedLine; 3910 if (doRewrap) 3911 wrappedLine = wrapLine(txt, _firstVisibleLine + i); 3912 else 3913 if (i < _span.length) 3914 wrappedLine = _span[i].wrappedContent; 3915 int accumulativeLength; 3916 CustomCharProps[] wrapProps; 3917 foreach (size_t q_, curWrap; wrappedLine) 3918 { 3919 int q = cast(int)q_; 3920 auto lineOffset = q + i + wrapsUpTo(i + _firstVisibleLine); 3921 if (highlight) 3922 { 3923 wrapProps = highlight[accumulativeLength .. $]; 3924 accumulativeLength += curWrap.length; 3925 font.drawColoredText(buf, rc.left - _scrollPos.x, rc.top + lineOffset * _lineHeight, curWrap, wrapProps, tabSize); 3926 } 3927 else 3928 font.drawText(buf, rc.left - _scrollPos.x, rc.top + lineOffset * _lineHeight, curWrap, textColor, tabSize); 3929 3930 } 3931 previousWraps += to!int(wrappedLine.length - 1); 3932 } 3933 else 3934 { 3935 if (highlight) 3936 font.drawColoredText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, highlight, tabSize); 3937 else 3938 font.drawText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, textColor, tabSize); 3939 } 3940 } 3941 } 3942 3943 drawCaret(buf); 3944 } 3945 3946 protected override bool onLeftPaneMouseClick(MouseEvent event) { 3947 if (_leftPaneWidth <= 0) 3948 return false; 3949 Rect rc = _clientRect; 3950 FontRef font = font(); 3951 int i = _firstVisibleLine; 3952 int lc = lineCount; 3953 for (;;) { 3954 Rect lineRect = rc; 3955 lineRect.left = _clientRect.left - _leftPaneWidth; 3956 lineRect.right = _clientRect.left; 3957 lineRect.bottom = lineRect.top + _lineHeight; 3958 if (lineRect.top >= _clientRect.bottom) 3959 break; 3960 if (event.y >= lineRect.top && event.y < lineRect.bottom) { 3961 return handleLeftPaneMouseClick(event, lineRect, i); 3962 } 3963 i++; 3964 rc.top += _lineHeight; 3965 } 3966 return false; 3967 } 3968 3969 override protected MenuItem getLeftPaneIconsPopupMenu(int line) { 3970 MenuItem menu = new MenuItem(); 3971 Action toggleBookmarkAction = ACTION_EDITOR_TOGGLE_BOOKMARK.clone(); 3972 toggleBookmarkAction.longParam = line; 3973 toggleBookmarkAction.objectParam = this; 3974 MenuItem item = menu.add(toggleBookmarkAction); 3975 return menu; 3976 } 3977 3978 protected FindPanel _findPanel; 3979 3980 dstring selectionText(bool singleLineOnly = false) { 3981 TextRange range = _selectionRange; 3982 if (range.empty) { 3983 return null; 3984 } 3985 dstring res = getRangeText(range); 3986 if (singleLineOnly) { 3987 for (int i = 0; i < res.length; i++) { 3988 if (res[i] == '\n') { 3989 res = res[0 .. i]; 3990 break; 3991 } 3992 } 3993 } 3994 return res; 3995 } 3996 3997 protected void findNext(bool backward) { 3998 createFindPanel(false, false); 3999 _findPanel.findNext(backward); 4000 // don't change replace mode 4001 } 4002 4003 protected void openFindPanel() { 4004 createFindPanel(false, false); 4005 _findPanel.replaceMode = false; 4006 _findPanel.activate(); 4007 } 4008 4009 protected void openReplacePanel() { 4010 createFindPanel(false, true); 4011 _findPanel.replaceMode = true; 4012 _findPanel.activate(); 4013 } 4014 4015 /// create find panel; returns true if panel was not yet visible 4016 protected bool createFindPanel(bool selectionOnly, bool replaceMode) { 4017 bool res = false; 4018 dstring txt = selectionText(true); 4019 if (!_findPanel) { 4020 _findPanel = new FindPanel(this, selectionOnly, replaceMode, txt); 4021 addChild(_findPanel); 4022 res = true; 4023 } else { 4024 if (_findPanel.visibility != Visibility.Visible) { 4025 _findPanel.visibility = Visibility.Visible; 4026 if (txt.length) 4027 _findPanel.searchText = txt; 4028 res = true; 4029 } 4030 } 4031 if (!pos.empty) 4032 layout(pos); 4033 requestLayout(); 4034 return res; 4035 } 4036 4037 /// close find panel 4038 protected void closeFindPanel(bool hideOnly = true) { 4039 if (_findPanel) { 4040 setFocus(); 4041 if (hideOnly) { 4042 _findPanel.visibility = Visibility.Gone; 4043 } else { 4044 removeChild(_findPanel); 4045 destroy(_findPanel); 4046 _findPanel = null; 4047 requestLayout(); 4048 } 4049 } 4050 } 4051 4052 /// Draw widget at its position to buffer 4053 override void onDraw(DrawBuf buf) { 4054 if (visibility != Visibility.Visible) 4055 return; 4056 super.onDraw(buf); 4057 if (_findPanel && _findPanel.visibility == Visibility.Visible) { 4058 _findPanel.onDraw(buf); 4059 } 4060 } 4061 } 4062 4063 /// Read only edit box for displaying logs with lines append operation 4064 class LogWidget : EditBox { 4065 4066 protected int _maxLines; 4067 /// max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit 4068 @property int maxLines() { return _maxLines; } 4069 /// set max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit 4070 @property void maxLines(int n) { _maxLines = n; } 4071 4072 protected bool _scrollLock; 4073 /// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction) 4074 @property bool scrollLock() { return _scrollLock; } 4075 /// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction) 4076 @property void scrollLock(bool flg) { _scrollLock = flg; } 4077 4078 this() { 4079 this(null); 4080 } 4081 4082 this(string ID) { 4083 super(ID); 4084 styleId = STYLE_LOG_WIDGET; 4085 _scrollLock = true; 4086 _enableScrollAfterText = false; 4087 enabled = false; 4088 minFontSize(pointsToPixels(6)).maxFontSize(pointsToPixels(32)); // allow font zoom with Ctrl + MouseWheel 4089 onThemeChanged(); 4090 } 4091 4092 /// append lines to the end of text 4093 void appendText(dstring text) { 4094 import std.array : split; 4095 if (text.length == 0) 4096 return; 4097 dstring[] lines = text.split("\n"); 4098 //lines ~= ""d; // append new line after last line 4099 content.appendLines(lines); 4100 if (_maxLines > 0 && lineCount > _maxLines) { 4101 TextRange range; 4102 range.end.line = lineCount - _maxLines; 4103 EditOperation op = new EditOperation(EditAction.Replace, range, [""d]); 4104 _content.performOperation(op, this); 4105 _contentChanged = true; 4106 } 4107 updateScrollBars(); 4108 if (_scrollLock) { 4109 _caretPos = lastLineBegin(); 4110 ensureCaretVisible(); 4111 } 4112 } 4113 4114 TextPosition lastLineBegin() { 4115 TextPosition res; 4116 if (_content.length == 0) 4117 return res; 4118 if (_content.lineLength(_content.length - 1) == 0 && _content.length > 1) 4119 res.line = _content.length - 2; 4120 else 4121 res.line = _content.length - 1; 4122 return res; 4123 } 4124 4125 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 4126 override void layout(Rect rc) { 4127 if (visibility == Visibility.Gone) 4128 return; 4129 4130 super.layout(rc); 4131 if (_scrollLock) { 4132 measureVisibleText(); 4133 _caretPos = lastLineBegin(); 4134 ensureCaretVisible(); 4135 } 4136 } 4137 4138 } 4139 4140 class FindPanel : HorizontalLayout { 4141 protected EditBox _editor; 4142 protected EditLine _edFind; 4143 protected EditLine _edReplace; 4144 protected ImageCheckButton _cbCaseSensitive; 4145 protected ImageCheckButton _cbWholeWords; 4146 protected CheckBox _cbSelection; 4147 protected Button _btnFindNext; 4148 protected Button _btnFindPrev; 4149 protected Button _btnReplace; 4150 protected Button _btnReplaceAndFind; 4151 protected Button _btnReplaceAll; 4152 protected ImageButton _btnClose; 4153 protected bool _replaceMode; 4154 /// returns true if panel is working in replace mode 4155 @property bool replaceMode() { return _replaceMode; } 4156 @property FindPanel replaceMode(bool newMode) { 4157 if (newMode != _replaceMode) { 4158 _replaceMode = newMode; 4159 childById("replace").visibility = newMode ? Visibility.Visible : Visibility.Gone; 4160 } 4161 return this; 4162 } 4163 4164 @property dstring searchText() { 4165 return _edFind.text; 4166 } 4167 4168 @property FindPanel searchText(dstring newText) { 4169 _edFind.text = newText; 4170 return this; 4171 } 4172 4173 this(EditBox editor, bool selectionOnly, bool replace, dstring initialText = ""d) { 4174 _replaceMode = replace; 4175 import dlangui.dml.parser; 4176 try { 4177 parseML(q{ 4178 { 4179 layoutWidth: fill 4180 VerticalLayout { 4181 layoutWidth: fill 4182 HorizontalLayout { 4183 layoutWidth: fill 4184 EditLine { id: edFind; layoutWidth: fill; alignment: vcenter } 4185 Button { id: btnFindNext; text: EDIT_FIND_NEXT } 4186 Button { id: btnFindPrev; text: EDIT_FIND_PREV } 4187 VerticalLayout { 4188 VSpacer {} 4189 HorizontalLayout { 4190 ImageCheckButton { id: cbCaseSensitive; drawableId: "find_case_sensitive"; tooltipText: EDIT_FIND_CASE_SENSITIVE; styleId: TOOLBAR_BUTTON; alignment: vcenter } 4191 ImageCheckButton { id: cbWholeWords; drawableId: "find_whole_words"; tooltipText: EDIT_FIND_WHOLE_WORDS; styleId: TOOLBAR_BUTTON; alignment: vcenter } 4192 CheckBox { id: cbSelection; text: "Sel" } 4193 } 4194 VSpacer {} 4195 } 4196 } 4197 HorizontalLayout { 4198 id: replace 4199 layoutWidth: fill; 4200 EditLine { id: edReplace; layoutWidth: fill; alignment: vcenter } 4201 Button { id: btnReplace; text: EDIT_REPLACE_NEXT } 4202 Button { id: btnReplaceAndFind; text: EDIT_REPLACE_AND_FIND } 4203 Button { id: btnReplaceAll; text: EDIT_REPLACE_ALL } 4204 } 4205 } 4206 VerticalLayout { 4207 VSpacer {} 4208 ImageButton { id: btnClose; drawableId: close; styleId: BUTTON_TRANSPARENT } 4209 VSpacer {} 4210 } 4211 } 4212 }, null, this); 4213 } catch (Exception e) { 4214 Log.e("Exception while parsing DML: ", e); 4215 } 4216 _editor = editor; 4217 _edFind = childById!EditLine("edFind"); 4218 _edReplace = childById!EditLine("edReplace"); 4219 4220 if (initialText.length) { 4221 _edFind.text = initialText; 4222 _edReplace.text = initialText; 4223 } 4224 4225 _edFind.editorAction.connect(&onFindEditorAction); 4226 _edFind.contentChange.connect(&onFindTextChange); 4227 4228 //_edFind.keyEvent = &onEditorKeyEvent; 4229 //_edReplace.keyEvent = &onEditorKeyEvent; 4230 4231 _btnFindNext = childById!Button("btnFindNext"); 4232 _btnFindNext.click = &onButtonClick; 4233 _btnFindPrev = childById!Button("btnFindPrev"); 4234 _btnFindPrev.click = &onButtonClick; 4235 _btnReplace = childById!Button("btnReplace"); 4236 _btnReplace.click = &onButtonClick; 4237 _btnReplaceAndFind = childById!Button("btnReplaceAndFind"); 4238 _btnReplaceAndFind.click = &onButtonClick; 4239 _btnReplaceAll = childById!Button("btnReplaceAll"); 4240 _btnReplaceAll.click = &onButtonClick; 4241 _btnClose = childById!ImageButton("btnClose"); 4242 _btnClose.click = &onButtonClick; 4243 _cbCaseSensitive = childById!ImageCheckButton("cbCaseSensitive"); 4244 _cbWholeWords = childById!ImageCheckButton("cbWholeWords"); 4245 _cbSelection = childById!CheckBox("cbSelection"); 4246 _cbCaseSensitive.checkChange = &onCaseSensitiveCheckChange; 4247 _cbWholeWords.checkChange = &onCaseSensitiveCheckChange; 4248 _cbSelection.checkChange = &onCaseSensitiveCheckChange; 4249 focusGroup = true; 4250 if (!replace) 4251 childById("replace").visibility = Visibility.Gone; 4252 //_edFind = new EditLine("edFind" 4253 dstring currentText = _edFind.text; 4254 Log.d("currentText=", currentText); 4255 setDirection(false); 4256 updateHighlight(); 4257 } 4258 void activate() { 4259 _edFind.setFocus(); 4260 dstring currentText = _edFind.text; 4261 Log.d("activate.currentText=", currentText); 4262 _edFind.setCaretPos(0, cast(int)currentText.length, true); 4263 } 4264 4265 bool onButtonClick(Widget source) { 4266 switch (source.id) { 4267 case "btnFindNext": 4268 findNext(false); 4269 return true; 4270 case "btnFindPrev": 4271 findNext(true); 4272 return true; 4273 case "btnClose": 4274 close(); 4275 return true; 4276 case "btnReplace": 4277 replaceOne(); 4278 return true; 4279 case "btnReplaceAndFind": 4280 replaceOne(); 4281 findNext(_backDirection); 4282 return true; 4283 case "btnReplaceAll": 4284 replaceAll(); 4285 return true; 4286 default: 4287 return true; 4288 } 4289 } 4290 4291 void close() { 4292 _editor.setTextToHighlight(null, 0); 4293 _editor.closeFindPanel(); 4294 } 4295 4296 override bool onKeyEvent(KeyEvent event) { 4297 if (event.keyCode == KeyCode.TAB) 4298 return super.onKeyEvent(event); 4299 if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.ESCAPE) { 4300 close(); 4301 return true; 4302 } 4303 return true; 4304 } 4305 4306 /// override to handle specific actions 4307 override bool handleAction(const Action a) { 4308 switch (a.id) { 4309 case EditorActions.FindNext: 4310 findNext(false); 4311 return true; 4312 case EditorActions.FindPrev: 4313 findNext(true); 4314 return true; 4315 default: 4316 return false; 4317 } 4318 } 4319 4320 protected bool _backDirection; 4321 void setDirection(bool back) { 4322 _backDirection = back; 4323 if (back) { 4324 _btnFindNext.resetState(State.Default); 4325 _btnFindPrev.setState(State.Default); 4326 } else { 4327 _btnFindNext.setState(State.Default); 4328 _btnFindPrev.resetState(State.Default); 4329 } 4330 } 4331 4332 uint makeSearchFlags() { 4333 uint res = 0; 4334 if (_cbCaseSensitive.checked) 4335 res |= TextSearchFlag.CaseSensitive; 4336 if (_cbWholeWords.checked) 4337 res |= TextSearchFlag.WholeWords; 4338 if (_cbSelection.checked) 4339 res |= TextSearchFlag.SelectionOnly; 4340 return res; 4341 } 4342 bool findNext(bool back) { 4343 setDirection(back); 4344 dstring currentText = _edFind.text; 4345 Log.d("findNext text=", currentText, " back=", back); 4346 if (!currentText.length) 4347 return false; 4348 _editor.setTextToHighlight(currentText, makeSearchFlags); 4349 TextPosition pos = _editor.caretPos; 4350 bool res = _editor.findNextPattern(pos, currentText, makeSearchFlags, back ? -1 : 1); 4351 if (res) { 4352 _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)currentText.length)); 4353 _editor.ensureCaretVisible(); 4354 //_editor.setCaretPos(pos.line, pos.pos, true); 4355 } 4356 return res; 4357 } 4358 4359 bool replaceOne() { 4360 dstring currentText = _edFind.text; 4361 dstring newText = _edReplace.text; 4362 Log.d("replaceOne text=", currentText, " back=", _backDirection, " newText=", newText); 4363 if (!currentText.length) 4364 return false; 4365 _editor.setTextToHighlight(currentText, makeSearchFlags); 4366 TextPosition pos = _editor.caretPos; 4367 bool res = _editor.findNextPattern(pos, currentText, makeSearchFlags, 0); 4368 if (res) { 4369 _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)currentText.length)); 4370 _editor.replaceSelectionText(newText); 4371 _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)newText.length)); 4372 _editor.ensureCaretVisible(); 4373 //_editor.setCaretPos(pos.line, pos.pos, true); 4374 } 4375 return res; 4376 } 4377 4378 int replaceAll() { 4379 int count = 0; 4380 for(int i = 0; ; i++) { 4381 debug Log.d("replaceAll - calling replaceOne, iteration ", i); 4382 if (!replaceOne()) 4383 break; 4384 count++; 4385 TextPosition initialPosition = _editor.caretPos; 4386 debug Log.d("replaceAll - position is ", initialPosition); 4387 if (!findNext(_backDirection)) 4388 break; 4389 TextPosition newPosition = _editor.caretPos; 4390 debug Log.d("replaceAll - next position is ", newPosition); 4391 if (_backDirection && newPosition >= initialPosition) 4392 break; 4393 if (!_backDirection && newPosition <= initialPosition) 4394 break; 4395 } 4396 debug Log.d("replaceAll - done, replace count = ", count); 4397 _editor.ensureCaretVisible(); 4398 return count; 4399 } 4400 4401 void updateHighlight() { 4402 dstring currentText = _edFind.text; 4403 Log.d("onFindTextChange.currentText=", currentText); 4404 _editor.setTextToHighlight(currentText, makeSearchFlags); 4405 } 4406 4407 void onFindTextChange(EditableContent source) { 4408 Log.d("onFindTextChange"); 4409 updateHighlight(); 4410 } 4411 4412 bool onCaseSensitiveCheckChange(Widget source, bool checkValue) { 4413 updateHighlight(); 4414 return true; 4415 } 4416 4417 bool onFindEditorAction(const Action action) { 4418 Log.d("onFindEditorAction ", action); 4419 if (action.id == EditorActions.InsertNewLine) { 4420 findNext(_backDirection); 4421 return true; 4422 } 4423 return false; 4424 } 4425 } 4426 4427 //import dlangui.widgets.metadata; 4428 //mixin(registerWidgets!(EditLine, EditBox, LogWidget)());