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