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 { 1465 if(_span.length == 0) 1466 return clientToTextPos(Point(x,y)); 1467 int selectedVisibleLine = y / _lineHeight; 1468 1469 LineSpan _curSpan; 1470 1471 int wrapLine = 0; 1472 int curLine = 0; 1473 bool foundWrap = false; 1474 int accumulativeWidths = 0; 1475 int curWrapOfSpan = 0; 1476 1477 lineSpanIterate(delegate(LineSpan curSpan){ 1478 while (!foundWrap) 1479 { 1480 if (wrapLine == selectedVisibleLine) 1481 { 1482 foundWrap = true; 1483 break; 1484 } 1485 accumulativeWidths += curSpan.wrapPoints[curWrapOfSpan].wrapWidth; 1486 wrapLine++; 1487 curWrapOfSpan++; 1488 if (curWrapOfSpan >= curSpan.len) 1489 { 1490 break; 1491 } 1492 } 1493 if (!foundWrap) 1494 { 1495 accumulativeWidths = 0; 1496 curLine++; 1497 } 1498 curWrapOfSpan = 0; 1499 }); 1500 1501 int fakeLineHeight = curLine * _lineHeight; 1502 return clientToTextPos(Point(x + accumulativeWidths,fakeLineHeight)); 1503 } 1504 1505 protected void selectWordByMouse(int x, int y) { 1506 TextPosition oldCaretPos = _caretPos; 1507 TextPosition newPos = _wordWrap ? wordWrapMouseOffset(x,y) : clientToTextPos(Point(x,y)); 1508 TextRange r = content.wordBounds(newPos); 1509 if (r.start < r.end) { 1510 _selectionRange = r; 1511 _caretPos = r.end; 1512 invalidate(); 1513 requestActionsUpdate(); 1514 } else { 1515 _caretPos = newPos; 1516 updateSelectionAfterCursorMovement(oldCaretPos, false); 1517 } 1518 handleEditorStateChange(); 1519 } 1520 1521 protected void selectLineByMouse(int x, int y, bool onSameLineOnly = true) { 1522 TextPosition oldCaretPos = _caretPos; 1523 TextPosition newPos = _wordWrap ? wordWrapMouseOffset(x,y) : clientToTextPos(Point(x,y)); 1524 if (onSameLineOnly && newPos.line != oldCaretPos.line) 1525 return; // different lines 1526 TextRange r = content.lineRange(newPos.line); 1527 if (r.start < r.end) { 1528 _selectionRange = r; 1529 _caretPos = r.end; 1530 invalidate(); 1531 requestActionsUpdate(); 1532 } else { 1533 _caretPos = newPos; 1534 updateSelectionAfterCursorMovement(oldCaretPos, false); 1535 } 1536 handleEditorStateChange(); 1537 } 1538 1539 protected void updateCaretPositionByMouse(int x, int y, bool selecting) { 1540 TextPosition oldCaretPos = _caretPos; 1541 TextPosition newPos = _wordWrap ? wordWrapMouseOffset(x,y) : clientToTextPos(Point(x,y)); 1542 if (newPos != _caretPos) { 1543 _caretPos = newPos; 1544 updateSelectionAfterCursorMovement(oldCaretPos, selecting); 1545 invalidate(); 1546 } 1547 handleEditorStateChange(); 1548 } 1549 1550 /// generate string of spaces, to reach next tab position 1551 protected dstring spacesForTab(int currentPos) { 1552 int newPos = (currentPos + tabSize + 1) / tabSize * tabSize; 1553 return " "d[0..(newPos - currentPos)]; 1554 } 1555 1556 /// returns true if one or more lines selected fully 1557 protected bool multipleLinesSelected() { 1558 return _selectionRange.end.line > _selectionRange.start.line; 1559 } 1560 1561 protected bool _camelCasePartsAsWords = true; 1562 1563 void replaceSelectionText(dstring newText) { 1564 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [newText]); 1565 _content.performOperation(op, this); 1566 ensureCaretVisible(); 1567 } 1568 1569 protected bool removeSelectionTextIfSelected() { 1570 if (_selectionRange.empty) 1571 return false; 1572 // clear selection 1573 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d]); 1574 _content.performOperation(op, this); 1575 ensureCaretVisible(); 1576 return true; 1577 } 1578 1579 /// returns current selection text (joined with LF when span over multiple lines) 1580 public dstring getSelectedText() { 1581 return getRangeText(_selectionRange); 1582 } 1583 1584 /// returns text for specified range (joined with LF when span over multiple lines) 1585 public dstring getRangeText(TextRange range) { 1586 dstring selectionText = concatDStrings(_content.rangeText(range)); 1587 return selectionText; 1588 } 1589 1590 /// returns range for line with cursor 1591 @property public TextRange currentLineRange() { 1592 return _content.lineRange(_caretPos.line); 1593 } 1594 1595 /// clears selection (don't change text, just unselect) 1596 void clearSelection() { 1597 _selectionRange = TextRange(_caretPos, _caretPos); 1598 invalidate(); 1599 } 1600 1601 protected bool removeRangeText(TextRange range) { 1602 if (range.empty) 1603 return false; 1604 _selectionRange = range; 1605 _caretPos = _selectionRange.start; 1606 EditOperation op = new EditOperation(EditAction.Replace, range, [""d]); 1607 _content.performOperation(op, this); 1608 //_selectionRange.start = _caretPos; 1609 //_selectionRange.end = _caretPos; 1610 ensureCaretVisible(); 1611 handleEditorStateChange(); 1612 return true; 1613 } 1614 1615 /// returns current selection range 1616 @property TextRange selectionRange() { 1617 return _selectionRange; 1618 } 1619 /// sets current selection range 1620 @property void selectionRange(TextRange range) { 1621 if (range.empty) 1622 return; 1623 _selectionRange = range; 1624 _caretPos = range.end; 1625 handleEditorStateChange(); 1626 } 1627 1628 /// override to handle specific actions state (e.g. change enabled state for supported actions) 1629 override bool handleActionStateRequest(const Action a) { 1630 switch (a.id) with(EditorActions) 1631 { 1632 case ToggleBlockComment: 1633 if (!_content.syntaxSupport || !_content.syntaxSupport.supportsToggleBlockComment) 1634 a.state = ACTION_STATE_INVISIBLE; 1635 else if (enabled && _content.syntaxSupport.canToggleBlockComment(_selectionRange)) 1636 a.state = ACTION_STATE_ENABLED; 1637 else 1638 a.state = ACTION_STATE_DISABLE; 1639 return true; 1640 case ToggleLineComment: 1641 if (!_content.syntaxSupport || !_content.syntaxSupport.supportsToggleLineComment) 1642 a.state = ACTION_STATE_INVISIBLE; 1643 else if (enabled && _content.syntaxSupport.canToggleLineComment(_selectionRange)) 1644 a.state = ACTION_STATE_ENABLED; 1645 else 1646 a.state = ACTION_STATE_DISABLE; 1647 return true; 1648 case Copy: 1649 case Cut: 1650 case Paste: 1651 case Undo: 1652 case Redo: 1653 case Tab: 1654 case BackTab: 1655 case Indent: 1656 case Unindent: 1657 if (isActionEnabled(a)) 1658 a.state = ACTION_STATE_ENABLED; 1659 else 1660 a.state = ACTION_STATE_DISABLE; 1661 return true; 1662 default: 1663 return super.handleActionStateRequest(a); 1664 } 1665 } 1666 1667 override protected bool handleAction(const Action a) { 1668 TextPosition oldCaretPos = _caretPos; 1669 dstring currentLine = _content[_caretPos.line]; 1670 switch (a.id) with(EditorActions) 1671 { 1672 case Left: 1673 case SelectLeft: 1674 correctCaretPos(); 1675 if (_caretPos.pos > 0) { 1676 _caretPos.pos--; 1677 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1678 ensureCaretVisible(); 1679 } else if (_caretPos.line > 0) { 1680 _caretPos = _content.lineEnd(_caretPos.line - 1); 1681 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1682 ensureCaretVisible(); 1683 } 1684 return true; 1685 case Right: 1686 case SelectRight: 1687 correctCaretPos(); 1688 if (_caretPos.pos < currentLine.length) { 1689 _caretPos.pos++; 1690 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1691 ensureCaretVisible(); 1692 } else if (_caretPos.line < _content.length - 1 && _content.multiline) { 1693 _caretPos.pos = 0; 1694 _caretPos.line++; 1695 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1696 ensureCaretVisible(); 1697 } 1698 return true; 1699 case WordLeft: 1700 case SelectWordLeft: 1701 { 1702 TextPosition newpos = _content.moveByWord(_caretPos, -1, _camelCasePartsAsWords); 1703 if (newpos != _caretPos) { 1704 _caretPos = newpos; 1705 updateSelectionAfterCursorMovement(oldCaretPos, a.id == EditorActions.SelectWordLeft); 1706 ensureCaretVisible(); 1707 } 1708 } 1709 return true; 1710 case WordRight: 1711 case SelectWordRight: 1712 { 1713 TextPosition newpos = _content.moveByWord(_caretPos, 1, _camelCasePartsAsWords); 1714 if (newpos != _caretPos) { 1715 _caretPos = newpos; 1716 updateSelectionAfterCursorMovement(oldCaretPos, a.id == EditorActions.SelectWordRight); 1717 ensureCaretVisible(); 1718 } 1719 } 1720 return true; 1721 case DocumentBegin: 1722 case SelectDocumentBegin: 1723 if (_caretPos.pos > 0 || _caretPos.line > 0) { 1724 _caretPos.line = 0; 1725 _caretPos.pos = 0; 1726 ensureCaretVisible(); 1727 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1728 } 1729 return true; 1730 case LineBegin: 1731 case SelectLineBegin: 1732 auto space = _content.getLineWhiteSpace(_caretPos.line); 1733 if (_caretPos.pos > 0) { 1734 if (_caretPos.pos > space.firstNonSpaceIndex && space.firstNonSpaceIndex > 0) 1735 _caretPos.pos = space.firstNonSpaceIndex; 1736 else 1737 _caretPos.pos = 0; 1738 ensureCaretVisible(); 1739 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1740 } else { 1741 // caret pos is 0 1742 if (space.firstNonSpaceIndex > 0) 1743 _caretPos.pos = space.firstNonSpaceIndex; 1744 ensureCaretVisible(); 1745 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1746 if (a.id == EditorActions.LineBegin && _caretPos == oldCaretPos) { 1747 clearSelection(); 1748 } 1749 } 1750 return true; 1751 case DocumentEnd: 1752 case SelectDocumentEnd: 1753 if (_caretPos.line < _content.length - 1 || _caretPos.pos < _content[_content.length - 1].length) { 1754 _caretPos.line = _content.length - 1; 1755 _caretPos.pos = cast(int)_content[_content.length - 1].length; 1756 ensureCaretVisible(); 1757 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1758 } 1759 return true; 1760 case LineEnd: 1761 case SelectLineEnd: 1762 if (_caretPos.pos < currentLine.length) { 1763 _caretPos.pos = cast(int)currentLine.length; 1764 ensureCaretVisible(); 1765 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1766 } else if (a.id == EditorActions.LineEnd) { 1767 clearSelection(); 1768 } 1769 return true; 1770 case DelPrevWord: 1771 if (readOnly) 1772 return true; 1773 correctCaretPos(); 1774 if (removeSelectionTextIfSelected()) // clear selection 1775 return true; 1776 TextPosition newpos = _content.moveByWord(_caretPos, -1, _camelCasePartsAsWords); 1777 if (newpos < _caretPos) 1778 removeRangeText(TextRange(newpos, _caretPos)); 1779 return true; 1780 case DelNextWord: 1781 if (readOnly) 1782 return true; 1783 correctCaretPos(); 1784 if (removeSelectionTextIfSelected()) // clear selection 1785 return true; 1786 TextPosition newpos = _content.moveByWord(_caretPos, 1, _camelCasePartsAsWords); 1787 if (newpos > _caretPos) 1788 removeRangeText(TextRange(_caretPos, newpos)); 1789 return true; 1790 case DelPrevChar: 1791 if (readOnly) 1792 return true; 1793 correctCaretPos(); 1794 if (removeSelectionTextIfSelected()) // clear selection 1795 return true; 1796 if (_caretPos.pos > 0) { 1797 // delete prev char in current line 1798 TextRange range = TextRange(_caretPos, _caretPos); 1799 range.start.pos--; 1800 removeRangeText(range); 1801 } else if (_caretPos.line > 0) { 1802 // merge with previous line 1803 TextRange range = TextRange(_caretPos, _caretPos); 1804 range.start = _content.lineEnd(range.start.line - 1); 1805 removeRangeText(range); 1806 } 1807 return true; 1808 case DelNextChar: 1809 if (readOnly) 1810 return true; 1811 correctCaretPos(); 1812 if (removeSelectionTextIfSelected()) // clear selection 1813 return true; 1814 if (_caretPos.pos < currentLine.length) { 1815 // delete char in current line 1816 TextRange range = TextRange(_caretPos, _caretPos); 1817 range.end.pos++; 1818 removeRangeText(range); 1819 } else if (_caretPos.line < _content.length - 1) { 1820 // merge with next line 1821 TextRange range = TextRange(_caretPos, _caretPos); 1822 range.end.line++; 1823 range.end.pos = 0; 1824 removeRangeText(range); 1825 } 1826 return true; 1827 case Copy: 1828 case Cut: 1829 TextRange range = _selectionRange; 1830 if (range.empty && _copyCurrentLineWhenNoSelection) { 1831 range = currentLineRange; 1832 } 1833 if (!range.empty) { 1834 dstring selectionText = getRangeText(range); 1835 platform.setClipboardText(selectionText); 1836 if (!readOnly && a.id == Cut) { 1837 EditOperation op = new EditOperation(EditAction.Replace, range, [""d]); 1838 _content.performOperation(op, this); 1839 } 1840 } 1841 return true; 1842 case Paste: 1843 { 1844 if (readOnly) 1845 return true; 1846 dstring selectionText = platform.getClipboardText(); 1847 dstring[] lines; 1848 if (_content.multiline) { 1849 lines = splitDString(selectionText); 1850 } else { 1851 lines = [replaceEolsWithSpaces(selectionText)]; 1852 } 1853 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, lines); 1854 _content.performOperation(op, this); 1855 } 1856 return true; 1857 case Undo: 1858 { 1859 if (readOnly) 1860 return true; 1861 _content.undo(this); 1862 } 1863 return true; 1864 case Redo: 1865 { 1866 if (readOnly) 1867 return true; 1868 _content.redo(this); 1869 } 1870 return true; 1871 case Indent: 1872 indentRange(false); 1873 return true; 1874 case Unindent: 1875 indentRange(true); 1876 return true; 1877 case Tab: 1878 { 1879 if (readOnly) 1880 return true; 1881 if (_selectionRange.empty) { 1882 if (useSpacesForTabs) { 1883 // insert one or more spaces to 1884 EditOperation op = new EditOperation(EditAction.Replace, TextRange(_caretPos, _caretPos), [spacesForTab(_caretPos.pos)]); 1885 _content.performOperation(op, this); 1886 } else { 1887 // just insert tab character 1888 EditOperation op = new EditOperation(EditAction.Replace, TextRange(_caretPos, _caretPos), ["\t"d]); 1889 _content.performOperation(op, this); 1890 } 1891 } else { 1892 if (multipleLinesSelected()) { 1893 // indent range 1894 return handleAction(new Action(EditorActions.Indent)); 1895 } else { 1896 // insert tab 1897 if (useSpacesForTabs) { 1898 // insert one or more spaces to 1899 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [spacesForTab(_selectionRange.start.pos)]); 1900 _content.performOperation(op, this); 1901 } else { 1902 // just insert tab character 1903 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, ["\t"d]); 1904 _content.performOperation(op, this); 1905 } 1906 } 1907 1908 } 1909 } 1910 return true; 1911 case BackTab: 1912 { 1913 if (readOnly) 1914 return true; 1915 if (_selectionRange.empty) { 1916 // remove spaces before caret 1917 TextRange r = spaceBefore(_caretPos); 1918 if (!r.empty) { 1919 EditOperation op = new EditOperation(EditAction.Replace, r, [""d]); 1920 _content.performOperation(op, this); 1921 } 1922 } else { 1923 if (multipleLinesSelected()) { 1924 // unindent range 1925 return handleAction(new Action(EditorActions.Unindent)); 1926 } else { 1927 // remove space before selection 1928 TextRange r = spaceBefore(_selectionRange.start); 1929 if (!r.empty) { 1930 int nchars = r.end.pos - r.start.pos; 1931 TextRange saveRange = _selectionRange; 1932 TextPosition saveCursor = _caretPos; 1933 EditOperation op = new EditOperation(EditAction.Replace, r, [""d]); 1934 _content.performOperation(op, this); 1935 if (saveCursor.line == saveRange.start.line) 1936 saveCursor.pos -= nchars; 1937 if (saveRange.end.line == saveRange.start.line) 1938 saveRange.end.pos -= nchars; 1939 saveRange.start.pos -= nchars; 1940 _selectionRange = saveRange; 1941 _caretPos = saveCursor; 1942 ensureCaretVisible(); 1943 } 1944 } 1945 } 1946 } 1947 return true; 1948 case ToggleReplaceMode: 1949 replaceMode = !replaceMode; 1950 return true; 1951 case SelectAll: 1952 selectAll(); 1953 ensureCaretVisible(); 1954 return true; 1955 case ToggleBookmark: 1956 if (_content.multiline) { 1957 int line = a.longParam >= 0 ? cast(int)a.longParam : _caretPos.line; 1958 _content.lineIcons.toggleBookmark(line); 1959 return true; 1960 } 1961 return false; 1962 case GoToNextBookmark: 1963 case GoToPreviousBookmark: 1964 if (_content.multiline) { 1965 LineIcon mark = _content.lineIcons.findNext(LineIconType.bookmark, _selectionRange.end.line, a.id == EditorActions.GoToNextBookmark ? 1 : -1); 1966 if (mark) { 1967 setCaretPos(mark.line, 0, true); 1968 return true; 1969 } 1970 } 1971 return false; 1972 default: 1973 break; 1974 } 1975 return super.handleAction(a); 1976 } 1977 1978 /// Select whole text 1979 void selectAll() { 1980 _selectionRange.start.line = 0; 1981 _selectionRange.start.pos = 0; 1982 _selectionRange.end = _content.lineEnd(_content.length - 1); 1983 _caretPos = _selectionRange.end; 1984 requestActionsUpdate(); 1985 } 1986 1987 protected TextRange spaceBefore(TextPosition pos) { 1988 TextRange res = TextRange(pos, pos); 1989 dstring s = _content[pos.line]; 1990 int x = 0; 1991 int start = -1; 1992 for (int i = 0; i < pos.pos; i++) { 1993 dchar ch = s[i]; 1994 if (ch == ' ') { 1995 if (start == -1 || (x % tabSize) == 0) 1996 start = i; 1997 x++; 1998 } else if (ch == '\t') { 1999 if (start == -1 || (x % tabSize) == 0) 2000 start = i; 2001 x = (x + tabSize + 1) / tabSize * tabSize; 2002 } else { 2003 x++; 2004 start = -1; 2005 } 2006 } 2007 if (start != -1) { 2008 res.start.pos = start; 2009 } 2010 return res; 2011 } 2012 2013 /// change line indent 2014 protected dstring indentLine(dstring src, bool back, TextPosition * cursorPos) { 2015 int firstNonSpace = -1; 2016 int x = 0; 2017 int unindentPos = -1; 2018 int cursor = cursorPos ? cursorPos.pos : 0; 2019 for (int i = 0; i < src.length; i++) { 2020 dchar ch = src[i]; 2021 if (ch == ' ') { 2022 x++; 2023 } else if (ch == '\t') { 2024 x = (x + tabSize + 1) / tabSize * tabSize; 2025 } else { 2026 firstNonSpace = i; 2027 break; 2028 } 2029 if (x <= tabSize) 2030 unindentPos = i + 1; 2031 } 2032 if (firstNonSpace == -1) // only spaces or empty line -- do not change it 2033 return src; 2034 if (back) { 2035 // unindent 2036 if (unindentPos == -1) 2037 return src; // no change 2038 if (unindentPos == src.length) { 2039 if (cursorPos) 2040 cursorPos.pos = 0; 2041 return ""d; 2042 } 2043 if (cursor >= unindentPos) 2044 cursorPos.pos -= unindentPos; 2045 return src[unindentPos .. $].dup; 2046 } else { 2047 // indent 2048 if (useSpacesForTabs) { 2049 if (cursor > 0) 2050 cursorPos.pos += tabSize; 2051 return spacesForTab(0) ~ src; 2052 } else { 2053 if (cursor > 0) 2054 cursorPos.pos++; 2055 return "\t"d ~ src; 2056 } 2057 } 2058 } 2059 2060 /// indent / unindent range 2061 protected void indentRange(bool back) { 2062 TextRange r = _selectionRange; 2063 r.start.pos = 0; 2064 if (r.end.pos > 0) 2065 r.end = _content.lineBegin(r.end.line + 1); 2066 if (r.end.line <= r.start.line) 2067 r = TextRange(_content.lineBegin(_caretPos.line), _content.lineBegin(_caretPos.line + 1)); 2068 int lineCount = r.end.line - r.start.line; 2069 if (r.end.pos > 0) 2070 lineCount++; 2071 dstring[] newContent = new dstring[lineCount + 1]; 2072 bool changed = false; 2073 for (int i = 0; i < lineCount; i++) { 2074 dstring srcline = _content.line(r.start.line + i); 2075 dstring dstline = indentLine(srcline, back, r.start.line + i == _caretPos.line ? &_caretPos : null); 2076 newContent[i] = dstline; 2077 if (dstline.length != srcline.length) 2078 changed = true; 2079 } 2080 if (changed) { 2081 TextRange saveRange = r; 2082 TextPosition saveCursor = _caretPos; 2083 EditOperation op = new EditOperation(EditAction.Replace, r, newContent); 2084 _content.performOperation(op, this); 2085 _selectionRange = saveRange; 2086 _caretPos = saveCursor; 2087 ensureCaretVisible(); 2088 } 2089 } 2090 2091 /// map key to action 2092 override protected Action findKeyAction(uint keyCode, uint flags) { 2093 // don't handle tabs when disabled 2094 if (keyCode == KeyCode.TAB && (flags == 0 || flags == KeyFlag.Shift) && (!_wantTabs || readOnly)) 2095 return null; 2096 return super.findKeyAction(keyCode, flags); 2097 } 2098 2099 static bool isAZaz(dchar ch) { 2100 return (ch >= 'a' && ch <='z') || (ch >= 'A' && ch <='Z'); 2101 } 2102 2103 /// handle keys 2104 override bool onKeyEvent(KeyEvent event) { 2105 //Log.d("onKeyEvent ", event.action, " ", event.keyCode, " flags ", event.flags); 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 super.onKeyEvent(event); 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 return true; 2207 } 2208 if (event.action == MouseAction.Move && event.flags == 0) { 2209 // hover 2210 if (focused && !insideLeftPane) { 2211 onHover(event.pos); 2212 } else { 2213 cancelHoverTimer(); 2214 } 2215 return true; 2216 } 2217 if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) { 2218 cancelHoverTimer(); 2219 return true; 2220 } 2221 if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) { 2222 cancelHoverTimer(); 2223 return true; 2224 } 2225 if (event.action == MouseAction.FocusIn) { 2226 cancelHoverTimer(); 2227 return true; 2228 } 2229 if (event.action == MouseAction.Wheel) { 2230 cancelHoverTimer(); 2231 uint keyFlags = event.flags & (MouseFlag.Shift | MouseFlag.Control | MouseFlag.Alt); 2232 if (event.wheelDelta < 0) { 2233 if (keyFlags == MouseFlag.Shift) 2234 return handleAction(new Action(EditorActions.ScrollRight)); 2235 if (keyFlags == MouseFlag.Control) 2236 return handleAction(new Action(EditorActions.ZoomOut)); 2237 return handleAction(new Action(EditorActions.ScrollLineDown)); 2238 } else if (event.wheelDelta > 0) { 2239 if (keyFlags == MouseFlag.Shift) 2240 return handleAction(new Action(EditorActions.ScrollLeft)); 2241 if (keyFlags == MouseFlag.Control) 2242 return handleAction(new Action(EditorActions.ZoomIn)); 2243 return handleAction(new Action(EditorActions.ScrollLineUp)); 2244 } 2245 } 2246 cancelHoverTimer(); 2247 return super.onMouseEvent(event); 2248 } 2249 2250 /// returns caret position 2251 @property TextPosition caretPos() { 2252 return _caretPos; 2253 } 2254 2255 /// change caret position and ensure it is visible 2256 void setCaretPos(int line, int column, bool makeVisible = true, bool center = false) 2257 { 2258 _caretPos = TextPosition(line,column); 2259 correctCaretPos(); 2260 invalidate(); 2261 if (makeVisible) 2262 ensureCaretVisible(center); 2263 handleEditorStateChange(); 2264 } 2265 } 2266 2267 interface EditorActionHandler { 2268 bool onEditorAction(const Action action); 2269 } 2270 2271 interface EnterKeyHandler { 2272 bool onEnterKey(EditWidgetBase editor); 2273 } 2274 2275 /// single line editor 2276 class EditLine : EditWidgetBase { 2277 2278 Signal!EditorActionHandler editorAction; 2279 /// handle Enter key press inside line editor 2280 Signal!EnterKeyHandler enterKey; 2281 2282 /// empty parameter list constructor - for usage by factory 2283 this() { 2284 this(null); 2285 } 2286 /// create with ID parameter 2287 this(string ID, dstring initialContent = null) { 2288 super(ID, ScrollBarMode.Invisible, ScrollBarMode.Invisible); 2289 _content = new EditableContent(false); 2290 _content.contentChanged = this; 2291 _selectAllWhenFocusedWithTab = true; 2292 _deselectAllWhenUnfocused = true; 2293 wantTabs = false; 2294 styleId = STYLE_EDIT_LINE; 2295 text = initialContent; 2296 onThemeChanged(); 2297 } 2298 2299 /// sets default popup menu with copy/paste/cut/undo/redo 2300 EditLine setDefaultPopupMenu() { 2301 MenuItem items = new MenuItem(); 2302 items.add(ACTION_EDITOR_COPY, ACTION_EDITOR_PASTE, ACTION_EDITOR_CUT, 2303 ACTION_EDITOR_UNDO, ACTION_EDITOR_REDO); 2304 popupMenu = items; 2305 return this; 2306 } 2307 2308 protected dstring _measuredText; 2309 protected int[] _measuredTextWidths; 2310 protected Point _measuredTextSize; 2311 2312 protected Point _measuredTextToSetWidgetSize; 2313 protected dstring _textToSetWidgetSize = "aaaaa"d; 2314 2315 @property void textToSetWidgetSize(dstring newText) { 2316 _textToSetWidgetSize = newText; 2317 requestLayout(); 2318 } 2319 2320 @property dstring textToSetWidgetSize() { 2321 return _textToSetWidgetSize; 2322 } 2323 2324 protected int[] _measuredTextToSetWidgetSizeWidths; 2325 2326 protected dchar _passwordChar = 0; 2327 /// password character - 0 for normal editor, some character, e.g. '*' to hide text by replacing all characters with this char 2328 @property dchar passwordChar() { return _passwordChar; } 2329 @property EditLine passwordChar(dchar ch) { 2330 if (_passwordChar != ch) { 2331 _passwordChar = ch; 2332 requestLayout(); 2333 } 2334 return this; 2335 } 2336 2337 override protected Rect textPosToClient(TextPosition p) { 2338 Rect res; 2339 res.bottom = _clientRect.height; 2340 if (p.pos == 0) 2341 res.left = 0; 2342 else if (p.pos >= _measuredText.length) 2343 res.left = _measuredTextSize.x; 2344 else 2345 res.left = _measuredTextWidths[p.pos - 1]; 2346 res.left -= _scrollPos.x; 2347 res.right = res.left + 1; 2348 return res; 2349 } 2350 2351 override protected TextPosition clientToTextPos(Point pt) { 2352 pt.x += _scrollPos.x; 2353 TextPosition res; 2354 for (int i = 0; i < _measuredText.length; i++) { 2355 int x0 = i > 0 ? _measuredTextWidths[i - 1] : 0; 2356 int x1 = _measuredTextWidths[i]; 2357 int mx = (x0 + x1) >> 1; 2358 if (pt.x <= mx) { 2359 res.pos = i; 2360 return res; 2361 } 2362 } 2363 res.pos = cast(int)_measuredText.length; 2364 return res; 2365 } 2366 2367 override protected void ensureCaretVisible(bool center = false) { 2368 //_scrollPos 2369 Rect rc = textPosToClient(_caretPos); 2370 if (rc.left < 0) { 2371 // scroll left 2372 _scrollPos.x -= -rc.left + _clientRect.width / 10; 2373 if (_scrollPos.x < 0) 2374 _scrollPos.x = 0; 2375 invalidate(); 2376 } else if (rc.left >= _clientRect.width - 10) { 2377 // scroll right 2378 _scrollPos.x += (rc.left - _clientRect.width) + _spaceWidth * 4; 2379 invalidate(); 2380 } 2381 updateScrollBars(); 2382 handleEditorStateChange(); 2383 } 2384 2385 protected dstring applyPasswordChar(dstring s) { 2386 if (!_passwordChar || s.length == 0) 2387 return s; 2388 dchar[] ss = s.dup; 2389 foreach(ref ch; ss) 2390 ch = _passwordChar; 2391 return cast(dstring)ss; 2392 } 2393 2394 override protected Point measureVisibleText() { 2395 FontRef font = font(); 2396 //Point sz = font.textSize(text); 2397 _measuredText = applyPasswordChar(text); 2398 _measuredTextWidths.length = _measuredText.length; 2399 int charsMeasured = font.measureText(_measuredText, _measuredTextWidths, MAX_WIDTH_UNSPECIFIED, tabSize); 2400 _measuredTextSize.x = charsMeasured > 0 ? _measuredTextWidths[charsMeasured - 1]: 0; 2401 _measuredTextSize.y = font.height; 2402 return _measuredTextSize; 2403 } 2404 2405 protected Point measureTextToSetWidgetSize() { 2406 FontRef font = font(); 2407 _measuredTextToSetWidgetSizeWidths.length = _textToSetWidgetSize.length; 2408 int charsMeasured = font.measureText(_textToSetWidgetSize, _measuredTextToSetWidgetSizeWidths, MAX_WIDTH_UNSPECIFIED, tabSize); 2409 _measuredTextToSetWidgetSize.x = charsMeasured > 0 ? _measuredTextToSetWidgetSizeWidths[charsMeasured - 1]: 0; 2410 _measuredTextToSetWidgetSize.y = font.height; 2411 return _measuredTextToSetWidgetSize; 2412 } 2413 2414 /// measure 2415 override void measure(int parentWidth, int parentHeight) { 2416 if (visibility == Visibility.Gone) 2417 return; 2418 2419 updateFontProps(); 2420 measureVisibleText(); 2421 measureTextToSetWidgetSize(); 2422 measuredContent(parentWidth, parentHeight, _measuredTextToSetWidgetSize.x + _leftPaneWidth, _measuredTextToSetWidgetSize.y); 2423 } 2424 2425 override bool handleAction(const Action a) { 2426 switch (a.id) with(EditorActions) 2427 { 2428 case InsertNewLine: 2429 case PrependNewLine: 2430 case AppendNewLine: 2431 if (editorAction.assigned) { 2432 return editorAction(a); 2433 } 2434 break; 2435 case Up: 2436 break; 2437 case Down: 2438 break; 2439 case PageUp: 2440 break; 2441 case PageDown: 2442 break; 2443 default: 2444 break; 2445 } 2446 return super.handleAction(a); 2447 } 2448 2449 2450 /// handle keys 2451 override bool onKeyEvent(KeyEvent event) { 2452 if (enterKey.assigned) { 2453 if (event.keyCode == KeyCode.RETURN && event.modifiers == 0) { 2454 if (event.action == KeyAction.KeyDown) 2455 return true; 2456 if (event.action == KeyAction.KeyUp) { 2457 if (enterKey(this)) 2458 return true; 2459 } 2460 } 2461 } 2462 return super.onKeyEvent(event); 2463 } 2464 2465 /// process mouse event; return true if event is processed by widget. 2466 override bool onMouseEvent(MouseEvent event) { 2467 return super.onMouseEvent(event); 2468 } 2469 2470 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 2471 override void layout(Rect rc) { 2472 if (visibility == Visibility.Gone) { 2473 return; 2474 } 2475 _needLayout = false; 2476 Point sz = Point(rc.width, measuredHeight); 2477 applyAlign(rc, sz); 2478 _pos = rc; 2479 _clientRect = rc; 2480 applyMargins(_clientRect); 2481 applyPadding(_clientRect); 2482 if (_contentChanged) { 2483 measureVisibleText(); 2484 _contentChanged = false; 2485 } 2486 } 2487 2488 2489 /// override to custom highlight of line background 2490 protected void drawLineBackground(DrawBuf buf, Rect lineRect, Rect visibleRect) { 2491 if (!_selectionRange.empty) { 2492 // line inside selection 2493 Rect startrc = textPosToClient(_selectionRange.start); 2494 Rect endrc = textPosToClient(_selectionRange.end); 2495 Rect rc = lineRect; 2496 rc.left = startrc.left + _clientRect.left; 2497 rc.right = endrc.left + _clientRect.left; 2498 if (!rc.empty) { 2499 // draw selection rect for line 2500 buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal); 2501 } 2502 if (_leftPaneWidth > 0) { 2503 Rect leftPaneRect = visibleRect; 2504 leftPaneRect.right = leftPaneRect.left; 2505 leftPaneRect.left -= _leftPaneWidth; 2506 drawLeftPane(buf, leftPaneRect, 0); 2507 } 2508 } 2509 } 2510 2511 /// draw content 2512 override void onDraw(DrawBuf buf) { 2513 if (visibility != Visibility.Visible) 2514 return; 2515 super.onDraw(buf); 2516 Rect rc = _pos; 2517 applyMargins(rc); 2518 applyPadding(rc); 2519 auto saver = ClipRectSaver(buf, rc, alpha); 2520 2521 FontRef font = font(); 2522 dstring txt = applyPasswordChar(text); 2523 2524 drawLineBackground(buf, _clientRect, _clientRect); 2525 font.drawText(buf, rc.left - _scrollPos.x, rc.top, txt, textColor, tabSize); 2526 2527 drawCaret(buf); 2528 } 2529 } 2530 2531 // SpinCtrl 2532 private { 2533 import std.ascii; 2534 } 2535 2536 class SpinCtrl : HorizontalLayout { 2537 2538 TextWidget label; 2539 int min, max; 2540 2541 private EditLine linEdit; 2542 private Button butUp, butDown; 2543 2544 2545 @property int value() { return linEdit.text.to!int; } 2546 @property void value(int val) { 2547 linEdit.text = val.to!dstring; 2548 } 2549 2550 override @property bool enabled() { return linEdit.enabled; } 2551 alias enabled = Widget.enabled; 2552 @property void enabled(bool status) { 2553 linEdit.enabled = status; 2554 butUp.enabled = status; 2555 butDown.enabled = status; 2556 } 2557 2558 this(int min, int max, int initialVal = 0, dstring labelText = null){ 2559 this.min = min; 2560 this.max = max; 2561 2562 if(labelText !is null){ 2563 label = new TextWidget("label", labelText); 2564 addChild(label); 2565 } 2566 2567 linEdit = new class EditLine { 2568 this(){super("linEdit", "0"d);} 2569 override bool onKeyEvent(KeyEvent event) { 2570 if (( KeyAction.Text == event.action && event.text[0].isDigit) 2571 || event.keyCode == KeyCode.BACK 2572 || event.keyCode == KeyCode.DEL 2573 || event.keyCode == KeyCode.LEFT 2574 || event.keyCode == KeyCode.RIGHT 2575 || event.keyCode == KeyCode.TAB 2576 ){ 2577 return super.onKeyEvent(event); 2578 } 2579 return false; 2580 } 2581 2582 override bool onMouseEvent(MouseEvent event) { 2583 if(enabled && event.action == MouseAction.Wheel){ 2584 if((event.wheelDelta == 1) && (value < max)) 2585 value = value + event.wheelDelta; 2586 if((event.wheelDelta == -1) && (value > min)) 2587 value = value + event.wheelDelta; 2588 return true; 2589 } 2590 return super.onMouseEvent(event); 2591 } 2592 }; 2593 2594 linEdit.addOnFocusChangeListener((w, t){ 2595 if(linEdit.text == "") 2596 linEdit.text = "0"; 2597 if(linEdit.text.to!int > max) 2598 value = max; 2599 if(linEdit.text.to!int < min) 2600 value = min; 2601 return true; 2602 }); 2603 2604 linEdit.minHeight = 35; 2605 if(initialVal != 0) 2606 value = initialVal; 2607 addChild(linEdit); 2608 2609 2610 auto butContainer = new VerticalLayout(); 2611 butContainer.maxHeight = linEdit.minHeight; 2612 2613 butUp = new Button("butUp", "+"d); 2614 butUp.margins(Rect(1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels)); 2615 2616 butDown = new Button("butDown", "-"d); 2617 butDown.margins(Rect(1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels)); 2618 2619 butContainer.addChild(butUp); 2620 butContainer.addChild(butDown); 2621 2622 addChild(butContainer); 2623 2624 butUp.click = delegate(Widget w) { 2625 immutable val = linEdit.text.to!int; 2626 if(val < max ) 2627 linEdit.text = (val + 1).to!dstring; 2628 return true; 2629 }; 2630 2631 butDown.click = delegate(Widget w) { 2632 immutable val = linEdit.text.to!int; 2633 if(val > min ) 2634 linEdit.text = (val - 1).to!dstring; 2635 return true; 2636 }; 2637 2638 enabled = true; 2639 } 2640 2641 } 2642 2643 /// multiline editor 2644 class EditBox : EditWidgetBase { 2645 /// empty parameter list constructor - for usage by factory 2646 this() { 2647 this(null); 2648 } 2649 /// create with ID parameter 2650 this(string ID, dstring initialContent = null, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) { 2651 super(ID, hscrollbarMode, vscrollbarMode); 2652 _content = new EditableContent(true); // multiline 2653 _content.contentChanged = this; 2654 styleId = STYLE_EDIT_BOX; 2655 text = initialContent; 2656 acceleratorMap.add( [ 2657 // zoom 2658 new Action(EditorActions.ZoomIn, KeyCode.ADD, KeyFlag.Control), 2659 new Action(EditorActions.ZoomOut, KeyCode.SUB, KeyFlag.Control), 2660 ]); 2661 onThemeChanged(); 2662 } 2663 2664 ~this() { 2665 if (_findPanel) { 2666 destroy(_findPanel); 2667 _findPanel = null; 2668 } 2669 } 2670 2671 protected int _firstVisibleLine; 2672 2673 protected int _maxLineWidth; 2674 protected int _numVisibleLines; // number of lines visible in client area 2675 protected dstring[] _visibleLines; // text for visible lines 2676 protected int[][] _visibleLinesMeasurement; // char positions for visible lines 2677 protected int[] _visibleLinesWidths; // width (in pixels) of visible lines 2678 protected CustomCharProps[][] _visibleLinesHighlights; 2679 protected CustomCharProps[][] _visibleLinesHighlightsBuf; 2680 2681 protected Point _measuredTextToSetWidgetSize; 2682 protected dstring _textToSetWidgetSize = "aaaaa/naaaaa"d; 2683 protected int[] _measuredTextToSetWidgetSizeWidths; 2684 2685 /// Set _needRewrap to true; 2686 override void wordWrapRefresh() 2687 { 2688 _needRewrap = true; 2689 } 2690 2691 override @property int fontSize() const { return super.fontSize(); } 2692 override @property Widget fontSize(int size) { 2693 // Need to rewrap if fontSize changed 2694 _needRewrap = true; 2695 return super.fontSize(size); 2696 } 2697 2698 override protected int lineCount() { 2699 return _content.length; 2700 } 2701 2702 override protected void updateMaxLineWidth() { 2703 // find max line width. TODO: optimize!!! 2704 int maxw; 2705 int[] buf; 2706 for (int i = 0; i < _content.length; i++) { 2707 dstring s = _content[i]; 2708 int w = calcLineWidth(s); 2709 if (maxw < w) 2710 maxw = w; 2711 } 2712 _maxLineWidth = maxw; 2713 } 2714 2715 @property int minFontSize() { 2716 return _minFontSize; 2717 } 2718 @property EditBox minFontSize(int size) { 2719 _minFontSize = size; 2720 return this; 2721 } 2722 2723 @property int maxFontSize() { 2724 return _maxFontSize; 2725 } 2726 2727 @property EditBox maxFontSize(int size) { 2728 _maxFontSize = size; 2729 return this; 2730 } 2731 2732 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 2733 override void layout(Rect rc) { 2734 if (visibility == Visibility.Gone) 2735 return; 2736 2737 if (rc != _pos) 2738 _contentChanged = true; 2739 Rect contentRc = rc; 2740 int findPanelHeight; 2741 if (_findPanel && _findPanel.visibility != Visibility.Gone) { 2742 _findPanel.measure(rc.width, rc.height); 2743 findPanelHeight = _findPanel.measuredHeight; 2744 _findPanel.layout(Rect(rc.left, rc.bottom - findPanelHeight, rc.right, rc.bottom)); 2745 contentRc.bottom -= findPanelHeight; 2746 } 2747 2748 super.layout(contentRc); 2749 if (_contentChanged) { 2750 measureVisibleText(); 2751 _needRewrap = true; 2752 _contentChanged = false; 2753 } 2754 2755 _pos = rc; 2756 } 2757 2758 override protected Point measureVisibleText() { 2759 Point sz; 2760 FontRef font = font(); 2761 _lineHeight = font.height; 2762 _numVisibleLines = (_clientRect.height + _lineHeight - 1) / _lineHeight; 2763 if (_firstVisibleLine >= _content.length) { 2764 _firstVisibleLine = _content.length - _numVisibleLines + 1; 2765 if (_firstVisibleLine < 0) 2766 _firstVisibleLine = 0; 2767 _caretPos.line = _content.length - 1; 2768 _caretPos.pos = 0; 2769 } 2770 if (_numVisibleLines < 1) 2771 _numVisibleLines = 1; 2772 if (_firstVisibleLine + _numVisibleLines > _content.length) 2773 _numVisibleLines = _content.length - _firstVisibleLine; 2774 if (_numVisibleLines < 1) 2775 _numVisibleLines = 1; 2776 _visibleLines.length = _numVisibleLines; 2777 if (_visibleLinesMeasurement.length < _numVisibleLines) 2778 _visibleLinesMeasurement.length = _numVisibleLines; 2779 if (_visibleLinesWidths.length < _numVisibleLines) 2780 _visibleLinesWidths.length = _numVisibleLines; 2781 if (_visibleLinesHighlights.length < _numVisibleLines) { 2782 _visibleLinesHighlights.length = _numVisibleLines; 2783 _visibleLinesHighlightsBuf.length = _numVisibleLines; 2784 } 2785 for (int i = 0; i < _numVisibleLines; i++) { 2786 _visibleLines[i] = _content[_firstVisibleLine + i]; 2787 size_t len = _visibleLines[i].length; 2788 if (_visibleLinesMeasurement[i].length < len) 2789 _visibleLinesMeasurement[i].length = len; 2790 if (_visibleLinesHighlightsBuf[i].length < len) 2791 _visibleLinesHighlightsBuf[i].length = len; 2792 _visibleLinesHighlights[i] = handleCustomLineHighlight(_firstVisibleLine + i, _visibleLines[i], _visibleLinesHighlightsBuf[i]); 2793 int charsMeasured = font.measureText(_visibleLines[i], _visibleLinesMeasurement[i], int.max, tabSize); 2794 _visibleLinesWidths[i] = charsMeasured > 0 ? _visibleLinesMeasurement[i][charsMeasured - 1] : 0; 2795 if (sz.x < _visibleLinesWidths[i]) 2796 sz.x = _visibleLinesWidths[i]; // width - max from visible lines 2797 } 2798 sz.x = _maxLineWidth; 2799 sz.y = _lineHeight * _content.length; // height - for all lines 2800 return sz; 2801 } 2802 2803 protected bool _extendRightScrollBound = true; 2804 /// override to determine if scrollbars are needed or not 2805 override protected void checkIfScrollbarsNeeded(ref bool needHScroll, ref bool needVScroll) { 2806 needHScroll = _hscrollbar && (_hscrollbarMode == ScrollBarMode.Visible || _hscrollbarMode == ScrollBarMode.Auto); 2807 needVScroll = _vscrollbar && (_vscrollbarMode == ScrollBarMode.Visible || _vscrollbarMode == ScrollBarMode.Auto); 2808 if (!needHScroll && !needVScroll) 2809 return; // not needed 2810 if (_hscrollbarMode != ScrollBarMode.Auto && _vscrollbarMode != ScrollBarMode.Auto) 2811 return; // no auto scrollbars 2812 // either h or v scrollbar is in auto mode 2813 2814 int hsbHeight = _hscrollbar.measuredHeight; 2815 int vsbWidth = _hscrollbar.measuredWidth; 2816 2817 int visibleLines = _lineHeight > 0 ? (_clientRect.height / _lineHeight) : 1; // fully visible lines 2818 if (visibleLines < 1) 2819 visibleLines = 1; 2820 int visibleLinesWithScrollbar = _lineHeight > 0 ? ((_clientRect.height - hsbHeight) / _lineHeight) : 1; // fully visible lines 2821 if (visibleLinesWithScrollbar < 1) 2822 visibleLinesWithScrollbar = 1; 2823 2824 // either h or v scrollbar is in auto mode 2825 //Point contentSize = fullContentSize(); 2826 int contentWidth = _maxLineWidth + (_extendRightScrollBound ? _clientRect.width / 16 : 0); 2827 int contentHeight = _content.length; 2828 2829 int clientWidth = _clientRect.width; 2830 int clientHeight = visibleLines; 2831 2832 int clientWidthWithScrollbar = clientWidth - vsbWidth; 2833 int clientHeightWithScrollbar = visibleLinesWithScrollbar; 2834 2835 if (_hscrollbarMode == ScrollBarMode.Auto && _vscrollbarMode == ScrollBarMode.Auto) { 2836 // both scrollbars in auto mode 2837 bool xFits = contentWidth <= clientWidth; 2838 bool yFits = contentHeight <= clientHeight; 2839 if (!xFits && !yFits) { 2840 // none fits, need both scrollbars 2841 } else if (xFits && yFits) { 2842 // everything fits! 2843 needHScroll = false; 2844 needVScroll = false; 2845 } else if (xFits) { 2846 // only X fits 2847 if (contentWidth <= clientWidthWithScrollbar) 2848 needHScroll = false; // disable hscroll 2849 } else { // yFits 2850 // only Y fits 2851 if (contentHeight <= clientHeightWithScrollbar) 2852 needVScroll = false; // disable vscroll 2853 } 2854 } else if (_hscrollbarMode == ScrollBarMode.Auto) { 2855 // only hscroll is in auto mode 2856 if (needVScroll) 2857 clientWidth = clientWidthWithScrollbar; 2858 needHScroll = contentWidth > clientWidth; 2859 } else { 2860 // only vscroll is in auto mode 2861 if (needHScroll) 2862 clientHeight = clientHeightWithScrollbar; 2863 needVScroll = contentHeight > clientHeight; 2864 } 2865 } 2866 2867 /// update horizontal scrollbar widget position 2868 override protected void updateHScrollBar() { 2869 _hscrollbar.setRange(0, _maxLineWidth + (_extendRightScrollBound ? _clientRect.width / 16 : 0)); 2870 _hscrollbar.pageSize = _clientRect.width; 2871 _hscrollbar.position = _scrollPos.x; 2872 } 2873 2874 /// update verticat scrollbar widget position 2875 override protected void updateVScrollBar() { 2876 int visibleLines = _lineHeight ? _clientRect.height / _lineHeight : 1; // fully visible lines 2877 if (visibleLines < 1) 2878 visibleLines = 1; 2879 _vscrollbar.setRange(0, _content.length); 2880 _vscrollbar.pageSize = visibleLines; 2881 _vscrollbar.position = _firstVisibleLine; 2882 } 2883 2884 /// process horizontal scrollbar event 2885 override bool onHScroll(ScrollEvent event) { 2886 if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) { 2887 if (_scrollPos.x != event.position) { 2888 _scrollPos.x = event.position; 2889 invalidate(); 2890 } 2891 } else if (event.action == ScrollAction.PageUp) { 2892 dispatchAction(new Action(EditorActions.ScrollLeft)); 2893 } else if (event.action == ScrollAction.PageDown) { 2894 dispatchAction(new Action(EditorActions.ScrollRight)); 2895 } else if (event.action == ScrollAction.LineUp) { 2896 dispatchAction(new Action(EditorActions.ScrollLeft)); 2897 } else if (event.action == ScrollAction.LineDown) { 2898 dispatchAction(new Action(EditorActions.ScrollRight)); 2899 } 2900 return true; 2901 } 2902 2903 /// process vertical scrollbar event 2904 override bool onVScroll(ScrollEvent event) { 2905 if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) { 2906 if (_firstVisibleLine != event.position) { 2907 _firstVisibleLine = event.position; 2908 measureVisibleText(); 2909 invalidate(); 2910 } 2911 } else if (event.action == ScrollAction.PageUp) { 2912 dispatchAction(new Action(EditorActions.ScrollPageUp)); 2913 } else if (event.action == ScrollAction.PageDown) { 2914 dispatchAction(new Action(EditorActions.ScrollPageDown)); 2915 } else if (event.action == ScrollAction.LineUp) { 2916 dispatchAction(new Action(EditorActions.ScrollLineUp)); 2917 } else if (event.action == ScrollAction.LineDown) { 2918 dispatchAction(new Action(EditorActions.ScrollLineDown)); 2919 } 2920 return true; 2921 } 2922 2923 protected bool _enableScrollAfterText = true; 2924 override protected void ensureCaretVisible(bool center = false) { 2925 if (_caretPos.line >= _content.length) 2926 _caretPos.line = _content.length - 1; 2927 if (_caretPos.line < 0) 2928 _caretPos.line = 0; 2929 int visibleLines = _lineHeight > 0 ? _clientRect.height / _lineHeight : 1; // fully visible lines 2930 if (visibleLines < 1) 2931 visibleLines = 1; 2932 int maxFirstVisibleLine = _content.length - 1; 2933 if (!_enableScrollAfterText) 2934 maxFirstVisibleLine = _content.length - visibleLines; 2935 if (maxFirstVisibleLine < 0) 2936 maxFirstVisibleLine = 0; 2937 2938 if (_caretPos.line < _firstVisibleLine) { 2939 _firstVisibleLine = _caretPos.line; 2940 if (center) { 2941 _firstVisibleLine -= visibleLines / 2; 2942 if (_firstVisibleLine < 0) 2943 _firstVisibleLine = 0; 2944 } 2945 if (_firstVisibleLine > maxFirstVisibleLine) 2946 _firstVisibleLine = maxFirstVisibleLine; 2947 measureVisibleText(); 2948 invalidate(); 2949 } else if(_wordWrap && !(_firstVisibleLine > maxFirstVisibleLine)) { 2950 //For wordwrap mode, move down sooner 2951 int offsetLines = -1 * caretHeightOffset / _lineHeight; 2952 //Log.d("offsetLines: ", offsetLines); 2953 if (_caretPos.line >= _firstVisibleLine + visibleLines - offsetLines) 2954 { 2955 _firstVisibleLine = _caretPos.line - visibleLines + 1 + offsetLines; 2956 if (center) 2957 _firstVisibleLine += visibleLines / 2; 2958 if (_firstVisibleLine > maxFirstVisibleLine) 2959 _firstVisibleLine = maxFirstVisibleLine; 2960 if (_firstVisibleLine < 0) 2961 _firstVisibleLine = 0; 2962 measureVisibleText(); 2963 invalidate(); 2964 } 2965 } else if (_caretPos.line >= _firstVisibleLine + visibleLines) { 2966 _firstVisibleLine = _caretPos.line - visibleLines + 1; 2967 if (center) 2968 _firstVisibleLine += visibleLines / 2; 2969 if (_firstVisibleLine > maxFirstVisibleLine) 2970 _firstVisibleLine = maxFirstVisibleLine; 2971 if (_firstVisibleLine < 0) 2972 _firstVisibleLine = 0; 2973 measureVisibleText(); 2974 invalidate(); 2975 } else if (_firstVisibleLine > maxFirstVisibleLine) { 2976 _firstVisibleLine = maxFirstVisibleLine; 2977 if (_firstVisibleLine < 0) 2978 _firstVisibleLine = 0; 2979 measureVisibleText(); 2980 invalidate(); 2981 } 2982 //_scrollPos 2983 Rect rc = textPosToClient(_caretPos); 2984 if (rc.left < 0) { 2985 // scroll left 2986 _scrollPos.x -= -rc.left + _clientRect.width / 4; 2987 if (_scrollPos.x < 0) 2988 _scrollPos.x = 0; 2989 invalidate(); 2990 } else if (rc.left >= _clientRect.width - 10) { 2991 // scroll right 2992 if (!_wordWrap) 2993 _scrollPos.x += (rc.left - _clientRect.width) + _clientRect.width / 4; 2994 invalidate(); 2995 } 2996 updateScrollBars(); 2997 handleEditorStateChange(); 2998 } 2999 3000 override protected Rect textPosToClient(TextPosition p) { 3001 Rect res; 3002 int lineIndex = p.line - _firstVisibleLine; 3003 res.top = lineIndex * _lineHeight; 3004 res.bottom = res.top + _lineHeight; 3005 // if visible 3006 if (lineIndex >= 0 && lineIndex < _visibleLines.length) { 3007 if (p.pos == 0) 3008 res.left = 0; 3009 else if (p.pos >= _visibleLinesMeasurement[lineIndex].length) 3010 res.left = _visibleLinesWidths[lineIndex]; 3011 else 3012 res.left = _visibleLinesMeasurement[lineIndex][p.pos - 1]; 3013 } 3014 res.left -= _scrollPos.x; 3015 res.right = res.left + 1; 3016 return res; 3017 } 3018 3019 override protected TextPosition clientToTextPos(Point pt) { 3020 TextPosition res; 3021 pt.x += _scrollPos.x; 3022 int lineIndex = pt.y / _lineHeight; 3023 if (lineIndex < 0) 3024 lineIndex = 0; 3025 if (lineIndex < _visibleLines.length) { 3026 res.line = lineIndex + _firstVisibleLine; 3027 int len = cast(int)_visibleLines[lineIndex].length; 3028 for (int i = 0; i < len; i++) { 3029 int x0 = i > 0 ? _visibleLinesMeasurement[lineIndex][i - 1] : 0; 3030 int x1 = _visibleLinesMeasurement[lineIndex][i]; 3031 int mx = (x0 + x1) >> 1; 3032 if (pt.x <= mx) { 3033 res.pos = i; 3034 return res; 3035 } 3036 } 3037 res.pos = cast(int)_visibleLines[lineIndex].length; 3038 } else if (_visibleLines.length > 0) { 3039 res.line = _firstVisibleLine + cast(int)_visibleLines.length - 1; 3040 res.pos = cast(int)_visibleLines[$ - 1].length; 3041 } else { 3042 res.line = 0; 3043 res.pos = 0; 3044 } 3045 return res; 3046 } 3047 3048 override protected bool handleAction(const Action a) { 3049 TextPosition oldCaretPos = _caretPos; 3050 dstring currentLine = _content[_caretPos.line]; 3051 switch (a.id) with(EditorActions) 3052 { 3053 case PrependNewLine: 3054 if (!readOnly) { 3055 correctCaretPos(); 3056 _caretPos.pos = 0; 3057 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]); 3058 _content.performOperation(op, this); 3059 } 3060 return true; 3061 case InsertNewLine: 3062 if (!readOnly) { 3063 correctCaretPos(); 3064 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]); 3065 _content.performOperation(op, this); 3066 } 3067 return true; 3068 case Up: 3069 case SelectUp: 3070 if ((_caretPos.line > 0) | wordWrap) { 3071 if (_wordWrap) 3072 { 3073 LineSpan curSpan = getSpan(_caretPos.line); 3074 int curWrap = findWrapLine(_caretPos); 3075 if (curWrap > 0) 3076 { 3077 _caretPos.pos-= curSpan.wrapPoints[curWrap - 1].wrapPos; 3078 } 3079 else 3080 { 3081 int previousPos = _caretPos.pos; 3082 curSpan = getSpan(_caretPos.line - 1); 3083 curWrap = curSpan.len - 1; 3084 if (curWrap > 0) 3085 { 3086 int accumulativePoint = curSpan.accumulation(curSpan.len - 1, LineSpan.WrapPointInfo.Position); 3087 _caretPos.line--; 3088 _caretPos.pos = accumulativePoint + previousPos; 3089 } 3090 else 3091 { 3092 _caretPos.line--; 3093 } 3094 } 3095 } 3096 else if(_caretPos.line > 0) 3097 _caretPos.line--; 3098 correctCaretPos(); 3099 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3100 ensureCaretVisible(); 3101 } 3102 return true; 3103 case Down: 3104 case SelectDown: 3105 if (_caretPos.line < _content.length - 1) { 3106 if (_wordWrap) 3107 { 3108 LineSpan curSpan = getSpan(_caretPos.line); 3109 int curWrap = findWrapLine(_caretPos); 3110 if (curWrap < curSpan.len - 1) 3111 { 3112 int previousPos = _caretPos.pos; 3113 _caretPos.pos+= curSpan.wrapPoints[curWrap].wrapPos; 3114 correctCaretPos(); 3115 if (_caretPos.pos == previousPos) 3116 { 3117 _caretPos.pos = 0; 3118 _caretPos.line++; 3119 } 3120 } 3121 else if (curSpan.len > 1) 3122 { 3123 int previousPos = _caretPos.pos; 3124 int previousAccumulatedPosition = curSpan.accumulation(curSpan.len - 1, LineSpan.WrapPointInfo.Position); 3125 _caretPos.line++; 3126 _caretPos.pos = previousPos - previousAccumulatedPosition; 3127 } 3128 else 3129 { 3130 _caretPos.line++; 3131 } 3132 } 3133 else 3134 { 3135 _caretPos.line++; 3136 } 3137 correctCaretPos(); 3138 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3139 ensureCaretVisible(); 3140 } 3141 return true; 3142 case PageBegin: 3143 case SelectPageBegin: 3144 { 3145 ensureCaretVisible(); 3146 _caretPos.line = _firstVisibleLine; 3147 correctCaretPos(); 3148 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3149 } 3150 return true; 3151 case PageEnd: 3152 case SelectPageEnd: 3153 { 3154 ensureCaretVisible(); 3155 int fullLines = _clientRect.height / _lineHeight; 3156 int newpos = _firstVisibleLine + fullLines - 1; 3157 if (newpos >= _content.length) 3158 newpos = _content.length - 1; 3159 _caretPos.line = newpos; 3160 correctCaretPos(); 3161 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3162 } 3163 return true; 3164 case PageUp: 3165 case SelectPageUp: 3166 { 3167 ensureCaretVisible(); 3168 int fullLines = _clientRect.height / _lineHeight; 3169 int newpos = _firstVisibleLine - fullLines; 3170 if (newpos < 0) { 3171 _firstVisibleLine = 0; 3172 _caretPos.line = 0; 3173 } else { 3174 int delta = _firstVisibleLine - newpos; 3175 _firstVisibleLine = newpos; 3176 _caretPos.line -= delta; 3177 } 3178 correctCaretPos(); 3179 measureVisibleText(); 3180 updateScrollBars(); 3181 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3182 } 3183 return true; 3184 case PageDown: 3185 case SelectPageDown: 3186 { 3187 ensureCaretVisible(); 3188 int fullLines = _clientRect.height / _lineHeight; 3189 int newpos = _firstVisibleLine + fullLines; 3190 if (newpos >= _content.length) { 3191 _caretPos.line = _content.length - 1; 3192 } else { 3193 int delta = newpos - _firstVisibleLine; 3194 _firstVisibleLine = newpos; 3195 _caretPos.line += delta; 3196 } 3197 correctCaretPos(); 3198 measureVisibleText(); 3199 updateScrollBars(); 3200 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3201 } 3202 return true; 3203 case ScrollLeft: 3204 { 3205 if (_scrollPos.x > 0) { 3206 int newpos = _scrollPos.x - _spaceWidth * 4; 3207 if (newpos < 0) 3208 newpos = 0; 3209 _scrollPos.x = newpos; 3210 updateScrollBars(); 3211 invalidate(); 3212 } 3213 } 3214 return true; 3215 case ScrollRight: 3216 { 3217 if (_scrollPos.x < _maxLineWidth - _clientRect.width) { 3218 int newpos = _scrollPos.x + _spaceWidth * 4; 3219 if (newpos > _maxLineWidth - _clientRect.width) 3220 newpos = _maxLineWidth - _clientRect.width; 3221 _scrollPos.x = newpos; 3222 updateScrollBars(); 3223 invalidate(); 3224 } 3225 } 3226 return true; 3227 case ScrollLineUp: 3228 { 3229 if (_firstVisibleLine > 0) { 3230 _firstVisibleLine -= 3; 3231 if (_firstVisibleLine < 0) 3232 _firstVisibleLine = 0; 3233 measureVisibleText(); 3234 updateScrollBars(); 3235 invalidate(); 3236 } 3237 } 3238 return true; 3239 case ScrollPageUp: 3240 { 3241 int fullLines = _clientRect.height / _lineHeight; 3242 if (_firstVisibleLine > 0) { 3243 _firstVisibleLine -= fullLines * 3 / 4; 3244 if (_firstVisibleLine < 0) 3245 _firstVisibleLine = 0; 3246 measureVisibleText(); 3247 updateScrollBars(); 3248 invalidate(); 3249 } 3250 } 3251 return true; 3252 case ScrollLineDown: 3253 { 3254 int fullLines = _clientRect.height / _lineHeight; 3255 if (_firstVisibleLine + fullLines < _content.length) { 3256 _firstVisibleLine += 3; 3257 if (_firstVisibleLine > _content.length - fullLines) 3258 _firstVisibleLine = _content.length - fullLines; 3259 if (_firstVisibleLine < 0) 3260 _firstVisibleLine = 0; 3261 measureVisibleText(); 3262 updateScrollBars(); 3263 invalidate(); 3264 } 3265 } 3266 return true; 3267 case ScrollPageDown: 3268 { 3269 int fullLines = _clientRect.height / _lineHeight; 3270 if (_firstVisibleLine + fullLines < _content.length) { 3271 _firstVisibleLine += fullLines * 3 / 4; 3272 if (_firstVisibleLine > _content.length - fullLines) 3273 _firstVisibleLine = _content.length - fullLines; 3274 if (_firstVisibleLine < 0) 3275 _firstVisibleLine = 0; 3276 measureVisibleText(); 3277 updateScrollBars(); 3278 invalidate(); 3279 } 3280 } 3281 return true; 3282 case ZoomOut: 3283 case ZoomIn: 3284 { 3285 int dir = a.id == ZoomIn ? 1 : -1; 3286 if (_minFontSize < _maxFontSize && _minFontSize > 0 && _maxFontSize > 0) { 3287 int currentFontSize = fontSize; 3288 int increment = currentFontSize >= 30 ? 2 : 1; 3289 int newFontSize = currentFontSize + increment * dir; //* 110 / 100; 3290 if (newFontSize > 30) 3291 newFontSize &= 0xFFFE; 3292 if (currentFontSize != newFontSize && newFontSize <= _maxFontSize && newFontSize >= _minFontSize) { 3293 Log.i("Font size in editor ", id, " zoomed to ", newFontSize); 3294 fontSize = cast(ushort)newFontSize; 3295 updateFontProps(); 3296 _needRewrap = true; 3297 measureVisibleText(); 3298 updateScrollBars(); 3299 invalidate(); 3300 } 3301 } 3302 } 3303 return true; 3304 case ToggleBlockComment: 3305 if (!readOnly && _content.syntaxSupport && _content.syntaxSupport.supportsToggleBlockComment && _content.syntaxSupport.canToggleBlockComment(_selectionRange)) 3306 _content.syntaxSupport.toggleBlockComment(_selectionRange, this); 3307 return true; 3308 case ToggleLineComment: 3309 if (!readOnly && _content.syntaxSupport && _content.syntaxSupport.supportsToggleLineComment && _content.syntaxSupport.canToggleLineComment(_selectionRange)) 3310 _content.syntaxSupport.toggleLineComment(_selectionRange, this); 3311 return true; 3312 case AppendNewLine: 3313 if (!readOnly) { 3314 correctCaretPos(); 3315 TextPosition p = _content.lineEnd(_caretPos.line); 3316 TextRange r = TextRange(p, p); 3317 EditOperation op = new EditOperation(EditAction.Replace, r, [""d, ""d]); 3318 _content.performOperation(op, this); 3319 _caretPos = oldCaretPos; 3320 handleEditorStateChange(); 3321 } 3322 return true; 3323 case DeleteLine: 3324 if (!readOnly) { 3325 correctCaretPos(); 3326 EditOperation op = new EditOperation(EditAction.Replace, _content.lineRange(_caretPos.line), [""d]); 3327 _content.performOperation(op, this); 3328 } 3329 return true; 3330 case Find: 3331 openFindPanel(); 3332 return true; 3333 case FindNext: 3334 findNext(false); 3335 return true; 3336 case FindPrev: 3337 findNext(true); 3338 return true; 3339 case Replace: 3340 openReplacePanel(); 3341 return true; 3342 default: 3343 break; 3344 } 3345 return super.handleAction(a); 3346 } 3347 3348 /// calculate full content size in pixels 3349 override Point fullContentSize() { 3350 Point textSz; 3351 textSz.y = _lineHeight * _content.length; 3352 textSz.x = _maxLineWidth; 3353 //int maxy = _lineHeight * 5; // limit measured height 3354 //if (textSz.y > maxy) 3355 // textSz.y = maxy; 3356 return textSz; 3357 } 3358 3359 // override to set minimum scrollwidget size - default 100x100 3360 override protected Point minimumVisibleContentSize() { 3361 FontRef font = font(); 3362 _measuredTextToSetWidgetSizeWidths.length = _textToSetWidgetSize.length; 3363 int charsMeasured = font.measureText(_textToSetWidgetSize, _measuredTextToSetWidgetSizeWidths, MAX_WIDTH_UNSPECIFIED, tabSize); 3364 _measuredTextToSetWidgetSize.x = charsMeasured > 0 ? _measuredTextToSetWidgetSizeWidths[charsMeasured - 1]: 0; 3365 _measuredTextToSetWidgetSize.y = font.height; 3366 return _measuredTextToSetWidgetSize; 3367 } 3368 3369 /// measure 3370 override void measure(int parentWidth, int parentHeight) { 3371 if (visibility == Visibility.Gone) 3372 return; 3373 3374 updateFontProps(); 3375 updateMaxLineWidth(); 3376 int findPanelHeight; 3377 if (_findPanel) { 3378 _findPanel.measure(parentWidth, parentHeight); 3379 findPanelHeight = _findPanel.measuredHeight; 3380 if (parentHeight != SIZE_UNSPECIFIED) 3381 parentHeight -= findPanelHeight; 3382 } 3383 3384 super.measure(parentWidth, parentHeight); 3385 } 3386 3387 3388 protected void highlightTextPattern(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) { 3389 dstring pattern = _textToHighlight; 3390 uint options = _textToHighlightOptions; 3391 if (!pattern.length) { 3392 // support highlighting selection text - if whole word is selected 3393 if (_selectionRange.empty || !_selectionRange.singleLine) 3394 return; 3395 if (_selectionRange.start.line >= _content.length) 3396 return; 3397 dstring selLine = _content.line(_selectionRange.start.line); 3398 int start = _selectionRange.start.pos; 3399 int end = _selectionRange.end.pos; 3400 if (start >= selLine.length) 3401 return; 3402 pattern = selLine[start .. end]; 3403 if (!isWordChar(pattern[0]) || !isWordChar(pattern[$-1])) 3404 return; 3405 if (!isWholeWord(selLine, start, end)) 3406 return; 3407 // whole word is selected - enable highlight for it 3408 options = TextSearchFlag.CaseSensitive | TextSearchFlag.WholeWords; 3409 } 3410 if (!pattern.length) 3411 return; 3412 dstring lineText = _content.line(lineIndex); 3413 if (lineText.length < pattern.length) 3414 return; 3415 ptrdiff_t start = 0; 3416 import std.string : indexOf, CaseSensitive; 3417 import std.typecons : Flag; 3418 bool caseSensitive = (options & TextSearchFlag.CaseSensitive) != 0; 3419 bool wholeWords = (options & TextSearchFlag.WholeWords) != 0; 3420 bool selectionOnly = (options & TextSearchFlag.SelectionOnly) != 0; 3421 for (;;) { 3422 ptrdiff_t pos = lineText[start .. $].indexOf(pattern, caseSensitive ? Yes.caseSensitive : No.caseSensitive); 3423 if (pos < 0) 3424 break; 3425 // found text to highlight 3426 start += pos; 3427 if (!wholeWords || isWholeWord(lineText, start, start + pattern.length)) { 3428 TextRange r = TextRange(TextPosition(lineIndex, cast(int)start), TextPosition(lineIndex, cast(int)(start + pattern.length))); 3429 uint color = r.isInsideOrNext(caretPos) ? _searchHighlightColorCurrent : _searchHighlightColorOther; 3430 highlightLineRange(buf, lineRect, color, r); 3431 } 3432 start += pattern.length; 3433 } 3434 } 3435 3436 static bool isWordChar(dchar ch) { 3437 if (ch >= 'a' && ch <= 'z') 3438 return true; 3439 if (ch >= 'A' && ch <= 'Z') 3440 return true; 3441 if (ch == '_') 3442 return true; 3443 return false; 3444 } 3445 static bool isValidWordBound(dchar innerChar, dchar outerChar) { 3446 return !isWordChar(innerChar) || !isWordChar(outerChar); 3447 } 3448 /// returns true if selected range of string is whole word 3449 static bool isWholeWord(dstring lineText, size_t start, size_t end) { 3450 if (start >= lineText.length || start >= end) 3451 return false; 3452 if (start > 0 && !isValidWordBound(lineText[start], lineText[start - 1])) 3453 return false; 3454 if (end > 0 && end < lineText.length && !isValidWordBound(lineText[end - 1], lineText[end])) 3455 return false; 3456 return true; 3457 } 3458 3459 /// find all occurences of text pattern in content; options = bitset of TextSearchFlag 3460 TextRange[] findAll(dstring pattern, uint options) { 3461 TextRange[] res; 3462 res.assumeSafeAppend(); 3463 if (!pattern.length) 3464 return res; 3465 import std.string : indexOf, CaseSensitive; 3466 bool caseSensitive = (options & TextSearchFlag.CaseSensitive) != 0; 3467 bool wholeWords = (options & TextSearchFlag.WholeWords) != 0; 3468 bool selectionOnly = (options & TextSearchFlag.SelectionOnly) != 0; 3469 for (int i = 0; i < _content.length; i++) { 3470 dstring lineText = _content.line(i); 3471 if (lineText.length < pattern.length) 3472 continue; 3473 ptrdiff_t start = 0; 3474 for (;;) { 3475 ptrdiff_t pos = lineText[start .. $].indexOf(pattern, caseSensitive ? Yes.caseSensitive : No.caseSensitive); 3476 if (pos < 0) 3477 break; 3478 // found text to highlight 3479 start += pos; 3480 if (!wholeWords || isWholeWord(lineText, start, start + pattern.length)) { 3481 TextRange r = TextRange(TextPosition(i, cast(int)start), TextPosition(i, cast(int)(start + pattern.length))); 3482 res ~= r; 3483 } 3484 start += _textToHighlight.length; 3485 } 3486 } 3487 return res; 3488 } 3489 3490 /// find next occurence of text pattern in content, returns true if found 3491 bool findNextPattern(ref TextPosition pos, dstring pattern, uint searchOptions, int direction) { 3492 TextRange[] all = findAll(pattern, searchOptions); 3493 if (!all.length) 3494 return false; 3495 int currentIndex = -1; 3496 int nearestIndex = cast(int)all.length; 3497 for (int i = 0; i < all.length; i++) { 3498 if (all[i].isInsideOrNext(pos)) { 3499 currentIndex = i; 3500 break; 3501 } 3502 } 3503 for (int i = 0; i < all.length; i++) { 3504 if (pos < all[i].start) { 3505 nearestIndex = i; 3506 break; 3507 } 3508 if (pos > all[i].end) { 3509 nearestIndex = i + 1; 3510 } 3511 } 3512 if (currentIndex >= 0) { 3513 if (all.length < 2 && direction != 0) 3514 return false; 3515 currentIndex += direction; 3516 if (currentIndex < 0) 3517 currentIndex = cast(int)all.length - 1; 3518 else if (currentIndex >= all.length) 3519 currentIndex = 0; 3520 pos = all[currentIndex].start; 3521 return true; 3522 } 3523 if (direction < 0) 3524 nearestIndex--; 3525 if (nearestIndex < 0) 3526 nearestIndex = cast(int)all.length - 1; 3527 else if (nearestIndex >= all.length) 3528 nearestIndex = 0; 3529 pos = all[nearestIndex].start; 3530 return true; 3531 } 3532 3533 protected void highlightLineRange(DrawBuf buf, Rect lineRect, uint color, TextRange r) { 3534 Rect startrc = textPosToClient(r.start); 3535 Rect endrc = textPosToClient(r.end); 3536 Rect rc = lineRect; 3537 rc.left = _clientRect.left + startrc.left; 3538 rc.right = _clientRect.left + endrc.right; 3539 if (_wordWrap && !rc.empty) 3540 { 3541 wordWrapFillRect(buf, r.start.line, rc, color); 3542 } 3543 else if (!rc.empty) { 3544 // draw selection rect for matching bracket 3545 buf.fillRect(rc, color); 3546 } 3547 } 3548 3549 /// Used in place of directly calling buf.fillRect in word wrap mode 3550 void wordWrapFillRect(DrawBuf buf, int line, Rect lineToDivide, uint color) 3551 { 3552 Rect rc = lineToDivide; 3553 auto limitNumber = (int num, int limit) => num > limit ? limit : num; 3554 LineSpan curSpan = getSpan(line); 3555 int yOffset = _lineHeight * (wrapsUpTo(line)); 3556 rc.offset(0, yOffset); 3557 Rect[] wrappedSelection; 3558 wrappedSelection.length = curSpan.len; 3559 foreach (size_t i_, wrapLineRect; wrappedSelection) 3560 { 3561 int i = cast(int)i_; 3562 int startingDifference = rc.left - _clientRect.left; 3563 wrapLineRect = rc; 3564 wrapLineRect.offset(-1 * curSpan.accumulation(cast(int)i, LineSpan.WrapPointInfo.Width), cast(int)i * _lineHeight); 3565 wrapLineRect.right = limitNumber(wrapLineRect.right,(rc.left + curSpan.wrapPoints[i].wrapWidth) - startingDifference); 3566 buf.fillRect(wrapLineRect, color); 3567 } 3568 } 3569 3570 /// override to custom highlight of line background 3571 protected void drawLineBackground(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) { 3572 // highlight odd lines 3573 //if ((lineIndex & 1)) 3574 // buf.fillRect(visibleRect, 0xF4808080); 3575 3576 if (!_selectionRange.empty && _selectionRange.start.line <= lineIndex && _selectionRange.end.line >= lineIndex) { 3577 // line inside selection 3578 Rect startrc = textPosToClient(_selectionRange.start); 3579 Rect endrc = textPosToClient(_selectionRange.end); 3580 int startx = lineIndex == _selectionRange.start.line ? startrc.left + _clientRect.left : lineRect.left; 3581 int endx = lineIndex == _selectionRange.end.line ? endrc.left + _clientRect.left : lineRect.right + _spaceWidth; 3582 Rect rc = lineRect; 3583 rc.left = startx; 3584 rc.right = endx; 3585 if (!rc.empty && _wordWrap) 3586 { 3587 wordWrapFillRect(buf, lineIndex, rc, focused ? _selectionColorFocused : _selectionColorNormal); 3588 } 3589 else if (!rc.empty) { 3590 // draw selection rect for line 3591 buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal); 3592 } 3593 } 3594 3595 highlightTextPattern(buf, lineIndex, lineRect, visibleRect); 3596 3597 if (_matchingBraces.start.line == lineIndex) { 3598 TextRange r = TextRange(_matchingBraces.start, _matchingBraces.start.offset(1)); 3599 highlightLineRange(buf, lineRect, _matchingBracketHightlightColor, r); 3600 } 3601 if (_matchingBraces.end.line == lineIndex) { 3602 TextRange r = TextRange(_matchingBraces.end, _matchingBraces.end.offset(1)); 3603 highlightLineRange(buf, lineRect, _matchingBracketHightlightColor, r); 3604 } 3605 3606 // frame around current line 3607 if (focused && lineIndex == _caretPos.line && _selectionRange.singleLine && _selectionRange.start.line == _caretPos.line) { 3608 //TODO: Figure out why a little slow to catch up 3609 if (_wordWrap) 3610 visibleRect.offset(0, -caretHeightOffset); 3611 buf.drawFrame(visibleRect, 0xA0808080, Rect(1,1,1,1)); 3612 } 3613 3614 } 3615 3616 override protected void drawExtendedArea(DrawBuf buf) { 3617 if (_leftPaneWidth <= 0) 3618 return; 3619 Rect rc = _clientRect; 3620 3621 FontRef font = font(); 3622 int i = _firstVisibleLine; 3623 int lc = lineCount; 3624 for (;;) { 3625 Rect lineRect = rc; 3626 lineRect.left = _clientRect.left - _leftPaneWidth; 3627 lineRect.right = _clientRect.left; 3628 lineRect.bottom = lineRect.top + _lineHeight; 3629 if (lineRect.top >= _clientRect.bottom) 3630 break; 3631 drawLeftPane(buf, lineRect, i < lc ? i : -1); 3632 rc.top += _lineHeight; 3633 if (_wordWrap) 3634 { 3635 int currentWrap = 1; 3636 for (;;) 3637 { 3638 LineSpan curSpan = getSpan(i); 3639 if (currentWrap > curSpan.len - 1) 3640 break; 3641 Rect lineRect2 = rc; 3642 lineRect2.left = _clientRect.left - _leftPaneWidth; 3643 lineRect2.right = _clientRect.left; 3644 lineRect2.bottom = lineRect.top + _lineHeight; 3645 if (lineRect2.top >= _clientRect.bottom) 3646 break; 3647 drawLeftPane(buf, lineRect2, -1); 3648 rc.top += _lineHeight; 3649 3650 currentWrap++; 3651 } 3652 } 3653 i++; 3654 } 3655 } 3656 3657 3658 protected CustomCharProps[ubyte] _tokenHighlightColors; 3659 3660 /// set highlight options for particular token category 3661 void setTokenHightlightColor(ubyte tokenCategory, uint color, bool underline = false, bool strikeThrough = false) { 3662 _tokenHighlightColors[tokenCategory] = CustomCharProps(color, underline, strikeThrough); 3663 } 3664 /// clear highlight colors 3665 void clearTokenHightlightColors() { 3666 destroy(_tokenHighlightColors); 3667 } 3668 3669 /** 3670 Custom text color and style highlight (using text highlight) support. 3671 3672 Return null if no syntax highlight required for line. 3673 */ 3674 protected CustomCharProps[] handleCustomLineHighlight(int line, dstring txt, ref CustomCharProps[] buf) { 3675 if (!_tokenHighlightColors) 3676 return null; // no highlight colors set 3677 TokenPropString tokenProps = _content.lineTokenProps(line); 3678 if (tokenProps.length > 0) { 3679 bool hasNonzeroTokens = false; 3680 foreach(t; tokenProps) 3681 if (t) { 3682 hasNonzeroTokens = true; 3683 break; 3684 } 3685 if (!hasNonzeroTokens) 3686 return null; // all characters are of unknown token type (or white space) 3687 if (buf.length < tokenProps.length) 3688 buf.length = tokenProps.length; 3689 CustomCharProps[] colors = buf[0..tokenProps.length]; //new CustomCharProps[tokenProps.length]; 3690 for (int i = 0; i < tokenProps.length; i++) { 3691 ubyte p = tokenProps[i]; 3692 if (p in _tokenHighlightColors) 3693 colors[i] = _tokenHighlightColors[p]; 3694 else if ((p & TOKEN_CATEGORY_MASK) in _tokenHighlightColors) 3695 colors[i] = _tokenHighlightColors[(p & TOKEN_CATEGORY_MASK)]; 3696 else 3697 colors[i].color = textColor; 3698 if (isFullyTransparentColor(colors[i].color)) 3699 colors[i].color = textColor; 3700 } 3701 return colors; 3702 } 3703 return null; 3704 } 3705 3706 TextRange _matchingBraces; 3707 3708 bool _showWhiteSpaceMarks; 3709 /// when true, show marks for tabs and spaces at beginning and end of line, and tabs inside line 3710 @property bool showWhiteSpaceMarks() const { return _showWhiteSpaceMarks; } 3711 @property void showWhiteSpaceMarks(bool show) { 3712 if (_showWhiteSpaceMarks != show) { 3713 _showWhiteSpaceMarks = show; 3714 invalidate(); 3715 } 3716 } 3717 3718 /// find max tab mark column position for line 3719 protected int findMaxTabMarkColumn(int lineIndex) { 3720 if (lineIndex < 0 || lineIndex >= content.length) 3721 return -1; 3722 int maxSpace = -1; 3723 auto space = content.getLineWhiteSpace(lineIndex); 3724 maxSpace = space.firstNonSpaceColumn; 3725 if (maxSpace >= 0) 3726 return maxSpace; 3727 for(int i = lineIndex - 1; i >= 0; i--) { 3728 space = content.getLineWhiteSpace(i); 3729 if (!space.empty) { 3730 maxSpace = space.firstNonSpaceColumn; 3731 break; 3732 } 3733 } 3734 for(int i = lineIndex + 1; i < content.length; i++) { 3735 space = content.getLineWhiteSpace(i); 3736 if (!space.empty) { 3737 if (maxSpace < 0 || maxSpace < space.firstNonSpaceColumn) 3738 maxSpace = space.firstNonSpaceColumn; 3739 break; 3740 } 3741 } 3742 return maxSpace; 3743 } 3744 3745 void drawTabPositionMarks(DrawBuf buf, ref FontRef font, int lineIndex, Rect lineRect) { 3746 int maxCol = findMaxTabMarkColumn(lineIndex); 3747 if (maxCol > 0) { 3748 int spaceWidth = font.charWidth(' '); 3749 Rect rc = lineRect; 3750 uint color = addAlpha(textColor, 0xC0); 3751 for (int i = 0; i < maxCol; i += tabSize) { 3752 rc.left = lineRect.left + i * spaceWidth; 3753 rc.right = rc.left + 1; 3754 buf.fillRectPattern(rc, color, PatternType.dotted); 3755 } 3756 } 3757 } 3758 3759 void drawWhiteSpaceMarks(DrawBuf buf, ref FontRef font, dstring txt, int tabSize, Rect lineRect, Rect visibleRect) { 3760 // _showTabPositionMarks 3761 // _showWhiteSpaceMarks 3762 int firstNonSpace = -1; 3763 int lastNonSpace = -1; 3764 bool hasTabs = false; 3765 for(int i = 0; i < txt.length; i++) { 3766 if (txt[i] == '\t') { 3767 hasTabs = true; 3768 } else if (txt[i] != ' ') { 3769 if (firstNonSpace == -1) 3770 firstNonSpace = i; 3771 lastNonSpace = i + 1; 3772 } 3773 } 3774 bool spacesOnly = txt.length > 0 && firstNonSpace < 0; 3775 if (firstNonSpace <= 0 && lastNonSpace >= txt.length && !hasTabs && !spacesOnly) 3776 return; 3777 uint color = addAlpha(textColor, 0xC0); 3778 static int[] textSizeBuffer; 3779 int charsMeasured = font.measureText(txt, textSizeBuffer, MAX_WIDTH_UNSPECIFIED, tabSize, 0, 0); 3780 int ts = tabSize; 3781 if (ts < 1) 3782 ts = 1; 3783 if (ts > 8) 3784 ts = 8; 3785 int spaceIndex = 0; 3786 for (int i = 0; i < txt.length && i < charsMeasured; i++) { 3787 dchar ch = txt[i]; 3788 bool outsideText = (i < firstNonSpace || i >= lastNonSpace || spacesOnly); 3789 if ((ch == ' ' && outsideText) || ch == '\t') { 3790 Rect rc = lineRect; 3791 rc.left = lineRect.left + (i > 0 ? textSizeBuffer[i - 1] : 0); 3792 rc.right = lineRect.left + textSizeBuffer[i]; 3793 int h = rc.height; 3794 if (rc.intersects(visibleRect)) { 3795 // draw space mark 3796 if (ch == ' ') { 3797 // space 3798 int sz = h / 6; 3799 if (sz < 1) 3800 sz = 1; 3801 rc.top += h / 2 - sz / 2; 3802 rc.bottom = rc.top + sz; 3803 rc.left += rc.width / 2 - sz / 2; 3804 rc.right = rc.left + sz; 3805 buf.fillRect(rc, color); 3806 } else if (ch == '\t') { 3807 // tab 3808 Point p1 = Point(rc.left + 1, rc.top + h / 2); 3809 Point p2 = p1; 3810 p2.x = rc.right - 1; 3811 int sz = h / 4; 3812 if (sz < 2) 3813 sz = 2; 3814 if (sz > p2.x - p1.x) 3815 sz = p2.x - p1.x; 3816 buf.drawLine(p1, p2, color); 3817 buf.drawLine(p2, Point(p2.x - sz, p2.y - sz), color); 3818 buf.drawLine(p2, Point(p2.x - sz, p2.y + sz), color); 3819 } 3820 } 3821 } 3822 } 3823 } 3824 3825 /// Clear _span 3826 void resetVisibleSpans() 3827 { 3828 //TODO: Don't erase spans which have not been modified, cache them 3829 _span = []; 3830 } 3831 3832 private bool _needRewrap = true; 3833 private int lastStartingLine; 3834 3835 override protected void drawClient(DrawBuf buf) { 3836 // update matched braces 3837 if (!content.findMatchedBraces(_caretPos, _matchingBraces)) { 3838 _matchingBraces.start.line = -1; 3839 _matchingBraces.end.line = -1; 3840 } 3841 3842 Rect rc = _clientRect; 3843 3844 if (_contentChanged) 3845 _needRewrap = true; 3846 if (lastStartingLine != _firstVisibleLine) 3847 { 3848 _needRewrap = true; 3849 lastStartingLine = _firstVisibleLine; 3850 } 3851 if (rc.width <= 0 && _wordWrap) 3852 { 3853 //Prevent drawClient from getting stuck in loop 3854 return; 3855 } 3856 bool doRewrap = false; 3857 if (_needRewrap && _wordWrap) 3858 { 3859 resetVisibleSpans(); 3860 _needRewrap = false; 3861 doRewrap = true; 3862 } 3863 3864 FontRef font = font(); 3865 int previousWraps; 3866 for (int i = 0; i < _visibleLines.length; i++) { 3867 dstring txt = _visibleLines[i]; 3868 Rect lineRect; 3869 lineRect.left = _clientRect.left - _scrollPos.x; 3870 lineRect.right = lineRect.left + calcLineWidth(_content[_firstVisibleLine + i]); 3871 lineRect.top = _clientRect.top + i * _lineHeight; 3872 lineRect.bottom = lineRect.top + _lineHeight; 3873 Rect visibleRect = lineRect; 3874 visibleRect.left = _clientRect.left; 3875 visibleRect.right = _clientRect.right; 3876 drawLineBackground(buf, _firstVisibleLine + i, lineRect, visibleRect); 3877 if (_showTabPositionMarks) 3878 drawTabPositionMarks(buf, font, _firstVisibleLine + i, lineRect); 3879 if (!txt.length && !_wordWrap) 3880 continue; 3881 if (_showWhiteSpaceMarks) 3882 { 3883 Rect whiteSpaceRc = lineRect; 3884 Rect whiteSpaceRcVisible = visibleRect; 3885 for(int z; z < previousWraps; z++) 3886 { 3887 whiteSpaceRc.offset(0, _lineHeight); 3888 whiteSpaceRcVisible.offset(0, _lineHeight); 3889 } 3890 drawWhiteSpaceMarks(buf, font, txt, tabSize, whiteSpaceRc, whiteSpaceRcVisible); 3891 } 3892 if (_leftPaneWidth > 0) { 3893 Rect leftPaneRect = visibleRect; 3894 leftPaneRect.right = leftPaneRect.left; 3895 leftPaneRect.left -= _leftPaneWidth; 3896 drawLeftPane(buf, leftPaneRect, 0); 3897 } 3898 if (txt.length > 0 || _wordWrap) { 3899 CustomCharProps[] highlight = _visibleLinesHighlights[i]; 3900 if (_wordWrap) 3901 { 3902 dstring[] wrappedLine; 3903 if (doRewrap) 3904 wrappedLine = wrapLine(txt, _firstVisibleLine + i); 3905 else 3906 if (i < _span.length) 3907 wrappedLine = _span[i].wrappedContent; 3908 int accumulativeLength; 3909 CustomCharProps[] wrapProps; 3910 foreach (size_t q_, curWrap; wrappedLine) 3911 { 3912 int q = cast(int)q_; 3913 auto lineOffset = q + i + wrapsUpTo(i + _firstVisibleLine); 3914 if (highlight) 3915 { 3916 wrapProps = highlight[accumulativeLength .. $]; 3917 accumulativeLength += curWrap.length; 3918 font.drawColoredText(buf, rc.left - _scrollPos.x, rc.top + lineOffset * _lineHeight, curWrap, wrapProps, tabSize); 3919 } 3920 else 3921 font.drawText(buf, rc.left - _scrollPos.x, rc.top + lineOffset * _lineHeight, curWrap, textColor, tabSize); 3922 3923 } 3924 previousWraps += to!int(wrappedLine.length - 1); 3925 } 3926 else 3927 { 3928 if (highlight) 3929 font.drawColoredText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, highlight, tabSize); 3930 else 3931 font.drawText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, textColor, tabSize); 3932 } 3933 } 3934 } 3935 3936 drawCaret(buf); 3937 } 3938 3939 protected override bool onLeftPaneMouseClick(MouseEvent event) { 3940 if (_leftPaneWidth <= 0) 3941 return false; 3942 Rect rc = _clientRect; 3943 FontRef font = font(); 3944 int i = _firstVisibleLine; 3945 int lc = lineCount; 3946 for (;;) { 3947 Rect lineRect = rc; 3948 lineRect.left = _clientRect.left - _leftPaneWidth; 3949 lineRect.right = _clientRect.left; 3950 lineRect.bottom = lineRect.top + _lineHeight; 3951 if (lineRect.top >= _clientRect.bottom) 3952 break; 3953 if (event.y >= lineRect.top && event.y < lineRect.bottom) { 3954 return handleLeftPaneMouseClick(event, lineRect, i); 3955 } 3956 i++; 3957 rc.top += _lineHeight; 3958 } 3959 return false; 3960 } 3961 3962 override protected MenuItem getLeftPaneIconsPopupMenu(int line) { 3963 MenuItem menu = new MenuItem(); 3964 Action toggleBookmarkAction = ACTION_EDITOR_TOGGLE_BOOKMARK.clone(); 3965 toggleBookmarkAction.longParam = line; 3966 toggleBookmarkAction.objectParam = this; 3967 MenuItem item = menu.add(toggleBookmarkAction); 3968 return menu; 3969 } 3970 3971 protected FindPanel _findPanel; 3972 3973 dstring selectionText(bool singleLineOnly = false) { 3974 TextRange range = _selectionRange; 3975 if (range.empty) { 3976 return null; 3977 } 3978 dstring res = getRangeText(range); 3979 if (singleLineOnly) { 3980 for (int i = 0; i < res.length; i++) { 3981 if (res[i] == '\n') { 3982 res = res[0 .. i]; 3983 break; 3984 } 3985 } 3986 } 3987 return res; 3988 } 3989 3990 protected void findNext(bool backward) { 3991 createFindPanel(false, false); 3992 _findPanel.findNext(backward); 3993 // don't change replace mode 3994 } 3995 3996 protected void openFindPanel() { 3997 createFindPanel(false, false); 3998 _findPanel.replaceMode = false; 3999 _findPanel.activate(); 4000 } 4001 4002 protected void openReplacePanel() { 4003 createFindPanel(false, true); 4004 _findPanel.replaceMode = true; 4005 _findPanel.activate(); 4006 } 4007 4008 /// create find panel; returns true if panel was not yet visible 4009 protected bool createFindPanel(bool selectionOnly, bool replaceMode) { 4010 bool res = false; 4011 dstring txt = selectionText(true); 4012 if (!_findPanel) { 4013 _findPanel = new FindPanel(this, selectionOnly, replaceMode, txt); 4014 addChild(_findPanel); 4015 res = true; 4016 } else { 4017 if (_findPanel.visibility != Visibility.Visible) { 4018 _findPanel.visibility = Visibility.Visible; 4019 if (txt.length) 4020 _findPanel.searchText = txt; 4021 res = true; 4022 } 4023 } 4024 if (!pos.empty) 4025 layout(pos); 4026 requestLayout(); 4027 return res; 4028 } 4029 4030 /// close find panel 4031 protected void closeFindPanel(bool hideOnly = true) { 4032 if (_findPanel) { 4033 setFocus(); 4034 if (hideOnly) { 4035 _findPanel.visibility = Visibility.Gone; 4036 } else { 4037 removeChild(_findPanel); 4038 destroy(_findPanel); 4039 _findPanel = null; 4040 requestLayout(); 4041 } 4042 } 4043 } 4044 4045 /// Draw widget at its position to buffer 4046 override void onDraw(DrawBuf buf) { 4047 if (visibility != Visibility.Visible) 4048 return; 4049 super.onDraw(buf); 4050 if (_findPanel && _findPanel.visibility == Visibility.Visible) { 4051 _findPanel.onDraw(buf); 4052 } 4053 } 4054 } 4055 4056 /// Read only edit box for displaying logs with lines append operation 4057 class LogWidget : EditBox { 4058 4059 protected int _maxLines; 4060 /// max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit 4061 @property int maxLines() { return _maxLines; } 4062 /// set max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit 4063 @property void maxLines(int n) { _maxLines = n; } 4064 4065 protected bool _scrollLock; 4066 /// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction) 4067 @property bool scrollLock() { return _scrollLock; } 4068 /// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction) 4069 @property void scrollLock(bool flg) { _scrollLock = flg; } 4070 4071 this() { 4072 this(null); 4073 } 4074 4075 this(string ID) { 4076 super(ID); 4077 styleId = STYLE_LOG_WIDGET; 4078 _scrollLock = true; 4079 _enableScrollAfterText = false; 4080 enabled = false; 4081 minFontSize(pointsToPixels(6)).maxFontSize(pointsToPixels(32)); // allow font zoom with Ctrl + MouseWheel 4082 onThemeChanged(); 4083 } 4084 4085 /// append lines to the end of text 4086 void appendText(dstring text) { 4087 import std.array : split; 4088 if (text.length == 0) 4089 return; 4090 dstring[] lines = text.split("\n"); 4091 //lines ~= ""d; // append new line after last line 4092 content.appendLines(lines); 4093 if (_maxLines > 0 && lineCount > _maxLines) { 4094 TextRange range; 4095 range.end.line = lineCount - _maxLines; 4096 EditOperation op = new EditOperation(EditAction.Replace, range, [""d]); 4097 _content.performOperation(op, this); 4098 _contentChanged = true; 4099 } 4100 updateScrollBars(); 4101 if (_scrollLock) { 4102 _caretPos = lastLineBegin(); 4103 ensureCaretVisible(); 4104 } 4105 } 4106 4107 TextPosition lastLineBegin() { 4108 TextPosition res; 4109 if (_content.length == 0) 4110 return res; 4111 if (_content.lineLength(_content.length - 1) == 0 && _content.length > 1) 4112 res.line = _content.length - 2; 4113 else 4114 res.line = _content.length - 1; 4115 return res; 4116 } 4117 4118 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 4119 override void layout(Rect rc) { 4120 if (visibility == Visibility.Gone) 4121 return; 4122 4123 super.layout(rc); 4124 if (_scrollLock) { 4125 measureVisibleText(); 4126 _caretPos = lastLineBegin(); 4127 ensureCaretVisible(); 4128 } 4129 } 4130 4131 } 4132 4133 class FindPanel : HorizontalLayout { 4134 protected EditBox _editor; 4135 protected EditLine _edFind; 4136 protected EditLine _edReplace; 4137 protected ImageCheckButton _cbCaseSensitive; 4138 protected ImageCheckButton _cbWholeWords; 4139 protected CheckBox _cbSelection; 4140 protected Button _btnFindNext; 4141 protected Button _btnFindPrev; 4142 protected Button _btnReplace; 4143 protected Button _btnReplaceAndFind; 4144 protected Button _btnReplaceAll; 4145 protected ImageButton _btnClose; 4146 protected bool _replaceMode; 4147 /// returns true if panel is working in replace mode 4148 @property bool replaceMode() { return _replaceMode; } 4149 @property FindPanel replaceMode(bool newMode) { 4150 if (newMode != _replaceMode) { 4151 _replaceMode = newMode; 4152 childById("replace").visibility = newMode ? Visibility.Visible : Visibility.Gone; 4153 } 4154 return this; 4155 } 4156 4157 @property dstring searchText() { 4158 return _edFind.text; 4159 } 4160 4161 @property FindPanel searchText(dstring newText) { 4162 _edFind.text = newText; 4163 return this; 4164 } 4165 4166 this(EditBox editor, bool selectionOnly, bool replace, dstring initialText = ""d) { 4167 _replaceMode = replace; 4168 import dlangui.dml.parser; 4169 try { 4170 parseML(q{ 4171 { 4172 layoutWidth: fill 4173 VerticalLayout { 4174 layoutWidth: fill 4175 HorizontalLayout { 4176 layoutWidth: fill 4177 EditLine { id: edFind; layoutWidth: fill; alignment: vcenter } 4178 Button { id: btnFindNext; text: EDIT_FIND_NEXT } 4179 Button { id: btnFindPrev; text: EDIT_FIND_PREV } 4180 VerticalLayout { 4181 VSpacer {} 4182 HorizontalLayout { 4183 ImageCheckButton { id: cbCaseSensitive; drawableId: "find_case_sensitive"; tooltipText: EDIT_FIND_CASE_SENSITIVE; styleId: TOOLBAR_BUTTON; alignment: vcenter } 4184 ImageCheckButton { id: cbWholeWords; drawableId: "find_whole_words"; tooltipText: EDIT_FIND_WHOLE_WORDS; styleId: TOOLBAR_BUTTON; alignment: vcenter } 4185 CheckBox { id: cbSelection; text: "Sel" } 4186 } 4187 VSpacer {} 4188 } 4189 } 4190 HorizontalLayout { 4191 id: replace 4192 layoutWidth: fill; 4193 EditLine { id: edReplace; layoutWidth: fill; alignment: vcenter } 4194 Button { id: btnReplace; text: EDIT_REPLACE_NEXT } 4195 Button { id: btnReplaceAndFind; text: EDIT_REPLACE_AND_FIND } 4196 Button { id: btnReplaceAll; text: EDIT_REPLACE_ALL } 4197 } 4198 } 4199 VerticalLayout { 4200 VSpacer {} 4201 ImageButton { id: btnClose; drawableId: close; styleId: BUTTON_TRANSPARENT } 4202 VSpacer {} 4203 } 4204 } 4205 }, null, this); 4206 } catch (Exception e) { 4207 Log.e("Exception while parsing DML: ", e); 4208 } 4209 _editor = editor; 4210 _edFind = childById!EditLine("edFind"); 4211 _edReplace = childById!EditLine("edReplace"); 4212 4213 if (initialText.length) { 4214 _edFind.text = initialText; 4215 _edReplace.text = initialText; 4216 } 4217 4218 _edFind.editorAction.connect(&onFindEditorAction); 4219 _edFind.contentChange.connect(&onFindTextChange); 4220 4221 //_edFind.keyEvent = &onEditorKeyEvent; 4222 //_edReplace.keyEvent = &onEditorKeyEvent; 4223 4224 _btnFindNext = childById!Button("btnFindNext"); 4225 _btnFindNext.click = &onButtonClick; 4226 _btnFindPrev = childById!Button("btnFindPrev"); 4227 _btnFindPrev.click = &onButtonClick; 4228 _btnReplace = childById!Button("btnReplace"); 4229 _btnReplace.click = &onButtonClick; 4230 _btnReplaceAndFind = childById!Button("btnReplaceAndFind"); 4231 _btnReplaceAndFind.click = &onButtonClick; 4232 _btnReplaceAll = childById!Button("btnReplaceAll"); 4233 _btnReplaceAll.click = &onButtonClick; 4234 _btnClose = childById!ImageButton("btnClose"); 4235 _btnClose.click = &onButtonClick; 4236 _cbCaseSensitive = childById!ImageCheckButton("cbCaseSensitive"); 4237 _cbWholeWords = childById!ImageCheckButton("cbWholeWords"); 4238 _cbSelection = childById!CheckBox("cbSelection"); 4239 _cbCaseSensitive.checkChange = &onCaseSensitiveCheckChange; 4240 _cbWholeWords.checkChange = &onCaseSensitiveCheckChange; 4241 _cbSelection.checkChange = &onCaseSensitiveCheckChange; 4242 focusGroup = true; 4243 if (!replace) 4244 childById("replace").visibility = Visibility.Gone; 4245 //_edFind = new EditLine("edFind" 4246 dstring currentText = _edFind.text; 4247 Log.d("currentText=", currentText); 4248 setDirection(false); 4249 updateHighlight(); 4250 } 4251 void activate() { 4252 _edFind.setFocus(); 4253 dstring currentText = _edFind.text; 4254 Log.d("activate.currentText=", currentText); 4255 _edFind.setCaretPos(0, cast(int)currentText.length, true); 4256 } 4257 4258 bool onButtonClick(Widget source) { 4259 switch (source.id) { 4260 case "btnFindNext": 4261 findNext(false); 4262 return true; 4263 case "btnFindPrev": 4264 findNext(true); 4265 return true; 4266 case "btnClose": 4267 close(); 4268 return true; 4269 case "btnReplace": 4270 replaceOne(); 4271 return true; 4272 case "btnReplaceAndFind": 4273 replaceOne(); 4274 findNext(_backDirection); 4275 return true; 4276 case "btnReplaceAll": 4277 replaceAll(); 4278 return true; 4279 default: 4280 return true; 4281 } 4282 } 4283 4284 void close() { 4285 _editor.setTextToHighlight(null, 0); 4286 _editor.closeFindPanel(); 4287 } 4288 4289 override bool onKeyEvent(KeyEvent event) { 4290 if (event.keyCode == KeyCode.TAB) 4291 return super.onKeyEvent(event); 4292 if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.ESCAPE) { 4293 close(); 4294 return true; 4295 } 4296 return true; 4297 } 4298 4299 /// override to handle specific actions 4300 override bool handleAction(const Action a) { 4301 switch (a.id) { 4302 case EditorActions.FindNext: 4303 findNext(false); 4304 return true; 4305 case EditorActions.FindPrev: 4306 findNext(true); 4307 return true; 4308 default: 4309 return false; 4310 } 4311 } 4312 4313 protected bool _backDirection; 4314 void setDirection(bool back) { 4315 _backDirection = back; 4316 if (back) { 4317 _btnFindNext.resetState(State.Default); 4318 _btnFindPrev.setState(State.Default); 4319 } else { 4320 _btnFindNext.setState(State.Default); 4321 _btnFindPrev.resetState(State.Default); 4322 } 4323 } 4324 4325 uint makeSearchFlags() { 4326 uint res = 0; 4327 if (_cbCaseSensitive.checked) 4328 res |= TextSearchFlag.CaseSensitive; 4329 if (_cbWholeWords.checked) 4330 res |= TextSearchFlag.WholeWords; 4331 if (_cbSelection.checked) 4332 res |= TextSearchFlag.SelectionOnly; 4333 return res; 4334 } 4335 bool findNext(bool back) { 4336 setDirection(back); 4337 dstring currentText = _edFind.text; 4338 Log.d("findNext text=", currentText, " back=", back); 4339 if (!currentText.length) 4340 return false; 4341 _editor.setTextToHighlight(currentText, makeSearchFlags); 4342 TextPosition pos = _editor.caretPos; 4343 bool res = _editor.findNextPattern(pos, currentText, makeSearchFlags, back ? -1 : 1); 4344 if (res) { 4345 _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)currentText.length)); 4346 _editor.ensureCaretVisible(); 4347 //_editor.setCaretPos(pos.line, pos.pos, true); 4348 } 4349 return res; 4350 } 4351 4352 bool replaceOne() { 4353 dstring currentText = _edFind.text; 4354 dstring newText = _edReplace.text; 4355 Log.d("replaceOne text=", currentText, " back=", _backDirection, " newText=", newText); 4356 if (!currentText.length) 4357 return false; 4358 _editor.setTextToHighlight(currentText, makeSearchFlags); 4359 TextPosition pos = _editor.caretPos; 4360 bool res = _editor.findNextPattern(pos, currentText, makeSearchFlags, 0); 4361 if (res) { 4362 _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)currentText.length)); 4363 _editor.replaceSelectionText(newText); 4364 _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)newText.length)); 4365 _editor.ensureCaretVisible(); 4366 //_editor.setCaretPos(pos.line, pos.pos, true); 4367 } 4368 return res; 4369 } 4370 4371 int replaceAll() { 4372 int count = 0; 4373 for(int i = 0; ; i++) { 4374 debug Log.d("replaceAll - calling replaceOne, iteration ", i); 4375 if (!replaceOne()) 4376 break; 4377 count++; 4378 TextPosition initialPosition = _editor.caretPos; 4379 debug Log.d("replaceAll - position is ", initialPosition); 4380 if (!findNext(_backDirection)) 4381 break; 4382 TextPosition newPosition = _editor.caretPos; 4383 debug Log.d("replaceAll - next position is ", newPosition); 4384 if (_backDirection && newPosition >= initialPosition) 4385 break; 4386 if (!_backDirection && newPosition <= initialPosition) 4387 break; 4388 } 4389 debug Log.d("replaceAll - done, replace count = ", count); 4390 _editor.ensureCaretVisible(); 4391 return count; 4392 } 4393 4394 void updateHighlight() { 4395 dstring currentText = _edFind.text; 4396 Log.d("onFindTextChange.currentText=", currentText); 4397 _editor.setTextToHighlight(currentText, makeSearchFlags); 4398 } 4399 4400 void onFindTextChange(EditableContent source) { 4401 Log.d("onFindTextChange"); 4402 updateHighlight(); 4403 } 4404 4405 bool onCaseSensitiveCheckChange(Widget source, bool checkValue) { 4406 updateHighlight(); 4407 return true; 4408 } 4409 4410 bool onFindEditorAction(const Action action) { 4411 Log.d("onFindEditorAction ", action); 4412 if (action.id == EditorActions.InsertNewLine) { 4413 findNext(_backDirection); 4414 return true; 4415 } 4416 return false; 4417 } 4418 } 4419 4420 //import dlangui.widgets.metadata; 4421 //mixin(registerWidgets!(EditLine, EditBox, LogWidget)());