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