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