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 dlangui.core.streams; 41 42 /// Modified state change listener 43 interface ModifiedStateListener { 44 void onModifiedStateChange(Widget source, bool modified); 45 } 46 47 /// Modified content listener 48 interface EditableContentChangeListener { 49 void onEditableContentChanged(EditableContent source); 50 } 51 52 /// Editor action codes 53 enum EditorActions : int { 54 None = 0, 55 /// move cursor one char left 56 Left = 1000, 57 /// move cursor one char left with selection 58 SelectLeft, 59 /// move cursor one char right 60 Right, 61 /// move cursor one char right with selection 62 SelectRight, 63 /// move cursor one line up 64 Up, 65 /// move cursor one line up with selection 66 SelectUp, 67 /// move cursor one line down 68 Down, 69 /// move cursor one line down with selection 70 SelectDown, 71 /// move cursor one word left 72 WordLeft, 73 /// move cursor one word left with selection 74 SelectWordLeft, 75 /// move cursor one word right 76 WordRight, 77 /// move cursor one word right with selection 78 SelectWordRight, 79 /// move cursor one page up 80 PageUp, 81 /// move cursor one page up with selection 82 SelectPageUp, 83 /// move cursor one page down 84 PageDown, 85 /// move cursor one page down with selection 86 SelectPageDown, 87 /// move cursor to the beginning of page 88 PageBegin, 89 /// move cursor to the beginning of page with selection 90 SelectPageBegin, 91 /// move cursor to the end of page 92 PageEnd, 93 /// move cursor to the end of page with selection 94 SelectPageEnd, 95 /// move cursor to the beginning of line 96 LineBegin, 97 /// move cursor to the beginning of line with selection 98 SelectLineBegin, 99 /// move cursor to the end of line 100 LineEnd, 101 /// move cursor to the end of line with selection 102 SelectLineEnd, 103 /// move cursor to the beginning of document 104 DocumentBegin, 105 /// move cursor to the beginning of document with selection 106 SelectDocumentBegin, 107 /// move cursor to the end of document 108 DocumentEnd, 109 /// move cursor to the end of document with selection 110 SelectDocumentEnd, 111 /// delete char before cursor (backspace) 112 DelPrevChar, 113 /// delete char after cursor (del key) 114 DelNextChar, 115 /// delete word before cursor (ctrl + backspace) 116 DelPrevWord, 117 /// delete char after cursor (ctrl + del key) 118 DelNextWord, 119 120 /// insert new line (Enter) 121 InsertNewLine, 122 /// insert new line before current position (Ctrl+Enter) 123 PrependNewLine, 124 /// insert new line after current position (Ctrl+Enter) 125 AppendNewLine, 126 127 /// Turn On/Off replace mode 128 ToggleReplaceMode, 129 130 /// Copy selection to clipboard 131 Copy, 132 /// Cut selection to clipboard 133 Cut, 134 /// Paste selection from clipboard 135 Paste, 136 /// Undo last change 137 Undo, 138 /// Redo last undoed change 139 Redo, 140 141 /// Tab (e.g., Tab key to insert tab character or indent text) 142 Tab, 143 /// Tab (unindent text, or remove whitespace before cursor, usually Shift+Tab) 144 BackTab, 145 /// Indent text block or single line 146 Indent, 147 /// Unindent text 148 Unindent, 149 150 /// Select whole content (usually, Ctrl+A) 151 SelectAll, 152 153 // Scroll operations 154 155 /// Scroll one line up (not changing cursor) 156 ScrollLineUp, 157 /// Scroll one line down (not changing cursor) 158 ScrollLineDown, 159 /// Scroll one page up (not changing cursor) 160 ScrollPageUp, 161 /// Scroll one page down (not changing cursor) 162 ScrollPageDown, 163 /// Scroll window left 164 ScrollLeft, 165 /// Scroll window right 166 ScrollRight, 167 168 /// Zoom in editor font 169 ZoomIn, 170 /// Zoom out editor font 171 ZoomOut, 172 173 /// Togle line comment 174 ToggleLineComment, 175 /// Toggle block comment 176 ToggleBlockComment, 177 /// Delete current line 178 DeleteLine, 179 /// Insert line 180 InsertLine, 181 182 /// Toggle bookmark in current line 183 ToggleBookmark, 184 /// move cursor to next bookmark 185 GoToNextBookmark, 186 /// move cursor to previous bookmark 187 GoToPreviousBookmark, 188 189 /// Find text 190 Find, 191 /// Replace text 192 Replace 193 } 194 195 196 void initStandardEditorActions() { 197 // register editor action names and ids 198 registerActionEnum!EditorActions(); 199 } 200 201 const Action ACTION_EDITOR_INSERT_NEW_LINE = (new Action(EditorActions.InsertNewLine, KeyCode.RETURN, 0)).addAccelerator(KeyCode.RETURN, KeyFlag.Shift); 202 const Action ACTION_EDITOR_PREPEND_NEW_LINE = (new Action(EditorActions.PrependNewLine, KeyCode.RETURN, KeyFlag.Control|KeyFlag.Shift)); 203 const Action ACTION_EDITOR_APPEND_NEW_LINE = (new Action(EditorActions.AppendNewLine, KeyCode.RETURN, KeyFlag.Control)); 204 const Action ACTION_EDITOR_DELETE_LINE = (new Action(EditorActions.DeleteLine, KeyCode.KEY_D, KeyFlag.Control)).addAccelerator(KeyCode.KEY_L, KeyFlag.Control); 205 const Action ACTION_EDITOR_TOGGLE_REPLACE_MODE = (new Action(EditorActions.ToggleReplaceMode, KeyCode.INS, 0)); 206 const Action ACTION_EDITOR_SELECT_ALL = (new Action(EditorActions.SelectAll, KeyCode.KEY_A, KeyFlag.Control)); 207 const Action ACTION_EDITOR_TOGGLE_LINE_COMMENT = (new Action(EditorActions.ToggleLineComment, KeyCode.KEY_DIVIDE, KeyFlag.Control)); 208 const Action ACTION_EDITOR_TOGGLE_BLOCK_COMMENT = (new Action(EditorActions.ToggleBlockComment, KeyCode.KEY_DIVIDE, KeyFlag.Control | KeyFlag.Shift)); 209 const Action ACTION_EDITOR_TOGGLE_BOOKMARK = (new Action(EditorActions.ToggleBookmark, "ACTION_EDITOR_TOGGLE_BOOKMARK"c, null, KeyCode.KEY_B, KeyFlag.Control | KeyFlag.Shift)); 210 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)); 211 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)); 212 const Action ACTION_EDITOR_FIND = (new Action(EditorActions.Find, KeyCode.KEY_F, KeyFlag.Control)); 213 const Action ACTION_EDITOR_REPLACE = (new Action(EditorActions.Replace, KeyCode.KEY_H, KeyFlag.Control)); 214 215 const Action[] STD_EDITOR_ACTIONS = [ACTION_EDITOR_INSERT_NEW_LINE, ACTION_EDITOR_PREPEND_NEW_LINE, 216 ACTION_EDITOR_APPEND_NEW_LINE, ACTION_EDITOR_DELETE_LINE, ACTION_EDITOR_TOGGLE_REPLACE_MODE, 217 ACTION_EDITOR_SELECT_ALL, ACTION_EDITOR_TOGGLE_LINE_COMMENT, ACTION_EDITOR_TOGGLE_BLOCK_COMMENT, 218 ACTION_EDITOR_TOGGLE_BOOKMARK, ACTION_EDITOR_GOTO_NEXT_BOOKMARK, ACTION_EDITOR_GOTO_PREVIOUS_BOOKMARK, 219 220 ]; 221 222 /// base for all editor widgets 223 class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemActionHandler { 224 protected EditableContent _content; 225 226 protected int _lineHeight; 227 protected Point _scrollPos; 228 protected bool _fixedFont; 229 protected int _spaceWidth; 230 protected int _leftPaneWidth; // left pane - can be used to show line numbers, collapse controls, bookmarks, breakpoints, custom icons 231 232 protected int _minFontSize = -1; // disable zooming 233 protected int _maxFontSize = -1; // disable zooming 234 235 protected bool _wantTabs = true; 236 protected bool _showLineNumbers = false; // show line numbers in left pane 237 protected bool _showModificationMarks = false; // show modification marks in left pane 238 protected bool _showIcons = false; // show icons in left pane 239 protected bool _showFolding = false; // show folding controls in left pane 240 protected int _lineNumbersWidth = 0; 241 protected int _modificationMarksWidth = 0; 242 protected int _iconsWidth = 0; 243 protected int _foldingWidth = 0; 244 245 protected bool _selectAllWhenFocusedWithTab = false; 246 protected bool _deselectAllWhenUnfocused = false; 247 248 protected bool _replaceMode; 249 250 // TODO: move to styles 251 protected uint _selectionColorFocused = 0xB060A0FF; 252 protected uint _selectionColorNormal = 0xD060A0FF; 253 protected uint _leftPaneBackgroundColor = 0xF4F4F4; 254 protected uint _leftPaneBackgroundColor2 = 0xFFFFFF; 255 protected uint _leftPaneBackgroundColor3 = 0xF8F8F8; 256 protected uint _leftPaneLineNumberColor = 0x4060D0; 257 protected uint _leftPaneLineNumberColorEdited = 0xC0C000; 258 protected uint _leftPaneLineNumberColorSaved = 0x00C000; 259 protected uint _leftPaneLineNumberColorCurrentLine = 0xFFFFFFFF; 260 protected uint _leftPaneLineNumberBackgroundColorCurrentLine = 0xC08080FF; 261 protected uint _leftPaneLineNumberBackgroundColor = 0xF4F4F4; 262 protected uint _colorIconBreakpoint = 0xFF0000; 263 protected uint _colorIconBookmark = 0x0000FF; 264 protected uint _colorIconError = 0x80FF0000; 265 266 protected uint _caretColor = 0x000000; 267 protected uint _caretColorReplace = 0x808080FF; 268 protected uint _matchingBracketHightlightColor = 0x60FFE0B0; 269 270 protected uint _iconsPaneWidth = BACKEND_CONSOLE ? 1 : 16; 271 protected uint _foldingPaneWidth = BACKEND_CONSOLE ? 1 : 12; 272 protected uint _modificationMarksPaneWidth = BACKEND_CONSOLE ? 1 : 4; 273 /// when true, call measureVisibileText on next layout 274 protected bool _contentChanged = true; 275 276 protected bool _copyCurrentLineWhenNoSelection = true; 277 /// when true allows copy / cut whole current line if there is no selection 278 @property bool copyCurrentLineWhenNoSelection() { return _copyCurrentLineWhenNoSelection; } 279 @property EditWidgetBase copyCurrentLineWhenNoSelection(bool flg) { _copyCurrentLineWhenNoSelection = flg; return this; } 280 281 protected bool _showTabPositionMarks = false; 282 /// when true shows mark on tab positions in beginning of line 283 @property bool showTabPositionMarks() { return _showTabPositionMarks; } 284 @property EditWidgetBase showTabPositionMarks(bool flg) { 285 if (flg != _showTabPositionMarks) { 286 _showTabPositionMarks = flg; 287 invalidate(); 288 } 289 return this; 290 } 291 292 /// Modified state change listener (e.g. content has been saved, or first time modified after save) 293 Signal!ModifiedStateListener modifiedStateChange; 294 295 /// Signal to emit when editor content is changed 296 Signal!EditableContentChangeListener contentChange; 297 298 /// override to support modification of client rect after change, e.g. apply offset 299 override protected void handleClientRectLayout(ref Rect rc) { 300 updateLeftPaneWidth(); 301 rc.left += _leftPaneWidth; 302 } 303 304 /// override for multiline editors 305 protected int lineCount() { 306 return 1; 307 } 308 309 protected bool _wordWrap; 310 /// true if word wrap mode is set 311 @property bool wordWrap() { 312 return _wordWrap; 313 } 314 /// true if word wrap mode is set 315 @property EditWidgetBase wordWrap(bool v) { 316 _wordWrap = v; 317 invalidate(); 318 return this; 319 } 320 321 void wrapLine(dstring line, int maxWidth) { 322 323 } 324 325 /// information about line span into several lines - in word wrap mode 326 protected LineSpan[] _span; 327 328 /// override to add custom items on left panel 329 protected void updateLeftPaneWidth() { 330 import std.conv : to; 331 _iconsWidth = _showIcons ? _iconsPaneWidth : 0; 332 _foldingWidth = _showFolding ? _foldingPaneWidth : 0; 333 _modificationMarksWidth = _showModificationMarks && (BACKEND_GUI || !_showLineNumbers) ? _modificationMarksPaneWidth : 0; 334 _lineNumbersWidth = 0; 335 if (_showLineNumbers) { 336 dchar[] s = to!(dchar[])(lineCount + 1); 337 foreach(ref ch; s) 338 ch = '9'; 339 FontRef fnt = font; 340 Point sz = fnt.textSize(s); 341 _lineNumbersWidth = sz.x; 342 } 343 _leftPaneWidth = _lineNumbersWidth + _modificationMarksWidth + _foldingWidth + _iconsWidth; 344 if (_leftPaneWidth) 345 _leftPaneWidth += BACKEND_CONSOLE ? 1 : 3; 346 } 347 348 protected void drawLeftPaneFolding(DrawBuf buf, Rect rc, int line) { 349 buf.fillRect(rc, _leftPaneBackgroundColor2); 350 } 351 352 protected void drawLeftPaneIcon(DrawBuf buf, Rect rc, LineIcon icon) { 353 if (!icon) 354 return; 355 if (icon.type == LineIconType.error) { 356 buf.fillRect(rc, _colorIconError); 357 } else if (icon.type == LineIconType.bookmark) { 358 int dh = rc.height / 4; 359 rc.top += dh; 360 rc.bottom -= dh; 361 buf.fillRect(rc, _colorIconBookmark); 362 } else if (icon.type == LineIconType.breakpoint) { 363 int dh = rc.height / 8; 364 rc.top += dh; 365 rc.bottom -= dh; 366 int dw = rc.width / 4; 367 rc.left += dw; 368 rc.right -= dw; 369 buf.fillRect(rc, _colorIconBreakpoint); 370 } 371 } 372 373 protected void drawLeftPaneIcons(DrawBuf buf, Rect rc, int line) { 374 buf.fillRect(rc, _leftPaneBackgroundColor3); 375 drawLeftPaneIcon(buf, rc, content.lineIcons.findByLineAndType(line, LineIconType.error)); 376 drawLeftPaneIcon(buf, rc, content.lineIcons.findByLineAndType(line, LineIconType.bookmark)); 377 drawLeftPaneIcon(buf, rc, content.lineIcons.findByLineAndType(line, LineIconType.breakpoint)); 378 } 379 380 protected void drawLeftPaneModificationMarks(DrawBuf buf, Rect rc, int line) { 381 if (line >= 0 && line < content.length) { 382 EditStateMark m = content.editMark(line); 383 if (m == EditStateMark.changed) { 384 // modified, not saved 385 buf.fillRect(rc, 0xFFD040); 386 } else if (m == EditStateMark.saved) { 387 // modified, saved 388 buf.fillRect(rc, 0x20C020); 389 } 390 } 391 } 392 393 protected void drawLeftPaneLineNumbers(DrawBuf buf, Rect rc, int line) { 394 import std.conv : to; 395 uint bgcolor = _leftPaneLineNumberBackgroundColor; 396 if (line == _caretPos.line && !isFullyTransparentColor(_leftPaneLineNumberBackgroundColorCurrentLine)) 397 bgcolor = _leftPaneLineNumberBackgroundColorCurrentLine; 398 buf.fillRect(rc, bgcolor); 399 if (line < 0) 400 return; 401 dstring s = to!dstring(line + 1); 402 FontRef fnt = font; 403 Point sz = fnt.textSize(s); 404 int x = rc.right - sz.x; 405 int y = rc.top + (rc.height - sz.y) / 2; 406 uint color = _leftPaneLineNumberColor; 407 if (line == _caretPos.line && !isFullyTransparentColor(_leftPaneLineNumberColorCurrentLine)) 408 color = _leftPaneLineNumberColorCurrentLine; 409 if (line >= 0 && line < content.length) { 410 EditStateMark m = content.editMark(line); 411 if (m == EditStateMark.changed) { 412 // modified, not saved 413 color = _leftPaneLineNumberColorEdited; 414 } else if (m == EditStateMark.saved) { 415 // modified, saved 416 color = _leftPaneLineNumberColorSaved; 417 } 418 } 419 fnt.drawText(buf, x, y, s, color); 420 } 421 422 protected bool onLeftPaneMouseClick(MouseEvent event) { 423 return false; 424 } 425 426 protected bool handleLeftPaneFoldingMouseClick(MouseEvent event, Rect rc, int line) { 427 return true; 428 } 429 430 protected bool handleLeftPaneModificationMarksMouseClick(MouseEvent event, Rect rc, int line) { 431 return true; 432 } 433 434 protected bool handleLeftPaneLineNumbersMouseClick(MouseEvent event, Rect rc, int line) { 435 return true; 436 } 437 438 protected MenuItem getLeftPaneIconsPopupMenu(int line) { 439 return null; 440 } 441 442 protected bool handleLeftPaneIconsMouseClick(MouseEvent event, Rect rc, int line) { 443 if (event.button == MouseButton.Right) { 444 MenuItem menu = getLeftPaneIconsPopupMenu(line); 445 if (menu) { 446 if (menu.openingSubmenu.assigned) 447 if (!menu.openingSubmenu(_popupMenu)) 448 return true; 449 menu.updateActionState(this); 450 PopupMenu popupMenu = new PopupMenu(menu); 451 popupMenu.menuItemAction = this; 452 PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, event.x, event.y); 453 popup.flags = PopupFlags.CloseOnClickOutside; 454 } 455 return true; 456 } 457 return true; 458 } 459 460 protected bool handleLeftPaneMouseClick(MouseEvent event, Rect rc, int line) { 461 rc.right -= 3; 462 if (_foldingWidth) { 463 Rect rc2 = rc; 464 rc.right = rc2.left = rc2.right - _foldingWidth; 465 if (event.x >= rc2.left && event.x < rc2.right) 466 return handleLeftPaneFoldingMouseClick(event, rc2, line); 467 } 468 if (_modificationMarksWidth) { 469 Rect rc2 = rc; 470 rc.right = rc2.left = rc2.right - _modificationMarksWidth; 471 if (event.x >= rc2.left && event.x < rc2.right) 472 return handleLeftPaneModificationMarksMouseClick(event, rc2, line); 473 } 474 if (_lineNumbersWidth) { 475 Rect rc2 = rc; 476 rc.right = rc2.left = rc2.right - _lineNumbersWidth; 477 if (event.x >= rc2.left && event.x < rc2.right) 478 return handleLeftPaneLineNumbersMouseClick(event, rc2, line); 479 } 480 if (_iconsWidth) { 481 Rect rc2 = rc; 482 rc.right = rc2.left = rc2.right - _iconsWidth; 483 if (event.x >= rc2.left && event.x < rc2.right) 484 return handleLeftPaneIconsMouseClick(event, rc2, line); 485 } 486 return true; 487 } 488 489 protected void drawLeftPane(DrawBuf buf, Rect rc, int line) { 490 // override for custom drawn left pane 491 buf.fillRect(rc, _leftPaneBackgroundColor); 492 //buf.fillRect(Rect(rc.right - 2, rc.top, rc.right - 1, rc.bottom), _leftPaneBackgroundColor2); 493 //buf.fillRect(Rect(rc.right - 1, rc.top, rc.right - 0, rc.bottom), _leftPaneBackgroundColor3); 494 rc.right -= BACKEND_CONSOLE ? 1 : 3; 495 if (_foldingWidth) { 496 Rect rc2 = rc; 497 rc.right = rc2.left = rc2.right - _foldingWidth; 498 drawLeftPaneFolding(buf, rc2, line); 499 } 500 if (_modificationMarksWidth) { 501 Rect rc2 = rc; 502 rc.right = rc2.left = rc2.right - _modificationMarksWidth; 503 drawLeftPaneModificationMarks(buf, rc2, line); 504 } 505 if (_lineNumbersWidth) { 506 Rect rc2 = rc; 507 rc.right = rc2.left = rc2.right - _lineNumbersWidth; 508 drawLeftPaneLineNumbers(buf, rc2, line); 509 } 510 if (_iconsWidth) { 511 Rect rc2 = rc; 512 rc.right = rc2.left = rc2.right - _iconsWidth; 513 drawLeftPaneIcons(buf, rc2, line); 514 } 515 } 516 517 this(string ID, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) { 518 super(ID, hscrollbarMode, vscrollbarMode); 519 focusable = true; 520 acceleratorMap.add( [ 521 new Action(EditorActions.Up, KeyCode.UP, 0), 522 new Action(EditorActions.SelectUp, KeyCode.UP, KeyFlag.Shift), 523 new Action(EditorActions.SelectUp, KeyCode.UP, KeyFlag.Control | KeyFlag.Shift), 524 new Action(EditorActions.Down, KeyCode.DOWN, 0), 525 new Action(EditorActions.SelectDown, KeyCode.DOWN, KeyFlag.Shift), 526 new Action(EditorActions.SelectDown, KeyCode.DOWN, KeyFlag.Control | KeyFlag.Shift), 527 new Action(EditorActions.Left, KeyCode.LEFT, 0), 528 new Action(EditorActions.SelectLeft, KeyCode.LEFT, KeyFlag.Shift), 529 new Action(EditorActions.Right, KeyCode.RIGHT, 0), 530 new Action(EditorActions.SelectRight, KeyCode.RIGHT, KeyFlag.Shift), 531 new Action(EditorActions.WordLeft, KeyCode.LEFT, KeyFlag.Control), 532 new Action(EditorActions.SelectWordLeft, KeyCode.LEFT, KeyFlag.Control | KeyFlag.Shift), 533 new Action(EditorActions.WordRight, KeyCode.RIGHT, KeyFlag.Control), 534 new Action(EditorActions.SelectWordRight, KeyCode.RIGHT, KeyFlag.Control | KeyFlag.Shift), 535 new Action(EditorActions.PageUp, KeyCode.PAGEUP, 0), 536 new Action(EditorActions.SelectPageUp, KeyCode.PAGEUP, KeyFlag.Shift), 537 new Action(EditorActions.PageDown, KeyCode.PAGEDOWN, 0), 538 new Action(EditorActions.SelectPageDown, KeyCode.PAGEDOWN, KeyFlag.Shift), 539 new Action(EditorActions.PageBegin, KeyCode.PAGEUP, KeyFlag.Control), 540 new Action(EditorActions.SelectPageBegin, KeyCode.PAGEUP, KeyFlag.Control | KeyFlag.Shift), 541 new Action(EditorActions.PageEnd, KeyCode.PAGEDOWN, KeyFlag.Control), 542 new Action(EditorActions.SelectPageEnd, KeyCode.PAGEDOWN, KeyFlag.Control | KeyFlag.Shift), 543 new Action(EditorActions.LineBegin, KeyCode.HOME, 0), 544 new Action(EditorActions.SelectLineBegin, KeyCode.HOME, KeyFlag.Shift), 545 new Action(EditorActions.LineEnd, KeyCode.END, 0), 546 new Action(EditorActions.SelectLineEnd, KeyCode.END, KeyFlag.Shift), 547 new Action(EditorActions.DocumentBegin, KeyCode.HOME, KeyFlag.Control), 548 new Action(EditorActions.SelectDocumentBegin, KeyCode.HOME, KeyFlag.Control | KeyFlag.Shift), 549 new Action(EditorActions.DocumentEnd, KeyCode.END, KeyFlag.Control), 550 new Action(EditorActions.SelectDocumentEnd, KeyCode.END, KeyFlag.Control | KeyFlag.Shift), 551 552 new Action(EditorActions.ScrollLineUp, KeyCode.UP, KeyFlag.Control), 553 new Action(EditorActions.ScrollLineDown, KeyCode.DOWN, KeyFlag.Control), 554 555 // Backspace/Del 556 new Action(EditorActions.DelPrevChar, KeyCode.BACK, 0), 557 new Action(EditorActions.DelNextChar, KeyCode.DEL, 0), 558 new Action(EditorActions.DelPrevWord, KeyCode.BACK, KeyFlag.Control), 559 new Action(EditorActions.DelNextWord, KeyCode.DEL, KeyFlag.Control), 560 561 // Copy/Paste 562 new Action(EditorActions.Copy, KeyCode.KEY_C, KeyFlag.Control), 563 new Action(EditorActions.Copy, KeyCode.KEY_C, KeyFlag.Control|KeyFlag.Shift), 564 new Action(EditorActions.Copy, KeyCode.INS, KeyFlag.Control), 565 new Action(EditorActions.Cut, KeyCode.KEY_X, KeyFlag.Control), 566 new Action(EditorActions.Cut, KeyCode.KEY_X, KeyFlag.Control|KeyFlag.Shift), 567 new Action(EditorActions.Cut, KeyCode.DEL, KeyFlag.Shift), 568 new Action(EditorActions.Paste, KeyCode.KEY_V, KeyFlag.Control), 569 new Action(EditorActions.Paste, KeyCode.KEY_V, KeyFlag.Control|KeyFlag.Shift), 570 new Action(EditorActions.Paste, KeyCode.INS, KeyFlag.Shift), 571 572 // Undo/Redo 573 new Action(EditorActions.Undo, KeyCode.KEY_Z, KeyFlag.Control), 574 new Action(EditorActions.Redo, KeyCode.KEY_Y, KeyFlag.Control), 575 new Action(EditorActions.Redo, KeyCode.KEY_Z, KeyFlag.Control|KeyFlag.Shift), 576 577 new Action(EditorActions.Tab, KeyCode.TAB, 0), 578 new Action(EditorActions.BackTab, KeyCode.TAB, KeyFlag.Shift), 579 580 new Action(EditorActions.Find, KeyCode.KEY_F, KeyFlag.Control), 581 new Action(EditorActions.Replace, KeyCode.KEY_H, KeyFlag.Control), 582 ]); 583 acceleratorMap.add(STD_EDITOR_ACTIONS); 584 } 585 586 protected MenuItem _popupMenu; 587 @property MenuItem popupMenu() { return _popupMenu; } 588 @property EditWidgetBase popupMenu(MenuItem popupMenu) { 589 _popupMenu = popupMenu; 590 return this; 591 } 592 593 /// 594 override bool onMenuItemAction(const Action action) { 595 return dispatchAction(action); 596 } 597 598 /// returns true if widget can show popup (e.g. by mouse right click at point x,y) 599 override bool canShowPopupMenu(int x, int y) { 600 if (_popupMenu is null) 601 return false; 602 if (_popupMenu.openingSubmenu.assigned) 603 if (!_popupMenu.openingSubmenu(_popupMenu)) 604 return false; 605 return true; 606 } 607 608 /// returns true if widget is focusable and visible and enabled 609 override @property bool canFocus() { 610 // allow to focus even if not enabled 611 return focusable && visible; 612 } 613 614 /// override to change popup menu items state 615 override bool isActionEnabled(const Action action) { 616 switch (action.id) with(EditorActions) 617 { 618 case Tab: 619 case BackTab: 620 case Indent: 621 case Unindent: 622 return enabled; 623 case Copy: 624 return _copyCurrentLineWhenNoSelection || !_selectionRange.empty; 625 case Cut: 626 return enabled && (_copyCurrentLineWhenNoSelection || !_selectionRange.empty); 627 case Paste: 628 return enabled && Platform.instance.getClipboardText().length > 0; 629 case Undo: 630 return enabled && _content.hasUndo; 631 case Redo: 632 return enabled && _content.hasRedo; 633 case ToggleBookmark: 634 return _content.multiline; 635 case GoToNextBookmark: 636 return _content.multiline && _content.lineIcons.hasBookmarks; 637 case GoToPreviousBookmark: 638 return _content.multiline && _content.lineIcons.hasBookmarks; 639 case Replace: 640 return _content.multiline && !readOnly; 641 case Find: 642 return true; 643 default: 644 return super.isActionEnabled(action); 645 } 646 } 647 648 /// shows popup at (x,y) 649 override void showPopupMenu(int x, int y) { 650 /// if preparation signal handler assigned, call it; don't show popup if false is returned from handler 651 if (_popupMenu.openingSubmenu.assigned) 652 if (!_popupMenu.openingSubmenu(_popupMenu)) 653 return; 654 _popupMenu.updateActionState(this); 655 PopupMenu popupMenu = new PopupMenu(_popupMenu); 656 popupMenu.menuItemAction = this; 657 PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, x, y); 658 popup.flags = PopupFlags.CloseOnClickOutside; 659 } 660 661 void onPopupMenuItem(MenuItem item) { 662 // TODO 663 } 664 665 /// returns mouse cursor type for widget 666 override uint getCursorType(int x, int y) { 667 return x < _pos.left + _leftPaneWidth ? CursorType.Arrow : CursorType.IBeam; 668 } 669 670 /// set bool property value, for ML loaders 671 mixin(generatePropertySettersMethodOverride("setBoolProperty", "bool", 672 "wantTabs", "showIcons", "showFolding", "showModificationMarks", "showLineNumbers", "readOnly", "replaceMode", "useSpacesForTabs", "copyCurrentLineWhenNoSelection", "showTabPositionMarks")); 673 674 /// set int property value, for ML loaders 675 mixin(generatePropertySettersMethodOverride("setIntProperty", "int", 676 "tabSize")); 677 678 /// when true, Tab / Shift+Tab presses are processed internally in widget (e.g. insert tab character) instead of focus change navigation. 679 @property bool wantTabs() { 680 return _wantTabs; 681 } 682 683 /// sets tab size (in number of spaces) 684 @property EditWidgetBase wantTabs(bool wantTabs) { 685 _wantTabs = wantTabs; 686 return this; 687 } 688 689 /// when true, show icons like bookmarks or breakpoints at the left 690 @property bool showIcons() { 691 return _showIcons; 692 } 693 694 /// when true, show icons like bookmarks or breakpoints at the left 695 @property EditWidgetBase showIcons(bool flg) { 696 if (_showIcons != flg) { 697 _showIcons = flg; 698 updateLeftPaneWidth(); 699 requestLayout(); 700 } 701 return this; 702 } 703 704 /// when true, show folding controls at the left 705 @property bool showFolding() { 706 return _showFolding; 707 } 708 709 /// when true, show folding controls at the left 710 @property EditWidgetBase showFolding(bool flg) { 711 if (_showFolding != flg) { 712 _showFolding = flg; 713 updateLeftPaneWidth(); 714 requestLayout(); 715 } 716 return this; 717 } 718 719 /// when true, show modification marks for lines (whether line is unchanged/modified/modified_saved) 720 @property bool showModificationMarks() { 721 return _showModificationMarks; 722 } 723 724 /// when true, show modification marks for lines (whether line is unchanged/modified/modified_saved) 725 @property EditWidgetBase showModificationMarks(bool flg) { 726 if (_showModificationMarks != flg) { 727 _showModificationMarks = flg; 728 updateLeftPaneWidth(); 729 requestLayout(); 730 } 731 return this; 732 } 733 734 /// when true, line numbers are shown 735 @property bool showLineNumbers() { 736 return _showLineNumbers; 737 } 738 739 /// when true, line numbers are shown 740 @property EditWidgetBase showLineNumbers(bool flg) { 741 if (_showLineNumbers != flg) { 742 _showLineNumbers = flg; 743 updateLeftPaneWidth(); 744 requestLayout(); 745 } 746 return this; 747 } 748 749 /// readonly flag (when true, user cannot change content of editor) 750 @property bool readOnly() { 751 return !enabled || _content.readOnly; 752 } 753 754 /// sets readonly flag 755 @property EditWidgetBase readOnly(bool readOnly) { 756 enabled = !readOnly; 757 invalidate(); 758 return this; 759 } 760 761 /// replace mode flag (when true, entered character replaces character under cursor) 762 @property bool replaceMode() { 763 return _replaceMode; 764 } 765 766 /// sets replace mode flag 767 @property EditWidgetBase replaceMode(bool replaceMode) { 768 _replaceMode = replaceMode; 769 invalidate(); 770 return this; 771 } 772 773 /// when true, spaces will be inserted instead of tabs 774 @property bool useSpacesForTabs() { 775 return _content.useSpacesForTabs; 776 } 777 778 /// set new Tab key behavior flag: when true, spaces will be inserted instead of tabs 779 @property EditWidgetBase useSpacesForTabs(bool useSpacesForTabs) { 780 _content.useSpacesForTabs = useSpacesForTabs; 781 return this; 782 } 783 784 /// returns tab size (in number of spaces) 785 @property int tabSize() { 786 return _content.tabSize; 787 } 788 789 /// sets tab size (in number of spaces) 790 @property EditWidgetBase tabSize(int newTabSize) { 791 if (newTabSize < 1) 792 newTabSize = 1; 793 else if (newTabSize > 16) 794 newTabSize = 16; 795 if (newTabSize != tabSize) { 796 _content.tabSize = newTabSize; 797 requestLayout(); 798 } 799 return this; 800 } 801 802 /// true if smart indents are supported 803 @property bool supportsSmartIndents() { return _content.supportsSmartIndents; } 804 /// true if smart indents are enabled 805 @property bool smartIndents() { return _content.smartIndents; } 806 /// set smart indents enabled flag 807 @property EditWidgetBase smartIndents(bool enabled) { _content.smartIndents = enabled; return this; } 808 809 /// true if smart indents are enabled 810 @property bool smartIndentsAfterPaste() { return _content.smartIndentsAfterPaste; } 811 /// set smart indents enabled flag 812 @property EditWidgetBase smartIndentsAfterPaste(bool enabled) { _content.smartIndentsAfterPaste = enabled; return this; } 813 814 815 /// editor content object 816 @property EditableContent content() { 817 return _content; 818 } 819 820 /// when _ownContent is false, _content should not be destroyed in editor destructor 821 protected bool _ownContent = true; 822 /// set content object 823 @property EditWidgetBase content(EditableContent content) { 824 if (_content is content) 825 return this; // not changed 826 if (_content !is null) { 827 // disconnect old content 828 _content.contentChanged.disconnect(this); 829 if (_ownContent) { 830 destroy(_content); 831 } 832 } 833 _content = content; 834 _ownContent = false; 835 _content.contentChanged.connect(this); 836 if (_content.readOnly) 837 enabled = false; 838 return this; 839 } 840 841 /// free resources 842 ~this() { 843 if (_ownContent) { 844 destroy(_content); 845 _content = null; 846 } 847 } 848 849 protected void updateMaxLineWidth() { 850 } 851 852 protected void processSmartIndent(EditOperation operation) { 853 if (!supportsSmartIndents) 854 return; 855 if (!smartIndents && !smartIndentsAfterPaste) 856 return; 857 _content.syntaxSupport.applySmartIndent(operation, this); 858 } 859 860 override void onContentChange(EditableContent content, EditOperation operation, ref TextRange rangeBefore, ref TextRange rangeAfter, Object source) { 861 //Log.d("onContentChange rangeBefore=", rangeBefore, " rangeAfter=", rangeAfter, " text=", operation.content); 862 _contentChanged = true; 863 if (source is this) { 864 if (operation.action == EditAction.ReplaceContent) { 865 // fully replaced, e.g., loaded from file or text property is assigned 866 _caretPos = rangeAfter.end; 867 _selectionRange.start = _caretPos; 868 _selectionRange.end = _caretPos; 869 updateMaxLineWidth(); 870 measureVisibleText(); 871 ensureCaretVisible(); 872 correctCaretPos(); 873 requestLayout(); 874 requestActionsUpdate(); 875 } else if (operation.action == EditAction.SaveContent) { 876 // saved 877 } else { 878 // modified 879 _caretPos = rangeAfter.end; 880 _selectionRange.start = _caretPos; 881 _selectionRange.end = _caretPos; 882 updateMaxLineWidth(); 883 measureVisibleText(); 884 ensureCaretVisible(); 885 requestActionsUpdate(); 886 processSmartIndent(operation); 887 } 888 } else { 889 updateMaxLineWidth(); 890 measureVisibleText(); 891 correctCaretPos(); 892 requestLayout(); 893 requestActionsUpdate(); 894 } 895 invalidate(); 896 if (modifiedStateChange.assigned) { 897 if (_lastReportedModifiedState != content.modified) { 898 _lastReportedModifiedState = content.modified; 899 modifiedStateChange(this, content.modified); 900 requestActionsUpdate(); 901 } 902 } 903 if (contentChange.assigned) { 904 contentChange(_content); 905 } 906 return; 907 } 908 protected bool _lastReportedModifiedState; 909 910 /// get widget text 911 override @property dstring text() { return _content.text; } 912 913 /// set text 914 override @property Widget text(dstring s) { 915 _content.text = s; 916 requestLayout(); 917 return this; 918 } 919 920 /// set text 921 override @property Widget text(UIString s) { 922 _content.text = s; 923 requestLayout(); 924 return this; 925 } 926 927 protected TextPosition _caretPos; 928 protected TextRange _selectionRange; 929 930 abstract protected Rect textPosToClient(TextPosition p); 931 932 abstract protected TextPosition clientToTextPos(Point pt); 933 934 abstract protected void ensureCaretVisible(bool center = false); 935 936 abstract protected Point measureVisibleText(); 937 938 protected int _caretBlingingInterval = 800; 939 protected ulong _caretTimerId; 940 protected bool _caretBlinkingPhase; 941 protected long _lastBlinkStartTs; 942 943 protected void startCaretBlinking() { 944 if (window) { 945 static if (BACKEND_CONSOLE) { 946 window.caretRect = caretRect; 947 window.caretReplace = _replaceMode; 948 } else { 949 long ts = currentTimeMillis; 950 if (_caretTimerId) { 951 if (_lastBlinkStartTs + _caretBlingingInterval / 4 > ts) 952 return; // don't update timer too frequently 953 cancelTimer(_caretTimerId); 954 } 955 _caretTimerId = setTimer(_caretBlingingInterval / 2); 956 _lastBlinkStartTs = ts; 957 _caretBlinkingPhase = false; 958 invalidate(); 959 } 960 } 961 } 962 963 protected void stopCaretBlinking() { 964 if (window) { 965 static if (BACKEND_CONSOLE) { 966 window.caretRect = Rect.init; 967 } else { 968 if (_caretTimerId) { 969 cancelTimer(_caretTimerId); 970 _caretTimerId = 0; 971 } 972 } 973 } 974 } 975 976 /// handle timer; return true to repeat timer event after next interval, false cancel timer 977 override bool onTimer(ulong id) { 978 if (id == _caretTimerId) { 979 _caretBlinkingPhase = !_caretBlinkingPhase; 980 if (!_caretBlinkingPhase) 981 _lastBlinkStartTs = currentTimeMillis; 982 invalidate(); 983 //window.update(true); 984 bool res = focused; 985 if (!res) 986 _caretTimerId = 0; 987 return res; 988 } 989 if (id == _hoverTimer) { 990 cancelHoverTimer(); 991 onHoverTimeout(_hoverMousePosition, _hoverTextPosition); 992 return false; 993 } 994 return super.onTimer(id); 995 } 996 997 /// override to handle focus changes 998 override protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) { 999 if (focused) 1000 startCaretBlinking(); 1001 else { 1002 stopCaretBlinking(); 1003 cancelHoverTimer(); 1004 1005 if(_deselectAllWhenUnfocused) { 1006 _selectionRange.start = _caretPos; 1007 _selectionRange.end = _caretPos; 1008 } 1009 } 1010 if(focused && _selectAllWhenFocusedWithTab && receivedFocusFromKeyboard) 1011 handleAction(ACTION_EDITOR_SELECT_ALL); 1012 super.handleFocusChange(focused); 1013 } 1014 1015 /// returns cursor rectangle 1016 protected Rect caretRect() { 1017 Rect caretRc = textPosToClient(_caretPos); 1018 if (_replaceMode) { 1019 dstring s = _content[_caretPos.line]; 1020 if (_caretPos.pos < s.length) { 1021 TextPosition nextPos = _caretPos; 1022 nextPos.pos++; 1023 Rect nextRect = textPosToClient(nextPos); 1024 caretRc.right = nextRect.right; 1025 } else { 1026 caretRc.right += _spaceWidth; 1027 } 1028 } 1029 caretRc.offset(_clientRect.left, _clientRect.top); 1030 return caretRc; 1031 } 1032 1033 /// handle theme change: e.g. reload some themed resources 1034 override void onThemeChanged() { 1035 super.onThemeChanged(); 1036 _caretColor = style.customColor("edit_caret"); 1037 _caretColorReplace = style.customColor("edit_caret_replace"); 1038 _selectionColorFocused = style.customColor("editor_selection_focused"); 1039 _selectionColorNormal = style.customColor("editor_selection_normal"); 1040 _leftPaneBackgroundColor = style.customColor("editor_left_pane_background"); 1041 _leftPaneBackgroundColor2 = style.customColor("editor_left_pane_background2"); 1042 _leftPaneBackgroundColor3 = style.customColor("editor_left_pane_background3"); 1043 _leftPaneLineNumberColor = style.customColor("editor_left_pane_line_number_text"); 1044 _leftPaneLineNumberColorEdited = style.customColor("editor_left_pane_line_number_text_edited", 0xC0C000); 1045 _leftPaneLineNumberColorSaved = style.customColor("editor_left_pane_line_number_text_saved", 0x00C000); 1046 _leftPaneLineNumberColorCurrentLine = style.customColor("editor_left_pane_line_number_text_current_line", 0xFFFFFFFF); 1047 _leftPaneLineNumberBackgroundColorCurrentLine = style.customColor("editor_left_pane_line_number_background_current_line", 0xC08080FF); 1048 _leftPaneLineNumberBackgroundColor = style.customColor("editor_left_pane_line_number_background"); 1049 _colorIconBreakpoint = style.customColor("editor_left_pane_line_icon_color_breakpoint", 0xFF0000); 1050 _colorIconBookmark = style.customColor("editor_left_pane_line_icon_color_bookmark", 0x0000FF); 1051 _colorIconError = style.customColor("editor_left_pane_line_icon_color_error", 0x80FF0000); 1052 _matchingBracketHightlightColor = style.customColor("editor_matching_bracket_highlight"); 1053 } 1054 1055 /// draws caret 1056 protected void drawCaret(DrawBuf buf) { 1057 if (focused) { 1058 if (_caretBlinkingPhase) { 1059 return; 1060 } 1061 // draw caret 1062 Rect caretRc = caretRect(); 1063 if (caretRc.intersects(_clientRect)) { 1064 //caretRc.left++; 1065 if (_replaceMode && BACKEND_GUI) 1066 buf.fillRect(caretRc, _caretColorReplace); 1067 //buf.drawLine(Point(caretRc.left, caretRc.bottom), Point(caretRc.left, caretRc.top), _caretColor); 1068 buf.fillRect(Rect(caretRc.left, caretRc.top, caretRc.left + 1, caretRc.bottom), _caretColor); 1069 } 1070 } 1071 } 1072 1073 protected void updateFontProps() { 1074 FontRef font = font(); 1075 _fixedFont = font.isFixed; 1076 _spaceWidth = font.spaceWidth; 1077 _lineHeight = font.height; 1078 } 1079 1080 /// when cursor position or selection is out of content bounds, fix it to nearest valid position 1081 protected void correctCaretPos() { 1082 _content.correctPosition(_caretPos); 1083 _content.correctPosition(_selectionRange.start); 1084 _content.correctPosition(_selectionRange.end); 1085 if (_selectionRange.empty) 1086 _selectionRange = TextRange(_caretPos, _caretPos); 1087 } 1088 1089 1090 private int[] _lineWidthBuf; 1091 protected int calcLineWidth(dstring s) { 1092 int w = 0; 1093 if (_fixedFont) { 1094 int tabw = tabSize * _spaceWidth; 1095 // version optimized for fixed font 1096 for (int i = 0; i < s.length; i++) { 1097 if (s[i] == '\t') { 1098 w += _spaceWidth; 1099 w = (w + tabw - 1) / tabw * tabw; 1100 } else { 1101 w += _spaceWidth; 1102 } 1103 } 1104 } else { 1105 // variable pitch font 1106 if (_lineWidthBuf.length < s.length) 1107 _lineWidthBuf.length = s.length; 1108 int charsMeasured = font.measureText(s, _lineWidthBuf, int.max); 1109 if (charsMeasured > 0) 1110 w = _lineWidthBuf[charsMeasured - 1]; 1111 } 1112 return w; 1113 } 1114 1115 protected void updateSelectionAfterCursorMovement(TextPosition oldCaretPos, bool selecting) { 1116 if (selecting) { 1117 if (oldCaretPos == _selectionRange.start) { 1118 if (_caretPos >= _selectionRange.end) { 1119 _selectionRange.start = _selectionRange.end; 1120 _selectionRange.end = _caretPos; 1121 } else { 1122 _selectionRange.start = _caretPos; 1123 } 1124 } else if (oldCaretPos == _selectionRange.end) { 1125 if (_caretPos < _selectionRange.start) { 1126 _selectionRange.end = _selectionRange.start; 1127 _selectionRange.start = _caretPos; 1128 } else { 1129 _selectionRange.end = _caretPos; 1130 } 1131 } else { 1132 if (oldCaretPos < _caretPos) { 1133 // start selection forward 1134 _selectionRange.start = oldCaretPos; 1135 _selectionRange.end = _caretPos; 1136 } else { 1137 // start selection backward 1138 _selectionRange.start = _caretPos; 1139 _selectionRange.end = oldCaretPos; 1140 } 1141 } 1142 } else { 1143 _selectionRange.start = _caretPos; 1144 _selectionRange.end = _caretPos; 1145 } 1146 invalidate(); 1147 requestActionsUpdate(); 1148 } 1149 1150 protected void selectWordByMouse(int x, int y) { 1151 TextPosition oldCaretPos = _caretPos; 1152 TextPosition newPos = clientToTextPos(Point(x,y)); 1153 TextRange r = content.wordBounds(newPos); 1154 if (r.start < r.end) { 1155 _selectionRange = r; 1156 _caretPos = r.end; 1157 invalidate(); 1158 requestActionsUpdate(); 1159 } else { 1160 _caretPos = newPos; 1161 updateSelectionAfterCursorMovement(oldCaretPos, false); 1162 } 1163 } 1164 1165 protected void selectLineByMouse(int x, int y, bool onSameLineOnly = true) { 1166 TextPosition oldCaretPos = _caretPos; 1167 TextPosition newPos = clientToTextPos(Point(x,y)); 1168 if (onSameLineOnly && newPos.line != oldCaretPos.line) 1169 return; // different lines 1170 TextRange r = content.lineRange(newPos.line); 1171 if (r.start < r.end) { 1172 _selectionRange = r; 1173 _caretPos = r.end; 1174 invalidate(); 1175 requestActionsUpdate(); 1176 } else { 1177 _caretPos = newPos; 1178 updateSelectionAfterCursorMovement(oldCaretPos, false); 1179 } 1180 } 1181 1182 protected void updateCaretPositionByMouse(int x, int y, bool selecting) { 1183 TextPosition oldCaretPos = _caretPos; 1184 TextPosition newPos = clientToTextPos(Point(x,y)); 1185 if (newPos != _caretPos) { 1186 _caretPos = newPos; 1187 updateSelectionAfterCursorMovement(oldCaretPos, selecting); 1188 invalidate(); 1189 } 1190 } 1191 1192 /// generate string of spaces, to reach next tab position 1193 protected dstring spacesForTab(int currentPos) { 1194 int newPos = (currentPos + tabSize + 1) / tabSize * tabSize; 1195 return " "d[0..(newPos - currentPos)]; 1196 } 1197 1198 /// returns true if one or more lines selected fully 1199 protected bool multipleLinesSelected() { 1200 return _selectionRange.end.line > _selectionRange.start.line; 1201 } 1202 1203 protected bool _camelCasePartsAsWords = true; 1204 1205 protected bool removeSelectionTextIfSelected() { 1206 if (_selectionRange.empty) 1207 return false; 1208 // clear selection 1209 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d]); 1210 _content.performOperation(op, this); 1211 ensureCaretVisible(); 1212 return true; 1213 } 1214 1215 /// returns current selection text (joined with LF when span over multiple lines) 1216 public dstring getSelectedText() { 1217 return getRangeText(_selectionRange); 1218 } 1219 1220 /// returns text for specified range (joined with LF when span over multiple lines) 1221 public dstring getRangeText(TextRange range) { 1222 dstring selectionText = concatDStrings(_content.rangeText(range)); 1223 return selectionText; 1224 } 1225 1226 /// returns range for line with cursor 1227 @property public TextRange currentLineRange() { 1228 return _content.lineRange(_caretPos.line); 1229 } 1230 1231 protected bool removeRangeText(TextRange range) { 1232 if (range.empty) 1233 return false; 1234 _selectionRange = range; 1235 _caretPos = _selectionRange.start; 1236 EditOperation op = new EditOperation(EditAction.Replace, range, [""d]); 1237 _content.performOperation(op, this); 1238 //_selectionRange.start = _caretPos; 1239 //_selectionRange.end = _caretPos; 1240 ensureCaretVisible(); 1241 return true; 1242 } 1243 1244 /// override to handle specific actions state (e.g. change enabled state for supported actions) 1245 override bool handleActionStateRequest(const Action a) { 1246 switch (a.id) with(EditorActions) 1247 { 1248 case ToggleBlockComment: 1249 if (!_content.syntaxSupport || !_content.syntaxSupport.supportsToggleBlockComment) 1250 a.state = ACTION_STATE_INVISIBLE; 1251 else if (enabled && _content.syntaxSupport.canToggleBlockComment(_selectionRange)) 1252 a.state = ACTION_STATE_ENABLED; 1253 else 1254 a.state = ACTION_STATE_DISABLE; 1255 return true; 1256 case ToggleLineComment: 1257 if (!_content.syntaxSupport || !_content.syntaxSupport.supportsToggleLineComment) 1258 a.state = ACTION_STATE_INVISIBLE; 1259 else if (enabled && _content.syntaxSupport.canToggleLineComment(_selectionRange)) 1260 a.state = ACTION_STATE_ENABLED; 1261 else 1262 a.state = ACTION_STATE_DISABLE; 1263 return true; 1264 case Copy: 1265 case Cut: 1266 case Paste: 1267 case Undo: 1268 case Redo: 1269 case Tab: 1270 case BackTab: 1271 case Indent: 1272 case Unindent: 1273 if (isActionEnabled(a)) 1274 a.state = ACTION_STATE_ENABLED; 1275 else 1276 a.state = ACTION_STATE_DISABLE; 1277 return true; 1278 default: 1279 return super.handleActionStateRequest(a); 1280 } 1281 } 1282 1283 override protected bool handleAction(const Action a) { 1284 TextPosition oldCaretPos = _caretPos; 1285 dstring currentLine = _content[_caretPos.line]; 1286 switch (a.id) with(EditorActions) 1287 { 1288 case Left: 1289 case SelectLeft: 1290 correctCaretPos(); 1291 if (_caretPos.pos > 0) { 1292 _caretPos.pos--; 1293 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1294 ensureCaretVisible(); 1295 } else if (_caretPos.line > 0) { 1296 _caretPos = _content.lineEnd(_caretPos.line - 1); 1297 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1298 ensureCaretVisible(); 1299 } 1300 return true; 1301 case Right: 1302 case SelectRight: 1303 correctCaretPos(); 1304 if (_caretPos.pos < currentLine.length) { 1305 _caretPos.pos++; 1306 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1307 ensureCaretVisible(); 1308 } else if (_caretPos.line < _content.length && _content.multiline) { 1309 _caretPos.pos = 0; 1310 _caretPos.line++; 1311 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1312 ensureCaretVisible(); 1313 } 1314 return true; 1315 case WordLeft: 1316 case SelectWordLeft: 1317 { 1318 TextPosition newpos = _content.moveByWord(_caretPos, -1, _camelCasePartsAsWords); 1319 if (newpos != _caretPos) { 1320 _caretPos = newpos; 1321 updateSelectionAfterCursorMovement(oldCaretPos, a.id == EditorActions.SelectWordLeft); 1322 ensureCaretVisible(); 1323 } 1324 } 1325 return true; 1326 case WordRight: 1327 case SelectWordRight: 1328 { 1329 TextPosition newpos = _content.moveByWord(_caretPos, 1, _camelCasePartsAsWords); 1330 if (newpos != _caretPos) { 1331 _caretPos = newpos; 1332 updateSelectionAfterCursorMovement(oldCaretPos, a.id == EditorActions.SelectWordRight); 1333 ensureCaretVisible(); 1334 } 1335 } 1336 return true; 1337 case DocumentBegin: 1338 case SelectDocumentBegin: 1339 if (_caretPos.pos > 0 || _caretPos.line > 0) { 1340 _caretPos.line = 0; 1341 _caretPos.pos = 0; 1342 ensureCaretVisible(); 1343 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1344 } 1345 return true; 1346 case LineBegin: 1347 case SelectLineBegin: 1348 auto space = _content.getLineWhiteSpace(_caretPos.line); 1349 if (_caretPos.pos > 0) { 1350 if (_caretPos.pos > space.firstNonSpaceIndex && space.firstNonSpaceIndex > 0) 1351 _caretPos.pos = space.firstNonSpaceIndex; 1352 else 1353 _caretPos.pos = 0; 1354 ensureCaretVisible(); 1355 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1356 } else { 1357 // caret pos is 0 1358 if (space.firstNonSpaceIndex > 0) 1359 _caretPos.pos = space.firstNonSpaceIndex; 1360 ensureCaretVisible(); 1361 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1362 } 1363 return true; 1364 case DocumentEnd: 1365 case SelectDocumentEnd: 1366 if (_caretPos.line < _content.length - 1 || _caretPos.pos < _content[_content.length - 1].length) { 1367 _caretPos.line = _content.length - 1; 1368 _caretPos.pos = cast(int)_content[_content.length - 1].length; 1369 ensureCaretVisible(); 1370 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1371 } 1372 return true; 1373 case LineEnd: 1374 case SelectLineEnd: 1375 if (_caretPos.pos < currentLine.length) { 1376 _caretPos.pos = cast(int)currentLine.length; 1377 ensureCaretVisible(); 1378 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 1379 } 1380 return true; 1381 case DelPrevWord: 1382 if (readOnly) 1383 return true; 1384 correctCaretPos(); 1385 if (removeSelectionTextIfSelected()) // clear selection 1386 return true; 1387 TextPosition newpos = _content.moveByWord(_caretPos, -1, _camelCasePartsAsWords); 1388 if (newpos < _caretPos) 1389 removeRangeText(TextRange(newpos, _caretPos)); 1390 return true; 1391 case DelNextWord: 1392 if (readOnly) 1393 return true; 1394 correctCaretPos(); 1395 if (removeSelectionTextIfSelected()) // clear selection 1396 return true; 1397 TextPosition newpos = _content.moveByWord(_caretPos, 1, _camelCasePartsAsWords); 1398 if (newpos > _caretPos) 1399 removeRangeText(TextRange(_caretPos, newpos)); 1400 return true; 1401 case DelPrevChar: 1402 if (readOnly) 1403 return true; 1404 correctCaretPos(); 1405 if (removeSelectionTextIfSelected()) // clear selection 1406 return true; 1407 if (_caretPos.pos > 0) { 1408 // delete prev char in current line 1409 TextRange range = TextRange(_caretPos, _caretPos); 1410 range.start.pos--; 1411 removeRangeText(range); 1412 } else if (_caretPos.line > 0) { 1413 // merge with previous line 1414 TextRange range = TextRange(_caretPos, _caretPos); 1415 range.start = _content.lineEnd(range.start.line - 1); 1416 removeRangeText(range); 1417 } 1418 return true; 1419 case DelNextChar: 1420 if (readOnly) 1421 return true; 1422 correctCaretPos(); 1423 if (removeSelectionTextIfSelected()) // clear selection 1424 return true; 1425 if (_caretPos.pos < currentLine.length) { 1426 // delete char in current line 1427 TextRange range = TextRange(_caretPos, _caretPos); 1428 range.end.pos++; 1429 removeRangeText(range); 1430 } else if (_caretPos.line < _content.length - 1) { 1431 // merge with next line 1432 TextRange range = TextRange(_caretPos, _caretPos); 1433 range.end.line++; 1434 range.end.pos = 0; 1435 removeRangeText(range); 1436 } 1437 return true; 1438 case Copy: 1439 case Cut: 1440 TextRange range = _selectionRange; 1441 if (range.empty && _copyCurrentLineWhenNoSelection) { 1442 range = currentLineRange; 1443 } 1444 if (!range.empty) { 1445 dstring selectionText = getRangeText(range); 1446 platform.setClipboardText(selectionText); 1447 if (!readOnly && a.id == Cut) { 1448 EditOperation op = new EditOperation(EditAction.Replace, range, [""d]); 1449 _content.performOperation(op, this); 1450 } 1451 } 1452 return true; 1453 case Paste: 1454 { 1455 if (readOnly) 1456 return true; 1457 dstring selectionText = platform.getClipboardText(); 1458 dstring[] lines; 1459 if (_content.multiline) { 1460 lines = splitDString(selectionText); 1461 } else { 1462 lines = [replaceEolsWithSpaces(selectionText)]; 1463 } 1464 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, lines); 1465 _content.performOperation(op, this); 1466 } 1467 return true; 1468 case Undo: 1469 { 1470 if (readOnly) 1471 return true; 1472 _content.undo(this); 1473 } 1474 return true; 1475 case Redo: 1476 { 1477 if (readOnly) 1478 return true; 1479 _content.redo(this); 1480 } 1481 return true; 1482 case Indent: 1483 indentRange(false); 1484 return true; 1485 case Unindent: 1486 indentRange(true); 1487 return true; 1488 case Tab: 1489 { 1490 if (readOnly) 1491 return true; 1492 if (_selectionRange.empty) { 1493 if (useSpacesForTabs) { 1494 // insert one or more spaces to 1495 EditOperation op = new EditOperation(EditAction.Replace, TextRange(_caretPos, _caretPos), [spacesForTab(_caretPos.pos)]); 1496 _content.performOperation(op, this); 1497 } else { 1498 // just insert tab character 1499 EditOperation op = new EditOperation(EditAction.Replace, TextRange(_caretPos, _caretPos), ["\t"d]); 1500 _content.performOperation(op, this); 1501 } 1502 } else { 1503 if (multipleLinesSelected()) { 1504 // indent range 1505 return handleAction(new Action(EditorActions.Indent)); 1506 } else { 1507 // insert tab 1508 if (useSpacesForTabs) { 1509 // insert one or more spaces to 1510 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [spacesForTab(_selectionRange.start.pos)]); 1511 _content.performOperation(op, this); 1512 } else { 1513 // just insert tab character 1514 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, ["\t"d]); 1515 _content.performOperation(op, this); 1516 } 1517 } 1518 1519 } 1520 } 1521 return true; 1522 case BackTab: 1523 { 1524 if (readOnly) 1525 return true; 1526 if (_selectionRange.empty) { 1527 // remove spaces before caret 1528 TextRange r = spaceBefore(_caretPos); 1529 if (!r.empty) { 1530 EditOperation op = new EditOperation(EditAction.Replace, r, [""d]); 1531 _content.performOperation(op, this); 1532 } 1533 } else { 1534 if (multipleLinesSelected()) { 1535 // unindent range 1536 return handleAction(new Action(EditorActions.Unindent)); 1537 } else { 1538 // remove space before selection 1539 TextRange r = spaceBefore(_selectionRange.start); 1540 if (!r.empty) { 1541 int nchars = r.end.pos - r.start.pos; 1542 TextRange saveRange = _selectionRange; 1543 TextPosition saveCursor = _caretPos; 1544 EditOperation op = new EditOperation(EditAction.Replace, r, [""d]); 1545 _content.performOperation(op, this); 1546 if (saveCursor.line == saveRange.start.line) 1547 saveCursor.pos -= nchars; 1548 if (saveRange.end.line == saveRange.start.line) 1549 saveRange.end.pos -= nchars; 1550 saveRange.start.pos -= nchars; 1551 _selectionRange = saveRange; 1552 _caretPos = saveCursor; 1553 ensureCaretVisible(); 1554 } 1555 } 1556 } 1557 } 1558 return true; 1559 case ToggleReplaceMode: 1560 replaceMode = !replaceMode; 1561 return true; 1562 case SelectAll: 1563 _selectionRange.start.line = 0; 1564 _selectionRange.start.pos = 0; 1565 _selectionRange.end = _content.lineEnd(_content.length - 1); 1566 _caretPos = _selectionRange.end; 1567 ensureCaretVisible(); 1568 requestActionsUpdate(); 1569 return true; 1570 case ToggleBookmark: 1571 if (_content.multiline) { 1572 int line = a.longParam >= 0 ? cast(int)a.longParam : _caretPos.line; 1573 _content.lineIcons.toggleBookmark(line); 1574 return true; 1575 } 1576 return false; 1577 case GoToNextBookmark: 1578 case GoToPreviousBookmark: 1579 if (_content.multiline) { 1580 LineIcon mark = _content.lineIcons.findNext(LineIconType.bookmark, _selectionRange.end.line, a.id == EditorActions.GoToNextBookmark ? 1 : -1); 1581 if (mark) { 1582 setCaretPos(mark.line, 0, true); 1583 return true; 1584 } 1585 } 1586 return false; 1587 default: 1588 break; 1589 } 1590 return super.handleAction(a); 1591 } 1592 1593 protected TextRange spaceBefore(TextPosition pos) { 1594 TextRange res = TextRange(pos, pos); 1595 dstring s = _content[pos.line]; 1596 int x = 0; 1597 int start = -1; 1598 for (int i = 0; i < pos.pos; i++) { 1599 dchar ch = s[i]; 1600 if (ch == ' ') { 1601 if (start == -1 || (x % tabSize) == 0) 1602 start = i; 1603 x++; 1604 } else if (ch == '\t') { 1605 if (start == -1 || (x % tabSize) == 0) 1606 start = i; 1607 x = (x + tabSize + 1) / tabSize * tabSize; 1608 } else { 1609 x++; 1610 start = -1; 1611 } 1612 } 1613 if (start != -1) { 1614 res.start.pos = start; 1615 } 1616 return res; 1617 } 1618 1619 /// change line indent 1620 protected dstring indentLine(dstring src, bool back, TextPosition * cursorPos) { 1621 int firstNonSpace = -1; 1622 int x = 0; 1623 int unindentPos = -1; 1624 int cursor = cursorPos ? cursorPos.pos : 0; 1625 for (int i = 0; i < src.length; i++) { 1626 dchar ch = src[i]; 1627 if (ch == ' ') { 1628 x++; 1629 } else if (ch == '\t') { 1630 x = (x + tabSize + 1) / tabSize * tabSize; 1631 } else { 1632 firstNonSpace = i; 1633 break; 1634 } 1635 if (x <= tabSize) 1636 unindentPos = i + 1; 1637 } 1638 if (firstNonSpace == -1) // only spaces or empty line -- do not change it 1639 return src; 1640 if (back) { 1641 // unindent 1642 if (unindentPos == -1) 1643 return src; // no change 1644 if (unindentPos == src.length) { 1645 if (cursorPos) 1646 cursorPos.pos = 0; 1647 return ""d; 1648 } 1649 if (cursor >= unindentPos) 1650 cursorPos.pos -= unindentPos; 1651 return src[unindentPos .. $].dup; 1652 } else { 1653 // indent 1654 if (useSpacesForTabs) { 1655 if (cursor > 0) 1656 cursorPos.pos += tabSize; 1657 return spacesForTab(0) ~ src; 1658 } else { 1659 if (cursor > 0) 1660 cursorPos.pos++; 1661 return "\t"d ~ src; 1662 } 1663 } 1664 } 1665 1666 /// indent / unindent range 1667 protected void indentRange(bool back) { 1668 TextRange r = _selectionRange; 1669 r.start.pos = 0; 1670 if (r.end.pos > 0) 1671 r.end = _content.lineBegin(r.end.line + 1); 1672 if (r.end.line <= r.start.line) 1673 r = TextRange(_content.lineBegin(_caretPos.line), _content.lineBegin(_caretPos.line + 1)); 1674 int lineCount = r.end.line - r.start.line; 1675 if (r.end.pos > 0) 1676 lineCount++; 1677 dstring[] newContent = new dstring[lineCount + 1]; 1678 bool changed = false; 1679 for (int i = 0; i < lineCount; i++) { 1680 dstring srcline = _content.line(r.start.line + i); 1681 dstring dstline = indentLine(srcline, back, r.start.line + i == _caretPos.line ? &_caretPos : null); 1682 newContent[i] = dstline; 1683 if (dstline.length != srcline.length) 1684 changed = true; 1685 } 1686 if (changed) { 1687 TextRange saveRange = r; 1688 TextPosition saveCursor = _caretPos; 1689 EditOperation op = new EditOperation(EditAction.Replace, r, newContent); 1690 _content.performOperation(op, this); 1691 _selectionRange = saveRange; 1692 _caretPos = saveCursor; 1693 ensureCaretVisible(); 1694 } 1695 } 1696 1697 /// map key to action 1698 override protected Action findKeyAction(uint keyCode, uint flags) { 1699 // don't handle tabs when disabled 1700 if (keyCode == KeyCode.TAB && (flags == 0 || flags == KeyFlag.Shift) && (!_wantTabs || readOnly)) 1701 return null; 1702 return super.findKeyAction(keyCode, flags); 1703 } 1704 1705 static bool isAZaz(dchar ch) { 1706 return (ch >= 'a' && ch <='z') || (ch >= 'A' && ch <='Z'); 1707 } 1708 1709 /// handle keys 1710 override bool onKeyEvent(KeyEvent event) { 1711 //Log.d("onKeyEvent ", event.action, " ", event.keyCode, " flags ", event.flags); 1712 if (focused) startCaretBlinking(); 1713 cancelHoverTimer(); 1714 bool ctrlOrAltPressed = !!(event.flags & KeyFlag.Control); //(event.flags & (KeyFlag.Control /* | KeyFlag.Alt */)); 1715 //if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.SPACE && (event.flags & KeyFlag.Control)) { 1716 // Log.d("Ctrl+Space pressed"); 1717 //} 1718 if (event.action == KeyAction.Text && event.text.length && !ctrlOrAltPressed) { 1719 //Log.d("text entered: ", event.text); 1720 if (readOnly) 1721 return true; 1722 if (!(!!(event.flags & KeyFlag.Alt) && event.text.length == 1 && isAZaz(event.text[0]))) { // filter out Alt+A..Z 1723 if (replaceMode && _selectionRange.empty && _content[_caretPos.line].length >= _caretPos.pos + event.text.length) { 1724 // replace next char(s) 1725 TextRange range = _selectionRange; 1726 range.end.pos += cast(int)event.text.length; 1727 EditOperation op = new EditOperation(EditAction.Replace, range, [event.text]); 1728 _content.performOperation(op, this); 1729 } else { 1730 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [event.text]); 1731 _content.performOperation(op, this); 1732 } 1733 if (focused) startCaretBlinking(); 1734 return true; 1735 } 1736 } 1737 //if (event.keyCode == KeyCode.SPACE && !readOnly) { 1738 // return true; 1739 //} 1740 //if (event.keyCode == KeyCode.RETURN && !readOnly && !_content.multiline) { 1741 // return true; 1742 //} 1743 bool res = super.onKeyEvent(event); 1744 if (focused) startCaretBlinking(); 1745 return res; 1746 } 1747 1748 /// Handle Ctrl + Left mouse click on text 1749 protected void onControlClick() { 1750 // override to do something useful on Ctrl + Left mouse click in text 1751 } 1752 1753 protected TextPosition _hoverTextPosition; 1754 protected Point _hoverMousePosition; 1755 protected ulong _hoverTimer; 1756 protected long _hoverTimeoutMillis = 800; 1757 1758 /// override to handle mouse hover timeout in text 1759 protected void onHoverTimeout(Point pt, TextPosition pos) { 1760 // override to do something useful on hover timeout 1761 } 1762 1763 protected void onHover(Point pos) { 1764 if (_hoverMousePosition == pos) 1765 return; 1766 //Log.d("onHover ", pos); 1767 int x = pos.x - left - _leftPaneWidth; 1768 int y = pos.y - top; 1769 _hoverMousePosition = pos; 1770 _hoverTextPosition = clientToTextPos(Point(x, y)); 1771 cancelHoverTimer(); 1772 _hoverTimer = setTimer(_hoverTimeoutMillis); 1773 } 1774 1775 protected void cancelHoverTimer() { 1776 if (_hoverTimer) { 1777 cancelTimer(_hoverTimer); 1778 _hoverTimer = 0; 1779 } 1780 } 1781 1782 /// process mouse event; return true if event is processed by widget. 1783 override bool onMouseEvent(MouseEvent event) { 1784 //Log.d("onMouseEvent ", id, " ", event.action, " (", event.x, ",", event.y, ")"); 1785 // support onClick 1786 bool insideLeftPane = event.x < _clientRect.left && event.x >= _clientRect.left - _leftPaneWidth; 1787 if (event.action == MouseAction.ButtonDown && insideLeftPane) { 1788 setFocus(); 1789 cancelHoverTimer(); 1790 if (onLeftPaneMouseClick(event)) 1791 return true; 1792 } 1793 if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) { 1794 setFocus(); 1795 cancelHoverTimer(); 1796 if (event.tripleClick) { 1797 selectLineByMouse(event.x - _clientRect.left, event.y - _clientRect.top); 1798 } else if (event.doubleClick) { 1799 selectWordByMouse(event.x - _clientRect.left, event.y - _clientRect.top); 1800 } else { 1801 auto doSelect = cast(bool)(event.keyFlags & MouseFlag.Shift); 1802 updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, doSelect); 1803 1804 if (event.keyFlags == MouseFlag.Control) 1805 onControlClick(); 1806 } 1807 startCaretBlinking(); 1808 invalidate(); 1809 return true; 1810 } 1811 if (event.action == MouseAction.Move && (event.flags & MouseButton.Left) != 0) { 1812 updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, true); 1813 return true; 1814 } 1815 if (event.action == MouseAction.Move && event.flags == 0) { 1816 // hover 1817 if (focused && !insideLeftPane) { 1818 onHover(event.pos); 1819 } else { 1820 cancelHoverTimer(); 1821 } 1822 return true; 1823 } 1824 if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) { 1825 cancelHoverTimer(); 1826 return true; 1827 } 1828 if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) { 1829 cancelHoverTimer(); 1830 return true; 1831 } 1832 if (event.action == MouseAction.FocusIn) { 1833 cancelHoverTimer(); 1834 return true; 1835 } 1836 if (event.action == MouseAction.Wheel) { 1837 cancelHoverTimer(); 1838 uint keyFlags = event.flags & (MouseFlag.Shift | MouseFlag.Control | MouseFlag.Alt); 1839 if (event.wheelDelta < 0) { 1840 if (keyFlags == MouseFlag.Shift) 1841 return handleAction(new Action(EditorActions.ScrollRight)); 1842 if (keyFlags == MouseFlag.Control) 1843 return handleAction(new Action(EditorActions.ZoomOut)); 1844 return handleAction(new Action(EditorActions.ScrollLineDown)); 1845 } else if (event.wheelDelta > 0) { 1846 if (keyFlags == MouseFlag.Shift) 1847 return handleAction(new Action(EditorActions.ScrollLeft)); 1848 if (keyFlags == MouseFlag.Control) 1849 return handleAction(new Action(EditorActions.ZoomIn)); 1850 return handleAction(new Action(EditorActions.ScrollLineUp)); 1851 } 1852 } 1853 cancelHoverTimer(); 1854 return super.onMouseEvent(event); 1855 } 1856 1857 /// returns caret position 1858 @property TextPosition caretPos() { 1859 return _caretPos; 1860 } 1861 1862 /// change caret position and ensure it is visible 1863 void setCaretPos(int line, int column, bool makeVisible = true, bool center = false) 1864 { 1865 _caretPos = TextPosition(line,column); 1866 correctCaretPos(); 1867 invalidate(); 1868 if (makeVisible) 1869 ensureCaretVisible(center); 1870 } 1871 } 1872 1873 interface EditorActionHandler { 1874 bool onEditorAction(const Action action); 1875 } 1876 1877 /// single line editor 1878 class EditLine : EditWidgetBase { 1879 1880 Signal!EditorActionHandler editorAction; 1881 1882 /// empty parameter list constructor - for usage by factory 1883 this() { 1884 this(null); 1885 } 1886 /// create with ID parameter 1887 this(string ID, dstring initialContent = null) { 1888 super(ID, ScrollBarMode.Invisible, ScrollBarMode.Invisible); 1889 _content = new EditableContent(false); 1890 _content.contentChanged = this; 1891 _selectAllWhenFocusedWithTab = true; 1892 _deselectAllWhenUnfocused = true; 1893 wantTabs = false; 1894 styleId = STYLE_EDIT_LINE; 1895 text = initialContent; 1896 onThemeChanged(); 1897 } 1898 1899 protected dstring _measuredText; 1900 protected int[] _measuredTextWidths; 1901 protected Point _measuredTextSize; 1902 1903 protected dchar _passwordChar = 0; 1904 /// password character - 0 for normal editor, some character, e.g. '*' to hide text by replacing all characters with this char 1905 @property dchar passwordChar() { return _passwordChar; } 1906 @property EditLine passwordChar(dchar ch) { 1907 if (_passwordChar != ch) { 1908 _passwordChar = ch; 1909 requestLayout(); 1910 } 1911 return this; 1912 } 1913 1914 override protected Rect textPosToClient(TextPosition p) { 1915 Rect res; 1916 res.bottom = _clientRect.height; 1917 if (p.pos == 0) 1918 res.left = 0; 1919 else if (p.pos >= _measuredText.length) 1920 res.left = _measuredTextSize.x; 1921 else 1922 res.left = _measuredTextWidths[p.pos - 1]; 1923 res.left -= _scrollPos.x; 1924 res.right = res.left + 1; 1925 return res; 1926 } 1927 1928 override protected TextPosition clientToTextPos(Point pt) { 1929 pt.x += _scrollPos.x; 1930 TextPosition res; 1931 for (int i = 0; i < _measuredText.length; i++) { 1932 int x0 = i > 0 ? _measuredTextWidths[i - 1] : 0; 1933 int x1 = _measuredTextWidths[i]; 1934 int mx = (x0 + x1) >> 1; 1935 if (pt.x <= mx) { 1936 res.pos = i; 1937 return res; 1938 } 1939 } 1940 res.pos = cast(int)_measuredText.length; 1941 return res; 1942 } 1943 1944 override protected void ensureCaretVisible(bool center = false) { 1945 //_scrollPos 1946 Rect rc = textPosToClient(_caretPos); 1947 if (rc.left < 0) { 1948 // scroll left 1949 _scrollPos.x -= -rc.left + _clientRect.width / 10; 1950 if (_scrollPos.x < 0) 1951 _scrollPos.x = 0; 1952 invalidate(); 1953 } else if (rc.left >= _clientRect.width - 10) { 1954 // scroll right 1955 _scrollPos.x += (rc.left - _clientRect.width) + _spaceWidth * 4; 1956 invalidate(); 1957 } 1958 updateScrollBars(); 1959 } 1960 1961 protected dstring applyPasswordChar(dstring s) { 1962 if (!_passwordChar || s.length == 0) 1963 return s; 1964 dchar[] ss = s.dup; 1965 foreach(ref ch; ss) 1966 ch = _passwordChar; 1967 return cast(dstring)ss; 1968 } 1969 1970 override protected Point measureVisibleText() { 1971 FontRef font = font(); 1972 //Point sz = font.textSize(text); 1973 _measuredText = applyPasswordChar(text); 1974 _measuredTextWidths.length = _measuredText.length; 1975 int charsMeasured = font.measureText(_measuredText, _measuredTextWidths, int.max, tabSize); 1976 _measuredTextSize.x = charsMeasured > 0 ? _measuredTextWidths[charsMeasured - 1]: 0; 1977 _measuredTextSize.y = font.height; 1978 return _measuredTextSize; 1979 } 1980 1981 /// measure 1982 override void measure(int parentWidth, int parentHeight) { 1983 updateFontProps(); 1984 measureVisibleText(); 1985 measuredContent(parentWidth, parentHeight, _measuredTextSize.x + _leftPaneWidth, _measuredTextSize.y); 1986 } 1987 1988 override bool handleAction(const Action a) { 1989 switch (a.id) with(EditorActions) 1990 { 1991 case InsertNewLine: 1992 case PrependNewLine: 1993 case AppendNewLine: 1994 if (editorAction.assigned) { 1995 return editorAction(a); 1996 } 1997 break; 1998 case Up: 1999 break; 2000 case Down: 2001 break; 2002 case PageUp: 2003 break; 2004 case PageDown: 2005 break; 2006 default: 2007 break; 2008 } 2009 return super.handleAction(a); 2010 } 2011 2012 2013 /// handle keys 2014 override bool onKeyEvent(KeyEvent event) { 2015 return super.onKeyEvent(event); 2016 } 2017 2018 /// process mouse event; return true if event is processed by widget. 2019 override bool onMouseEvent(MouseEvent event) { 2020 return super.onMouseEvent(event); 2021 } 2022 2023 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 2024 override void layout(Rect rc) { 2025 if (visibility == Visibility.Gone) { 2026 return; 2027 } 2028 _needLayout = false; 2029 Point sz = Point(rc.width, measuredHeight); 2030 applyAlign(rc, sz); 2031 _pos = rc; 2032 _clientRect = rc; 2033 applyMargins(_clientRect); 2034 applyPadding(_clientRect); 2035 if (_contentChanged) { 2036 measureVisibleText(); 2037 _contentChanged = false; 2038 } 2039 } 2040 2041 2042 /// override to custom highlight of line background 2043 protected void drawLineBackground(DrawBuf buf, Rect lineRect, Rect visibleRect) { 2044 if (!_selectionRange.empty) { 2045 // line inside selection 2046 Rect startrc = textPosToClient(_selectionRange.start); 2047 Rect endrc = textPosToClient(_selectionRange.end); 2048 int startx = startrc.left + _clientRect.left; 2049 int endx = endrc.left + _clientRect.left; 2050 Rect rc = lineRect; 2051 rc.left = startx; 2052 rc.right = endx; 2053 if (!rc.empty) { 2054 // draw selection rect for line 2055 buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal); 2056 } 2057 if (_leftPaneWidth > 0) { 2058 Rect leftPaneRect = visibleRect; 2059 leftPaneRect.right = leftPaneRect.left; 2060 leftPaneRect.left -= _leftPaneWidth; 2061 drawLeftPane(buf, leftPaneRect, 0); 2062 } 2063 } 2064 } 2065 2066 /// draw content 2067 override void onDraw(DrawBuf buf) { 2068 if (visibility != Visibility.Visible) 2069 return; 2070 super.onDraw(buf); 2071 Rect rc = _pos; 2072 applyMargins(rc); 2073 applyPadding(rc); 2074 auto saver = ClipRectSaver(buf, rc, alpha); 2075 FontRef font = font(); 2076 dstring txt = applyPasswordChar(text); 2077 Point sz = font.textSize(txt); 2078 //applyAlign(rc, sz); 2079 Rect lineRect = _clientRect; 2080 lineRect.left = _clientRect.left - _scrollPos.x; 2081 lineRect.right = lineRect.left + calcLineWidth(txt); 2082 Rect visibleRect = lineRect; 2083 visibleRect.left = _clientRect.left; 2084 visibleRect.right = _clientRect.right; 2085 drawLineBackground(buf, lineRect, visibleRect); 2086 font.drawText(buf, rc.left - _scrollPos.x, rc.top, txt, textColor, tabSize); 2087 2088 drawCaret(buf); 2089 } 2090 } 2091 2092 2093 2094 /// multiline editor 2095 class EditBox : EditWidgetBase { 2096 /// empty parameter list constructor - for usage by factory 2097 this() { 2098 this(null); 2099 } 2100 /// create with ID parameter 2101 this(string ID, dstring initialContent = null, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) { 2102 super(ID, hscrollbarMode, vscrollbarMode); 2103 _content = new EditableContent(true); // multiline 2104 _content.contentChanged = this; 2105 styleId = STYLE_EDIT_BOX; 2106 text = initialContent; 2107 acceleratorMap.add( [ 2108 // zoom 2109 new Action(EditorActions.ZoomIn, KeyCode.ADD, KeyFlag.Control), 2110 new Action(EditorActions.ZoomOut, KeyCode.SUB, KeyFlag.Control), 2111 ]); 2112 onThemeChanged(); 2113 } 2114 2115 ~this() { 2116 if (_findPanel) { 2117 destroy(_findPanel); 2118 _findPanel = null; 2119 } 2120 } 2121 2122 protected int _firstVisibleLine; 2123 2124 protected int _maxLineWidth; 2125 protected int _numVisibleLines; // number of lines visible in client area 2126 protected dstring[] _visibleLines; // text for visible lines 2127 protected int[][] _visibleLinesMeasurement; // char positions for visible lines 2128 protected int[] _visibleLinesWidths; // width (in pixels) of visible lines 2129 protected CustomCharProps[][] _visibleLinesHighlights; 2130 protected CustomCharProps[][] _visibleLinesHighlightsBuf; 2131 2132 override protected int lineCount() { 2133 return _content.length; 2134 } 2135 2136 override protected void updateMaxLineWidth() { 2137 // find max line width. TODO: optimize!!! 2138 int maxw; 2139 int[] buf; 2140 for (int i = 0; i < _content.length; i++) { 2141 dstring s = _content[i]; 2142 int w = calcLineWidth(s); 2143 if (maxw < w) 2144 maxw = w; 2145 } 2146 _maxLineWidth = maxw; 2147 } 2148 2149 @property int minFontSize() { 2150 return _minFontSize; 2151 } 2152 @property EditBox minFontSize(int size) { 2153 _minFontSize = size; 2154 return this; 2155 } 2156 2157 @property int maxFontSize() { 2158 return _maxFontSize; 2159 } 2160 2161 @property EditBox maxFontSize(int size) { 2162 _maxFontSize = size; 2163 return this; 2164 } 2165 2166 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 2167 override void layout(Rect rc) { 2168 if (visibility == Visibility.Gone) { 2169 return; 2170 } 2171 if (rc != _pos) 2172 _contentChanged = true; 2173 Rect contentRc = rc; 2174 int findPanelHeight; 2175 if (_findPanel && _findPanel.visibility != Visibility.Gone) { 2176 _findPanel.measure(rc.width, rc.height); 2177 findPanelHeight = _findPanel.measuredHeight; 2178 _findPanel.layout(Rect(rc.left, rc.bottom - findPanelHeight, rc.right, rc.bottom)); 2179 contentRc.bottom -= findPanelHeight; 2180 } 2181 2182 super.layout(contentRc); 2183 if (_contentChanged) { 2184 measureVisibleText(); 2185 _contentChanged = false; 2186 } 2187 2188 _pos = rc; 2189 } 2190 2191 override protected Point measureVisibleText() { 2192 Point sz; 2193 FontRef font = font(); 2194 _lineHeight = font.height; 2195 _numVisibleLines = (_clientRect.height + _lineHeight - 1) / _lineHeight; 2196 if (_firstVisibleLine >= _content.length) { 2197 _firstVisibleLine = _content.length - _numVisibleLines + 1; 2198 if (_firstVisibleLine < 0) 2199 _firstVisibleLine = 0; 2200 _caretPos.line = _content.length - 1; 2201 _caretPos.pos = 0; 2202 } 2203 if (_numVisibleLines < 1) 2204 _numVisibleLines = 1; 2205 if (_firstVisibleLine + _numVisibleLines > _content.length) 2206 _numVisibleLines = _content.length - _firstVisibleLine; 2207 if (_numVisibleLines < 1) 2208 _numVisibleLines = 1; 2209 _visibleLines.length = _numVisibleLines; 2210 if (_visibleLinesMeasurement.length < _numVisibleLines) 2211 _visibleLinesMeasurement.length = _numVisibleLines; 2212 if (_visibleLinesWidths.length < _numVisibleLines) 2213 _visibleLinesWidths.length = _numVisibleLines; 2214 if (_visibleLinesHighlights.length < _numVisibleLines) { 2215 _visibleLinesHighlights.length = _numVisibleLines; 2216 _visibleLinesHighlightsBuf.length = _numVisibleLines; 2217 } 2218 for (int i = 0; i < _numVisibleLines; i++) { 2219 _visibleLines[i] = _content[_firstVisibleLine + i]; 2220 size_t len = _visibleLines[i].length; 2221 if (_visibleLinesMeasurement[i].length < len) 2222 _visibleLinesMeasurement[i].length = len; 2223 if (_visibleLinesHighlightsBuf[i].length < len) 2224 _visibleLinesHighlightsBuf[i].length = len; 2225 _visibleLinesHighlights[i] = handleCustomLineHighlight(_firstVisibleLine + i, _visibleLines[i], _visibleLinesHighlightsBuf[i]); 2226 int charsMeasured = font.measureText(_visibleLines[i], _visibleLinesMeasurement[i], int.max, tabSize); 2227 _visibleLinesWidths[i] = charsMeasured > 0 ? _visibleLinesMeasurement[i][charsMeasured - 1] : 0; 2228 if (sz.x < _visibleLinesWidths[i]) 2229 sz.x = _visibleLinesWidths[i]; // width - max from visible lines 2230 } 2231 sz.x = _maxLineWidth; 2232 sz.y = _lineHeight * _content.length; // height - for all lines 2233 return sz; 2234 } 2235 2236 /// update horizontal scrollbar widget position 2237 override protected void updateHScrollBar() { 2238 _hscrollbar.setRange(0, _maxLineWidth + _clientRect.width / 4); 2239 _hscrollbar.pageSize = _clientRect.width; 2240 _hscrollbar.position = _scrollPos.x; 2241 } 2242 2243 /// update verticat scrollbar widget position 2244 override protected void updateVScrollBar() { 2245 int visibleLines = _lineHeight ? _clientRect.height / _lineHeight : 1; // fully visible lines 2246 if (visibleLines < 1) 2247 visibleLines = 1; 2248 _vscrollbar.setRange(0, _content.length - 1); 2249 _vscrollbar.pageSize = visibleLines; 2250 _vscrollbar.position = _firstVisibleLine; 2251 } 2252 2253 /// process horizontal scrollbar event 2254 override bool onHScroll(ScrollEvent event) { 2255 if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) { 2256 if (_scrollPos.x != event.position) { 2257 _scrollPos.x = event.position; 2258 invalidate(); 2259 } 2260 } else if (event.action == ScrollAction.PageUp) { 2261 dispatchAction(new Action(EditorActions.ScrollLeft)); 2262 } else if (event.action == ScrollAction.PageDown) { 2263 dispatchAction(new Action(EditorActions.ScrollRight)); 2264 } else if (event.action == ScrollAction.LineUp) { 2265 dispatchAction(new Action(EditorActions.ScrollLeft)); 2266 } else if (event.action == ScrollAction.LineDown) { 2267 dispatchAction(new Action(EditorActions.ScrollRight)); 2268 } 2269 return true; 2270 } 2271 2272 /// process vertical scrollbar event 2273 override bool onVScroll(ScrollEvent event) { 2274 if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) { 2275 if (_firstVisibleLine != event.position) { 2276 _firstVisibleLine = event.position; 2277 measureVisibleText(); 2278 invalidate(); 2279 } 2280 } else if (event.action == ScrollAction.PageUp) { 2281 dispatchAction(new Action(EditorActions.ScrollPageUp)); 2282 } else if (event.action == ScrollAction.PageDown) { 2283 dispatchAction(new Action(EditorActions.ScrollPageDown)); 2284 } else if (event.action == ScrollAction.LineUp) { 2285 dispatchAction(new Action(EditorActions.ScrollLineUp)); 2286 } else if (event.action == ScrollAction.LineDown) { 2287 dispatchAction(new Action(EditorActions.ScrollLineDown)); 2288 } 2289 return true; 2290 } 2291 2292 protected bool _enableScrollAfterText = true; 2293 override protected void ensureCaretVisible(bool center = false) { 2294 if (_caretPos.line >= _content.length) 2295 _caretPos.line = _content.length - 1; 2296 if (_caretPos.line < 0) 2297 _caretPos.line = 0; 2298 int visibleLines = _lineHeight > 0 ? _clientRect.height / _lineHeight : 1; // fully visible lines 2299 if (visibleLines < 1) 2300 visibleLines = 1; 2301 int maxFirstVisibleLine = _content.length - 1; 2302 if (!_enableScrollAfterText) 2303 maxFirstVisibleLine = _content.length - visibleLines; 2304 if (maxFirstVisibleLine < 0) 2305 maxFirstVisibleLine = 0; 2306 2307 if (_caretPos.line < _firstVisibleLine) { 2308 _firstVisibleLine = _caretPos.line; 2309 if (center) { 2310 _firstVisibleLine -= visibleLines / 2; 2311 if (_firstVisibleLine < 0) 2312 _firstVisibleLine = 0; 2313 } 2314 if (_firstVisibleLine > maxFirstVisibleLine) 2315 _firstVisibleLine = maxFirstVisibleLine; 2316 measureVisibleText(); 2317 invalidate(); 2318 } else if (_caretPos.line >= _firstVisibleLine + visibleLines) { 2319 _firstVisibleLine = _caretPos.line - visibleLines + 1; 2320 if (center) 2321 _firstVisibleLine += visibleLines / 2; 2322 if (_firstVisibleLine > maxFirstVisibleLine) 2323 _firstVisibleLine = maxFirstVisibleLine; 2324 if (_firstVisibleLine < 0) 2325 _firstVisibleLine = 0; 2326 measureVisibleText(); 2327 invalidate(); 2328 } else if (_firstVisibleLine > maxFirstVisibleLine) { 2329 _firstVisibleLine = maxFirstVisibleLine; 2330 if (_firstVisibleLine < 0) 2331 _firstVisibleLine = 0; 2332 measureVisibleText(); 2333 invalidate(); 2334 } 2335 //_scrollPos 2336 Rect rc = textPosToClient(_caretPos); 2337 if (rc.left < 0) { 2338 // scroll left 2339 _scrollPos.x -= -rc.left + _clientRect.width / 4; 2340 if (_scrollPos.x < 0) 2341 _scrollPos.x = 0; 2342 invalidate(); 2343 } else if (rc.left >= _clientRect.width - 10) { 2344 // scroll right 2345 _scrollPos.x += (rc.left - _clientRect.width) + _clientRect.width / 4; 2346 invalidate(); 2347 } 2348 updateScrollBars(); 2349 } 2350 2351 override protected Rect textPosToClient(TextPosition p) { 2352 Rect res; 2353 int lineIndex = p.line - _firstVisibleLine; 2354 res.top = lineIndex * _lineHeight; 2355 res.bottom = res.top + _lineHeight; 2356 // if visible 2357 if (lineIndex >= 0 && lineIndex < _visibleLines.length) { 2358 if (p.pos == 0) 2359 res.left = 0; 2360 else if (p.pos >= _visibleLinesMeasurement[lineIndex].length) 2361 res.left = _visibleLinesWidths[lineIndex]; 2362 else 2363 res.left = _visibleLinesMeasurement[lineIndex][p.pos - 1]; 2364 } 2365 res.left -= _scrollPos.x; 2366 res.right = res.left + 1; 2367 return res; 2368 } 2369 2370 override protected TextPosition clientToTextPos(Point pt) { 2371 TextPosition res; 2372 pt.x += _scrollPos.x; 2373 int lineIndex = pt.y / _lineHeight; 2374 if (lineIndex < 0) 2375 lineIndex = 0; 2376 if (lineIndex < _visibleLines.length) { 2377 res.line = lineIndex + _firstVisibleLine; 2378 int len = cast(int)_visibleLines[lineIndex].length; 2379 for (int i = 0; i < len; i++) { 2380 int x0 = i > 0 ? _visibleLinesMeasurement[lineIndex][i - 1] : 0; 2381 int x1 = _visibleLinesMeasurement[lineIndex][i]; 2382 int mx = (x0 + x1) >> 1; 2383 if (pt.x <= mx) { 2384 res.pos = i; 2385 return res; 2386 } 2387 } 2388 res.pos = cast(int)_visibleLines[lineIndex].length; 2389 } else if (_visibleLines.length > 0) { 2390 res.line = _firstVisibleLine + cast(int)_visibleLines.length - 1; 2391 res.pos = cast(int)_visibleLines[$ - 1].length; 2392 } else { 2393 res.line = 0; 2394 res.pos = 0; 2395 } 2396 return res; 2397 } 2398 2399 override protected bool handleAction(const Action a) { 2400 TextPosition oldCaretPos = _caretPos; 2401 dstring currentLine = _content[_caretPos.line]; 2402 switch (a.id) with(EditorActions) 2403 { 2404 case PrependNewLine: 2405 if (!readOnly) { 2406 correctCaretPos(); 2407 _caretPos.pos = 0; 2408 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]); 2409 _content.performOperation(op, this); 2410 } 2411 return true; 2412 case InsertNewLine: 2413 if (!readOnly) { 2414 correctCaretPos(); 2415 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]); 2416 _content.performOperation(op, this); 2417 } 2418 return true; 2419 case Up: 2420 case SelectUp: 2421 if (_caretPos.line > 0) { 2422 _caretPos.line--; 2423 correctCaretPos(); 2424 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 2425 ensureCaretVisible(); 2426 } 2427 return true; 2428 case Down: 2429 case SelectDown: 2430 if (_caretPos.line < _content.length - 1) { 2431 _caretPos.line++; 2432 correctCaretPos(); 2433 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 2434 ensureCaretVisible(); 2435 } 2436 return true; 2437 case PageBegin: 2438 case SelectPageBegin: 2439 { 2440 ensureCaretVisible(); 2441 _caretPos.line = _firstVisibleLine; 2442 correctCaretPos(); 2443 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 2444 } 2445 return true; 2446 case PageEnd: 2447 case SelectPageEnd: 2448 { 2449 ensureCaretVisible(); 2450 int fullLines = _clientRect.height / _lineHeight; 2451 int newpos = _firstVisibleLine + fullLines - 1; 2452 if (newpos >= _content.length) 2453 newpos = _content.length - 1; 2454 _caretPos.line = newpos; 2455 correctCaretPos(); 2456 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 2457 } 2458 return true; 2459 case PageUp: 2460 case SelectPageUp: 2461 { 2462 ensureCaretVisible(); 2463 int fullLines = _clientRect.height / _lineHeight; 2464 int newpos = _firstVisibleLine - fullLines; 2465 if (newpos < 0) { 2466 _firstVisibleLine = 0; 2467 _caretPos.line = 0; 2468 } else { 2469 int delta = _firstVisibleLine - newpos; 2470 _firstVisibleLine = newpos; 2471 _caretPos.line -= delta; 2472 } 2473 correctCaretPos(); 2474 measureVisibleText(); 2475 updateScrollBars(); 2476 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 2477 } 2478 return true; 2479 case PageDown: 2480 case SelectPageDown: 2481 { 2482 ensureCaretVisible(); 2483 int fullLines = _clientRect.height / _lineHeight; 2484 int newpos = _firstVisibleLine + fullLines; 2485 if (newpos >= _content.length) { 2486 _caretPos.line = _content.length - 1; 2487 } else { 2488 int delta = newpos - _firstVisibleLine; 2489 _firstVisibleLine = newpos; 2490 _caretPos.line += delta; 2491 } 2492 correctCaretPos(); 2493 measureVisibleText(); 2494 updateScrollBars(); 2495 updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0); 2496 } 2497 return true; 2498 case ScrollLeft: 2499 { 2500 if (_scrollPos.x > 0) { 2501 int newpos = _scrollPos.x - _spaceWidth * 4; 2502 if (newpos < 0) 2503 newpos = 0; 2504 _scrollPos.x = newpos; 2505 updateScrollBars(); 2506 invalidate(); 2507 } 2508 } 2509 return true; 2510 case ScrollRight: 2511 { 2512 if (_scrollPos.x < _maxLineWidth - _clientRect.width) { 2513 int newpos = _scrollPos.x + _spaceWidth * 4; 2514 if (newpos > _maxLineWidth - _clientRect.width) 2515 newpos = _maxLineWidth - _clientRect.width; 2516 _scrollPos.x = newpos; 2517 updateScrollBars(); 2518 invalidate(); 2519 } 2520 } 2521 return true; 2522 case ScrollLineUp: 2523 { 2524 if (_firstVisibleLine > 0) { 2525 _firstVisibleLine -= 3; 2526 if (_firstVisibleLine < 0) 2527 _firstVisibleLine = 0; 2528 measureVisibleText(); 2529 updateScrollBars(); 2530 invalidate(); 2531 } 2532 } 2533 return true; 2534 case ScrollPageUp: 2535 { 2536 int fullLines = _clientRect.height / _lineHeight; 2537 if (_firstVisibleLine > 0) { 2538 _firstVisibleLine -= fullLines * 3 / 4; 2539 if (_firstVisibleLine < 0) 2540 _firstVisibleLine = 0; 2541 measureVisibleText(); 2542 updateScrollBars(); 2543 invalidate(); 2544 } 2545 } 2546 return true; 2547 case ScrollLineDown: 2548 { 2549 int fullLines = _clientRect.height / _lineHeight; 2550 if (_firstVisibleLine + fullLines < _content.length) { 2551 _firstVisibleLine += 3; 2552 if (_firstVisibleLine > _content.length - fullLines) 2553 _firstVisibleLine = _content.length - fullLines; 2554 if (_firstVisibleLine < 0) 2555 _firstVisibleLine = 0; 2556 measureVisibleText(); 2557 updateScrollBars(); 2558 invalidate(); 2559 } 2560 } 2561 return true; 2562 case ScrollPageDown: 2563 { 2564 int fullLines = _clientRect.height / _lineHeight; 2565 if (_firstVisibleLine + fullLines < _content.length) { 2566 _firstVisibleLine += fullLines * 3 / 4; 2567 if (_firstVisibleLine > _content.length - fullLines) 2568 _firstVisibleLine = _content.length - fullLines; 2569 if (_firstVisibleLine < 0) 2570 _firstVisibleLine = 0; 2571 measureVisibleText(); 2572 updateScrollBars(); 2573 invalidate(); 2574 } 2575 } 2576 return true; 2577 case ZoomOut: 2578 case ZoomIn: 2579 { 2580 int dir = a.id == ZoomIn ? 1 : -1; 2581 if (_minFontSize < _maxFontSize && _minFontSize > 0 && _maxFontSize > 0) { 2582 int currentFontSize = fontSize; 2583 int increment = currentFontSize >= 30 ? 2 : 1; 2584 int newFontSize = currentFontSize + increment * dir; //* 110 / 100; 2585 if (newFontSize > 30) 2586 newFontSize &= 0xFFFE; 2587 if (currentFontSize != newFontSize && newFontSize <= _maxFontSize && newFontSize >= _minFontSize) { 2588 Log.i("Font size in editor ", id, " zoomed to ", newFontSize); 2589 fontSize = cast(ushort)newFontSize; 2590 updateFontProps(); 2591 measureVisibleText(); 2592 updateScrollBars(); 2593 invalidate(); 2594 } 2595 } 2596 } 2597 return true; 2598 case ToggleBlockComment: 2599 if (!readOnly && _content.syntaxSupport && _content.syntaxSupport.supportsToggleBlockComment && _content.syntaxSupport.canToggleBlockComment(_selectionRange)) 2600 _content.syntaxSupport.toggleBlockComment(_selectionRange, this); 2601 return true; 2602 case ToggleLineComment: 2603 if (!readOnly && _content.syntaxSupport && _content.syntaxSupport.supportsToggleLineComment && _content.syntaxSupport.canToggleLineComment(_selectionRange)) 2604 _content.syntaxSupport.toggleLineComment(_selectionRange, this); 2605 return true; 2606 case AppendNewLine: 2607 if (!readOnly) { 2608 correctCaretPos(); 2609 TextPosition p = _content.lineEnd(_caretPos.line); 2610 TextRange r = TextRange(p, p); 2611 EditOperation op = new EditOperation(EditAction.Replace, r, [""d, ""d]); 2612 _content.performOperation(op, this); 2613 _caretPos = oldCaretPos; 2614 } 2615 return true; 2616 case DeleteLine: 2617 if (!readOnly) { 2618 correctCaretPos(); 2619 EditOperation op = new EditOperation(EditAction.Replace, _content.lineRange(_caretPos.line), [""d]); 2620 _content.performOperation(op, this); 2621 } 2622 return true; 2623 case Find: 2624 createFindPanel(false, false); 2625 return true; 2626 case Replace: 2627 createFindPanel(false, true); 2628 return true; 2629 default: 2630 break; 2631 } 2632 return super.handleAction(a); 2633 } 2634 2635 /// calculate full content size in pixels 2636 override Point fullContentSize() { 2637 Point textSz = measureVisibleText(); 2638 int maxy = _lineHeight * 5; // limit measured height 2639 if (textSz.y > maxy) 2640 textSz.y = maxy; 2641 return textSz; 2642 } 2643 2644 /// measure 2645 override void measure(int parentWidth, int parentHeight) { 2646 if (visibility == Visibility.Gone) { 2647 return; 2648 } 2649 updateFontProps(); 2650 updateMaxLineWidth(); 2651 int findPanelHeight; 2652 if (_findPanel) { 2653 _findPanel.measure(parentWidth, parentHeight); 2654 findPanelHeight = _findPanel.measuredHeight; 2655 if (parentHeight != SIZE_UNSPECIFIED) 2656 parentHeight -= findPanelHeight; 2657 } 2658 2659 super.measure(parentWidth, parentHeight); 2660 // do we need to add vsbwidth, hsbheight ??? 2661 //measuredContent(parentWidth, parentHeight, textSz.x + vsbwidth, textSz.y + hsbheight); 2662 } 2663 2664 2665 protected void highlightLineRange(DrawBuf buf, Rect lineRect, uint color, TextRange r) { 2666 Rect startrc = textPosToClient(r.start); 2667 Rect endrc = textPosToClient(r.end); 2668 Rect rc = lineRect; 2669 rc.left = _clientRect.left + startrc.left; 2670 rc.right = _clientRect.left + endrc.right; 2671 if (!rc.empty) { 2672 // draw selection rect for matching bracket 2673 buf.fillRect(rc, color); 2674 } 2675 } 2676 2677 /// override to custom highlight of line background 2678 protected void drawLineBackground(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) { 2679 // highlight odd lines 2680 //if ((lineIndex & 1)) 2681 // buf.fillRect(visibleRect, 0xF4808080); 2682 2683 if (!_selectionRange.empty && _selectionRange.start.line <= lineIndex && _selectionRange.end.line >= lineIndex) { 2684 // line inside selection 2685 Rect startrc = textPosToClient(_selectionRange.start); 2686 Rect endrc = textPosToClient(_selectionRange.end); 2687 int startx = lineIndex == _selectionRange.start.line ? startrc.left + _clientRect.left : lineRect.left; 2688 int endx = lineIndex == _selectionRange.end.line ? endrc.left + _clientRect.left : lineRect.right + _spaceWidth; 2689 Rect rc = lineRect; 2690 rc.left = startx; 2691 rc.right = endx; 2692 if (!rc.empty) { 2693 // draw selection rect for line 2694 buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal); 2695 } 2696 } 2697 2698 if (_matchingBraces.start.line == lineIndex) { 2699 TextRange r = TextRange(_matchingBraces.start, _matchingBraces.start.offset(1)); 2700 highlightLineRange(buf, lineRect, _matchingBracketHightlightColor, r); 2701 } 2702 if (_matchingBraces.end.line == lineIndex) { 2703 TextRange r = TextRange(_matchingBraces.end, _matchingBraces.end.offset(1)); 2704 highlightLineRange(buf, lineRect, _matchingBracketHightlightColor, r); 2705 } 2706 2707 // frame around current line 2708 if (focused && lineIndex == _caretPos.line && _selectionRange.singleLine && _selectionRange.start.line == _caretPos.line) { 2709 buf.drawFrame(visibleRect, 0xA0808080, Rect(1,1,1,1)); 2710 } 2711 2712 } 2713 2714 override protected void drawExtendedArea(DrawBuf buf) { 2715 if (_leftPaneWidth <= 0) 2716 return; 2717 Rect rc = _clientRect; 2718 2719 FontRef font = font(); 2720 int i = _firstVisibleLine; 2721 int lc = lineCount; 2722 for (;;) { 2723 Rect lineRect = rc; 2724 lineRect.left = _clientRect.left - _leftPaneWidth; 2725 lineRect.right = _clientRect.left; 2726 lineRect.bottom = lineRect.top + _lineHeight; 2727 if (lineRect.top >= _clientRect.bottom) 2728 break; 2729 drawLeftPane(buf, lineRect, i < lc ? i : -1); 2730 i++; 2731 rc.top += _lineHeight; 2732 } 2733 } 2734 2735 2736 protected CustomCharProps[ubyte] _tokenHighlightColors; 2737 2738 /// set highlight options for particular token category 2739 void setTokenHightlightColor(ubyte tokenCategory, uint color, bool underline = false, bool strikeThrough = false) { 2740 _tokenHighlightColors[tokenCategory] = CustomCharProps(color, underline, strikeThrough); 2741 } 2742 /// clear highlight colors 2743 void clearTokenHightlightColors() { 2744 destroy(_tokenHighlightColors); 2745 } 2746 2747 /** 2748 Custom text color and style highlight (using text highlight) support. 2749 2750 Return null if no syntax highlight required for line. 2751 */ 2752 protected CustomCharProps[] handleCustomLineHighlight(int line, dstring txt, ref CustomCharProps[] buf) { 2753 if (!_tokenHighlightColors) 2754 return null; // no highlight colors set 2755 TokenPropString tokenProps = _content.lineTokenProps(line); 2756 if (tokenProps.length > 0) { 2757 bool hasNonzeroTokens = false; 2758 foreach(t; tokenProps) 2759 if (t) { 2760 hasNonzeroTokens = true; 2761 break; 2762 } 2763 if (!hasNonzeroTokens) 2764 return null; // all characters are of unknown token type (or white space) 2765 if (buf.length < tokenProps.length) 2766 buf.length = tokenProps.length; 2767 CustomCharProps[] colors = buf[0..tokenProps.length]; //new CustomCharProps[tokenProps.length]; 2768 for (int i = 0; i < tokenProps.length; i++) { 2769 ubyte p = tokenProps[i]; 2770 if (p in _tokenHighlightColors) 2771 colors[i] = _tokenHighlightColors[p]; 2772 else if ((p & TOKEN_CATEGORY_MASK) in _tokenHighlightColors) 2773 colors[i] = _tokenHighlightColors[(p & TOKEN_CATEGORY_MASK)]; 2774 else 2775 colors[i].color = textColor; 2776 if (isFullyTransparentColor(colors[i].color)) 2777 colors[i].color = textColor; 2778 } 2779 return colors; 2780 } 2781 return null; 2782 } 2783 2784 TextRange _matchingBraces; 2785 2786 bool _showWhiteSpaceMarks; 2787 /// when true, show marks for tabs and spaces at beginning and end of line, and tabs inside line 2788 @property bool showWhiteSpaceMarks() const { return _showWhiteSpaceMarks; } 2789 @property void showWhiteSpaceMarks(bool show) { 2790 if (_showWhiteSpaceMarks != show) { 2791 _showWhiteSpaceMarks = show; 2792 invalidate(); 2793 } 2794 } 2795 2796 /// find max tab mark column position for line 2797 protected int findMaxTabMarkColumn(int lineIndex) { 2798 if (lineIndex < 0 || lineIndex >= content.length) 2799 return -1; 2800 int maxSpace = -1; 2801 auto space = content.getLineWhiteSpace(lineIndex); 2802 maxSpace = space.firstNonSpaceColumn; 2803 if (maxSpace >= 0) 2804 return maxSpace; 2805 for(int i = lineIndex - 1; i >= 0; i--) { 2806 space = content.getLineWhiteSpace(i); 2807 if (!space.empty) { 2808 maxSpace = space.firstNonSpaceColumn; 2809 break; 2810 } 2811 } 2812 for(int i = lineIndex + 1; i < content.length; i++) { 2813 space = content.getLineWhiteSpace(i); 2814 if (!space.empty) { 2815 if (maxSpace < 0 || maxSpace < space.firstNonSpaceColumn) 2816 maxSpace = space.firstNonSpaceColumn; 2817 break; 2818 } 2819 } 2820 return maxSpace; 2821 } 2822 2823 void drawTabPositionMarks(DrawBuf buf, ref FontRef font, int lineIndex, Rect lineRect) { 2824 int maxCol = findMaxTabMarkColumn(lineIndex); 2825 if (maxCol > 0) { 2826 int spaceWidth = font.charWidth(' '); 2827 Rect rc = lineRect; 2828 uint color = addAlpha(textColor, 0xC0); 2829 for (int i = 0; i < maxCol; i += tabSize) { 2830 rc.left = lineRect.left + i * spaceWidth; 2831 rc.right = rc.left + 1; 2832 buf.fillRectPattern(rc, color, PatternType.dotted); 2833 } 2834 } 2835 } 2836 2837 void drawWhiteSpaceMarks(DrawBuf buf, ref FontRef font, dstring txt, int tabSize, Rect lineRect, Rect visibleRect) { 2838 // _showTabPositionMarks 2839 // _showWhiteSpaceMarks 2840 int firstNonSpace = -1; 2841 int lastNonSpace = -1; 2842 bool hasTabs = false; 2843 for(int i = 0; i < txt.length; i++) { 2844 if (txt[i] == '\t') { 2845 hasTabs = true; 2846 } else if (txt[i] != ' ') { 2847 if (firstNonSpace == -1) 2848 firstNonSpace = i; 2849 lastNonSpace = i + 1; 2850 } 2851 } 2852 if (firstNonSpace <= 0 && lastNonSpace >= txt.length && !hasTabs) 2853 return; 2854 uint color = addAlpha(textColor, 0xC0); 2855 static int[] textSizeBuffer; 2856 int charsMeasured = font.measureText(txt, textSizeBuffer, MAX_WIDTH_UNSPECIFIED, tabSize, 0, 0); 2857 int ts = tabSize; 2858 if (ts < 1) 2859 ts = 1; 2860 if (ts > 8) 2861 ts = 8; 2862 int spaceIndex = 0; 2863 for (int i = 0; i < txt.length && i < charsMeasured; i++) { 2864 dchar ch = txt[i]; 2865 bool outsideText = (i < firstNonSpace || i >= lastNonSpace); 2866 if ((ch == ' ' && outsideText) || ch == '\t') { 2867 Rect rc = lineRect; 2868 rc.left = lineRect.left + (i > 0 ? textSizeBuffer[i - 1] : 0); 2869 rc.right = lineRect.left + textSizeBuffer[i]; 2870 int h = rc.height; 2871 if (rc.intersects(visibleRect)) { 2872 // draw space mark 2873 if (ch == ' ') { 2874 // space 2875 int sz = h / 6; 2876 if (sz < 1) 2877 sz = 1; 2878 rc.top += h / 2 - sz / 2; 2879 rc.bottom = rc.top + sz; 2880 rc.left += rc.width / 2 - sz / 2; 2881 rc.right = rc.left + sz; 2882 buf.fillRect(rc, color); 2883 } else if (ch == '\t') { 2884 // tab 2885 Point p1 = Point(rc.left + 1, rc.top + h / 2); 2886 Point p2 = p1; 2887 p2.x = rc.right - 1; 2888 int sz = h / 4; 2889 if (sz < 2) 2890 sz = 2; 2891 if (sz > p2.x - p1.x) 2892 sz = p2.x - p1.x; 2893 buf.drawLine(p1, p2, color); 2894 buf.drawLine(p2, Point(p2.x - sz, p2.y - sz), color); 2895 buf.drawLine(p2, Point(p2.x - sz, p2.y + sz), color); 2896 } 2897 } 2898 } 2899 } 2900 } 2901 2902 override protected void drawClient(DrawBuf buf) { 2903 // update matched braces 2904 if (!content.findMatchedBraces(_caretPos, _matchingBraces)) { 2905 _matchingBraces.start.line = -1; 2906 _matchingBraces.end.line = -1; 2907 } 2908 2909 Rect rc = _clientRect; 2910 2911 FontRef font = font(); 2912 for (int i = 0; i < _visibleLines.length; i++) { 2913 dstring txt = _visibleLines[i]; 2914 Rect lineRect = rc; 2915 lineRect.left = _clientRect.left - _scrollPos.x; 2916 lineRect.right = lineRect.left + calcLineWidth(_content[_firstVisibleLine + i]); 2917 lineRect.top = _clientRect.top + i * _lineHeight; 2918 lineRect.bottom = lineRect.top + _lineHeight; 2919 Rect visibleRect = lineRect; 2920 visibleRect.left = _clientRect.left; 2921 visibleRect.right = _clientRect.right; 2922 drawLineBackground(buf, _firstVisibleLine + i, lineRect, visibleRect); 2923 if (_showTabPositionMarks) 2924 drawTabPositionMarks(buf, font, _firstVisibleLine + i, lineRect); 2925 if (!txt.length) 2926 continue; 2927 if (_showWhiteSpaceMarks) 2928 drawWhiteSpaceMarks(buf, font, txt, tabSize, lineRect, visibleRect); 2929 if (_leftPaneWidth > 0) { 2930 Rect leftPaneRect = visibleRect; 2931 leftPaneRect.right = leftPaneRect.left; 2932 leftPaneRect.left -= _leftPaneWidth; 2933 drawLeftPane(buf, leftPaneRect, 0); 2934 } 2935 if (txt.length > 0) { 2936 CustomCharProps[] highlight = _visibleLinesHighlights[i]; 2937 if (highlight) 2938 font.drawColoredText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, highlight, tabSize); 2939 else 2940 font.drawText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, textColor, tabSize); 2941 } 2942 } 2943 2944 drawCaret(buf); 2945 } 2946 2947 protected override bool onLeftPaneMouseClick(MouseEvent event) { 2948 if (_leftPaneWidth <= 0) 2949 return false; 2950 Rect rc = _clientRect; 2951 FontRef font = font(); 2952 int i = _firstVisibleLine; 2953 int lc = lineCount; 2954 for (;;) { 2955 Rect lineRect = rc; 2956 lineRect.left = _clientRect.left - _leftPaneWidth; 2957 lineRect.right = _clientRect.left; 2958 lineRect.bottom = lineRect.top + _lineHeight; 2959 if (lineRect.top >= _clientRect.bottom) 2960 break; 2961 if (event.y >= lineRect.top && event.y < lineRect.bottom) { 2962 return handleLeftPaneMouseClick(event, lineRect, i); 2963 } 2964 i++; 2965 rc.top += _lineHeight; 2966 } 2967 return false; 2968 } 2969 2970 override protected MenuItem getLeftPaneIconsPopupMenu(int line) { 2971 MenuItem menu = new MenuItem(); 2972 Action toggleBookmarkAction = ACTION_EDITOR_TOGGLE_BOOKMARK.clone(); 2973 toggleBookmarkAction.longParam = line; 2974 toggleBookmarkAction.objectParam = this; 2975 MenuItem item = menu.add(toggleBookmarkAction); 2976 return menu; 2977 } 2978 2979 protected FindPanel _findPanel; 2980 2981 /// create find panel 2982 protected void createFindPanel(bool selectionOnly, bool replaceMode) { 2983 _findPanel = new FindPanel(selectionOnly, replaceMode); 2984 addChild(_findPanel); 2985 requestLayout(); 2986 } 2987 2988 /// close find panel 2989 protected void closeFindPanel() { 2990 if (_findPanel) { 2991 removeChild(_findPanel); 2992 destroy(_findPanel); 2993 _findPanel = null; 2994 requestLayout(); 2995 } 2996 } 2997 2998 /// Draw widget at its position to buffer 2999 override void onDraw(DrawBuf buf) { 3000 if (visibility != Visibility.Visible) 3001 return; 3002 super.onDraw(buf); 3003 if (_findPanel && _findPanel.visibility == Visibility.Visible) { 3004 _findPanel.onDraw(buf); 3005 } 3006 } 3007 } 3008 3009 /// Read only edit box for displaying logs with lines append operation 3010 class LogWidget : EditBox { 3011 3012 protected int _maxLines; 3013 /// max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit 3014 @property int maxLines() { return _maxLines; } 3015 /// set max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit 3016 @property void maxLines(int n) { _maxLines = n; } 3017 3018 protected bool _scrollLock; 3019 /// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction) 3020 @property bool scrollLock() { return _scrollLock; } 3021 /// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction) 3022 @property void scrollLock(bool flg) { _scrollLock = flg; } 3023 3024 this() { 3025 this(null); 3026 } 3027 3028 this(string ID) { 3029 super(ID); 3030 _scrollLock = true; 3031 _enableScrollAfterText = false; 3032 enabled = false; 3033 fontSize = makePointSize(9); 3034 //fontFace = "Consolas,Lucida Console,Courier New"; 3035 fontFace = "Menlo,Consolas,DejaVuSansMono,DejaVu Sans Mono,Lucida Sans Typewriter,Courier New,Lucida Console"; 3036 fontFamily = FontFamily.MonoSpace; 3037 minFontSize(pointsToPixels(6)).maxFontSize(pointsToPixels(32)); // allow font zoom with Ctrl + MouseWheel 3038 onThemeChanged(); 3039 } 3040 3041 /// append lines to the end of text 3042 void appendText(dstring text) { 3043 import std.array : split; 3044 if (text.length == 0) 3045 return; 3046 dstring[] lines = text.split("\n"); 3047 //lines ~= ""d; // append new line after last line 3048 content.appendLines(lines); 3049 if (_maxLines > 0 && lineCount > _maxLines) { 3050 TextRange range; 3051 range.end.line = lineCount - _maxLines; 3052 EditOperation op = new EditOperation(EditAction.Replace, range, [""d]); 3053 _content.performOperation(op, this); 3054 _contentChanged = true; 3055 } 3056 updateScrollBars(); 3057 if (_scrollLock) { 3058 _caretPos = lastLineBegin(); 3059 ensureCaretVisible(); 3060 } 3061 } 3062 3063 TextPosition lastLineBegin() { 3064 TextPosition res; 3065 if (_content.length == 0) 3066 return res; 3067 if (_content.lineLength(_content.length - 1) == 0 && _content.length > 1) 3068 res.line = _content.length - 2; 3069 else 3070 res.line = _content.length - 1; 3071 return res; 3072 } 3073 3074 /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout). 3075 override void layout(Rect rc) { 3076 if (visibility == Visibility.Gone) { 3077 return; 3078 } 3079 super.layout(rc); 3080 if (_scrollLock) { 3081 measureVisibleText(); 3082 _caretPos = lastLineBegin(); 3083 ensureCaretVisible(); 3084 } 3085 } 3086 3087 } 3088 3089 class FindPanel : HorizontalLayout { 3090 protected EditLine _edFind; 3091 protected EditLine _edReplace; 3092 protected ImageCheckButton _cbCaseSensitive; 3093 protected ImageCheckButton _cbWholeWords; 3094 protected CheckBox _cbSelection; 3095 protected Button _btnFindNext; 3096 protected Button _btnFindPrev; 3097 protected Button _btnReplace; 3098 protected Button _btnReplaceAll; 3099 this(bool selectionOnly, bool replace) { 3100 import dlangui.dml.parser; 3101 try { 3102 parseML(q{ 3103 { 3104 layoutWidth: fill 3105 VerticalLayout { 3106 layoutWidth: fill 3107 HorizontalLayout { 3108 layoutWidth: fill 3109 EditLine { id: edFind; layoutWidth: fill; alignment: vcenter } 3110 Button { id: btnFindNext; text: EDIT_FIND_NEXT } 3111 Button { id: btnFindPrev; text: EDIT_FIND_PREV } 3112 } 3113 HorizontalLayout { 3114 id: replace 3115 layoutWidth: fill; 3116 EditLine { id: edReplace; layoutWidth: fill; alignment: vcenter } 3117 Button { id: btnReplace; text: EDIT_REPLACE_NEXT } 3118 Button { id: btnReplaceAll; text: EDIT_REPLACE_ALL } 3119 } 3120 } 3121 VerticalLayout { 3122 HorizontalLayout { 3123 ImageCheckButton { id: cbCaseSensitive; drawableId: "find_case_sensitive"; tooltipText: EDIT_FIND_CASE_SENSITIVE; styleId: TOOLBAR_BUTTON; alignment: vcenter } 3124 ImageCheckButton { id: cbWholeWords; drawableId: "find_whole_words"; tooltipText: EDIT_FIND_WHOLE_WORDS; styleId: TOOLBAR_BUTTON; alignment: vcenter } 3125 CheckBox { id: cbSelection; text: "Sel" } 3126 } 3127 VSpacer {} 3128 } 3129 VerticalLayout { 3130 ImageButton { id: btnClose; drawableId: "close"; styleId: BUTTON_TRANSPARENT } 3131 VSpacer {} 3132 } 3133 } 3134 }, null, this); 3135 } catch (Exception e) { 3136 Log.e("Exception while parsing DML: ", e); 3137 } 3138 _edFind = childById!EditLine("edFind"); 3139 _edReplace = childById!EditLine("edReplace"); 3140 _btnFindNext = childById!Button("btnFindNext"); 3141 _btnFindPrev = childById!Button("btnFindPrev"); 3142 _btnReplace = childById!Button("btnReplace"); 3143 _btnReplaceAll = childById!Button("btnReplaceAll"); 3144 _cbCaseSensitive = childById!ImageCheckButton("cbCaseSensitive"); 3145 _cbWholeWords = childById!ImageCheckButton("cbWholeWords"); 3146 _cbSelection = childById!CheckBox("cbSelection"); 3147 if (!replace) 3148 childById("replace").visibility = Visibility.Gone; 3149 //_edFind = new EditLine("edFind" 3150 } 3151 } 3152 3153 //import dlangui.widgets.metadata; 3154 //mixin(registerWidgets!(EditLine, EditBox, LogWidget)());