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(super.onKeyEvent(event)) 2107 return true; 2108 if (focused) startCaretBlinking(); 2109 cancelHoverTimer(); 2110 bool ctrlOrAltPressed = !!(event.flags & KeyFlag.Control); //(event.flags & (KeyFlag.Control /* | KeyFlag.Alt */)); 2111 //if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.SPACE && (event.flags & KeyFlag.Control)) { 2112 // Log.d("Ctrl+Space pressed"); 2113 //} 2114 if (event.action == KeyAction.Text && event.text.length && !ctrlOrAltPressed) { 2115 //Log.d("text entered: ", event.text); 2116 if (readOnly) 2117 return true; 2118 if (!(!!(event.flags & KeyFlag.Alt) && event.text.length == 1 && isAZaz(event.text[0]))) { // filter out Alt+A..Z 2119 if (replaceMode && _selectionRange.empty && _content[_caretPos.line].length >= _caretPos.pos + event.text.length) { 2120 // replace next char(s) 2121 TextRange range = _selectionRange; 2122 range.end.pos += cast(int)event.text.length; 2123 EditOperation op = new EditOperation(EditAction.Replace, range, [event.text]); 2124 _content.performOperation(op, this); 2125 } else { 2126 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [event.text]); 2127 _content.performOperation(op, this); 2128 } 2129 return true; 2130 } 2131 } 2132 //if (event.keyCode == KeyCode.SPACE && !readOnly) { 2133 // return true; 2134 //} 2135 //if (event.keyCode == KeyCode.RETURN && !readOnly && !_content.multiline) { 2136 // return true; 2137 //} 2138 return true; 2139 } 2140 2141 /// Handle Ctrl + Left mouse click on text 2142 protected void onControlClick() { 2143 // override to do something useful on Ctrl + Left mouse click in text 2144 } 2145 2146 protected TextPosition _hoverTextPosition; 2147 protected Point _hoverMousePosition; 2148 protected ulong _hoverTimer; 2149 protected long _hoverTimeoutMillis = 800; 2150 2151 /// override to handle mouse hover timeout in text 2152 protected void onHoverTimeout(Point pt, TextPosition pos) { 2153 // override to do something useful on hover timeout 2154 } 2155 2156 protected void onHover(Point pos) { 2157 if (_hoverMousePosition == pos) 2158 return; 2159 //Log.d("onHover ", pos); 2160 int x = pos.x - left - _leftPaneWidth; 2161 int y = pos.y - top; 2162 _hoverMousePosition = pos; 2163 _hoverTextPosition = clientToTextPos(Point(x, y)); 2164 cancelHoverTimer(); 2165 Rect reversePos = textPosToClient(_hoverTextPosition); 2166 if (x < reversePos.left + 10.pointsToPixels) 2167 _hoverTimer = setTimer(_hoverTimeoutMillis); 2168 } 2169 2170 protected void cancelHoverTimer() { 2171 if (_hoverTimer) { 2172 cancelTimer(_hoverTimer); 2173 _hoverTimer = 0; 2174 } 2175 } 2176 2177 /// process mouse event; return true if event is processed by widget. 2178 override bool onMouseEvent(MouseEvent event) { 2179 //Log.d("onMouseEvent ", id, " ", event.action, " (", event.x, ",", event.y, ")"); 2180 // support onClick 2181 bool insideLeftPane = event.x < _clientRect.left && event.x >= _clientRect.left - _leftPaneWidth; 2182 if (event.action == MouseAction.ButtonDown && insideLeftPane) { 2183 setFocus(); 2184 cancelHoverTimer(); 2185 if (onLeftPaneMouseClick(event)) 2186 return true; 2187 } 2188 if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) { 2189 setFocus(); 2190 cancelHoverTimer(); 2191 if (event.tripleClick) { 2192 selectLineByMouse(event.x - _clientRect.left, event.y - _clientRect.top); 2193 } else if (event.doubleClick) { 2194 selectWordByMouse(event.x - _clientRect.left, event.y - _clientRect.top); 2195 } else { 2196 auto doSelect = cast(bool)(event.keyFlags & MouseFlag.Shift); 2197 updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, doSelect); 2198 2199 if (event.keyFlags == MouseFlag.Control) 2200 onControlClick(); 2201 } 2202 startCaretBlinking(); 2203 invalidate(); 2204 return true; 2205 } 2206 if (event.action == MouseAction.Move && (event.flags & MouseButton.Left) != 0) { 2207 updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, true); 2208 return true; 2209 } 2210 if (event.action == MouseAction.Move && event.flags == 0) { 2211 // hover 2212 if (focused && !insideLeftPane) { 2213 onHover(event.pos); 2214 } else { 2215 cancelHoverTimer(); 2216 } 2217 return true; 2218 } 2219 if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) { 2220 cancelHoverTimer(); 2221 return true; 2222 } 2223 if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) { 2224 cancelHoverTimer(); 2225 return true; 2226 } 2227 if (event.action == MouseAction.FocusIn) { 2228 cancelHoverTimer(); 2229 return true; 2230 } 2231 if (event.action == MouseAction.Wheel) { 2232 cancelHoverTimer(); 2233 uint keyFlags = event.flags & (MouseFlag.Shift | MouseFlag.Control | MouseFlag.Alt); 2234 if (event.wheelDelta < 0) { 2235 if (keyFlags == MouseFlag.Shift) 2236 return handleAction(new Action(EditorActions.ScrollRight)); 2237 if (keyFlags == MouseFlag.Control) 2238 return handleAction(new Action(EditorActions.ZoomOut)); 2239 return handleAction(new Action(EditorActions.ScrollLineDown)); 2240 } else if (event.wheelDelta > 0) { 2241 if (keyFlags == MouseFlag.Shift) 2242 return handleAction(new Action(EditorActions.ScrollLeft)); 2243 if (keyFlags == MouseFlag.Control) 2244 return handleAction(new Action(EditorActions.ZoomIn)); 2245 return handleAction(new Action(EditorActions.ScrollLineUp)); 2246 } 2247 } 2248 cancelHoverTimer(); 2249 return super.onMouseEvent(event); 2250 } 2251 2252 /// returns caret position 2253 @property TextPosition caretPos() { 2254 return _caretPos; 2255 } 2256 2257 /// change caret position and ensure it is visible 2258 void setCaretPos(int line, int column, bool makeVisible = true, bool center = false) 2259 { 2260 _caretPos = TextPosition(line,column); 2261 correctCaretPos(); 2262 invalidate(); 2263 if (makeVisible) 2264 ensureCaretVisible(center); 2265 handleEditorStateChange(); 2266 } 2267 } 2268 2269 interface EditorActionHandler { 2270 bool onEditorAction(const Action action); 2271 } 2272 2273 interface EnterKeyHandler { 2274 bool onEnterKey(EditWidgetBase editor); 2275 } 2276 2277 /// single line editor 2278 class EditLine : EditWidgetBase { 2279 2280 Signal!EditorActionHandler editorAction; 2281 /// handle Enter key press inside line editor 2282 Signal!EnterKeyHandler enterKey; 2283 2284 /// empty parameter list constructor - for usage by factory 2285 this() { 2286 this(null); 2287 } 2288 /// create with ID parameter 2289 this(string ID, dstring initialContent = null) { 2290 super(ID, ScrollBarMode.Invisible, ScrollBarMode.Invisible); 2291 _content = new EditableContent(false); 2292 _content.contentChanged = this; 2293 _selectAllWhenFocusedWithTab = true; 2294 _deselectAllWhenUnfocused = true; 2295 wantTabs = false; 2296 styleId = STYLE_EDIT_LINE; 2297 text = initialContent; 2298 onThemeChanged(); 2299 } 2300 2301 /// sets default popup menu with copy/paste/cut/undo/redo 2302 EditLine setDefaultPopupMenu() { 2303 MenuItem items = new MenuItem(); 2304 items.add(ACTION_EDITOR_COPY, ACTION_EDITOR_PASTE, ACTION_EDITOR_CUT, 2305 ACTION_EDITOR_UNDO, ACTION_EDITOR_REDO); 2306 popupMenu = items; 2307 return this; 2308 } 2309 2310 protected dstring _measuredText; 2311 protected int[] _measuredTextWidths; 2312 protected Point _measuredTextSize; 2313 2314 protected Point _measuredTextToSetWidgetSize; 2315 protected dstring _textToSetWidgetSize = "aaaaa"d; 2316 2317 @property void textToSetWidgetSize(dstring newText) { 2318 _textToSetWidgetSize = newText; 2319 requestLayout(); 2320 } 2321 2322 @property dstring textToSetWidgetSize() { 2323 return _textToSetWidgetSize; 2324 } 2325 2326 protected int[] _measuredTextToSetWidgetSizeWidths; 2327 2328 protected dchar _passwordChar = 0; 2329 /// password character - 0 for normal editor, some character, e.g. '*' to hide text by replacing all characters with this char 2330 @property dchar passwordChar() { return _passwordChar; } 2331 @property EditLine passwordChar(dchar ch) { 2332 if (_passwordChar != ch) { 2333 _passwordChar = ch; 2334 requestLayout(); 2335 } 2336 return this; 2337 } 2338 2339 override protected Rect textPosToClient(TextPosition p) { 2340 Rect res; 2341 res.bottom = _clientRect.height; 2342 if (p.pos == 0) 2343 res.left = 0; 2344 else if (p.pos >= _measuredText.length) 2345 res.left = _measuredTextSize.x; 2346 else 2347 res.left = _measuredTextWidths[p.pos - 1]; 2348 res.left -= _scrollPos.x; 2349 res.right = res.left + 1; 2350 return res; 2351 } 2352 2353 override protected TextPosition clientToTextPos(Point pt) { 2354 pt.x += _scrollPos.x; 2355 TextPosition res; 2356 for (int i = 0; i < _measuredText.length; i++) { 2357 int x0 = i > 0 ? _measuredTextWidths[i - 1] : 0; 2358 int x1 = _measuredTextWidths[i]; 2359 int mx = (x0 + x1) >> 1; 2360 if (pt.x <= mx) { 2361 res.pos = i; 2362 return res; 2363 } 2364 } 2365 res.pos = cast(int)_measuredText.length; 2366 return res; 2367 } 2368 2369 override protected void ensureCaretVisible(bool center = false) { 2370 //_scrollPos 2371 Rect rc = textPosToClient(_caretPos); 2372 if (rc.left < 0) { 2373 // scroll left 2374 _scrollPos.x -= -rc.left + _clientRect.width / 10; 2375 if (_scrollPos.x < 0) 2376 _scrollPos.x = 0; 2377 invalidate(); 2378 } else if (rc.left >= _clientRect.width - 10) { 2379 // scroll right 2380 _scrollPos.x += (rc.left - _clientRect.width) + _spaceWidth * 4; 2381 invalidate(); 2382 } 2383 updateScrollBars(); 2384 handleEditorStateChange(); 2385 } 2386 2387 protected dstring applyPasswordChar(dstring s) { 2388 if (!_passwordChar || s.length == 0) 2389 return s; 2390 dchar[] ss = s.dup; 2391 foreach(ref ch; ss) 2392 ch = _passwordChar; 2393 return cast(dstring)ss; 2394 } 2395 2396 override protected Point measureVisibleText() { 2397 FontRef font = font(); 2398 //Point sz = font.textSize(text); 2399 _measuredText = applyPasswordChar(text); 2400 _measuredTextWidths.length = _measuredText.length; 2401 int charsMeasured = font.measureText(_measuredText, _measuredTextWidths, MAX_WIDTH_UNSPECIFIED, tabSize); 2402 _measuredTextSize.x = charsMeasured > 0 ? _measuredTextWidths[charsMeasured - 1]: 0; 2403 _measuredTextSize.y = font.height; 2404 return _measuredTextSize; 2405 } 2406 2407 protected Point measureTextToSetWidgetSize() { 2408 FontRef font = font(); 2409 _measuredTextToSetWidgetSizeWidths.length = _textToSetWidgetSize.length; 2410 int charsMeasured = font.measureText(_textToSetWidgetSize, _measuredTextToSetWidgetSizeWidths, MAX_WIDTH_UNSPECIFIED, tabSize); 2411 _measuredTextToSetWidgetSize.x = charsMeasured > 0 ? _measuredTextToSetWidgetSizeWidths[charsMeasured - 1]: 0; 2412 _measuredTextToSetWidgetSize.y = font.height; 2413 return _measuredTextToSetWidgetSize; 2414 } 2415 2416 /// measure 2417 override void measure(int parentWidth, int parentHeight) { 2418 if (visibility == Visibility.Gone) 2419 return; 2420 2421 updateFontProps(); 2422 measureVisibleText(); 2423 measureTextToSetWidgetSize(); 2424 measuredContent(parentWidth, parentHeight, _measuredTextToSetWidgetSize.x + _leftPaneWidth, _measuredTextToSetWidgetSize.y); 2425 } 2426 2427 override bool handleAction(const Action a) { 2428 switch (a.id) with(EditorActions) 2429 { 2430 case InsertNewLine: 2431 case PrependNewLine: 2432 case AppendNewLine: 2433 if (editorAction.assigned) { 2434 return editorAction(a); 2435 } 2436 break; 2437 case Up: 2438 break; 2439 case Down: 2440 break; 2441 case PageUp: 2442 break; 2443 case PageDown: 2444 break; 2445 default: 2446 break; 2447 } 2448 return super.handleAction(a); 2449 } 2450 2451 2452 /// handle keys 2453 override bool onKeyEvent(KeyEvent event) { 2454 if(super.onKeyEvent(event)) 2455 return true; 2456 if (enterKey.assigned) { 2457 if (event.keyCode == KeyCode.RETURN && event.modifiers == 0) { 2458 if (event.action == KeyAction.KeyDown) 2459 return true; 2460 if (event.action == KeyAction.KeyUp) { 2461 if (enterKey(this)) 2462 return true; 2463 } 2464 } 2465 } 2466 return true; 2467 } 2468 2469 /// process mouse event; return true if event is processed by widget. 2470 override bool onMouseEvent(MouseEvent event) { 2471 return super.onMouseEvent(event); 2472 } 2473 2474 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 2475 override void layout(Rect rc) { 2476 if (visibility == Visibility.Gone) { 2477 return; 2478 } 2479 _needLayout = false; 2480 Point sz = Point(rc.width, measuredHeight); 2481 applyAlign(rc, sz); 2482 _pos = rc; 2483 _clientRect = rc; 2484 applyMargins(_clientRect); 2485 applyPadding(_clientRect); 2486 if (_contentChanged) { 2487 measureVisibleText(); 2488 _contentChanged = false; 2489 } 2490 } 2491 2492 2493 /// override to custom highlight of line background 2494 protected void drawLineBackground(DrawBuf buf, Rect lineRect, Rect visibleRect) { 2495 if (!_selectionRange.empty) { 2496 // line inside selection 2497 Rect startrc = textPosToClient(_selectionRange.start); 2498 Rect endrc = textPosToClient(_selectionRange.end); 2499 Rect rc = lineRect; 2500 rc.left = startrc.left + _clientRect.left; 2501 rc.right = endrc.left + _clientRect.left; 2502 if (!rc.empty) { 2503 // draw selection rect for line 2504 buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal); 2505 } 2506 if (_leftPaneWidth > 0) { 2507 Rect leftPaneRect = visibleRect; 2508 leftPaneRect.right = leftPaneRect.left; 2509 leftPaneRect.left -= _leftPaneWidth; 2510 drawLeftPane(buf, leftPaneRect, 0); 2511 } 2512 } 2513 } 2514 2515 /// draw content 2516 override void onDraw(DrawBuf buf) { 2517 if (visibility != Visibility.Visible) 2518 return; 2519 super.onDraw(buf); 2520 Rect rc = _pos; 2521 applyMargins(rc); 2522 applyPadding(rc); 2523 auto saver = ClipRectSaver(buf, rc, alpha); 2524 2525 FontRef font = font(); 2526 dstring txt = applyPasswordChar(text); 2527 2528 drawLineBackground(buf, _clientRect, _clientRect); 2529 font.drawText(buf, rc.left - _scrollPos.x, rc.top, txt, textColor, tabSize); 2530 2531 drawCaret(buf); 2532 } 2533 } 2534 2535 // SpinCtrl 2536 private { 2537 import std.ascii; 2538 } 2539 2540 class SpinCtrl : HorizontalLayout { 2541 2542 TextWidget label; 2543 int min, max; 2544 2545 private EditLine linEdit; 2546 private Button butUp, butDown; 2547 2548 2549 @property int value() { return linEdit.text.to!int; } 2550 @property void value(int val) { 2551 linEdit.text = val.to!dstring; 2552 } 2553 2554 override @property bool enabled() { return linEdit.enabled; } 2555 alias enabled = Widget.enabled; 2556 @property void enabled(bool status) { 2557 linEdit.enabled = status; 2558 butUp.enabled = status; 2559 butDown.enabled = status; 2560 } 2561 2562 this(int min, int max, int initialVal = 0, dstring labelText = null){ 2563 this.min = min; 2564 this.max = max; 2565 2566 if(labelText !is null){ 2567 label = new TextWidget("label", labelText); 2568 addChild(label); 2569 } 2570 2571 linEdit = new class EditLine { 2572 this(){super("linEdit", "0"d);} 2573 override bool onKeyEvent(KeyEvent event) { 2574 if (( KeyAction.Text == event.action && event.text[0].isDigit) 2575 || event.keyCode == KeyCode.BACK 2576 || event.keyCode == KeyCode.DEL 2577 || event.keyCode == KeyCode.LEFT 2578 || event.keyCode == KeyCode.RIGHT 2579 || event.keyCode == KeyCode.TAB 2580 ){ 2581 return super.onKeyEvent(event); 2582 } 2583 return false; 2584 } 2585 2586 override bool onMouseEvent(MouseEvent event) { 2587 if(enabled && event.action == MouseAction.Wheel){ 2588 if((event.wheelDelta == 1) && (value < max)) 2589 value = value + event.wheelDelta; 2590 if((event.wheelDelta == -1) && (value > min)) 2591 value = value + event.wheelDelta; 2592 return true; 2593 } 2594 return super.onMouseEvent(event); 2595 } 2596 }; 2597 2598 linEdit.addOnFocusChangeListener((w, t){ 2599 if(linEdit.text == "") 2600 linEdit.text = "0"; 2601 if(linEdit.text.to!int > max) 2602 value = max; 2603 if(linEdit.text.to!int < min) 2604 value = min; 2605 return true; 2606 }); 2607 2608 linEdit.minHeight = 35; 2609 if(initialVal != 0) 2610 value = initialVal; 2611 addChild(linEdit); 2612 2613 2614 auto butContainer = new VerticalLayout(); 2615 butContainer.maxHeight = linEdit.minHeight; 2616 2617 butUp = new Button("butUp", "+"d); 2618 butUp.margins(Rect(1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels)); 2619 2620 butDown = new Button("butDown", "-"d); 2621 butDown.margins(Rect(1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels)); 2622 2623 butContainer.addChild(butUp); 2624 butContainer.addChild(butDown); 2625 2626 addChild(butContainer); 2627 2628 butUp.click = delegate(Widget w) { 2629 immutable val = linEdit.text.to!int; 2630 if(val < max ) 2631 linEdit.text = (val + 1).to!dstring; 2632 return true; 2633 }; 2634 2635 butDown.click = delegate(Widget w) { 2636 immutable val = linEdit.text.to!int; 2637 if(val > min ) 2638 linEdit.text = (val - 1).to!dstring; 2639 return true; 2640 }; 2641 2642 enabled = true; 2643 } 2644 2645 } 2646 2647 /// multiline editor 2648 class EditBox : EditWidgetBase { 2649 /// empty parameter list constructor - for usage by factory 2650 this() { 2651 this(null); 2652 } 2653 /// create with ID parameter 2654 this(string ID, dstring initialContent = null, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) { 2655 super(ID, hscrollbarMode, vscrollbarMode); 2656 _content = new EditableContent(true); // multiline 2657 _content.contentChanged = this; 2658 styleId = STYLE_EDIT_BOX; 2659 text = initialContent; 2660 acceleratorMap.add( [ 2661 // zoom 2662 new Action(EditorActions.ZoomIn, KeyCode.ADD, KeyFlag.Control), 2663 new Action(EditorActions.ZoomOut, KeyCode.SUB, KeyFlag.Control), 2664 ]); 2665 onThemeChanged(); 2666 } 2667 2668 ~this() { 2669 if (_findPanel) { 2670 destroy(_findPanel); 2671 _findPanel = null; 2672 } 2673 } 2674 2675 protected int _firstVisibleLine; 2676 2677 protected int _maxLineWidth; 2678 protected int _numVisibleLines; // number of lines visible in client area 2679 protected dstring[] _visibleLines; // text for visible lines 2680 protected int[][] _visibleLinesMeasurement; // char positions for visible lines 2681 protected int[] _visibleLinesWidths; // width (in pixels) of visible lines 2682 protected CustomCharProps[][] _visibleLinesHighlights; 2683 protected CustomCharProps[][] _visibleLinesHighlightsBuf; 2684 2685 protected Point _measuredTextToSetWidgetSize; 2686 protected dstring _textToSetWidgetSize = "aaaaa/naaaaa"d; 2687 protected int[] _measuredTextToSetWidgetSizeWidths; 2688 2689 /// Set _needRewrap to true; 2690 override void wordWrapRefresh() 2691 { 2692 _needRewrap = true; 2693 } 2694 2695 override @property int fontSize() const { return super.fontSize(); } 2696 override @property Widget fontSize(int size) { 2697 // Need to rewrap if fontSize changed 2698 _needRewrap = true; 2699 return super.fontSize(size); 2700 } 2701 2702 override protected int lineCount() { 2703 return _content.length; 2704 } 2705 2706 override protected void updateMaxLineWidth() { 2707 // find max line width. TODO: optimize!!! 2708 int maxw; 2709 int[] buf; 2710 for (int i = 0; i < _content.length; i++) { 2711 dstring s = _content[i]; 2712 int w = calcLineWidth(s); 2713 if (maxw < w) 2714 maxw = w; 2715 } 2716 _maxLineWidth = maxw; 2717 } 2718 2719 @property int minFontSize() { 2720 return _minFontSize; 2721 } 2722 @property EditBox minFontSize(int size) { 2723 _minFontSize = size; 2724 return this; 2725 } 2726 2727 @property int maxFontSize() { 2728 return _maxFontSize; 2729 } 2730 2731 @property EditBox maxFontSize(int size) { 2732 _maxFontSize = size; 2733 return this; 2734 } 2735 2736 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 2737 override void layout(Rect rc) { 2738 if (visibility == Visibility.Gone) 2739 return; 2740 2741 if (rc != _pos) 2742 _contentChanged = true; 2743 Rect contentRc = rc; 2744 int findPanelHeight; 2745 if (_findPanel && _findPanel.visibility != Visibility.Gone) { 2746 _findPanel.measure(rc.width, rc.height); 2747 findPanelHeight = _findPanel.measuredHeight; 2748 _findPanel.layout(Rect(rc.left, rc.bottom - findPanelHeight, rc.right, rc.bottom)); 2749 contentRc.bottom -= findPanelHeight; 2750 } 2751 2752 super.layout(contentRc); 2753 if (_contentChanged) { 2754 measureVisibleText(); 2755 _needRewrap = true; 2756 _contentChanged = false; 2757 } 2758 2759 _pos = rc; 2760 } 2761 2762 override protected Point measureVisibleText() { 2763 Point sz; 2764 FontRef font = font(); 2765 _lineHeight = font.height; 2766 _numVisibleLines = (_clientRect.height + _lineHeight - 1) / _lineHeight; 2767 if (_firstVisibleLine >= _content.length) { 2768 _firstVisibleLine = _content.length - _numVisibleLines + 1; 2769 if (_firstVisibleLine < 0) 2770 _firstVisibleLine = 0; 2771 _caretPos.line = _content.length - 1; 2772 _caretPos.pos = 0; 2773 } 2774 if (_numVisibleLines < 1) 2775 _numVisibleLines = 1; 2776 if (_firstVisibleLine + _numVisibleLines > _content.length) 2777 _numVisibleLines = _content.length - _firstVisibleLine; 2778 if (_numVisibleLines < 1) 2779 _numVisibleLines = 1; 2780 _visibleLines.length = _numVisibleLines; 2781 if (_visibleLinesMeasurement.length < _numVisibleLines) 2782 _visibleLinesMeasurement.length = _numVisibleLines; 2783 if (_visibleLinesWidths.length < _numVisibleLines) 2784 _visibleLinesWidths.length = _numVisibleLines; 2785 if (_visibleLinesHighlights.length < _numVisibleLines) { 2786 _visibleLinesHighlights.length = _numVisibleLines; 2787 _visibleLinesHighlightsBuf.length = _numVisibleLines; 2788 } 2789 for (int i = 0; i < _numVisibleLines; i++) { 2790 _visibleLines[i] = _content[_firstVisibleLine + i]; 2791 size_t len = _visibleLines[i].length; 2792 if (_visibleLinesMeasurement[i].length < len) 2793 _visibleLinesMeasurement[i].length = len; 2794 if (_visibleLinesHighlightsBuf[i].length < len) 2795 _visibleLinesHighlightsBuf[i].length = len; 2796 _visibleLinesHighlights[i] = handleCustomLineHighlight(_firstVisibleLine + i, _visibleLines[i], _visibleLinesHighlightsBuf[i]); 2797 int charsMeasured = font.measureText(_visibleLines[i], _visibleLinesMeasurement[i], int.max, tabSize); 2798 _visibleLinesWidths[i] = charsMeasured > 0 ? _visibleLinesMeasurement[i][charsMeasured - 1] : 0; 2799 if (sz.x < _visibleLinesWidths[i]) 2800 sz.x = _visibleLinesWidths[i]; // width - max from visible lines 2801 } 2802 sz.x = _maxLineWidth; 2803 sz.y = _lineHeight * _content.length; // height - for all lines 2804 return sz; 2805 } 2806 2807 protected bool _extendRightScrollBound = true; 2808 /// override to determine if scrollbars are needed or not 2809 override protected void checkIfScrollbarsNeeded(ref bool needHScroll, ref bool needVScroll) { 2810 needHScroll = _hscrollbar && (_hscrollbarMode == ScrollBarMode.Visible || _hscrollbarMode == ScrollBarMode.Auto); 2811 needVScroll = _vscrollbar && (_vscrollbarMode == ScrollBarMode.Visible || _vscrollbarMode == ScrollBarMode.Auto); 2812 if (!needHScroll && !needVScroll) 2813 return; // not needed 2814 if (_hscrollbarMode != ScrollBarMode.Auto && _vscrollbarMode != ScrollBarMode.Auto) 2815 return; // no auto scrollbars 2816 // either h or v scrollbar is in auto mode 2817 2818 int hsbHeight = _hscrollbar.measuredHeight; 2819 int vsbWidth = _hscrollbar.measuredWidth; 2820 2821 int visibleLines = _lineHeight > 0 ? (_clientRect.height / _lineHeight) : 1; // fully visible lines 2822 if (visibleLines < 1) 2823 visibleLines = 1; 2824 int visibleLinesWithScrollbar = _lineHeight > 0 ? ((_clientRect.height - hsbHeight) / _lineHeight) : 1; // fully visible lines 2825 if (visibleLinesWithScrollbar < 1) 2826 visibleLinesWithScrollbar = 1; 2827 2828 // either h or v scrollbar is in auto mode 2829 //Point contentSize = fullContentSize(); 2830 int contentWidth = _maxLineWidth + (_extendRightScrollBound ? _clientRect.width / 16 : 0); 2831 int contentHeight = _content.length; 2832 2833 int clientWidth = _clientRect.width; 2834 int clientHeight = visibleLines; 2835 2836 int clientWidthWithScrollbar = clientWidth - vsbWidth; 2837 int clientHeightWithScrollbar = visibleLinesWithScrollbar; 2838 2839 if (_hscrollbarMode == ScrollBarMode.Auto && _vscrollbarMode == ScrollBarMode.Auto) { 2840 // both scrollbars in auto mode 2841 bool xFits = contentWidth <= clientWidth; 2842 bool yFits = contentHeight <= clientHeight; 2843 if (!xFits && !yFits) { 2844 // none fits, need both scrollbars 2845 } else if (xFits && yFits) { 2846 // everything fits! 2847 needHScroll = false; 2848 needVScroll = false; 2849 } else if (xFits) { 2850 // only X fits 2851 if (contentWidth <= clientWidthWithScrollbar) 2852 needHScroll = false; // disable hscroll 2853 } else { // yFits 2854 // only Y fits 2855 if (contentHeight <= clientHeightWithScrollbar) 2856 needVScroll = false; // disable vscroll 2857 } 2858 } else if (_hscrollbarMode == ScrollBarMode.Auto) { 2859 // only hscroll is in auto mode 2860 if (needVScroll) 2861 clientWidth = clientWidthWithScrollbar; 2862 needHScroll = contentWidth > clientWidth; 2863 } else { 2864 // only vscroll is in auto mode 2865 if (needHScroll) 2866 clientHeight = clientHeightWithScrollbar; 2867 needVScroll = contentHeight > clientHeight; 2868 } 2869 } 2870 2871 /// update horizontal scrollbar widget position 2872 override protected void updateHScrollBar() { 2873 _hscrollbar.setRange(0, _maxLineWidth + (_extendRightScrollBound ? _clientRect.width / 16 : 0)); 2874 _hscrollbar.pageSize = _clientRect.width; 2875 _hscrollbar.position = _scrollPos.x; 2876 } 2877 2878 /// update verticat scrollbar widget position 2879 override protected void updateVScrollBar() { 2880 int visibleLines = _lineHeight ? _clientRect.height / _lineHeight : 1; // fully visible lines 2881 if (visibleLines < 1) 2882 visibleLines = 1; 2883 _vscrollbar.setRange(0, _content.length); 2884 _vscrollbar.pageSize = visibleLines; 2885 _vscrollbar.position = _firstVisibleLine; 2886 } 2887 2888 /// process horizontal scrollbar event 2889 override bool onHScroll(ScrollEvent event) { 2890 if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) { 2891 if (_scrollPos.x != event.position) { 2892 _scrollPos.x = event.position; 2893 invalidate(); 2894 } 2895 } else if (event.action == ScrollAction.PageUp) { 2896 dispatchAction(new Action(EditorActions.ScrollLeft)); 2897 } else if (event.action == ScrollAction.PageDown) { 2898 dispatchAction(new Action(EditorActions.ScrollRight)); 2899 } else if (event.action == ScrollAction.LineUp) { 2900 dispatchAction(new Action(EditorActions.ScrollLeft)); 2901 } else if (event.action == ScrollAction.LineDown) { 2902 dispatchAction(new Action(EditorActions.ScrollRight)); 2903 } 2904 return true; 2905 } 2906 2907 /// process vertical scrollbar event 2908 override bool onVScroll(ScrollEvent event) { 2909 if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) { 2910 if (_firstVisibleLine != event.position) { 2911 _firstVisibleLine = event.position; 2912 measureVisibleText(); 2913 invalidate(); 2914 } 2915 } else if (event.action == ScrollAction.PageUp) { 2916 dispatchAction(new Action(EditorActions.ScrollPageUp)); 2917 } else if (event.action == ScrollAction.PageDown) { 2918 dispatchAction(new Action(EditorActions.ScrollPageDown)); 2919 } else if (event.action == ScrollAction.LineUp) { 2920 dispatchAction(new Action(EditorActions.ScrollLineUp)); 2921 } else if (event.action == ScrollAction.LineDown) { 2922 dispatchAction(new Action(EditorActions.ScrollLineDown)); 2923 } 2924 return true; 2925 } 2926 2927 protected bool _enableScrollAfterText = true; 2928 override protected void ensureCaretVisible(bool center = false) { 2929 if (_caretPos.line >= _content.length) 2930 _caretPos.line = _content.length - 1; 2931 if (_caretPos.line < 0) 2932 _caretPos.line = 0; 2933 int visibleLines = _lineHeight > 0 ? _clientRect.height / _lineHeight : 1; // fully visible lines 2934 if (visibleLines < 1) 2935 visibleLines = 1; 2936 int maxFirstVisibleLine = _content.length - 1; 2937 if (!_enableScrollAfterText) 2938 maxFirstVisibleLine = _content.length - visibleLines; 2939 if (maxFirstVisibleLine < 0) 2940 maxFirstVisibleLine = 0; 2941 2942 if (_caretPos.line < _firstVisibleLine) { 2943 _firstVisibleLine = _caretPos.line; 2944 if (center) { 2945 _firstVisibleLine -= visibleLines / 2; 2946 if (_firstVisibleLine < 0) 2947 _firstVisibleLine = 0; 2948 } 2949 if (_firstVisibleLine > maxFirstVisibleLine) 2950 _firstVisibleLine = maxFirstVisibleLine; 2951 measureVisibleText(); 2952 invalidate(); 2953 } else if(_wordWrap && !(_firstVisibleLine > maxFirstVisibleLine)) { 2954 //For wordwrap mode, move down sooner 2955 int offsetLines = -1 * caretHeightOffset / _lineHeight; 2956 //Log.d("offsetLines: ", offsetLines); 2957 if (_caretPos.line >= _firstVisibleLine + visibleLines - offsetLines) 2958 { 2959 _firstVisibleLine = _caretPos.line - visibleLines + 1 + offsetLines; 2960 if (center) 2961 _firstVisibleLine += visibleLines / 2; 2962 if (_firstVisibleLine > maxFirstVisibleLine) 2963 _firstVisibleLine = maxFirstVisibleLine; 2964 if (_firstVisibleLine < 0) 2965 _firstVisibleLine = 0; 2966 measureVisibleText(); 2967 invalidate(); 2968 } 2969 } else if (_caretPos.line >= _firstVisibleLine + visibleLines) { 2970 _firstVisibleLine = _caretPos.line - visibleLines + 1; 2971 if (center) 2972 _firstVisibleLine += visibleLines / 2; 2973 if (_firstVisibleLine > maxFirstVisibleLine) 2974 _firstVisibleLine = maxFirstVisibleLine; 2975 if (_firstVisibleLine < 0) 2976 _firstVisibleLine = 0; 2977 measureVisibleText(); 2978 invalidate(); 2979 } else if (_firstVisibleLine > maxFirstVisibleLine) { 2980 _firstVisibleLine = maxFirstVisibleLine; 2981 if (_firstVisibleLine < 0) 2982 _firstVisibleLine = 0; 2983 measureVisibleText(); 2984 invalidate(); 2985 } 2986 //_scrollPos 2987 Rect rc = textPosToClient(_caretPos); 2988 if (rc.left < 0) { 2989 // scroll left 2990 _scrollPos.x -= -rc.left + _clientRect.width / 4; 2991 if (_scrollPos.x < 0) 2992 _scrollPos.x = 0; 2993 invalidate(); 2994 } else if (rc.left >= _clientRect.width - 10) { 2995 // scroll right 2996 if (!_wordWrap) 2997 _scrollPos.x += (rc.left - _clientRect.width) + _clientRect.width / 4; 2998 invalidate(); 2999 } 3000 updateScrollBars(); 3001 handleEditorStateChange(); 3002 } 3003 3004 override protected Rect textPosToClient(TextPosition p) { 3005 Rect res; 3006 int lineIndex = p.line - _firstVisibleLine; 3007 res.top = lineIndex * _lineHeight; 3008 res.bottom = res.top + _lineHeight; 3009 // if visible 3010 if (lineIndex >= 0 && lineIndex < _visibleLines.length) { 3011 if (p.pos == 0) 3012 res.left = 0; 3013 else if (p.pos >= _visibleLinesMeasurement[lineIndex].length) 3014 res.left = _visibleLinesWidths[lineIndex]; 3015 else 3016 res.left = _visibleLinesMeasurement[lineIndex][p.pos - 1]; 3017 } 3018 res.left -= _scrollPos.x; 3019 res.right = res.left + 1; 3020 return res; 3021 } 3022 3023 override protected TextPosition clientToTextPos(Point pt) { 3024 TextPosition res; 3025 pt.x += _scrollPos.x; 3026 int lineIndex = pt.y / _lineHeight; 3027 if (lineIndex < 0) 3028 lineIndex = 0; 3029 if (lineIndex < _visibleLines.length) { 3030 res.line = lineIndex + _firstVisibleLine; 3031 int len = cast(int)_visibleLines[lineIndex].length; 3032 for (int i = 0; i < len; i++) { 3033 int x0 = i > 0 ? _visibleLinesMeasurement[lineIndex][i - 1] : 0; 3034 int x1 = _visibleLinesMeasurement[lineIndex][i]; 3035 int mx = (x0 + x1) >> 1; 3036 if (pt.x <= mx) { 3037 res.pos = i; 3038 return res; 3039 } 3040 } 3041 res.pos = cast(int)_visibleLines[lineIndex].length; 3042 } else if (_visibleLines.length > 0) { 3043 res.line = _firstVisibleLine + cast(int)_visibleLines.length - 1; 3044 res.pos = cast(int)_visibleLines[$ - 1].length; 3045 } else { 3046 res.line = 0; 3047 res.pos = 0; 3048 } 3049 return res; 3050 } 3051 3052 override protected bool handleAction(const Action a) { 3053 TextPosition oldCaretPos = _caretPos; 3054 dstring currentLine = _content[_caretPos.line]; 3055 switch (a.id) with(EditorActions) 3056 { 3057 case PrependNewLine: 3058 if (!readOnly) { 3059 correctCaretPos(); 3060 _caretPos.pos = 0; 3061 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]); 3062 _content.performOperation(op, this); 3063 } 3064 return true; 3065 case InsertNewLine: 3066 if (!readOnly) { 3067 correctCaretPos(); 3068 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]); 3069 _content.performOperation(op, this); 3070 } 3071 return true; 3072 case Up: 3073 case SelectUp: 3074 if ((_caretPos.line > 0) | wordWrap) { 3075 if (_wordWrap) 3076 { 3077 LineSpan curSpan = getSpan(_caretPos.line); 3078 int curWrap = findWrapLine(_caretPos); 3079 if (curWrap > 0) 3080 { 3081 _caretPos.pos-= curSpan.wrapPoints[curWrap - 1].wrapPos; 3082 } 3083 else 3084 { 3085 int previousPos = _caretPos.pos; 3086 curSpan = getSpan(_caretPos.line - 1); 3087 curWrap = curSpan.len - 1; 3088 if (curWrap > 0) 3089 { 3090 int accumulativePoint = curSpan.accumulation(curSpan.len - 1, LineSpan.WrapPointInfo.Position); 3091 _caretPos.line--; 3092 _caretPos.pos = accumulativePoint + previousPos; 3093 } 3094 else 3095 { 3096 _caretPos.line--; 3097 } 3098 } 3099 } 3100 else if(_caretPos.line > 0) 3101 _caretPos.line--; 3102 correctCaretPos(); 3103 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3104 ensureCaretVisible(); 3105 } 3106 return true; 3107 case Down: 3108 case SelectDown: 3109 if (_caretPos.line < _content.length - 1) { 3110 if (_wordWrap) 3111 { 3112 LineSpan curSpan = getSpan(_caretPos.line); 3113 int curWrap = findWrapLine(_caretPos); 3114 if (curWrap < curSpan.len - 1) 3115 { 3116 int previousPos = _caretPos.pos; 3117 _caretPos.pos+= curSpan.wrapPoints[curWrap].wrapPos; 3118 correctCaretPos(); 3119 if (_caretPos.pos == previousPos) 3120 { 3121 _caretPos.pos = 0; 3122 _caretPos.line++; 3123 } 3124 } 3125 else if (curSpan.len > 1) 3126 { 3127 int previousPos = _caretPos.pos; 3128 int previousAccumulatedPosition = curSpan.accumulation(curSpan.len - 1, LineSpan.WrapPointInfo.Position); 3129 _caretPos.line++; 3130 _caretPos.pos = previousPos - previousAccumulatedPosition; 3131 } 3132 else 3133 { 3134 _caretPos.line++; 3135 } 3136 } 3137 else 3138 { 3139 _caretPos.line++; 3140 } 3141 correctCaretPos(); 3142 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3143 ensureCaretVisible(); 3144 } 3145 return true; 3146 case PageBegin: 3147 case SelectPageBegin: 3148 { 3149 ensureCaretVisible(); 3150 _caretPos.line = _firstVisibleLine; 3151 correctCaretPos(); 3152 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3153 } 3154 return true; 3155 case PageEnd: 3156 case SelectPageEnd: 3157 { 3158 ensureCaretVisible(); 3159 int fullLines = _clientRect.height / _lineHeight; 3160 int newpos = _firstVisibleLine + fullLines - 1; 3161 if (newpos >= _content.length) 3162 newpos = _content.length - 1; 3163 _caretPos.line = newpos; 3164 correctCaretPos(); 3165 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3166 } 3167 return true; 3168 case PageUp: 3169 case SelectPageUp: 3170 { 3171 ensureCaretVisible(); 3172 int fullLines = _clientRect.height / _lineHeight; 3173 int newpos = _firstVisibleLine - fullLines; 3174 if (newpos < 0) { 3175 _firstVisibleLine = 0; 3176 _caretPos.line = 0; 3177 } else { 3178 int delta = _firstVisibleLine - newpos; 3179 _firstVisibleLine = newpos; 3180 _caretPos.line -= delta; 3181 } 3182 correctCaretPos(); 3183 measureVisibleText(); 3184 updateScrollBars(); 3185 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3186 } 3187 return true; 3188 case PageDown: 3189 case SelectPageDown: 3190 { 3191 ensureCaretVisible(); 3192 int fullLines = _clientRect.height / _lineHeight; 3193 int newpos = _firstVisibleLine + fullLines; 3194 if (newpos >= _content.length) { 3195 _caretPos.line = _content.length - 1; 3196 } else { 3197 int delta = newpos - _firstVisibleLine; 3198 _firstVisibleLine = newpos; 3199 _caretPos.line += delta; 3200 } 3201 correctCaretPos(); 3202 measureVisibleText(); 3203 updateScrollBars(); 3204 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 3205 } 3206 return true; 3207 case ScrollLeft: 3208 { 3209 if (_scrollPos.x > 0) { 3210 int newpos = _scrollPos.x - _spaceWidth * 4; 3211 if (newpos < 0) 3212 newpos = 0; 3213 _scrollPos.x = newpos; 3214 updateScrollBars(); 3215 invalidate(); 3216 } 3217 } 3218 return true; 3219 case ScrollRight: 3220 { 3221 if (_scrollPos.x < _maxLineWidth - _clientRect.width) { 3222 int newpos = _scrollPos.x + _spaceWidth * 4; 3223 if (newpos > _maxLineWidth - _clientRect.width) 3224 newpos = _maxLineWidth - _clientRect.width; 3225 _scrollPos.x = newpos; 3226 updateScrollBars(); 3227 invalidate(); 3228 } 3229 } 3230 return true; 3231 case ScrollLineUp: 3232 { 3233 if (_firstVisibleLine > 0) { 3234 _firstVisibleLine -= 3; 3235 if (_firstVisibleLine < 0) 3236 _firstVisibleLine = 0; 3237 measureVisibleText(); 3238 updateScrollBars(); 3239 invalidate(); 3240 } 3241 } 3242 return true; 3243 case ScrollPageUp: 3244 { 3245 int fullLines = _clientRect.height / _lineHeight; 3246 if (_firstVisibleLine > 0) { 3247 _firstVisibleLine -= fullLines * 3 / 4; 3248 if (_firstVisibleLine < 0) 3249 _firstVisibleLine = 0; 3250 measureVisibleText(); 3251 updateScrollBars(); 3252 invalidate(); 3253 } 3254 } 3255 return true; 3256 case ScrollLineDown: 3257 { 3258 int fullLines = _clientRect.height / _lineHeight; 3259 if (_firstVisibleLine + fullLines < _content.length) { 3260 _firstVisibleLine += 3; 3261 if (_firstVisibleLine > _content.length - fullLines) 3262 _firstVisibleLine = _content.length - fullLines; 3263 if (_firstVisibleLine < 0) 3264 _firstVisibleLine = 0; 3265 measureVisibleText(); 3266 updateScrollBars(); 3267 invalidate(); 3268 } 3269 } 3270 return true; 3271 case ScrollPageDown: 3272 { 3273 int fullLines = _clientRect.height / _lineHeight; 3274 if (_firstVisibleLine + fullLines < _content.length) { 3275 _firstVisibleLine += fullLines * 3 / 4; 3276 if (_firstVisibleLine > _content.length - fullLines) 3277 _firstVisibleLine = _content.length - fullLines; 3278 if (_firstVisibleLine < 0) 3279 _firstVisibleLine = 0; 3280 measureVisibleText(); 3281 updateScrollBars(); 3282 invalidate(); 3283 } 3284 } 3285 return true; 3286 case ZoomOut: 3287 case ZoomIn: 3288 { 3289 int dir = a.id == ZoomIn ? 1 : -1; 3290 if (_minFontSize < _maxFontSize && _minFontSize > 0 && _maxFontSize > 0) { 3291 int currentFontSize = fontSize; 3292 int increment = currentFontSize >= 30 ? 2 : 1; 3293 int newFontSize = currentFontSize + increment * dir; //* 110 / 100; 3294 if (newFontSize > 30) 3295 newFontSize &= 0xFFFE; 3296 if (currentFontSize != newFontSize && newFontSize <= _maxFontSize && newFontSize >= _minFontSize) { 3297 Log.i("Font size in editor ", id, " zoomed to ", newFontSize); 3298 fontSize = cast(ushort)newFontSize; 3299 updateFontProps(); 3300 _needRewrap = true; 3301 measureVisibleText(); 3302 updateScrollBars(); 3303 invalidate(); 3304 } 3305 } 3306 } 3307 return true; 3308 case ToggleBlockComment: 3309 if (!readOnly && _content.syntaxSupport && _content.syntaxSupport.supportsToggleBlockComment && _content.syntaxSupport.canToggleBlockComment(_selectionRange)) 3310 _content.syntaxSupport.toggleBlockComment(_selectionRange, this); 3311 return true; 3312 case ToggleLineComment: 3313 if (!readOnly && _content.syntaxSupport && _content.syntaxSupport.supportsToggleLineComment && _content.syntaxSupport.canToggleLineComment(_selectionRange)) 3314 _content.syntaxSupport.toggleLineComment(_selectionRange, this); 3315 return true; 3316 case AppendNewLine: 3317 if (!readOnly) { 3318 correctCaretPos(); 3319 TextPosition p = _content.lineEnd(_caretPos.line); 3320 TextRange r = TextRange(p, p); 3321 EditOperation op = new EditOperation(EditAction.Replace, r, [""d, ""d]); 3322 _content.performOperation(op, this); 3323 _caretPos = oldCaretPos; 3324 handleEditorStateChange(); 3325 } 3326 return true; 3327 case DeleteLine: 3328 if (!readOnly) { 3329 correctCaretPos(); 3330 EditOperation op = new EditOperation(EditAction.Replace, _content.lineRange(_caretPos.line), [""d]); 3331 _content.performOperation(op, this); 3332 } 3333 return true; 3334 case Find: 3335 openFindPanel(); 3336 return true; 3337 case FindNext: 3338 findNext(false); 3339 return true; 3340 case FindPrev: 3341 findNext(true); 3342 return true; 3343 case Replace: 3344 openReplacePanel(); 3345 return true; 3346 default: 3347 break; 3348 } 3349 return super.handleAction(a); 3350 } 3351 3352 /// calculate full content size in pixels 3353 override Point fullContentSize() { 3354 Point textSz; 3355 textSz.y = _lineHeight * _content.length; 3356 textSz.x = _maxLineWidth; 3357 //int maxy = _lineHeight * 5; // limit measured height 3358 //if (textSz.y > maxy) 3359 // textSz.y = maxy; 3360 return textSz; 3361 } 3362 3363 // override to set minimum scrollwidget size - default 100x100 3364 override protected Point minimumVisibleContentSize() { 3365 FontRef font = font(); 3366 _measuredTextToSetWidgetSizeWidths.length = _textToSetWidgetSize.length; 3367 int charsMeasured = font.measureText(_textToSetWidgetSize, _measuredTextToSetWidgetSizeWidths, MAX_WIDTH_UNSPECIFIED, tabSize); 3368 _measuredTextToSetWidgetSize.x = charsMeasured > 0 ? _measuredTextToSetWidgetSizeWidths[charsMeasured - 1]: 0; 3369 _measuredTextToSetWidgetSize.y = font.height; 3370 return _measuredTextToSetWidgetSize; 3371 } 3372 3373 /// measure 3374 override void measure(int parentWidth, int parentHeight) { 3375 if (visibility == Visibility.Gone) 3376 return; 3377 3378 updateFontProps(); 3379 updateMaxLineWidth(); 3380 int findPanelHeight; 3381 if (_findPanel) { 3382 _findPanel.measure(parentWidth, parentHeight); 3383 findPanelHeight = _findPanel.measuredHeight; 3384 if (parentHeight != SIZE_UNSPECIFIED) 3385 parentHeight -= findPanelHeight; 3386 } 3387 3388 super.measure(parentWidth, parentHeight); 3389 } 3390 3391 3392 protected void highlightTextPattern(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) { 3393 dstring pattern = _textToHighlight; 3394 uint options = _textToHighlightOptions; 3395 if (!pattern.length) { 3396 // support highlighting selection text - if whole word is selected 3397 if (_selectionRange.empty || !_selectionRange.singleLine) 3398 return; 3399 if (_selectionRange.start.line >= _content.length) 3400 return; 3401 dstring selLine = _content.line(_selectionRange.start.line); 3402 int start = _selectionRange.start.pos; 3403 int end = _selectionRange.end.pos; 3404 if (start >= selLine.length) 3405 return; 3406 pattern = selLine[start .. end]; 3407 if (!isWordChar(pattern[0]) || !isWordChar(pattern[$-1])) 3408 return; 3409 if (!isWholeWord(selLine, start, end)) 3410 return; 3411 // whole word is selected - enable highlight for it 3412 options = TextSearchFlag.CaseSensitive | TextSearchFlag.WholeWords; 3413 } 3414 if (!pattern.length) 3415 return; 3416 dstring lineText = _content.line(lineIndex); 3417 if (lineText.length < pattern.length) 3418 return; 3419 ptrdiff_t start = 0; 3420 import std.string : indexOf, CaseSensitive; 3421 import std.typecons : Flag; 3422 bool caseSensitive = (options & TextSearchFlag.CaseSensitive) != 0; 3423 bool wholeWords = (options & TextSearchFlag.WholeWords) != 0; 3424 bool selectionOnly = (options & TextSearchFlag.SelectionOnly) != 0; 3425 for (;;) { 3426 ptrdiff_t pos = lineText[start .. $].indexOf(pattern, caseSensitive ? Yes.caseSensitive : No.caseSensitive); 3427 if (pos < 0) 3428 break; 3429 // found text to highlight 3430 start += pos; 3431 if (!wholeWords || isWholeWord(lineText, start, start + pattern.length)) { 3432 TextRange r = TextRange(TextPosition(lineIndex, cast(int)start), TextPosition(lineIndex, cast(int)(start + pattern.length))); 3433 uint color = r.isInsideOrNext(caretPos) ? _searchHighlightColorCurrent : _searchHighlightColorOther; 3434 highlightLineRange(buf, lineRect, color, r); 3435 } 3436 start += pattern.length; 3437 } 3438 } 3439 3440 static bool isWordChar(dchar ch) { 3441 if (ch >= 'a' && ch <= 'z') 3442 return true; 3443 if (ch >= 'A' && ch <= 'Z') 3444 return true; 3445 if (ch == '_') 3446 return true; 3447 return false; 3448 } 3449 static bool isValidWordBound(dchar innerChar, dchar outerChar) { 3450 return !isWordChar(innerChar) || !isWordChar(outerChar); 3451 } 3452 /// returns true if selected range of string is whole word 3453 static bool isWholeWord(dstring lineText, size_t start, size_t end) { 3454 if (start >= lineText.length || start >= end) 3455 return false; 3456 if (start > 0 && !isValidWordBound(lineText[start], lineText[start - 1])) 3457 return false; 3458 if (end > 0 && end < lineText.length && !isValidWordBound(lineText[end - 1], lineText[end])) 3459 return false; 3460 return true; 3461 } 3462 3463 /// find all occurences of text pattern in content; options = bitset of TextSearchFlag 3464 TextRange[] findAll(dstring pattern, uint options) { 3465 TextRange[] res; 3466 res.assumeSafeAppend(); 3467 if (!pattern.length) 3468 return res; 3469 import std.string : indexOf, CaseSensitive; 3470 bool caseSensitive = (options & TextSearchFlag.CaseSensitive) != 0; 3471 bool wholeWords = (options & TextSearchFlag.WholeWords) != 0; 3472 bool selectionOnly = (options & TextSearchFlag.SelectionOnly) != 0; 3473 for (int i = 0; i < _content.length; i++) { 3474 dstring lineText = _content.line(i); 3475 if (lineText.length < pattern.length) 3476 continue; 3477 ptrdiff_t start = 0; 3478 for (;;) { 3479 ptrdiff_t pos = lineText[start .. $].indexOf(pattern, caseSensitive ? Yes.caseSensitive : No.caseSensitive); 3480 if (pos < 0) 3481 break; 3482 // found text to highlight 3483 start += pos; 3484 if (!wholeWords || isWholeWord(lineText, start, start + pattern.length)) { 3485 TextRange r = TextRange(TextPosition(i, cast(int)start), TextPosition(i, cast(int)(start + pattern.length))); 3486 res ~= r; 3487 } 3488 start += _textToHighlight.length; 3489 } 3490 } 3491 return res; 3492 } 3493 3494 /// find next occurence of text pattern in content, returns true if found 3495 bool findNextPattern(ref TextPosition pos, dstring pattern, uint searchOptions, int direction) { 3496 TextRange[] all = findAll(pattern, searchOptions); 3497 if (!all.length) 3498 return false; 3499 int currentIndex = -1; 3500 int nearestIndex = cast(int)all.length; 3501 for (int i = 0; i < all.length; i++) { 3502 if (all[i].isInsideOrNext(pos)) { 3503 currentIndex = i; 3504 break; 3505 } 3506 } 3507 for (int i = 0; i < all.length; i++) { 3508 if (pos < all[i].start) { 3509 nearestIndex = i; 3510 break; 3511 } 3512 if (pos > all[i].end) { 3513 nearestIndex = i + 1; 3514 } 3515 } 3516 if (currentIndex >= 0) { 3517 if (all.length < 2 && direction != 0) 3518 return false; 3519 currentIndex += direction; 3520 if (currentIndex < 0) 3521 currentIndex = cast(int)all.length - 1; 3522 else if (currentIndex >= all.length) 3523 currentIndex = 0; 3524 pos = all[currentIndex].start; 3525 return true; 3526 } 3527 if (direction < 0) 3528 nearestIndex--; 3529 if (nearestIndex < 0) 3530 nearestIndex = cast(int)all.length - 1; 3531 else if (nearestIndex >= all.length) 3532 nearestIndex = 0; 3533 pos = all[nearestIndex].start; 3534 return true; 3535 } 3536 3537 protected void highlightLineRange(DrawBuf buf, Rect lineRect, uint color, TextRange r) { 3538 Rect startrc = textPosToClient(r.start); 3539 Rect endrc = textPosToClient(r.end); 3540 Rect rc = lineRect; 3541 rc.left = _clientRect.left + startrc.left; 3542 rc.right = _clientRect.left + endrc.right; 3543 if (_wordWrap && !rc.empty) 3544 { 3545 wordWrapFillRect(buf, r.start.line, rc, color); 3546 } 3547 else if (!rc.empty) { 3548 // draw selection rect for matching bracket 3549 buf.fillRect(rc, color); 3550 } 3551 } 3552 3553 /// Used in place of directly calling buf.fillRect in word wrap mode 3554 void wordWrapFillRect(DrawBuf buf, int line, Rect lineToDivide, uint color) 3555 { 3556 Rect rc = lineToDivide; 3557 auto limitNumber = (int num, int limit) => num > limit ? limit : num; 3558 LineSpan curSpan = getSpan(line); 3559 int yOffset = _lineHeight * (wrapsUpTo(line)); 3560 rc.offset(0, yOffset); 3561 Rect[] wrappedSelection; 3562 wrappedSelection.length = curSpan.len; 3563 foreach (size_t i_, wrapLineRect; wrappedSelection) 3564 { 3565 int i = cast(int)i_; 3566 int startingDifference = rc.left - _clientRect.left; 3567 wrapLineRect = rc; 3568 wrapLineRect.offset(-1 * curSpan.accumulation(cast(int)i, LineSpan.WrapPointInfo.Width), cast(int)i * _lineHeight); 3569 wrapLineRect.right = limitNumber(wrapLineRect.right,(rc.left + curSpan.wrapPoints[i].wrapWidth) - startingDifference); 3570 buf.fillRect(wrapLineRect, color); 3571 } 3572 } 3573 3574 /// override to custom highlight of line background 3575 protected void drawLineBackground(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) { 3576 // highlight odd lines 3577 //if ((lineIndex & 1)) 3578 // buf.fillRect(visibleRect, 0xF4808080); 3579 3580 if (!_selectionRange.empty && _selectionRange.start.line <= lineIndex && _selectionRange.end.line >= lineIndex) { 3581 // line inside selection 3582 Rect startrc = textPosToClient(_selectionRange.start); 3583 Rect endrc = textPosToClient(_selectionRange.end); 3584 int startx = lineIndex == _selectionRange.start.line ? startrc.left + _clientRect.left : lineRect.left; 3585 int endx = lineIndex == _selectionRange.end.line ? endrc.left + _clientRect.left : lineRect.right + _spaceWidth; 3586 Rect rc = lineRect; 3587 rc.left = startx; 3588 rc.right = endx; 3589 if (!rc.empty && _wordWrap) 3590 { 3591 wordWrapFillRect(buf, lineIndex, rc, focused ? _selectionColorFocused : _selectionColorNormal); 3592 } 3593 else if (!rc.empty) { 3594 // draw selection rect for line 3595 buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal); 3596 } 3597 } 3598 3599 highlightTextPattern(buf, lineIndex, lineRect, visibleRect); 3600 3601 if (_matchingBraces.start.line == lineIndex) { 3602 TextRange r = TextRange(_matchingBraces.start, _matchingBraces.start.offset(1)); 3603 highlightLineRange(buf, lineRect, _matchingBracketHightlightColor, r); 3604 } 3605 if (_matchingBraces.end.line == lineIndex) { 3606 TextRange r = TextRange(_matchingBraces.end, _matchingBraces.end.offset(1)); 3607 highlightLineRange(buf, lineRect, _matchingBracketHightlightColor, r); 3608 } 3609 3610 // frame around current line 3611 if (focused && lineIndex == _caretPos.line && _selectionRange.singleLine && _selectionRange.start.line == _caretPos.line) { 3612 //TODO: Figure out why a little slow to catch up 3613 if (_wordWrap) 3614 visibleRect.offset(0, -caretHeightOffset); 3615 buf.drawFrame(visibleRect, 0xA0808080, Rect(1,1,1,1)); 3616 } 3617 3618 } 3619 3620 override protected void drawExtendedArea(DrawBuf buf) { 3621 if (_leftPaneWidth <= 0) 3622 return; 3623 Rect rc = _clientRect; 3624 3625 FontRef font = font(); 3626 int i = _firstVisibleLine; 3627 int lc = lineCount; 3628 for (;;) { 3629 Rect lineRect = rc; 3630 lineRect.left = _clientRect.left - _leftPaneWidth; 3631 lineRect.right = _clientRect.left; 3632 lineRect.bottom = lineRect.top + _lineHeight; 3633 if (lineRect.top >= _clientRect.bottom) 3634 break; 3635 drawLeftPane(buf, lineRect, i < lc ? i : -1); 3636 rc.top += _lineHeight; 3637 if (_wordWrap) 3638 { 3639 int currentWrap = 1; 3640 for (;;) 3641 { 3642 LineSpan curSpan = getSpan(i); 3643 if (currentWrap > curSpan.len - 1) 3644 break; 3645 Rect lineRect2 = rc; 3646 lineRect2.left = _clientRect.left - _leftPaneWidth; 3647 lineRect2.right = _clientRect.left; 3648 lineRect2.bottom = lineRect.top + _lineHeight; 3649 if (lineRect2.top >= _clientRect.bottom) 3650 break; 3651 drawLeftPane(buf, lineRect2, -1); 3652 rc.top += _lineHeight; 3653 3654 currentWrap++; 3655 } 3656 } 3657 i++; 3658 } 3659 } 3660 3661 3662 protected CustomCharProps[ubyte] _tokenHighlightColors; 3663 3664 /// set highlight options for particular token category 3665 void setTokenHightlightColor(ubyte tokenCategory, uint color, bool underline = false, bool strikeThrough = false) { 3666 _tokenHighlightColors[tokenCategory] = CustomCharProps(color, underline, strikeThrough); 3667 } 3668 /// clear highlight colors 3669 void clearTokenHightlightColors() { 3670 destroy(_tokenHighlightColors); 3671 } 3672 3673 /** 3674 Custom text color and style highlight (using text highlight) support. 3675 3676 Return null if no syntax highlight required for line. 3677 */ 3678 protected CustomCharProps[] handleCustomLineHighlight(int line, dstring txt, ref CustomCharProps[] buf) { 3679 if (!_tokenHighlightColors) 3680 return null; // no highlight colors set 3681 TokenPropString tokenProps = _content.lineTokenProps(line); 3682 if (tokenProps.length > 0) { 3683 bool hasNonzeroTokens = false; 3684 foreach(t; tokenProps) 3685 if (t) { 3686 hasNonzeroTokens = true; 3687 break; 3688 } 3689 if (!hasNonzeroTokens) 3690 return null; // all characters are of unknown token type (or white space) 3691 if (buf.length < tokenProps.length) 3692 buf.length = tokenProps.length; 3693 CustomCharProps[] colors = buf[0..tokenProps.length]; //new CustomCharProps[tokenProps.length]; 3694 for (int i = 0; i < tokenProps.length; i++) { 3695 ubyte p = tokenProps[i]; 3696 if (p in _tokenHighlightColors) 3697 colors[i] = _tokenHighlightColors[p]; 3698 else if ((p & TOKEN_CATEGORY_MASK) in _tokenHighlightColors) 3699 colors[i] = _tokenHighlightColors[(p & TOKEN_CATEGORY_MASK)]; 3700 else 3701 colors[i].color = textColor; 3702 if (isFullyTransparentColor(colors[i].color)) 3703 colors[i].color = textColor; 3704 } 3705 return colors; 3706 } 3707 return null; 3708 } 3709 3710 TextRange _matchingBraces; 3711 3712 bool _showWhiteSpaceMarks; 3713 /// when true, show marks for tabs and spaces at beginning and end of line, and tabs inside line 3714 @property bool showWhiteSpaceMarks() const { return _showWhiteSpaceMarks; } 3715 @property void showWhiteSpaceMarks(bool show) { 3716 if (_showWhiteSpaceMarks != show) { 3717 _showWhiteSpaceMarks = show; 3718 invalidate(); 3719 } 3720 } 3721 3722 /// find max tab mark column position for line 3723 protected int findMaxTabMarkColumn(int lineIndex) { 3724 if (lineIndex < 0 || lineIndex >= content.length) 3725 return -1; 3726 int maxSpace = -1; 3727 auto space = content.getLineWhiteSpace(lineIndex); 3728 maxSpace = space.firstNonSpaceColumn; 3729 if (maxSpace >= 0) 3730 return maxSpace; 3731 for(int i = lineIndex - 1; i >= 0; i--) { 3732 space = content.getLineWhiteSpace(i); 3733 if (!space.empty) { 3734 maxSpace = space.firstNonSpaceColumn; 3735 break; 3736 } 3737 } 3738 for(int i = lineIndex + 1; i < content.length; i++) { 3739 space = content.getLineWhiteSpace(i); 3740 if (!space.empty) { 3741 if (maxSpace < 0 || maxSpace < space.firstNonSpaceColumn) 3742 maxSpace = space.firstNonSpaceColumn; 3743 break; 3744 } 3745 } 3746 return maxSpace; 3747 } 3748 3749 void drawTabPositionMarks(DrawBuf buf, ref FontRef font, int lineIndex, Rect lineRect) { 3750 int maxCol = findMaxTabMarkColumn(lineIndex); 3751 if (maxCol > 0) { 3752 int spaceWidth = font.charWidth(' '); 3753 Rect rc = lineRect; 3754 uint color = addAlpha(textColor, 0xC0); 3755 for (int i = 0; i < maxCol; i += tabSize) { 3756 rc.left = lineRect.left + i * spaceWidth; 3757 rc.right = rc.left + 1; 3758 buf.fillRectPattern(rc, color, PatternType.dotted); 3759 } 3760 } 3761 } 3762 3763 void drawWhiteSpaceMarks(DrawBuf buf, ref FontRef font, dstring txt, int tabSize, Rect lineRect, Rect visibleRect) { 3764 // _showTabPositionMarks 3765 // _showWhiteSpaceMarks 3766 int firstNonSpace = -1; 3767 int lastNonSpace = -1; 3768 bool hasTabs = false; 3769 for(int i = 0; i < txt.length; i++) { 3770 if (txt[i] == '\t') { 3771 hasTabs = true; 3772 } else if (txt[i] != ' ') { 3773 if (firstNonSpace == -1) 3774 firstNonSpace = i; 3775 lastNonSpace = i + 1; 3776 } 3777 } 3778 bool spacesOnly = txt.length > 0 && firstNonSpace < 0; 3779 if (firstNonSpace <= 0 && lastNonSpace >= txt.length && !hasTabs && !spacesOnly) 3780 return; 3781 uint color = addAlpha(textColor, 0xC0); 3782 static int[] textSizeBuffer; 3783 int charsMeasured = font.measureText(txt, textSizeBuffer, MAX_WIDTH_UNSPECIFIED, tabSize, 0, 0); 3784 int ts = tabSize; 3785 if (ts < 1) 3786 ts = 1; 3787 if (ts > 8) 3788 ts = 8; 3789 int spaceIndex = 0; 3790 for (int i = 0; i < txt.length && i < charsMeasured; i++) { 3791 dchar ch = txt[i]; 3792 bool outsideText = (i < firstNonSpace || i >= lastNonSpace || spacesOnly); 3793 if ((ch == ' ' && outsideText) || ch == '\t') { 3794 Rect rc = lineRect; 3795 rc.left = lineRect.left + (i > 0 ? textSizeBuffer[i - 1] : 0); 3796 rc.right = lineRect.left + textSizeBuffer[i]; 3797 int h = rc.height; 3798 if (rc.intersects(visibleRect)) { 3799 // draw space mark 3800 if (ch == ' ') { 3801 // space 3802 int sz = h / 6; 3803 if (sz < 1) 3804 sz = 1; 3805 rc.top += h / 2 - sz / 2; 3806 rc.bottom = rc.top + sz; 3807 rc.left += rc.width / 2 - sz / 2; 3808 rc.right = rc.left + sz; 3809 buf.fillRect(rc, color); 3810 } else if (ch == '\t') { 3811 // tab 3812 Point p1 = Point(rc.left + 1, rc.top + h / 2); 3813 Point p2 = p1; 3814 p2.x = rc.right - 1; 3815 int sz = h / 4; 3816 if (sz < 2) 3817 sz = 2; 3818 if (sz > p2.x - p1.x) 3819 sz = p2.x - p1.x; 3820 buf.drawLine(p1, p2, color); 3821 buf.drawLine(p2, Point(p2.x - sz, p2.y - sz), color); 3822 buf.drawLine(p2, Point(p2.x - sz, p2.y + sz), color); 3823 } 3824 } 3825 } 3826 } 3827 } 3828 3829 /// Clear _span 3830 void resetVisibleSpans() 3831 { 3832 //TODO: Don't erase spans which have not been modified, cache them 3833 _span = []; 3834 } 3835 3836 private bool _needRewrap = true; 3837 private int lastStartingLine; 3838 3839 override protected void drawClient(DrawBuf buf) { 3840 // update matched braces 3841 if (!content.findMatchedBraces(_caretPos, _matchingBraces)) { 3842 _matchingBraces.start.line = -1; 3843 _matchingBraces.end.line = -1; 3844 } 3845 3846 Rect rc = _clientRect; 3847 3848 if (_contentChanged) 3849 _needRewrap = true; 3850 if (lastStartingLine != _firstVisibleLine) 3851 { 3852 _needRewrap = true; 3853 lastStartingLine = _firstVisibleLine; 3854 } 3855 if (rc.width <= 0 && _wordWrap) 3856 { 3857 //Prevent drawClient from getting stuck in loop 3858 return; 3859 } 3860 bool doRewrap = false; 3861 if (_needRewrap && _wordWrap) 3862 { 3863 resetVisibleSpans(); 3864 _needRewrap = false; 3865 doRewrap = true; 3866 } 3867 3868 FontRef font = font(); 3869 int previousWraps; 3870 for (int i = 0; i < _visibleLines.length; i++) { 3871 dstring txt = _visibleLines[i]; 3872 Rect lineRect; 3873 lineRect.left = _clientRect.left - _scrollPos.x; 3874 lineRect.right = lineRect.left + calcLineWidth(_content[_firstVisibleLine + i]); 3875 lineRect.top = _clientRect.top + i * _lineHeight; 3876 lineRect.bottom = lineRect.top + _lineHeight; 3877 Rect visibleRect = lineRect; 3878 visibleRect.left = _clientRect.left; 3879 visibleRect.right = _clientRect.right; 3880 drawLineBackground(buf, _firstVisibleLine + i, lineRect, visibleRect); 3881 if (_showTabPositionMarks) 3882 drawTabPositionMarks(buf, font, _firstVisibleLine + i, lineRect); 3883 if (!txt.length && !_wordWrap) 3884 continue; 3885 if (_showWhiteSpaceMarks) 3886 { 3887 Rect whiteSpaceRc = lineRect; 3888 Rect whiteSpaceRcVisible = visibleRect; 3889 for(int z; z < previousWraps; z++) 3890 { 3891 whiteSpaceRc.offset(0, _lineHeight); 3892 whiteSpaceRcVisible.offset(0, _lineHeight); 3893 } 3894 drawWhiteSpaceMarks(buf, font, txt, tabSize, whiteSpaceRc, whiteSpaceRcVisible); 3895 } 3896 if (_leftPaneWidth > 0) { 3897 Rect leftPaneRect = visibleRect; 3898 leftPaneRect.right = leftPaneRect.left; 3899 leftPaneRect.left -= _leftPaneWidth; 3900 drawLeftPane(buf, leftPaneRect, 0); 3901 } 3902 if (txt.length > 0 || _wordWrap) { 3903 CustomCharProps[] highlight = _visibleLinesHighlights[i]; 3904 if (_wordWrap) 3905 { 3906 dstring[] wrappedLine; 3907 if (doRewrap) 3908 wrappedLine = wrapLine(txt, _firstVisibleLine + i); 3909 else 3910 if (i < _span.length) 3911 wrappedLine = _span[i].wrappedContent; 3912 int accumulativeLength; 3913 CustomCharProps[] wrapProps; 3914 foreach (size_t q_, curWrap; wrappedLine) 3915 { 3916 int q = cast(int)q_; 3917 auto lineOffset = q + i + wrapsUpTo(i + _firstVisibleLine); 3918 if (highlight) 3919 { 3920 wrapProps = highlight[accumulativeLength .. $]; 3921 accumulativeLength += curWrap.length; 3922 font.drawColoredText(buf, rc.left - _scrollPos.x, rc.top + lineOffset * _lineHeight, curWrap, wrapProps, tabSize); 3923 } 3924 else 3925 font.drawText(buf, rc.left - _scrollPos.x, rc.top + lineOffset * _lineHeight, curWrap, textColor, tabSize); 3926 3927 } 3928 previousWraps += to!int(wrappedLine.length - 1); 3929 } 3930 else 3931 { 3932 if (highlight) 3933 font.drawColoredText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, highlight, tabSize); 3934 else 3935 font.drawText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, textColor, tabSize); 3936 } 3937 } 3938 } 3939 3940 drawCaret(buf); 3941 } 3942 3943 protected override bool onLeftPaneMouseClick(MouseEvent event) { 3944 if (_leftPaneWidth <= 0) 3945 return false; 3946 Rect rc = _clientRect; 3947 FontRef font = font(); 3948 int i = _firstVisibleLine; 3949 int lc = lineCount; 3950 for (;;) { 3951 Rect lineRect = rc; 3952 lineRect.left = _clientRect.left - _leftPaneWidth; 3953 lineRect.right = _clientRect.left; 3954 lineRect.bottom = lineRect.top + _lineHeight; 3955 if (lineRect.top >= _clientRect.bottom) 3956 break; 3957 if (event.y >= lineRect.top && event.y < lineRect.bottom) { 3958 return handleLeftPaneMouseClick(event, lineRect, i); 3959 } 3960 i++; 3961 rc.top += _lineHeight; 3962 } 3963 return false; 3964 } 3965 3966 override protected MenuItem getLeftPaneIconsPopupMenu(int line) { 3967 MenuItem menu = new MenuItem(); 3968 Action toggleBookmarkAction = ACTION_EDITOR_TOGGLE_BOOKMARK.clone(); 3969 toggleBookmarkAction.longParam = line; 3970 toggleBookmarkAction.objectParam = this; 3971 MenuItem item = menu.add(toggleBookmarkAction); 3972 return menu; 3973 } 3974 3975 protected FindPanel _findPanel; 3976 3977 dstring selectionText(bool singleLineOnly = false) { 3978 TextRange range = _selectionRange; 3979 if (range.empty) { 3980 return null; 3981 } 3982 dstring res = getRangeText(range); 3983 if (singleLineOnly) { 3984 for (int i = 0; i < res.length; i++) { 3985 if (res[i] == '\n') { 3986 res = res[0 .. i]; 3987 break; 3988 } 3989 } 3990 } 3991 return res; 3992 } 3993 3994 protected void findNext(bool backward) { 3995 createFindPanel(false, false); 3996 _findPanel.findNext(backward); 3997 // don't change replace mode 3998 } 3999 4000 protected void openFindPanel() { 4001 createFindPanel(false, false); 4002 _findPanel.replaceMode = false; 4003 _findPanel.activate(); 4004 } 4005 4006 protected void openReplacePanel() { 4007 createFindPanel(false, true); 4008 _findPanel.replaceMode = true; 4009 _findPanel.activate(); 4010 } 4011 4012 /// create find panel; returns true if panel was not yet visible 4013 protected bool createFindPanel(bool selectionOnly, bool replaceMode) { 4014 bool res = false; 4015 dstring txt = selectionText(true); 4016 if (!_findPanel) { 4017 _findPanel = new FindPanel(this, selectionOnly, replaceMode, txt); 4018 addChild(_findPanel); 4019 res = true; 4020 } else { 4021 if (_findPanel.visibility != Visibility.Visible) { 4022 _findPanel.visibility = Visibility.Visible; 4023 if (txt.length) 4024 _findPanel.searchText = txt; 4025 res = true; 4026 } 4027 } 4028 if (!pos.empty) 4029 layout(pos); 4030 requestLayout(); 4031 return res; 4032 } 4033 4034 /// close find panel 4035 protected void closeFindPanel(bool hideOnly = true) { 4036 if (_findPanel) { 4037 setFocus(); 4038 if (hideOnly) { 4039 _findPanel.visibility = Visibility.Gone; 4040 } else { 4041 removeChild(_findPanel); 4042 destroy(_findPanel); 4043 _findPanel = null; 4044 requestLayout(); 4045 } 4046 } 4047 } 4048 4049 /// Draw widget at its position to buffer 4050 override void onDraw(DrawBuf buf) { 4051 if (visibility != Visibility.Visible) 4052 return; 4053 super.onDraw(buf); 4054 if (_findPanel && _findPanel.visibility == Visibility.Visible) { 4055 _findPanel.onDraw(buf); 4056 } 4057 } 4058 } 4059 4060 /// Read only edit box for displaying logs with lines append operation 4061 class LogWidget : EditBox { 4062 4063 protected int _maxLines; 4064 /// max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit 4065 @property int maxLines() { return _maxLines; } 4066 /// set max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit 4067 @property void maxLines(int n) { _maxLines = n; } 4068 4069 protected bool _scrollLock; 4070 /// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction) 4071 @property bool scrollLock() { return _scrollLock; } 4072 /// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction) 4073 @property void scrollLock(bool flg) { _scrollLock = flg; } 4074 4075 this() { 4076 this(null); 4077 } 4078 4079 this(string ID) { 4080 super(ID); 4081 styleId = STYLE_LOG_WIDGET; 4082 _scrollLock = true; 4083 _enableScrollAfterText = false; 4084 enabled = false; 4085 minFontSize(pointsToPixels(6)).maxFontSize(pointsToPixels(32)); // allow font zoom with Ctrl + MouseWheel 4086 onThemeChanged(); 4087 } 4088 4089 /// append lines to the end of text 4090 void appendText(dstring text) { 4091 import std.array : split; 4092 if (text.length == 0) 4093 return; 4094 dstring[] lines = text.split("\n"); 4095 //lines ~= ""d; // append new line after last line 4096 content.appendLines(lines); 4097 if (_maxLines > 0 && lineCount > _maxLines) { 4098 TextRange range; 4099 range.end.line = lineCount - _maxLines; 4100 EditOperation op = new EditOperation(EditAction.Replace, range, [""d]); 4101 _content.performOperation(op, this); 4102 _contentChanged = true; 4103 } 4104 updateScrollBars(); 4105 if (_scrollLock) { 4106 _caretPos = lastLineBegin(); 4107 ensureCaretVisible(); 4108 } 4109 } 4110 4111 TextPosition lastLineBegin() { 4112 TextPosition res; 4113 if (_content.length == 0) 4114 return res; 4115 if (_content.lineLength(_content.length - 1) == 0 && _content.length > 1) 4116 res.line = _content.length - 2; 4117 else 4118 res.line = _content.length - 1; 4119 return res; 4120 } 4121 4122 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 4123 override void layout(Rect rc) { 4124 if (visibility == Visibility.Gone) 4125 return; 4126 4127 super.layout(rc); 4128 if (_scrollLock) { 4129 measureVisibleText(); 4130 _caretPos = lastLineBegin(); 4131 ensureCaretVisible(); 4132 } 4133 } 4134 4135 } 4136 4137 class FindPanel : HorizontalLayout { 4138 protected EditBox _editor; 4139 protected EditLine _edFind; 4140 protected EditLine _edReplace; 4141 protected ImageCheckButton _cbCaseSensitive; 4142 protected ImageCheckButton _cbWholeWords; 4143 protected CheckBox _cbSelection; 4144 protected Button _btnFindNext; 4145 protected Button _btnFindPrev; 4146 protected Button _btnReplace; 4147 protected Button _btnReplaceAndFind; 4148 protected Button _btnReplaceAll; 4149 protected ImageButton _btnClose; 4150 protected bool _replaceMode; 4151 /// returns true if panel is working in replace mode 4152 @property bool replaceMode() { return _replaceMode; } 4153 @property FindPanel replaceMode(bool newMode) { 4154 if (newMode != _replaceMode) { 4155 _replaceMode = newMode; 4156 childById("replace").visibility = newMode ? Visibility.Visible : Visibility.Gone; 4157 } 4158 return this; 4159 } 4160 4161 @property dstring searchText() { 4162 return _edFind.text; 4163 } 4164 4165 @property FindPanel searchText(dstring newText) { 4166 _edFind.text = newText; 4167 return this; 4168 } 4169 4170 this(EditBox editor, bool selectionOnly, bool replace, dstring initialText = ""d) { 4171 _replaceMode = replace; 4172 import dlangui.dml.parser; 4173 try { 4174 parseML(q{ 4175 { 4176 layoutWidth: fill 4177 VerticalLayout { 4178 layoutWidth: fill 4179 HorizontalLayout { 4180 layoutWidth: fill 4181 EditLine { id: edFind; layoutWidth: fill; alignment: vcenter } 4182 Button { id: btnFindNext; text: EDIT_FIND_NEXT } 4183 Button { id: btnFindPrev; text: EDIT_FIND_PREV } 4184 VerticalLayout { 4185 VSpacer {} 4186 HorizontalLayout { 4187 ImageCheckButton { id: cbCaseSensitive; drawableId: "find_case_sensitive"; tooltipText: EDIT_FIND_CASE_SENSITIVE; styleId: TOOLBAR_BUTTON; alignment: vcenter } 4188 ImageCheckButton { id: cbWholeWords; drawableId: "find_whole_words"; tooltipText: EDIT_FIND_WHOLE_WORDS; styleId: TOOLBAR_BUTTON; alignment: vcenter } 4189 CheckBox { id: cbSelection; text: "Sel" } 4190 } 4191 VSpacer {} 4192 } 4193 } 4194 HorizontalLayout { 4195 id: replace 4196 layoutWidth: fill; 4197 EditLine { id: edReplace; layoutWidth: fill; alignment: vcenter } 4198 Button { id: btnReplace; text: EDIT_REPLACE_NEXT } 4199 Button { id: btnReplaceAndFind; text: EDIT_REPLACE_AND_FIND } 4200 Button { id: btnReplaceAll; text: EDIT_REPLACE_ALL } 4201 } 4202 } 4203 VerticalLayout { 4204 VSpacer {} 4205 ImageButton { id: btnClose; drawableId: close; styleId: BUTTON_TRANSPARENT } 4206 VSpacer {} 4207 } 4208 } 4209 }, null, this); 4210 } catch (Exception e) { 4211 Log.e("Exception while parsing DML: ", e); 4212 } 4213 _editor = editor; 4214 _edFind = childById!EditLine("edFind"); 4215 _edReplace = childById!EditLine("edReplace"); 4216 4217 if (initialText.length) { 4218 _edFind.text = initialText; 4219 _edReplace.text = initialText; 4220 } 4221 4222 _edFind.editorAction.connect(&onFindEditorAction); 4223 _edFind.contentChange.connect(&onFindTextChange); 4224 4225 //_edFind.keyEvent = &onEditorKeyEvent; 4226 //_edReplace.keyEvent = &onEditorKeyEvent; 4227 4228 _btnFindNext = childById!Button("btnFindNext"); 4229 _btnFindNext.click = &onButtonClick; 4230 _btnFindPrev = childById!Button("btnFindPrev"); 4231 _btnFindPrev.click = &onButtonClick; 4232 _btnReplace = childById!Button("btnReplace"); 4233 _btnReplace.click = &onButtonClick; 4234 _btnReplaceAndFind = childById!Button("btnReplaceAndFind"); 4235 _btnReplaceAndFind.click = &onButtonClick; 4236 _btnReplaceAll = childById!Button("btnReplaceAll"); 4237 _btnReplaceAll.click = &onButtonClick; 4238 _btnClose = childById!ImageButton("btnClose"); 4239 _btnClose.click = &onButtonClick; 4240 _cbCaseSensitive = childById!ImageCheckButton("cbCaseSensitive"); 4241 _cbWholeWords = childById!ImageCheckButton("cbWholeWords"); 4242 _cbSelection = childById!CheckBox("cbSelection"); 4243 _cbCaseSensitive.checkChange = &onCaseSensitiveCheckChange; 4244 _cbWholeWords.checkChange = &onCaseSensitiveCheckChange; 4245 _cbSelection.checkChange = &onCaseSensitiveCheckChange; 4246 focusGroup = true; 4247 if (!replace) 4248 childById("replace").visibility = Visibility.Gone; 4249 //_edFind = new EditLine("edFind" 4250 dstring currentText = _edFind.text; 4251 Log.d("currentText=", currentText); 4252 setDirection(false); 4253 updateHighlight(); 4254 } 4255 void activate() { 4256 _edFind.setFocus(); 4257 dstring currentText = _edFind.text; 4258 Log.d("activate.currentText=", currentText); 4259 _edFind.setCaretPos(0, cast(int)currentText.length, true); 4260 } 4261 4262 bool onButtonClick(Widget source) { 4263 switch (source.id) { 4264 case "btnFindNext": 4265 findNext(false); 4266 return true; 4267 case "btnFindPrev": 4268 findNext(true); 4269 return true; 4270 case "btnClose": 4271 close(); 4272 return true; 4273 case "btnReplace": 4274 replaceOne(); 4275 return true; 4276 case "btnReplaceAndFind": 4277 replaceOne(); 4278 findNext(_backDirection); 4279 return true; 4280 case "btnReplaceAll": 4281 replaceAll(); 4282 return true; 4283 default: 4284 return true; 4285 } 4286 } 4287 4288 void close() { 4289 _editor.setTextToHighlight(null, 0); 4290 _editor.closeFindPanel(); 4291 } 4292 4293 override bool onKeyEvent(KeyEvent event) { 4294 if (event.keyCode == KeyCode.TAB) 4295 return super.onKeyEvent(event); 4296 if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.ESCAPE) { 4297 close(); 4298 return true; 4299 } 4300 return true; 4301 } 4302 4303 /// override to handle specific actions 4304 override bool handleAction(const Action a) { 4305 switch (a.id) { 4306 case EditorActions.FindNext: 4307 findNext(false); 4308 return true; 4309 case EditorActions.FindPrev: 4310 findNext(true); 4311 return true; 4312 default: 4313 return false; 4314 } 4315 } 4316 4317 protected bool _backDirection; 4318 void setDirection(bool back) { 4319 _backDirection = back; 4320 if (back) { 4321 _btnFindNext.resetState(State.Default); 4322 _btnFindPrev.setState(State.Default); 4323 } else { 4324 _btnFindNext.setState(State.Default); 4325 _btnFindPrev.resetState(State.Default); 4326 } 4327 } 4328 4329 uint makeSearchFlags() { 4330 uint res = 0; 4331 if (_cbCaseSensitive.checked) 4332 res |= TextSearchFlag.CaseSensitive; 4333 if (_cbWholeWords.checked) 4334 res |= TextSearchFlag.WholeWords; 4335 if (_cbSelection.checked) 4336 res |= TextSearchFlag.SelectionOnly; 4337 return res; 4338 } 4339 bool findNext(bool back) { 4340 setDirection(back); 4341 dstring currentText = _edFind.text; 4342 Log.d("findNext text=", currentText, " back=", back); 4343 if (!currentText.length) 4344 return false; 4345 _editor.setTextToHighlight(currentText, makeSearchFlags); 4346 TextPosition pos = _editor.caretPos; 4347 bool res = _editor.findNextPattern(pos, currentText, makeSearchFlags, back ? -1 : 1); 4348 if (res) { 4349 _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)currentText.length)); 4350 _editor.ensureCaretVisible(); 4351 //_editor.setCaretPos(pos.line, pos.pos, true); 4352 } 4353 return res; 4354 } 4355 4356 bool replaceOne() { 4357 dstring currentText = _edFind.text; 4358 dstring newText = _edReplace.text; 4359 Log.d("replaceOne text=", currentText, " back=", _backDirection, " newText=", newText); 4360 if (!currentText.length) 4361 return false; 4362 _editor.setTextToHighlight(currentText, makeSearchFlags); 4363 TextPosition pos = _editor.caretPos; 4364 bool res = _editor.findNextPattern(pos, currentText, makeSearchFlags, 0); 4365 if (res) { 4366 _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)currentText.length)); 4367 _editor.replaceSelectionText(newText); 4368 _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)newText.length)); 4369 _editor.ensureCaretVisible(); 4370 //_editor.setCaretPos(pos.line, pos.pos, true); 4371 } 4372 return res; 4373 } 4374 4375 int replaceAll() { 4376 int count = 0; 4377 for(int i = 0; ; i++) { 4378 debug Log.d("replaceAll - calling replaceOne, iteration ", i); 4379 if (!replaceOne()) 4380 break; 4381 count++; 4382 TextPosition initialPosition = _editor.caretPos; 4383 debug Log.d("replaceAll - position is ", initialPosition); 4384 if (!findNext(_backDirection)) 4385 break; 4386 TextPosition newPosition = _editor.caretPos; 4387 debug Log.d("replaceAll - next position is ", newPosition); 4388 if (_backDirection && newPosition >= initialPosition) 4389 break; 4390 if (!_backDirection && newPosition <= initialPosition) 4391 break; 4392 } 4393 debug Log.d("replaceAll - done, replace count = ", count); 4394 _editor.ensureCaretVisible(); 4395 return count; 4396 } 4397 4398 void updateHighlight() { 4399 dstring currentText = _edFind.text; 4400 Log.d("onFindTextChange.currentText=", currentText); 4401 _editor.setTextToHighlight(currentText, makeSearchFlags); 4402 } 4403 4404 void onFindTextChange(EditableContent source) { 4405 Log.d("onFindTextChange"); 4406 updateHighlight(); 4407 } 4408 4409 bool onCaseSensitiveCheckChange(Widget source, bool checkValue) { 4410 updateHighlight(); 4411 return true; 4412 } 4413 4414 bool onFindEditorAction(const Action action) { 4415 Log.d("onFindEditorAction ", action); 4416 if (action.id == EditorActions.InsertNewLine) { 4417 findNext(_backDirection); 4418 return true; 4419 } 4420 return false; 4421 } 4422 } 4423 4424 //import dlangui.widgets.metadata; 4425 //mixin(registerWidgets!(EditLine, EditBox, LogWidget)());