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