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)());