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(super.onKeyEvent(event))
2107             return true;
2108         if (focused) startCaretBlinking();
2109         cancelHoverTimer();
2110         bool ctrlOrAltPressed = !!(event.flags & KeyFlag.Control); //(event.flags & (KeyFlag.Control /* | KeyFlag.Alt */));
2111         //if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.SPACE && (event.flags & KeyFlag.Control)) {
2112         //    Log.d("Ctrl+Space pressed");
2113         //}
2114         if (event.action == KeyAction.Text && event.text.length && !ctrlOrAltPressed) {
2115             //Log.d("text entered: ", event.text);
2116             if (readOnly)
2117                 return true;
2118             if (!(!!(event.flags & KeyFlag.Alt) && event.text.length == 1 && isAZaz(event.text[0]))) { // filter out Alt+A..Z
2119                 if (replaceMode && _selectionRange.empty && _content[_caretPos.line].length >= _caretPos.pos + event.text.length) {
2120                     // replace next char(s)
2121                     TextRange range = _selectionRange;
2122                     range.end.pos += cast(int)event.text.length;
2123                     EditOperation op = new EditOperation(EditAction.Replace, range, [event.text]);
2124                     _content.performOperation(op, this);
2125                 } else {
2126                     EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [event.text]);
2127                     _content.performOperation(op, this);
2128                 }
2129                 return true;
2130             }
2131         }
2132         //if (event.keyCode == KeyCode.SPACE && !readOnly) {
2133         //    return true;
2134         //}
2135         //if (event.keyCode == KeyCode.RETURN && !readOnly && !_content.multiline) {
2136         //    return true;
2137         //}
2138         return true;
2139     }
2140 
2141     /// Handle Ctrl + Left mouse click on text
2142     protected void onControlClick() {
2143         // override to do something useful on Ctrl + Left mouse click in text
2144     }
2145 
2146     protected TextPosition _hoverTextPosition;
2147     protected Point _hoverMousePosition;
2148     protected ulong _hoverTimer;
2149     protected long _hoverTimeoutMillis = 800;
2150 
2151     /// override to handle mouse hover timeout in text
2152     protected void onHoverTimeout(Point pt, TextPosition pos) {
2153         // override to do something useful on hover timeout
2154     }
2155 
2156     protected void onHover(Point pos) {
2157         if (_hoverMousePosition == pos)
2158             return;
2159         //Log.d("onHover ", pos);
2160         int x = pos.x - left - _leftPaneWidth;
2161         int y = pos.y - top;
2162         _hoverMousePosition = pos;
2163         _hoverTextPosition = clientToTextPos(Point(x, y));
2164         cancelHoverTimer();
2165         Rect reversePos = textPosToClient(_hoverTextPosition);
2166         if (x < reversePos.left + 10.pointsToPixels)
2167             _hoverTimer = setTimer(_hoverTimeoutMillis);
2168     }
2169 
2170     protected void cancelHoverTimer() {
2171         if (_hoverTimer) {
2172             cancelTimer(_hoverTimer);
2173             _hoverTimer = 0;
2174         }
2175     }
2176 
2177     /// process mouse event; return true if event is processed by widget.
2178     override bool onMouseEvent(MouseEvent event) {
2179         //Log.d("onMouseEvent ", id, " ", event.action, "  (", event.x, ",", event.y, ")");
2180         // support onClick
2181         bool insideLeftPane = event.x < _clientRect.left && event.x >= _clientRect.left - _leftPaneWidth;
2182         if (event.action == MouseAction.ButtonDown && insideLeftPane) {
2183             setFocus();
2184             cancelHoverTimer();
2185             if (onLeftPaneMouseClick(event))
2186                 return true;
2187         }
2188         if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
2189             setFocus();
2190             cancelHoverTimer();
2191             if (event.tripleClick) {
2192                 selectLineByMouse(event.x - _clientRect.left, event.y - _clientRect.top);
2193             } else if (event.doubleClick) {
2194                 selectWordByMouse(event.x - _clientRect.left, event.y - _clientRect.top);
2195             } else {
2196                 auto doSelect = cast(bool)(event.keyFlags & MouseFlag.Shift);
2197                 updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, doSelect);
2198 
2199                 if (event.keyFlags == MouseFlag.Control)
2200                     onControlClick();
2201             }
2202             startCaretBlinking();
2203             invalidate();
2204             return true;
2205         }
2206         if (event.action == MouseAction.Move && (event.flags & MouseButton.Left) != 0) {
2207             updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, true);
2208             return true;
2209         }
2210         if (event.action == MouseAction.Move && event.flags == 0) {
2211             // hover
2212             if (focused && !insideLeftPane) {
2213                 onHover(event.pos);
2214             } else {
2215                 cancelHoverTimer();
2216             }
2217             return true;
2218         }
2219         if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) {
2220             cancelHoverTimer();
2221             return true;
2222         }
2223         if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) {
2224             cancelHoverTimer();
2225             return true;
2226         }
2227         if (event.action == MouseAction.FocusIn) {
2228             cancelHoverTimer();
2229             return true;
2230         }
2231         if (event.action == MouseAction.Wheel) {
2232             cancelHoverTimer();
2233             uint keyFlags = event.flags & (MouseFlag.Shift | MouseFlag.Control | MouseFlag.Alt);
2234             if (event.wheelDelta < 0) {
2235                 if (keyFlags == MouseFlag.Shift)
2236                     return handleAction(new Action(EditorActions.ScrollRight));
2237                 if (keyFlags == MouseFlag.Control)
2238                     return handleAction(new Action(EditorActions.ZoomOut));
2239                 return handleAction(new Action(EditorActions.ScrollLineDown));
2240             } else if (event.wheelDelta > 0) {
2241                 if (keyFlags == MouseFlag.Shift)
2242                     return handleAction(new Action(EditorActions.ScrollLeft));
2243                 if (keyFlags == MouseFlag.Control)
2244                     return handleAction(new Action(EditorActions.ZoomIn));
2245                 return handleAction(new Action(EditorActions.ScrollLineUp));
2246             }
2247         }
2248         cancelHoverTimer();
2249         return super.onMouseEvent(event);
2250     }
2251 
2252     /// returns caret position
2253     @property TextPosition caretPos() {
2254         return _caretPos;
2255     }
2256 
2257     /// change caret position and ensure it is visible
2258     void setCaretPos(int line, int column, bool makeVisible = true, bool center = false)
2259     {
2260         _caretPos = TextPosition(line,column);
2261         correctCaretPos();
2262         invalidate();
2263         if (makeVisible)
2264             ensureCaretVisible(center);
2265         handleEditorStateChange();
2266     }
2267 }
2268 
2269 interface EditorActionHandler {
2270     bool onEditorAction(const Action action);
2271 }
2272 
2273 interface EnterKeyHandler {
2274     bool onEnterKey(EditWidgetBase editor);
2275 }
2276 
2277 /// single line editor
2278 class EditLine : EditWidgetBase {
2279 
2280     Signal!EditorActionHandler editorAction;
2281     /// handle Enter key press inside line editor
2282     Signal!EnterKeyHandler enterKey;
2283 
2284     /// empty parameter list constructor - for usage by factory
2285     this() {
2286         this(null);
2287     }
2288     /// create with ID parameter
2289     this(string ID, dstring initialContent = null) {
2290         super(ID, ScrollBarMode.Invisible, ScrollBarMode.Invisible);
2291         _content = new EditableContent(false);
2292         _content.contentChanged = this;
2293         _selectAllWhenFocusedWithTab = true;
2294         _deselectAllWhenUnfocused = true;
2295         wantTabs = false;
2296         styleId = STYLE_EDIT_LINE;
2297         text = initialContent;
2298         onThemeChanged();
2299     }
2300 
2301     /// sets default popup menu with copy/paste/cut/undo/redo
2302     EditLine setDefaultPopupMenu() {
2303         MenuItem items = new MenuItem();
2304         items.add(ACTION_EDITOR_COPY, ACTION_EDITOR_PASTE, ACTION_EDITOR_CUT,
2305                   ACTION_EDITOR_UNDO, ACTION_EDITOR_REDO);
2306         popupMenu = items;
2307         return this;
2308     }
2309 
2310     protected dstring _measuredText;
2311     protected int[] _measuredTextWidths;
2312     protected Point _measuredTextSize;
2313 
2314     protected Point _measuredTextToSetWidgetSize;
2315     protected dstring _textToSetWidgetSize = "aaaaa"d;
2316 
2317     @property void textToSetWidgetSize(dstring newText) {
2318         _textToSetWidgetSize = newText;
2319         requestLayout();
2320     }
2321 
2322     @property dstring textToSetWidgetSize() {
2323         return _textToSetWidgetSize;
2324     }
2325 
2326     protected int[] _measuredTextToSetWidgetSizeWidths;
2327 
2328     protected dchar _passwordChar = 0;
2329     /// password character - 0 for normal editor, some character, e.g. '*' to hide text by replacing all characters with this char
2330     @property dchar passwordChar() { return _passwordChar; }
2331     @property EditLine passwordChar(dchar ch) {
2332         if (_passwordChar != ch) {
2333             _passwordChar = ch;
2334             requestLayout();
2335         }
2336         return this;
2337     }
2338 
2339     override protected Rect textPosToClient(TextPosition p) {
2340         Rect res;
2341         res.bottom = _clientRect.height;
2342         if (p.pos == 0)
2343             res.left = 0;
2344         else if (p.pos >= _measuredText.length)
2345             res.left = _measuredTextSize.x;
2346         else
2347             res.left = _measuredTextWidths[p.pos - 1];
2348         res.left -= _scrollPos.x;
2349         res.right = res.left + 1;
2350         return res;
2351     }
2352 
2353     override protected TextPosition clientToTextPos(Point pt) {
2354         pt.x += _scrollPos.x;
2355         TextPosition res;
2356         for (int i = 0; i < _measuredText.length; i++) {
2357             int x0 = i > 0 ? _measuredTextWidths[i - 1] : 0;
2358             int x1 = _measuredTextWidths[i];
2359             int mx = (x0 + x1) >> 1;
2360             if (pt.x <= mx) {
2361                 res.pos = i;
2362                 return res;
2363             }
2364         }
2365         res.pos = cast(int)_measuredText.length;
2366         return res;
2367     }
2368 
2369     override protected void ensureCaretVisible(bool center = false) {
2370         //_scrollPos
2371         Rect rc = textPosToClient(_caretPos);
2372         if (rc.left < 0) {
2373             // scroll left
2374             _scrollPos.x -= -rc.left + _clientRect.width / 10;
2375             if (_scrollPos.x < 0)
2376                 _scrollPos.x = 0;
2377             invalidate();
2378         } else if (rc.left >= _clientRect.width - 10) {
2379             // scroll right
2380             _scrollPos.x += (rc.left - _clientRect.width) + _spaceWidth * 4;
2381             invalidate();
2382         }
2383         updateScrollBars();
2384         handleEditorStateChange();
2385     }
2386 
2387     protected dstring applyPasswordChar(dstring s) {
2388         if (!_passwordChar || s.length == 0)
2389             return s;
2390         dchar[] ss = s.dup;
2391         foreach(ref ch; ss)
2392             ch = _passwordChar;
2393         return cast(dstring)ss;
2394     }
2395 
2396     override protected Point measureVisibleText() {
2397         FontRef font = font();
2398         //Point sz = font.textSize(text);
2399         _measuredText = applyPasswordChar(text);
2400         _measuredTextWidths.length = _measuredText.length;
2401         int charsMeasured = font.measureText(_measuredText, _measuredTextWidths, MAX_WIDTH_UNSPECIFIED, tabSize);
2402         _measuredTextSize.x = charsMeasured > 0 ? _measuredTextWidths[charsMeasured - 1]: 0;
2403         _measuredTextSize.y = font.height;
2404         return _measuredTextSize;
2405     }
2406 
2407     protected Point measureTextToSetWidgetSize() {
2408         FontRef font = font();
2409         _measuredTextToSetWidgetSizeWidths.length = _textToSetWidgetSize.length;
2410         int charsMeasured = font.measureText(_textToSetWidgetSize, _measuredTextToSetWidgetSizeWidths, MAX_WIDTH_UNSPECIFIED, tabSize);
2411         _measuredTextToSetWidgetSize.x = charsMeasured > 0 ? _measuredTextToSetWidgetSizeWidths[charsMeasured - 1]: 0;
2412         _measuredTextToSetWidgetSize.y = font.height;
2413         return _measuredTextToSetWidgetSize;
2414     }
2415 
2416     /// measure
2417     override void measure(int parentWidth, int parentHeight) {
2418         if (visibility == Visibility.Gone)
2419             return;
2420 
2421         updateFontProps();
2422         measureVisibleText();
2423         measureTextToSetWidgetSize();
2424         measuredContent(parentWidth, parentHeight, _measuredTextToSetWidgetSize.x + _leftPaneWidth, _measuredTextToSetWidgetSize.y);
2425     }
2426 
2427     override bool handleAction(const Action a) {
2428         switch (a.id) with(EditorActions)
2429         {
2430             case InsertNewLine:
2431             case PrependNewLine:
2432             case AppendNewLine:
2433                 if (editorAction.assigned) {
2434                     return editorAction(a);
2435                 }
2436                 break;
2437             case Up:
2438                 break;
2439             case Down:
2440                 break;
2441             case PageUp:
2442                 break;
2443             case PageDown:
2444                 break;
2445             default:
2446                 break;
2447         }
2448         return super.handleAction(a);
2449     }
2450 
2451 
2452     /// handle keys
2453     override bool onKeyEvent(KeyEvent event) {
2454         if(super.onKeyEvent(event))
2455             return true;
2456         if (enterKey.assigned) {
2457             if (event.keyCode == KeyCode.RETURN && event.modifiers == 0) {
2458                 if (event.action == KeyAction.KeyDown)
2459                     return true;
2460                 if (event.action == KeyAction.KeyUp) {
2461                     if (enterKey(this))
2462                        return true;
2463                 }
2464             }
2465         }
2466         return true;
2467     }
2468 
2469     /// process mouse event; return true if event is processed by widget.
2470     override bool onMouseEvent(MouseEvent event) {
2471         return super.onMouseEvent(event);
2472     }
2473 
2474     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
2475     override void layout(Rect rc) {
2476         if (visibility == Visibility.Gone) {
2477             return;
2478         }
2479         _needLayout = false;
2480         Point sz = Point(rc.width, measuredHeight);
2481         applyAlign(rc, sz);
2482         _pos = rc;
2483         _clientRect = rc;
2484         applyMargins(_clientRect);
2485         applyPadding(_clientRect);
2486         if (_contentChanged) {
2487             measureVisibleText();
2488             _contentChanged = false;
2489         }
2490     }
2491 
2492 
2493     /// override to custom highlight of line background
2494     protected void drawLineBackground(DrawBuf buf, Rect lineRect, Rect visibleRect) {
2495         if (!_selectionRange.empty) {
2496             // line inside selection
2497             Rect startrc = textPosToClient(_selectionRange.start);
2498             Rect endrc = textPosToClient(_selectionRange.end);
2499             Rect rc = lineRect;
2500             rc.left = startrc.left + _clientRect.left;
2501             rc.right = endrc.left + _clientRect.left;
2502             if (!rc.empty) {
2503                 // draw selection rect for line
2504                 buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal);
2505             }
2506             if (_leftPaneWidth > 0) {
2507                 Rect leftPaneRect = visibleRect;
2508                 leftPaneRect.right = leftPaneRect.left;
2509                 leftPaneRect.left -= _leftPaneWidth;
2510                 drawLeftPane(buf, leftPaneRect, 0);
2511             }
2512         }
2513     }
2514 
2515     /// draw content
2516     override void onDraw(DrawBuf buf) {
2517         if (visibility != Visibility.Visible)
2518             return;
2519         super.onDraw(buf);
2520         Rect rc = _pos;
2521         applyMargins(rc);
2522         applyPadding(rc);
2523         auto saver = ClipRectSaver(buf, rc, alpha);
2524 
2525         FontRef font = font();
2526         dstring txt = applyPasswordChar(text);
2527 
2528         drawLineBackground(buf, _clientRect, _clientRect);
2529         font.drawText(buf, rc.left - _scrollPos.x, rc.top, txt, textColor, tabSize);
2530 
2531         drawCaret(buf);
2532     }
2533 }
2534 
2535 // SpinCtrl
2536 private {
2537     import std.ascii;
2538 }
2539 
2540 class SpinCtrl : HorizontalLayout {
2541 
2542     TextWidget label;
2543     int min, max;
2544 
2545     private EditLine linEdit;
2546     private Button butUp, butDown;
2547 
2548 
2549     @property int value() { return linEdit.text.to!int; }
2550     @property void value(int val) {
2551         linEdit.text = val.to!dstring;
2552     }
2553 
2554     override @property bool enabled() { return linEdit.enabled; }
2555     alias enabled = Widget.enabled;
2556     @property void enabled(bool status) {
2557         linEdit.enabled = status;
2558         butUp.enabled = status;
2559         butDown.enabled = status;
2560     }
2561 
2562     this(int min, int max, int initialVal = 0, dstring labelText = null){
2563         this.min = min;
2564         this.max = max;
2565 
2566         if(labelText !is null){
2567             label = new TextWidget("label", labelText);
2568             addChild(label);
2569         }
2570 
2571         linEdit = new class EditLine {
2572             this(){super("linEdit", "0"d);}
2573             override bool onKeyEvent(KeyEvent event) {
2574                 if (( KeyAction.Text == event.action && event.text[0].isDigit)
2575                     || event.keyCode == KeyCode.BACK
2576                     || event.keyCode == KeyCode.DEL
2577                     || event.keyCode == KeyCode.LEFT
2578                     || event.keyCode == KeyCode.RIGHT
2579                     || event.keyCode == KeyCode.TAB
2580                     ){
2581                         return super.onKeyEvent(event);
2582                 }
2583                 return false;
2584             }
2585 
2586             override bool onMouseEvent(MouseEvent event) {
2587                 if(enabled && event.action == MouseAction.Wheel){
2588                     if((event.wheelDelta == 1) && (value < max))
2589                         value = value + event.wheelDelta;
2590                     if((event.wheelDelta == -1) && (value > min))
2591                         value = value + event.wheelDelta;
2592                     return true;
2593                 }
2594                 return super.onMouseEvent(event);
2595             }
2596         };
2597 
2598         linEdit.addOnFocusChangeListener((w, t){
2599             if(linEdit.text == "")
2600                 linEdit.text = "0";
2601             if(linEdit.text.to!int > max)
2602                 value = max;
2603             if(linEdit.text.to!int < min)
2604                 value = min;
2605             return true;
2606         });
2607 
2608         linEdit.minHeight = 35;
2609         if(initialVal != 0)
2610             value = initialVal;
2611         addChild(linEdit);
2612 
2613 
2614         auto butContainer = new VerticalLayout();
2615         butContainer.maxHeight = linEdit.minHeight;
2616 
2617         butUp = new Button("butUp", "+"d);
2618         butUp.margins(Rect(1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels));
2619 
2620         butDown = new Button("butDown", "-"d);
2621         butDown.margins(Rect(1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels, 1.pointsToPixels));
2622 
2623         butContainer.addChild(butUp);
2624         butContainer.addChild(butDown);
2625 
2626         addChild(butContainer);
2627 
2628         butUp.click = delegate(Widget w) {
2629             immutable val = linEdit.text.to!int;
2630             if(val < max )
2631                 linEdit.text = (val + 1).to!dstring;
2632             return true;
2633         };
2634 
2635         butDown.click = delegate(Widget w) {
2636             immutable val = linEdit.text.to!int;
2637             if(val > min )
2638                 linEdit.text = (val - 1).to!dstring;
2639             return true;
2640         };
2641 
2642         enabled = true;
2643     }
2644 
2645 }
2646 
2647 /// multiline editor
2648 class EditBox : EditWidgetBase {
2649     /// empty parameter list constructor - for usage by factory
2650     this() {
2651         this(null);
2652     }
2653     /// create with ID parameter
2654     this(string ID, dstring initialContent = null, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) {
2655         super(ID, hscrollbarMode, vscrollbarMode);
2656         _content = new EditableContent(true); // multiline
2657         _content.contentChanged = this;
2658         styleId = STYLE_EDIT_BOX;
2659         text = initialContent;
2660         acceleratorMap.add( [
2661             // zoom
2662             new Action(EditorActions.ZoomIn, KeyCode.ADD, KeyFlag.Control),
2663             new Action(EditorActions.ZoomOut, KeyCode.SUB, KeyFlag.Control),
2664         ]);
2665         onThemeChanged();
2666     }
2667 
2668     ~this() {
2669         if (_findPanel) {
2670             destroy(_findPanel);
2671             _findPanel = null;
2672         }
2673     }
2674 
2675     protected int _firstVisibleLine;
2676 
2677     protected int _maxLineWidth;
2678     protected int _numVisibleLines;             // number of lines visible in client area
2679     protected dstring[] _visibleLines;          // text for visible lines
2680     protected int[][] _visibleLinesMeasurement; // char positions for visible lines
2681     protected int[] _visibleLinesWidths; // width (in pixels) of visible lines
2682     protected CustomCharProps[][] _visibleLinesHighlights;
2683     protected CustomCharProps[][] _visibleLinesHighlightsBuf;
2684 
2685     protected Point _measuredTextToSetWidgetSize;
2686     protected dstring _textToSetWidgetSize = "aaaaa/naaaaa"d;
2687     protected int[] _measuredTextToSetWidgetSizeWidths;
2688 
2689     /// Set _needRewrap to true;
2690     override void wordWrapRefresh()
2691     {
2692         _needRewrap = true;
2693     }
2694 
2695     override @property int fontSize() const { return super.fontSize(); }
2696     override @property Widget fontSize(int size) {
2697         // Need to rewrap if fontSize changed
2698         _needRewrap = true;
2699         return super.fontSize(size);
2700     }
2701 
2702     override protected int lineCount() {
2703         return _content.length;
2704     }
2705 
2706     override protected void updateMaxLineWidth() {
2707         // find max line width. TODO: optimize!!!
2708         int maxw;
2709         int[] buf;
2710         for (int i = 0; i < _content.length; i++) {
2711             dstring s = _content[i];
2712             int w = calcLineWidth(s);
2713             if (maxw < w)
2714                 maxw = w;
2715         }
2716         _maxLineWidth = maxw;
2717     }
2718 
2719     @property int minFontSize() {
2720         return _minFontSize;
2721     }
2722     @property EditBox minFontSize(int size) {
2723         _minFontSize = size;
2724         return this;
2725     }
2726 
2727     @property int maxFontSize() {
2728         return _maxFontSize;
2729     }
2730 
2731     @property EditBox maxFontSize(int size) {
2732         _maxFontSize = size;
2733         return this;
2734     }
2735 
2736     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
2737     override void layout(Rect rc) {
2738         if (visibility == Visibility.Gone)
2739             return;
2740 
2741         if (rc != _pos)
2742             _contentChanged = true;
2743         Rect contentRc = rc;
2744         int findPanelHeight;
2745         if (_findPanel && _findPanel.visibility != Visibility.Gone) {
2746             _findPanel.measure(rc.width, rc.height);
2747             findPanelHeight = _findPanel.measuredHeight;
2748             _findPanel.layout(Rect(rc.left, rc.bottom - findPanelHeight, rc.right, rc.bottom));
2749             contentRc.bottom -= findPanelHeight;
2750         }
2751 
2752         super.layout(contentRc);
2753         if (_contentChanged) {
2754             measureVisibleText();
2755             _needRewrap = true;
2756             _contentChanged = false;
2757         }
2758 
2759         _pos = rc;
2760     }
2761 
2762     override protected Point measureVisibleText() {
2763         Point sz;
2764         FontRef font = font();
2765         _lineHeight = font.height;
2766         _numVisibleLines = (_clientRect.height + _lineHeight - 1) / _lineHeight;
2767         if (_firstVisibleLine >= _content.length) {
2768             _firstVisibleLine = _content.length - _numVisibleLines + 1;
2769             if (_firstVisibleLine < 0)
2770                 _firstVisibleLine = 0;
2771             _caretPos.line = _content.length - 1;
2772             _caretPos.pos = 0;
2773         }
2774         if (_numVisibleLines < 1)
2775             _numVisibleLines = 1;
2776         if (_firstVisibleLine + _numVisibleLines > _content.length)
2777             _numVisibleLines = _content.length - _firstVisibleLine;
2778         if (_numVisibleLines < 1)
2779             _numVisibleLines = 1;
2780         _visibleLines.length = _numVisibleLines;
2781         if (_visibleLinesMeasurement.length < _numVisibleLines)
2782             _visibleLinesMeasurement.length = _numVisibleLines;
2783         if (_visibleLinesWidths.length < _numVisibleLines)
2784             _visibleLinesWidths.length = _numVisibleLines;
2785         if (_visibleLinesHighlights.length < _numVisibleLines) {
2786             _visibleLinesHighlights.length = _numVisibleLines;
2787             _visibleLinesHighlightsBuf.length = _numVisibleLines;
2788         }
2789         for (int i = 0; i < _numVisibleLines; i++) {
2790             _visibleLines[i] = _content[_firstVisibleLine + i];
2791             size_t len = _visibleLines[i].length;
2792             if (_visibleLinesMeasurement[i].length < len)
2793                 _visibleLinesMeasurement[i].length = len;
2794             if (_visibleLinesHighlightsBuf[i].length < len)
2795                 _visibleLinesHighlightsBuf[i].length = len;
2796             _visibleLinesHighlights[i] = handleCustomLineHighlight(_firstVisibleLine + i, _visibleLines[i], _visibleLinesHighlightsBuf[i]);
2797             int charsMeasured = font.measureText(_visibleLines[i], _visibleLinesMeasurement[i], int.max, tabSize);
2798             _visibleLinesWidths[i] = charsMeasured > 0 ? _visibleLinesMeasurement[i][charsMeasured - 1] : 0;
2799             if (sz.x < _visibleLinesWidths[i])
2800                 sz.x = _visibleLinesWidths[i]; // width - max from visible lines
2801         }
2802         sz.x = _maxLineWidth;
2803         sz.y = _lineHeight * _content.length; // height - for all lines
2804         return sz;
2805     }
2806 
2807     protected bool _extendRightScrollBound = true;
2808     /// override to determine if scrollbars are needed or not
2809     override protected void checkIfScrollbarsNeeded(ref bool needHScroll, ref bool needVScroll) {
2810         needHScroll = _hscrollbar && (_hscrollbarMode == ScrollBarMode.Visible || _hscrollbarMode == ScrollBarMode.Auto);
2811         needVScroll = _vscrollbar && (_vscrollbarMode == ScrollBarMode.Visible || _vscrollbarMode == ScrollBarMode.Auto);
2812         if (!needHScroll && !needVScroll)
2813             return; // not needed
2814         if (_hscrollbarMode != ScrollBarMode.Auto && _vscrollbarMode != ScrollBarMode.Auto)
2815             return; // no auto scrollbars
2816         // either h or v scrollbar is in auto mode
2817 
2818         int hsbHeight = _hscrollbar.measuredHeight;
2819         int vsbWidth = _hscrollbar.measuredWidth;
2820 
2821         int visibleLines = _lineHeight > 0 ? (_clientRect.height / _lineHeight) : 1; // fully visible lines
2822         if (visibleLines < 1)
2823             visibleLines = 1;
2824         int visibleLinesWithScrollbar = _lineHeight > 0 ? ((_clientRect.height - hsbHeight) / _lineHeight) : 1; // fully visible lines
2825         if (visibleLinesWithScrollbar < 1)
2826             visibleLinesWithScrollbar = 1;
2827 
2828         // either h or v scrollbar is in auto mode
2829         //Point contentSize = fullContentSize();
2830         int contentWidth = _maxLineWidth + (_extendRightScrollBound ? _clientRect.width / 16 : 0);
2831         int contentHeight = _content.length;
2832 
2833         int clientWidth = _clientRect.width;
2834         int clientHeight = visibleLines;
2835 
2836         int clientWidthWithScrollbar = clientWidth - vsbWidth;
2837         int clientHeightWithScrollbar = visibleLinesWithScrollbar;
2838 
2839         if (_hscrollbarMode == ScrollBarMode.Auto && _vscrollbarMode == ScrollBarMode.Auto) {
2840             // both scrollbars in auto mode
2841             bool xFits = contentWidth <= clientWidth;
2842             bool yFits = contentHeight <= clientHeight;
2843             if (!xFits && !yFits) {
2844                 // none fits, need both scrollbars
2845             } else if (xFits && yFits) {
2846                 // everything fits!
2847                 needHScroll = false;
2848                 needVScroll = false;
2849             } else if (xFits) {
2850                 // only X fits
2851                 if (contentWidth <= clientWidthWithScrollbar)
2852                     needHScroll = false; // disable hscroll
2853             } else { // yFits
2854                 // only Y fits
2855                 if (contentHeight <= clientHeightWithScrollbar)
2856                     needVScroll = false; // disable vscroll
2857             }
2858         } else if (_hscrollbarMode == ScrollBarMode.Auto) {
2859             // only hscroll is in auto mode
2860             if (needVScroll)
2861                 clientWidth = clientWidthWithScrollbar;
2862             needHScroll = contentWidth > clientWidth;
2863         } else {
2864             // only vscroll is in auto mode
2865             if (needHScroll)
2866                 clientHeight = clientHeightWithScrollbar;
2867             needVScroll = contentHeight > clientHeight;
2868         }
2869     }
2870 
2871     /// update horizontal scrollbar widget position
2872     override protected void updateHScrollBar() {
2873         _hscrollbar.setRange(0, _maxLineWidth + (_extendRightScrollBound ? _clientRect.width / 16 : 0));
2874         _hscrollbar.pageSize = _clientRect.width;
2875         _hscrollbar.position = _scrollPos.x;
2876     }
2877 
2878     /// update verticat scrollbar widget position
2879     override protected void updateVScrollBar() {
2880         int visibleLines = _lineHeight ? _clientRect.height / _lineHeight : 1; // fully visible lines
2881         if (visibleLines < 1)
2882             visibleLines = 1;
2883         _vscrollbar.setRange(0, _content.length);
2884         _vscrollbar.pageSize = visibleLines;
2885         _vscrollbar.position = _firstVisibleLine;
2886     }
2887 
2888     /// process horizontal scrollbar event
2889     override bool onHScroll(ScrollEvent event) {
2890         if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) {
2891             if (_scrollPos.x != event.position) {
2892                 _scrollPos.x = event.position;
2893                 invalidate();
2894             }
2895         } else if (event.action == ScrollAction.PageUp) {
2896             dispatchAction(new Action(EditorActions.ScrollLeft));
2897         } else if (event.action == ScrollAction.PageDown) {
2898             dispatchAction(new Action(EditorActions.ScrollRight));
2899         } else if (event.action == ScrollAction.LineUp) {
2900             dispatchAction(new Action(EditorActions.ScrollLeft));
2901         } else if (event.action == ScrollAction.LineDown) {
2902             dispatchAction(new Action(EditorActions.ScrollRight));
2903         }
2904         return true;
2905     }
2906 
2907     /// process vertical scrollbar event
2908     override bool onVScroll(ScrollEvent event) {
2909         if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) {
2910             if (_firstVisibleLine != event.position) {
2911                 _firstVisibleLine = event.position;
2912                 measureVisibleText();
2913                 invalidate();
2914             }
2915         } else if (event.action == ScrollAction.PageUp) {
2916             dispatchAction(new Action(EditorActions.ScrollPageUp));
2917         } else if (event.action == ScrollAction.PageDown) {
2918             dispatchAction(new Action(EditorActions.ScrollPageDown));
2919         } else if (event.action == ScrollAction.LineUp) {
2920             dispatchAction(new Action(EditorActions.ScrollLineUp));
2921         } else if (event.action == ScrollAction.LineDown) {
2922             dispatchAction(new Action(EditorActions.ScrollLineDown));
2923         }
2924         return true;
2925     }
2926 
2927     protected bool _enableScrollAfterText = true;
2928     override protected void ensureCaretVisible(bool center = false) {
2929         if (_caretPos.line >= _content.length)
2930             _caretPos.line = _content.length - 1;
2931         if (_caretPos.line < 0)
2932             _caretPos.line = 0;
2933         int visibleLines = _lineHeight > 0 ? _clientRect.height / _lineHeight : 1; // fully visible lines
2934         if (visibleLines < 1)
2935             visibleLines = 1;
2936         int maxFirstVisibleLine = _content.length - 1;
2937         if (!_enableScrollAfterText)
2938             maxFirstVisibleLine = _content.length - visibleLines;
2939         if (maxFirstVisibleLine < 0)
2940             maxFirstVisibleLine = 0;
2941 
2942         if (_caretPos.line < _firstVisibleLine) {
2943             _firstVisibleLine = _caretPos.line;
2944             if (center) {
2945                 _firstVisibleLine -= visibleLines / 2;
2946                 if (_firstVisibleLine < 0)
2947                     _firstVisibleLine = 0;
2948             }
2949             if (_firstVisibleLine > maxFirstVisibleLine)
2950                 _firstVisibleLine = maxFirstVisibleLine;
2951             measureVisibleText();
2952             invalidate();
2953         } else if(_wordWrap && !(_firstVisibleLine > maxFirstVisibleLine)) {
2954             //For wordwrap mode, move down sooner
2955             int offsetLines = -1 * caretHeightOffset / _lineHeight;
2956             //Log.d("offsetLines: ", offsetLines);
2957             if (_caretPos.line >= _firstVisibleLine + visibleLines - offsetLines)
2958             {
2959                 _firstVisibleLine = _caretPos.line - visibleLines + 1 + offsetLines;
2960                 if (center)
2961                     _firstVisibleLine += visibleLines / 2;
2962                 if (_firstVisibleLine > maxFirstVisibleLine)
2963                     _firstVisibleLine = maxFirstVisibleLine;
2964                 if (_firstVisibleLine < 0)
2965                     _firstVisibleLine = 0;
2966                 measureVisibleText();
2967                 invalidate();
2968             }
2969         } else if (_caretPos.line >= _firstVisibleLine + visibleLines) {
2970             _firstVisibleLine = _caretPos.line - visibleLines + 1;
2971             if (center)
2972                 _firstVisibleLine += visibleLines / 2;
2973             if (_firstVisibleLine > maxFirstVisibleLine)
2974                 _firstVisibleLine = maxFirstVisibleLine;
2975             if (_firstVisibleLine < 0)
2976                 _firstVisibleLine = 0;
2977             measureVisibleText();
2978             invalidate();
2979         } else if (_firstVisibleLine > maxFirstVisibleLine) {
2980             _firstVisibleLine = maxFirstVisibleLine;
2981             if (_firstVisibleLine < 0)
2982                 _firstVisibleLine = 0;
2983             measureVisibleText();
2984             invalidate();
2985         }
2986         //_scrollPos
2987         Rect rc = textPosToClient(_caretPos);
2988         if (rc.left < 0) {
2989             // scroll left
2990             _scrollPos.x -= -rc.left + _clientRect.width / 4;
2991             if (_scrollPos.x < 0)
2992                 _scrollPos.x = 0;
2993             invalidate();
2994         } else if (rc.left >= _clientRect.width - 10) {
2995             // scroll right
2996             if (!_wordWrap)
2997                 _scrollPos.x += (rc.left - _clientRect.width) + _clientRect.width / 4;
2998             invalidate();
2999         }
3000         updateScrollBars();
3001         handleEditorStateChange();
3002     }
3003 
3004     override protected Rect textPosToClient(TextPosition p) {
3005         Rect res;
3006         int lineIndex = p.line - _firstVisibleLine;
3007         res.top = lineIndex * _lineHeight;
3008         res.bottom = res.top + _lineHeight;
3009         // if visible
3010         if (lineIndex >= 0 && lineIndex < _visibleLines.length) {
3011             if (p.pos == 0)
3012                 res.left = 0;
3013             else if (p.pos >= _visibleLinesMeasurement[lineIndex].length)
3014                 res.left = _visibleLinesWidths[lineIndex];
3015             else
3016                 res.left = _visibleLinesMeasurement[lineIndex][p.pos - 1];
3017         }
3018         res.left -= _scrollPos.x;
3019         res.right = res.left + 1;
3020         return res;
3021     }
3022 
3023     override protected TextPosition clientToTextPos(Point pt) {
3024         TextPosition res;
3025         pt.x += _scrollPos.x;
3026         int lineIndex = pt.y / _lineHeight;
3027         if (lineIndex < 0)
3028             lineIndex = 0;
3029         if (lineIndex < _visibleLines.length) {
3030             res.line = lineIndex + _firstVisibleLine;
3031             int len = cast(int)_visibleLines[lineIndex].length;
3032             for (int i = 0; i < len; i++) {
3033                 int x0 = i > 0 ? _visibleLinesMeasurement[lineIndex][i - 1] : 0;
3034                 int x1 = _visibleLinesMeasurement[lineIndex][i];
3035                 int mx = (x0 + x1) >> 1;
3036                 if (pt.x <= mx) {
3037                     res.pos = i;
3038                     return res;
3039                 }
3040             }
3041             res.pos = cast(int)_visibleLines[lineIndex].length;
3042         } else if (_visibleLines.length > 0) {
3043             res.line = _firstVisibleLine + cast(int)_visibleLines.length - 1;
3044             res.pos = cast(int)_visibleLines[$ - 1].length;
3045         } else {
3046             res.line = 0;
3047             res.pos = 0;
3048         }
3049         return res;
3050     }
3051 
3052     override protected bool handleAction(const Action a) {
3053         TextPosition oldCaretPos = _caretPos;
3054         dstring currentLine = _content[_caretPos.line];
3055         switch (a.id) with(EditorActions)
3056         {
3057             case PrependNewLine:
3058                 if (!readOnly) {
3059                     correctCaretPos();
3060                     _caretPos.pos = 0;
3061                     EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]);
3062                     _content.performOperation(op, this);
3063                 }
3064                 return true;
3065             case InsertNewLine:
3066                 if (!readOnly) {
3067                     correctCaretPos();
3068                     EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]);
3069                     _content.performOperation(op, this);
3070                 }
3071                 return true;
3072             case Up:
3073             case SelectUp:
3074                 if ((_caretPos.line > 0) | wordWrap) {
3075                     if (_wordWrap)
3076                     {
3077                         LineSpan curSpan = getSpan(_caretPos.line);
3078                         int curWrap = findWrapLine(_caretPos);
3079                         if (curWrap > 0)
3080                         {
3081                             _caretPos.pos-= curSpan.wrapPoints[curWrap - 1].wrapPos;
3082                         }
3083                         else
3084                         {
3085                             int previousPos = _caretPos.pos;
3086                             curSpan = getSpan(_caretPos.line - 1);
3087                             curWrap = curSpan.len - 1;
3088                             if (curWrap > 0)
3089                             {
3090                                 int accumulativePoint = curSpan.accumulation(curSpan.len - 1, LineSpan.WrapPointInfo.Position);
3091                                 _caretPos.line--;
3092                                 _caretPos.pos = accumulativePoint + previousPos;
3093                             }
3094                             else
3095                             {
3096                                 _caretPos.line--;
3097                             }
3098                         }
3099                     }
3100                     else if(_caretPos.line > 0)
3101                         _caretPos.line--;
3102                      correctCaretPos();
3103                      updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
3104                      ensureCaretVisible();
3105                 }
3106                 return true;
3107             case Down:
3108             case SelectDown:
3109                 if (_caretPos.line < _content.length - 1) {
3110                     if (_wordWrap)
3111                     {
3112                         LineSpan curSpan = getSpan(_caretPos.line);
3113                         int curWrap = findWrapLine(_caretPos);
3114                         if (curWrap < curSpan.len - 1)
3115                         {
3116                             int previousPos = _caretPos.pos;
3117                             _caretPos.pos+= curSpan.wrapPoints[curWrap].wrapPos;
3118                             correctCaretPos();
3119                             if (_caretPos.pos == previousPos)
3120                             {
3121                                 _caretPos.pos = 0;
3122                                 _caretPos.line++;
3123                             }
3124                         }
3125                         else if (curSpan.len > 1)
3126                         {
3127                             int previousPos = _caretPos.pos;
3128                             int previousAccumulatedPosition = curSpan.accumulation(curSpan.len - 1, LineSpan.WrapPointInfo.Position);
3129                             _caretPos.line++;
3130                             _caretPos.pos = previousPos - previousAccumulatedPosition;
3131                         }
3132                         else
3133                         {
3134                             _caretPos.line++;
3135                         }
3136                     }
3137                     else
3138                     {
3139                         _caretPos.line++;
3140                     }
3141                     correctCaretPos();
3142                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
3143                     ensureCaretVisible();
3144                 }
3145                 return true;
3146             case PageBegin:
3147             case SelectPageBegin:
3148                 {
3149                     ensureCaretVisible();
3150                     _caretPos.line = _firstVisibleLine;
3151                     correctCaretPos();
3152                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
3153                 }
3154                 return true;
3155             case PageEnd:
3156             case SelectPageEnd:
3157                 {
3158                     ensureCaretVisible();
3159                     int fullLines = _clientRect.height / _lineHeight;
3160                     int newpos = _firstVisibleLine + fullLines - 1;
3161                     if (newpos >= _content.length)
3162                         newpos = _content.length - 1;
3163                     _caretPos.line = newpos;
3164                     correctCaretPos();
3165                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
3166                 }
3167                 return true;
3168             case PageUp:
3169             case SelectPageUp:
3170                 {
3171                     ensureCaretVisible();
3172                     int fullLines = _clientRect.height / _lineHeight;
3173                     int newpos = _firstVisibleLine - fullLines;
3174                     if (newpos < 0) {
3175                         _firstVisibleLine = 0;
3176                         _caretPos.line = 0;
3177                     } else {
3178                         int delta = _firstVisibleLine - newpos;
3179                         _firstVisibleLine = newpos;
3180                         _caretPos.line -= delta;
3181                     }
3182                     correctCaretPos();
3183                     measureVisibleText();
3184                     updateScrollBars();
3185                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
3186                 }
3187                 return true;
3188             case PageDown:
3189             case SelectPageDown:
3190                 {
3191                     ensureCaretVisible();
3192                     int fullLines = _clientRect.height / _lineHeight;
3193                     int newpos = _firstVisibleLine + fullLines;
3194                     if (newpos >= _content.length) {
3195                         _caretPos.line = _content.length - 1;
3196                     } else {
3197                         int delta = newpos - _firstVisibleLine;
3198                         _firstVisibleLine = newpos;
3199                         _caretPos.line += delta;
3200                     }
3201                     correctCaretPos();
3202                     measureVisibleText();
3203                     updateScrollBars();
3204                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
3205                 }
3206                 return true;
3207             case ScrollLeft:
3208                 {
3209                     if (_scrollPos.x > 0) {
3210                         int newpos = _scrollPos.x - _spaceWidth * 4;
3211                         if (newpos < 0)
3212                             newpos = 0;
3213                         _scrollPos.x = newpos;
3214                         updateScrollBars();
3215                         invalidate();
3216                     }
3217                 }
3218                 return true;
3219             case ScrollRight:
3220                 {
3221                     if (_scrollPos.x < _maxLineWidth - _clientRect.width) {
3222                         int newpos = _scrollPos.x + _spaceWidth * 4;
3223                         if (newpos > _maxLineWidth - _clientRect.width)
3224                             newpos = _maxLineWidth - _clientRect.width;
3225                         _scrollPos.x = newpos;
3226                         updateScrollBars();
3227                         invalidate();
3228                     }
3229                 }
3230                 return true;
3231             case ScrollLineUp:
3232                 {
3233                     if (_firstVisibleLine > 0) {
3234                         _firstVisibleLine -= 3;
3235                         if (_firstVisibleLine < 0)
3236                             _firstVisibleLine = 0;
3237                         measureVisibleText();
3238                         updateScrollBars();
3239                         invalidate();
3240                     }
3241                 }
3242                 return true;
3243             case ScrollPageUp:
3244                 {
3245                     int fullLines = _clientRect.height / _lineHeight;
3246                     if (_firstVisibleLine > 0) {
3247                         _firstVisibleLine -= fullLines * 3 / 4;
3248                         if (_firstVisibleLine < 0)
3249                             _firstVisibleLine = 0;
3250                         measureVisibleText();
3251                         updateScrollBars();
3252                         invalidate();
3253                     }
3254                 }
3255                 return true;
3256             case ScrollLineDown:
3257                 {
3258                     int fullLines = _clientRect.height / _lineHeight;
3259                     if (_firstVisibleLine + fullLines < _content.length) {
3260                         _firstVisibleLine += 3;
3261                         if (_firstVisibleLine > _content.length - fullLines)
3262                             _firstVisibleLine = _content.length - fullLines;
3263                         if (_firstVisibleLine < 0)
3264                             _firstVisibleLine = 0;
3265                         measureVisibleText();
3266                         updateScrollBars();
3267                         invalidate();
3268                     }
3269                 }
3270                 return true;
3271             case ScrollPageDown:
3272                 {
3273                     int fullLines = _clientRect.height / _lineHeight;
3274                     if (_firstVisibleLine + fullLines < _content.length) {
3275                         _firstVisibleLine += fullLines * 3 / 4;
3276                         if (_firstVisibleLine > _content.length - fullLines)
3277                             _firstVisibleLine = _content.length - fullLines;
3278                         if (_firstVisibleLine < 0)
3279                             _firstVisibleLine = 0;
3280                         measureVisibleText();
3281                         updateScrollBars();
3282                         invalidate();
3283                     }
3284                 }
3285                 return true;
3286             case ZoomOut:
3287             case ZoomIn:
3288                 {
3289                     int dir = a.id == ZoomIn ? 1 : -1;
3290                     if (_minFontSize < _maxFontSize && _minFontSize > 0 && _maxFontSize > 0) {
3291                         int currentFontSize = fontSize;
3292                         int increment = currentFontSize >= 30 ? 2 : 1;
3293                         int newFontSize = currentFontSize + increment * dir; //* 110 / 100;
3294                         if (newFontSize > 30)
3295                             newFontSize &= 0xFFFE;
3296                         if (currentFontSize != newFontSize && newFontSize <= _maxFontSize && newFontSize >= _minFontSize) {
3297                             Log.i("Font size in editor ", id, " zoomed to ", newFontSize);
3298                             fontSize = cast(ushort)newFontSize;
3299                             updateFontProps();
3300                             _needRewrap = true;
3301                             measureVisibleText();
3302                             updateScrollBars();
3303                             invalidate();
3304                         }
3305                     }
3306                 }
3307                 return true;
3308             case ToggleBlockComment:
3309                 if (!readOnly && _content.syntaxSupport && _content.syntaxSupport.supportsToggleBlockComment && _content.syntaxSupport.canToggleBlockComment(_selectionRange))
3310                     _content.syntaxSupport.toggleBlockComment(_selectionRange, this);
3311                 return true;
3312             case ToggleLineComment:
3313                 if (!readOnly && _content.syntaxSupport && _content.syntaxSupport.supportsToggleLineComment && _content.syntaxSupport.canToggleLineComment(_selectionRange))
3314                     _content.syntaxSupport.toggleLineComment(_selectionRange, this);
3315                 return true;
3316             case AppendNewLine:
3317                 if (!readOnly) {
3318                     correctCaretPos();
3319                     TextPosition p = _content.lineEnd(_caretPos.line);
3320                     TextRange r = TextRange(p, p);
3321                     EditOperation op = new EditOperation(EditAction.Replace, r, [""d, ""d]);
3322                     _content.performOperation(op, this);
3323                     _caretPos = oldCaretPos;
3324                     handleEditorStateChange();
3325                 }
3326                 return true;
3327             case DeleteLine:
3328                 if (!readOnly) {
3329                     correctCaretPos();
3330                     EditOperation op = new EditOperation(EditAction.Replace, _content.lineRange(_caretPos.line), [""d]);
3331                     _content.performOperation(op, this);
3332                 }
3333                 return true;
3334             case Find:
3335                 openFindPanel();
3336                 return true;
3337             case FindNext:
3338                 findNext(false);
3339                 return true;
3340             case FindPrev:
3341                 findNext(true);
3342                 return true;
3343             case Replace:
3344                 openReplacePanel();
3345                 return true;
3346             default:
3347                 break;
3348         }
3349         return super.handleAction(a);
3350     }
3351 
3352     /// calculate full content size in pixels
3353     override Point fullContentSize() {
3354         Point textSz;
3355         textSz.y = _lineHeight * _content.length;
3356         textSz.x = _maxLineWidth;
3357         //int maxy = _lineHeight * 5; // limit measured height
3358         //if (textSz.y > maxy)
3359         //    textSz.y = maxy;
3360         return textSz;
3361     }
3362 
3363     // override to set minimum scrollwidget size - default 100x100
3364     override protected Point minimumVisibleContentSize() {
3365         FontRef font = font();
3366         _measuredTextToSetWidgetSizeWidths.length = _textToSetWidgetSize.length;
3367         int charsMeasured = font.measureText(_textToSetWidgetSize, _measuredTextToSetWidgetSizeWidths, MAX_WIDTH_UNSPECIFIED, tabSize);
3368         _measuredTextToSetWidgetSize.x = charsMeasured > 0 ? _measuredTextToSetWidgetSizeWidths[charsMeasured - 1]: 0;
3369         _measuredTextToSetWidgetSize.y = font.height;
3370         return _measuredTextToSetWidgetSize;
3371     }
3372 
3373     /// measure
3374     override void measure(int parentWidth, int parentHeight) {
3375         if (visibility == Visibility.Gone)
3376             return;
3377 
3378         updateFontProps();
3379         updateMaxLineWidth();
3380         int findPanelHeight;
3381         if (_findPanel) {
3382             _findPanel.measure(parentWidth, parentHeight);
3383             findPanelHeight = _findPanel.measuredHeight;
3384             if (parentHeight != SIZE_UNSPECIFIED)
3385                 parentHeight -= findPanelHeight;
3386         }
3387 
3388         super.measure(parentWidth, parentHeight);
3389     }
3390 
3391 
3392     protected void highlightTextPattern(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) {
3393         dstring pattern = _textToHighlight;
3394         uint options = _textToHighlightOptions;
3395         if (!pattern.length) {
3396             // support highlighting selection text - if whole word is selected
3397             if (_selectionRange.empty || !_selectionRange.singleLine)
3398                 return;
3399             if (_selectionRange.start.line >= _content.length)
3400                 return;
3401             dstring selLine = _content.line(_selectionRange.start.line);
3402             int start = _selectionRange.start.pos;
3403             int end = _selectionRange.end.pos;
3404             if (start >= selLine.length)
3405                 return;
3406             pattern = selLine[start .. end];
3407             if (!isWordChar(pattern[0]) || !isWordChar(pattern[$-1]))
3408                 return;
3409             if (!isWholeWord(selLine, start, end))
3410                 return;
3411             // whole word is selected - enable highlight for it
3412             options = TextSearchFlag.CaseSensitive | TextSearchFlag.WholeWords;
3413         }
3414         if (!pattern.length)
3415             return;
3416         dstring lineText = _content.line(lineIndex);
3417         if (lineText.length < pattern.length)
3418             return;
3419         ptrdiff_t start = 0;
3420         import std.string : indexOf, CaseSensitive;
3421         import std.typecons : Flag;
3422         bool caseSensitive = (options & TextSearchFlag.CaseSensitive) != 0;
3423         bool wholeWords = (options & TextSearchFlag.WholeWords) != 0;
3424         bool selectionOnly = (options & TextSearchFlag.SelectionOnly) != 0;
3425         for (;;) {
3426             ptrdiff_t pos = lineText[start .. $].indexOf(pattern, caseSensitive ? Yes.caseSensitive : No.caseSensitive);
3427             if (pos < 0)
3428                 break;
3429             // found text to highlight
3430             start += pos;
3431             if (!wholeWords || isWholeWord(lineText, start, start + pattern.length)) {
3432                 TextRange r = TextRange(TextPosition(lineIndex, cast(int)start), TextPosition(lineIndex, cast(int)(start + pattern.length)));
3433                 uint color = r.isInsideOrNext(caretPos) ? _searchHighlightColorCurrent : _searchHighlightColorOther;
3434                 highlightLineRange(buf, lineRect, color, r);
3435             }
3436             start += pattern.length;
3437         }
3438     }
3439 
3440     static bool isWordChar(dchar ch) {
3441         if (ch >= 'a' && ch <= 'z')
3442             return true;
3443         if (ch >= 'A' && ch <= 'Z')
3444             return true;
3445         if (ch == '_')
3446             return true;
3447         return false;
3448     }
3449     static bool isValidWordBound(dchar innerChar, dchar outerChar) {
3450         return !isWordChar(innerChar) || !isWordChar(outerChar);
3451     }
3452     /// returns true if selected range of string is whole word
3453     static bool isWholeWord(dstring lineText, size_t start, size_t end) {
3454         if (start >= lineText.length || start >= end)
3455             return false;
3456         if (start > 0 && !isValidWordBound(lineText[start], lineText[start - 1]))
3457             return false;
3458         if (end > 0 && end < lineText.length && !isValidWordBound(lineText[end - 1], lineText[end]))
3459             return false;
3460         return true;
3461     }
3462 
3463     /// find all occurences of text pattern in content; options = bitset of TextSearchFlag
3464     TextRange[] findAll(dstring pattern, uint options) {
3465         TextRange[] res;
3466         res.assumeSafeAppend();
3467         if (!pattern.length)
3468             return res;
3469         import std.string : indexOf, CaseSensitive;
3470         bool caseSensitive = (options & TextSearchFlag.CaseSensitive) != 0;
3471         bool wholeWords = (options & TextSearchFlag.WholeWords) != 0;
3472         bool selectionOnly = (options & TextSearchFlag.SelectionOnly) != 0;
3473         for (int i = 0; i < _content.length; i++) {
3474             dstring lineText = _content.line(i);
3475             if (lineText.length < pattern.length)
3476                 continue;
3477             ptrdiff_t start = 0;
3478             for (;;) {
3479                 ptrdiff_t pos = lineText[start .. $].indexOf(pattern, caseSensitive ? Yes.caseSensitive : No.caseSensitive);
3480                 if (pos < 0)
3481                     break;
3482                 // found text to highlight
3483                 start += pos;
3484                 if (!wholeWords || isWholeWord(lineText, start, start + pattern.length)) {
3485                     TextRange r = TextRange(TextPosition(i, cast(int)start), TextPosition(i, cast(int)(start + pattern.length)));
3486                     res ~= r;
3487                 }
3488                 start += _textToHighlight.length;
3489             }
3490         }
3491         return res;
3492     }
3493 
3494     /// find next occurence of text pattern in content, returns true if found
3495     bool findNextPattern(ref TextPosition pos, dstring pattern, uint searchOptions, int direction) {
3496         TextRange[] all = findAll(pattern, searchOptions);
3497         if (!all.length)
3498             return false;
3499         int currentIndex = -1;
3500         int nearestIndex = cast(int)all.length;
3501         for (int i = 0; i < all.length; i++) {
3502             if (all[i].isInsideOrNext(pos)) {
3503                 currentIndex = i;
3504                 break;
3505             }
3506         }
3507         for (int i = 0; i < all.length; i++) {
3508             if (pos < all[i].start) {
3509                 nearestIndex = i;
3510                 break;
3511             }
3512             if (pos > all[i].end) {
3513                 nearestIndex = i + 1;
3514             }
3515         }
3516         if (currentIndex >= 0) {
3517             if (all.length < 2 && direction != 0)
3518                 return false;
3519             currentIndex += direction;
3520             if (currentIndex < 0)
3521                 currentIndex = cast(int)all.length - 1;
3522             else if (currentIndex >= all.length)
3523                 currentIndex = 0;
3524             pos = all[currentIndex].start;
3525             return true;
3526         }
3527         if (direction < 0)
3528             nearestIndex--;
3529         if (nearestIndex < 0)
3530             nearestIndex = cast(int)all.length - 1;
3531         else if (nearestIndex >= all.length)
3532             nearestIndex = 0;
3533         pos = all[nearestIndex].start;
3534         return true;
3535     }
3536 
3537     protected void highlightLineRange(DrawBuf buf, Rect lineRect, uint color, TextRange r) {
3538         Rect startrc = textPosToClient(r.start);
3539         Rect endrc = textPosToClient(r.end);
3540         Rect rc = lineRect;
3541         rc.left = _clientRect.left + startrc.left;
3542         rc.right = _clientRect.left + endrc.right;
3543         if (_wordWrap && !rc.empty)
3544         {
3545             wordWrapFillRect(buf, r.start.line, rc, color);
3546         }
3547         else if (!rc.empty) {
3548             // draw selection rect for matching bracket
3549             buf.fillRect(rc, color);
3550         }
3551     }
3552 
3553     /// Used in place of directly calling buf.fillRect in word wrap mode
3554     void wordWrapFillRect(DrawBuf buf, int line, Rect lineToDivide, uint color)
3555     {
3556         Rect rc = lineToDivide;
3557         auto limitNumber = (int num, int limit) => num > limit ? limit : num;
3558         LineSpan curSpan = getSpan(line);
3559         int yOffset = _lineHeight * (wrapsUpTo(line));
3560         rc.offset(0, yOffset);
3561         Rect[] wrappedSelection;
3562         wrappedSelection.length = curSpan.len;
3563         foreach (size_t i_, wrapLineRect; wrappedSelection)
3564         {
3565             int i = cast(int)i_;
3566             int startingDifference = rc.left - _clientRect.left;
3567             wrapLineRect = rc;
3568             wrapLineRect.offset(-1 * curSpan.accumulation(cast(int)i, LineSpan.WrapPointInfo.Width), cast(int)i * _lineHeight);
3569             wrapLineRect.right = limitNumber(wrapLineRect.right,(rc.left + curSpan.wrapPoints[i].wrapWidth) - startingDifference);
3570             buf.fillRect(wrapLineRect, color);
3571         }
3572     }
3573 
3574     /// override to custom highlight of line background
3575     protected void drawLineBackground(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) {
3576         // highlight odd lines
3577         //if ((lineIndex & 1))
3578         //    buf.fillRect(visibleRect, 0xF4808080);
3579 
3580         if (!_selectionRange.empty && _selectionRange.start.line <= lineIndex && _selectionRange.end.line >= lineIndex) {
3581             // line inside selection
3582             Rect startrc = textPosToClient(_selectionRange.start);
3583             Rect endrc = textPosToClient(_selectionRange.end);
3584             int startx = lineIndex == _selectionRange.start.line ? startrc.left + _clientRect.left : lineRect.left;
3585             int endx = lineIndex == _selectionRange.end.line ? endrc.left + _clientRect.left : lineRect.right + _spaceWidth;
3586             Rect rc = lineRect;
3587             rc.left = startx;
3588             rc.right = endx;
3589             if (!rc.empty && _wordWrap)
3590             {
3591                 wordWrapFillRect(buf, lineIndex, rc, focused ? _selectionColorFocused : _selectionColorNormal);
3592             }
3593             else if (!rc.empty) {
3594                 // draw selection rect for line
3595                 buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal);
3596             }
3597         }
3598 
3599         highlightTextPattern(buf, lineIndex, lineRect, visibleRect);
3600 
3601         if (_matchingBraces.start.line == lineIndex)  {
3602             TextRange r = TextRange(_matchingBraces.start, _matchingBraces.start.offset(1));
3603             highlightLineRange(buf, lineRect, _matchingBracketHightlightColor, r);
3604         }
3605         if (_matchingBraces.end.line == lineIndex)  {
3606             TextRange r = TextRange(_matchingBraces.end, _matchingBraces.end.offset(1));
3607             highlightLineRange(buf, lineRect, _matchingBracketHightlightColor, r);
3608         }
3609 
3610         // frame around current line
3611         if (focused && lineIndex == _caretPos.line && _selectionRange.singleLine && _selectionRange.start.line == _caretPos.line) {
3612             //TODO: Figure out why a little slow to catch up
3613             if (_wordWrap)
3614                 visibleRect.offset(0, -caretHeightOffset);
3615             buf.drawFrame(visibleRect, 0xA0808080, Rect(1,1,1,1));
3616         }
3617 
3618     }
3619 
3620     override protected void drawExtendedArea(DrawBuf buf) {
3621         if (_leftPaneWidth <= 0)
3622             return;
3623         Rect rc = _clientRect;
3624 
3625         FontRef font = font();
3626         int i = _firstVisibleLine;
3627         int lc = lineCount;
3628         for (;;) {
3629             Rect lineRect = rc;
3630             lineRect.left = _clientRect.left - _leftPaneWidth;
3631             lineRect.right = _clientRect.left;
3632             lineRect.bottom = lineRect.top + _lineHeight;
3633             if (lineRect.top >= _clientRect.bottom)
3634                 break;
3635             drawLeftPane(buf, lineRect, i < lc ? i : -1);
3636             rc.top += _lineHeight;
3637             if (_wordWrap)
3638             {
3639                 int currentWrap = 1;
3640                 for (;;)
3641                 {
3642                     LineSpan curSpan = getSpan(i);
3643                     if (currentWrap > curSpan.len - 1)
3644                         break;
3645                     Rect lineRect2 = rc;
3646                     lineRect2.left = _clientRect.left - _leftPaneWidth;
3647                     lineRect2.right = _clientRect.left;
3648                     lineRect2.bottom = lineRect.top + _lineHeight;
3649                     if (lineRect2.top >= _clientRect.bottom)
3650                         break;
3651                     drawLeftPane(buf, lineRect2, -1);
3652                     rc.top += _lineHeight;
3653 
3654                     currentWrap++;
3655                 }
3656             }
3657             i++;
3658         }
3659     }
3660 
3661 
3662     protected CustomCharProps[ubyte] _tokenHighlightColors;
3663 
3664     /// set highlight options for particular token category
3665     void setTokenHightlightColor(ubyte tokenCategory, uint color, bool underline = false, bool strikeThrough = false) {
3666          _tokenHighlightColors[tokenCategory] = CustomCharProps(color, underline, strikeThrough);
3667     }
3668     /// clear highlight colors
3669     void clearTokenHightlightColors() {
3670         destroy(_tokenHighlightColors);
3671     }
3672 
3673     /**
3674         Custom text color and style highlight (using text highlight) support.
3675 
3676         Return null if no syntax highlight required for line.
3677      */
3678     protected CustomCharProps[] handleCustomLineHighlight(int line, dstring txt, ref CustomCharProps[] buf) {
3679         if (!_tokenHighlightColors)
3680             return null; // no highlight colors set
3681         TokenPropString tokenProps = _content.lineTokenProps(line);
3682         if (tokenProps.length > 0) {
3683             bool hasNonzeroTokens = false;
3684             foreach(t; tokenProps)
3685                 if (t) {
3686                     hasNonzeroTokens = true;
3687                     break;
3688                 }
3689             if (!hasNonzeroTokens)
3690                 return null; // all characters are of unknown token type (or white space)
3691             if (buf.length < tokenProps.length)
3692                 buf.length = tokenProps.length;
3693             CustomCharProps[] colors = buf[0..tokenProps.length]; //new CustomCharProps[tokenProps.length];
3694             for (int i = 0; i < tokenProps.length; i++) {
3695                 ubyte p = tokenProps[i];
3696                 if (p in _tokenHighlightColors)
3697                     colors[i] = _tokenHighlightColors[p];
3698                 else if ((p & TOKEN_CATEGORY_MASK) in _tokenHighlightColors)
3699                     colors[i] = _tokenHighlightColors[(p & TOKEN_CATEGORY_MASK)];
3700                 else
3701                     colors[i].color = textColor;
3702                 if (isFullyTransparentColor(colors[i].color))
3703                     colors[i].color = textColor;
3704             }
3705             return colors;
3706         }
3707         return null;
3708     }
3709 
3710     TextRange _matchingBraces;
3711 
3712     bool _showWhiteSpaceMarks;
3713     /// when true, show marks for tabs and spaces at beginning and end of line, and tabs inside line
3714     @property bool showWhiteSpaceMarks() const { return _showWhiteSpaceMarks; }
3715     @property void showWhiteSpaceMarks(bool show) {
3716         if (_showWhiteSpaceMarks != show) {
3717             _showWhiteSpaceMarks = show;
3718             invalidate();
3719         }
3720     }
3721 
3722     /// find max tab mark column position for line
3723     protected int findMaxTabMarkColumn(int lineIndex) {
3724         if (lineIndex < 0 || lineIndex >= content.length)
3725             return -1;
3726         int maxSpace = -1;
3727         auto space = content.getLineWhiteSpace(lineIndex);
3728         maxSpace = space.firstNonSpaceColumn;
3729         if (maxSpace >= 0)
3730             return maxSpace;
3731         for(int i = lineIndex - 1; i >= 0; i--) {
3732             space = content.getLineWhiteSpace(i);
3733             if (!space.empty) {
3734                 maxSpace = space.firstNonSpaceColumn;
3735                 break;
3736             }
3737         }
3738         for(int i = lineIndex + 1; i < content.length; i++) {
3739             space = content.getLineWhiteSpace(i);
3740             if (!space.empty) {
3741                 if (maxSpace < 0 || maxSpace < space.firstNonSpaceColumn)
3742                     maxSpace = space.firstNonSpaceColumn;
3743                 break;
3744             }
3745         }
3746         return maxSpace;
3747     }
3748 
3749     void drawTabPositionMarks(DrawBuf buf, ref FontRef font, int lineIndex, Rect lineRect) {
3750         int maxCol = findMaxTabMarkColumn(lineIndex);
3751         if (maxCol > 0) {
3752             int spaceWidth = font.charWidth(' ');
3753             Rect rc = lineRect;
3754             uint color = addAlpha(textColor, 0xC0);
3755             for (int i = 0; i < maxCol; i += tabSize) {
3756                 rc.left = lineRect.left + i * spaceWidth;
3757                 rc.right = rc.left + 1;
3758                 buf.fillRectPattern(rc, color, PatternType.dotted);
3759             }
3760         }
3761     }
3762 
3763     void drawWhiteSpaceMarks(DrawBuf buf, ref FontRef font, dstring txt, int tabSize, Rect lineRect, Rect visibleRect) {
3764         // _showTabPositionMarks
3765         // _showWhiteSpaceMarks
3766         int firstNonSpace = -1;
3767         int lastNonSpace = -1;
3768         bool hasTabs = false;
3769         for(int i = 0; i < txt.length; i++) {
3770             if (txt[i] == '\t') {
3771                 hasTabs = true;
3772             } else if (txt[i] != ' ') {
3773                 if (firstNonSpace == -1)
3774                     firstNonSpace = i;
3775                 lastNonSpace = i + 1;
3776             }
3777         }
3778         bool spacesOnly = txt.length > 0 && firstNonSpace < 0;
3779         if (firstNonSpace <= 0 && lastNonSpace >= txt.length && !hasTabs && !spacesOnly)
3780             return;
3781         uint color = addAlpha(textColor, 0xC0);
3782         static int[] textSizeBuffer;
3783         int charsMeasured = font.measureText(txt, textSizeBuffer, MAX_WIDTH_UNSPECIFIED, tabSize, 0, 0);
3784         int ts = tabSize;
3785         if (ts < 1)
3786             ts = 1;
3787         if (ts > 8)
3788             ts = 8;
3789         int spaceIndex = 0;
3790         for (int i = 0; i < txt.length && i < charsMeasured; i++) {
3791             dchar ch = txt[i];
3792             bool outsideText = (i < firstNonSpace || i >= lastNonSpace || spacesOnly);
3793             if ((ch == ' ' && outsideText) || ch == '\t') {
3794                 Rect rc = lineRect;
3795                 rc.left = lineRect.left + (i > 0 ? textSizeBuffer[i - 1] : 0);
3796                 rc.right = lineRect.left + textSizeBuffer[i];
3797                 int h = rc.height;
3798                 if (rc.intersects(visibleRect)) {
3799                     // draw space mark
3800                     if (ch == ' ') {
3801                         // space
3802                         int sz = h / 6;
3803                         if (sz < 1)
3804                             sz = 1;
3805                         rc.top += h / 2 - sz / 2;
3806                         rc.bottom = rc.top + sz;
3807                         rc.left += rc.width / 2 - sz / 2;
3808                         rc.right = rc.left + sz;
3809                         buf.fillRect(rc, color);
3810                     } else if (ch == '\t') {
3811                         // tab
3812                         Point p1 = Point(rc.left + 1, rc.top + h / 2);
3813                         Point p2 = p1;
3814                         p2.x = rc.right - 1;
3815                         int sz = h / 4;
3816                         if (sz < 2)
3817                             sz = 2;
3818                         if (sz > p2.x - p1.x)
3819                             sz = p2.x - p1.x;
3820                         buf.drawLine(p1, p2, color);
3821                         buf.drawLine(p2, Point(p2.x - sz, p2.y - sz), color);
3822                         buf.drawLine(p2, Point(p2.x - sz, p2.y + sz), color);
3823                     }
3824                 }
3825             }
3826         }
3827     }
3828 
3829     /// Clear _span
3830     void resetVisibleSpans()
3831     {
3832         //TODO: Don't erase spans which have not been modified, cache them
3833         _span = [];
3834     }
3835 
3836     private bool _needRewrap = true;
3837     private int lastStartingLine;
3838 
3839     override protected void drawClient(DrawBuf buf) {
3840         // update matched braces
3841         if (!content.findMatchedBraces(_caretPos, _matchingBraces)) {
3842             _matchingBraces.start.line = -1;
3843             _matchingBraces.end.line = -1;
3844         }
3845 
3846         Rect rc = _clientRect;
3847 
3848         if (_contentChanged)
3849           _needRewrap = true;
3850         if (lastStartingLine != _firstVisibleLine)
3851         {
3852             _needRewrap = true;
3853             lastStartingLine = _firstVisibleLine;
3854         }
3855         if (rc.width <= 0 && _wordWrap)
3856         {
3857             //Prevent drawClient from getting stuck in loop
3858             return;
3859         }
3860         bool doRewrap = false;
3861         if (_needRewrap && _wordWrap)
3862         {
3863             resetVisibleSpans();
3864             _needRewrap = false;
3865             doRewrap = true;
3866         }
3867 
3868         FontRef font = font();
3869         int previousWraps;
3870         for (int i = 0; i < _visibleLines.length; i++) {
3871             dstring txt = _visibleLines[i];
3872             Rect lineRect;
3873             lineRect.left = _clientRect.left - _scrollPos.x;
3874             lineRect.right = lineRect.left + calcLineWidth(_content[_firstVisibleLine + i]);
3875             lineRect.top = _clientRect.top + i * _lineHeight;
3876             lineRect.bottom = lineRect.top + _lineHeight;
3877             Rect visibleRect = lineRect;
3878             visibleRect.left = _clientRect.left;
3879             visibleRect.right = _clientRect.right;
3880             drawLineBackground(buf, _firstVisibleLine + i, lineRect, visibleRect);
3881             if (_showTabPositionMarks)
3882                 drawTabPositionMarks(buf, font, _firstVisibleLine + i, lineRect);
3883             if (!txt.length && !_wordWrap)
3884                 continue;
3885             if (_showWhiteSpaceMarks)
3886             {
3887                 Rect whiteSpaceRc = lineRect;
3888                 Rect whiteSpaceRcVisible = visibleRect;
3889                 for(int z; z < previousWraps; z++)
3890                 {
3891                     whiteSpaceRc.offset(0, _lineHeight);
3892                     whiteSpaceRcVisible.offset(0, _lineHeight);
3893                 }
3894                 drawWhiteSpaceMarks(buf, font, txt, tabSize, whiteSpaceRc, whiteSpaceRcVisible);
3895             }
3896             if (_leftPaneWidth > 0) {
3897                 Rect leftPaneRect = visibleRect;
3898                 leftPaneRect.right = leftPaneRect.left;
3899                 leftPaneRect.left -= _leftPaneWidth;
3900                 drawLeftPane(buf, leftPaneRect, 0);
3901             }
3902             if (txt.length > 0 || _wordWrap) {
3903                 CustomCharProps[] highlight = _visibleLinesHighlights[i];
3904                 if (_wordWrap)
3905                 {
3906                     dstring[] wrappedLine;
3907                     if (doRewrap)
3908                         wrappedLine = wrapLine(txt, _firstVisibleLine + i);
3909                     else
3910                         if (i < _span.length)
3911                             wrappedLine = _span[i].wrappedContent;
3912                     int accumulativeLength;
3913                     CustomCharProps[] wrapProps;
3914                     foreach (size_t q_, curWrap; wrappedLine)
3915                     {
3916                         int q = cast(int)q_;
3917                         auto lineOffset = q + i + wrapsUpTo(i + _firstVisibleLine);
3918                         if (highlight)
3919                         {
3920                             wrapProps = highlight[accumulativeLength .. $];
3921                             accumulativeLength += curWrap.length;
3922                             font.drawColoredText(buf, rc.left - _scrollPos.x, rc.top + lineOffset * _lineHeight, curWrap, wrapProps, tabSize);
3923                         }
3924                         else
3925                             font.drawText(buf, rc.left - _scrollPos.x, rc.top + lineOffset * _lineHeight, curWrap, textColor, tabSize);
3926 
3927                     }
3928                     previousWraps += to!int(wrappedLine.length - 1);
3929                 }
3930                 else
3931                 {
3932                     if (highlight)
3933                         font.drawColoredText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, highlight, tabSize);
3934                     else
3935                         font.drawText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, textColor, tabSize);
3936                 }
3937             }
3938         }
3939 
3940         drawCaret(buf);
3941     }
3942 
3943     protected override bool onLeftPaneMouseClick(MouseEvent event) {
3944         if (_leftPaneWidth <= 0)
3945             return false;
3946         Rect rc = _clientRect;
3947         FontRef font = font();
3948         int i = _firstVisibleLine;
3949         int lc = lineCount;
3950         for (;;) {
3951             Rect lineRect = rc;
3952             lineRect.left = _clientRect.left - _leftPaneWidth;
3953             lineRect.right = _clientRect.left;
3954             lineRect.bottom = lineRect.top + _lineHeight;
3955             if (lineRect.top >= _clientRect.bottom)
3956                 break;
3957             if (event.y >= lineRect.top && event.y < lineRect.bottom) {
3958                 return handleLeftPaneMouseClick(event, lineRect, i);
3959             }
3960             i++;
3961             rc.top += _lineHeight;
3962         }
3963         return false;
3964     }
3965 
3966     override protected MenuItem getLeftPaneIconsPopupMenu(int line) {
3967         MenuItem menu = new MenuItem();
3968         Action toggleBookmarkAction = ACTION_EDITOR_TOGGLE_BOOKMARK.clone();
3969         toggleBookmarkAction.longParam = line;
3970         toggleBookmarkAction.objectParam = this;
3971         MenuItem item = menu.add(toggleBookmarkAction);
3972         return menu;
3973     }
3974 
3975     protected FindPanel _findPanel;
3976 
3977     dstring selectionText(bool singleLineOnly = false) {
3978         TextRange range = _selectionRange;
3979         if (range.empty) {
3980             return null;
3981         }
3982         dstring res = getRangeText(range);
3983         if (singleLineOnly) {
3984             for (int i = 0; i < res.length; i++) {
3985                 if (res[i] == '\n') {
3986                     res = res[0 .. i];
3987                     break;
3988                 }
3989             }
3990         }
3991         return res;
3992     }
3993 
3994     protected void findNext(bool backward) {
3995         createFindPanel(false, false);
3996         _findPanel.findNext(backward);
3997         // don't change replace mode
3998     }
3999 
4000     protected void openFindPanel() {
4001         createFindPanel(false, false);
4002         _findPanel.replaceMode = false;
4003         _findPanel.activate();
4004     }
4005 
4006     protected void openReplacePanel() {
4007         createFindPanel(false, true);
4008         _findPanel.replaceMode = true;
4009         _findPanel.activate();
4010     }
4011 
4012     /// create find panel; returns true if panel was not yet visible
4013     protected bool createFindPanel(bool selectionOnly, bool replaceMode) {
4014         bool res = false;
4015         dstring txt = selectionText(true);
4016         if (!_findPanel) {
4017             _findPanel = new FindPanel(this, selectionOnly, replaceMode, txt);
4018             addChild(_findPanel);
4019             res = true;
4020         } else {
4021             if (_findPanel.visibility != Visibility.Visible) {
4022                 _findPanel.visibility = Visibility.Visible;
4023                 if (txt.length)
4024                     _findPanel.searchText = txt;
4025                 res = true;
4026             }
4027         }
4028         if (!pos.empty)
4029             layout(pos);
4030         requestLayout();
4031         return res;
4032     }
4033 
4034     /// close find panel
4035     protected void closeFindPanel(bool hideOnly = true) {
4036         if (_findPanel) {
4037             setFocus();
4038             if (hideOnly) {
4039                 _findPanel.visibility = Visibility.Gone;
4040             } else {
4041                 removeChild(_findPanel);
4042                 destroy(_findPanel);
4043                 _findPanel = null;
4044                 requestLayout();
4045             }
4046         }
4047     }
4048 
4049     /// Draw widget at its position to buffer
4050     override void onDraw(DrawBuf buf) {
4051         if (visibility != Visibility.Visible)
4052             return;
4053         super.onDraw(buf);
4054         if (_findPanel && _findPanel.visibility == Visibility.Visible) {
4055             _findPanel.onDraw(buf);
4056         }
4057     }
4058 }
4059 
4060 /// Read only edit box for displaying logs with lines append operation
4061 class LogWidget : EditBox {
4062 
4063     protected int  _maxLines;
4064     /// max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit
4065     @property int maxLines() { return _maxLines; }
4066     /// set max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit
4067     @property void maxLines(int n) { _maxLines = n; }
4068 
4069     protected bool _scrollLock;
4070     /// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction)
4071     @property bool scrollLock() { return _scrollLock; }
4072     /// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction)
4073     @property void scrollLock(bool flg) { _scrollLock = flg; }
4074 
4075     this() {
4076         this(null);
4077     }
4078 
4079     this(string ID) {
4080         super(ID);
4081         styleId = STYLE_LOG_WIDGET;
4082         _scrollLock = true;
4083         _enableScrollAfterText = false;
4084         enabled = false;
4085         minFontSize(pointsToPixels(6)).maxFontSize(pointsToPixels(32)); // allow font zoom with Ctrl + MouseWheel
4086         onThemeChanged();
4087     }
4088 
4089     /// append lines to the end of text
4090     void appendText(dstring text) {
4091         import std.array : split;
4092         if (text.length == 0)
4093             return;
4094         dstring[] lines = text.split("\n");
4095         //lines ~= ""d; // append new line after last line
4096         content.appendLines(lines);
4097         if (_maxLines > 0 && lineCount > _maxLines) {
4098             TextRange range;
4099             range.end.line = lineCount - _maxLines;
4100             EditOperation op = new EditOperation(EditAction.Replace, range, [""d]);
4101             _content.performOperation(op, this);
4102             _contentChanged = true;
4103         }
4104         updateScrollBars();
4105         if (_scrollLock) {
4106             _caretPos = lastLineBegin();
4107             ensureCaretVisible();
4108         }
4109     }
4110 
4111     TextPosition lastLineBegin() {
4112         TextPosition res;
4113         if (_content.length == 0)
4114             return res;
4115         if (_content.lineLength(_content.length - 1) == 0 && _content.length > 1)
4116             res.line = _content.length - 2;
4117         else
4118             res.line = _content.length - 1;
4119         return res;
4120     }
4121 
4122     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
4123     override void layout(Rect rc) {
4124         if (visibility == Visibility.Gone)
4125             return;
4126 
4127         super.layout(rc);
4128         if (_scrollLock) {
4129             measureVisibleText();
4130             _caretPos = lastLineBegin();
4131             ensureCaretVisible();
4132         }
4133     }
4134 
4135 }
4136 
4137 class FindPanel : HorizontalLayout {
4138     protected EditBox _editor;
4139     protected EditLine _edFind;
4140     protected EditLine _edReplace;
4141     protected ImageCheckButton _cbCaseSensitive;
4142     protected ImageCheckButton _cbWholeWords;
4143     protected CheckBox _cbSelection;
4144     protected Button _btnFindNext;
4145     protected Button _btnFindPrev;
4146     protected Button _btnReplace;
4147     protected Button _btnReplaceAndFind;
4148     protected Button _btnReplaceAll;
4149     protected ImageButton _btnClose;
4150     protected bool _replaceMode;
4151     /// returns true if panel is working in replace mode
4152     @property bool replaceMode() { return _replaceMode; }
4153     @property FindPanel replaceMode(bool newMode) {
4154         if (newMode != _replaceMode) {
4155             _replaceMode = newMode;
4156             childById("replace").visibility = newMode ? Visibility.Visible : Visibility.Gone;
4157         }
4158         return this;
4159     }
4160 
4161     @property dstring searchText() {
4162         return _edFind.text;
4163     }
4164 
4165     @property FindPanel searchText(dstring newText) {
4166         _edFind.text = newText;
4167         return this;
4168     }
4169 
4170     this(EditBox editor, bool selectionOnly, bool replace, dstring initialText = ""d) {
4171         _replaceMode = replace;
4172         import dlangui.dml.parser;
4173         try {
4174             parseML(q{
4175                 {
4176                     layoutWidth: fill
4177                     VerticalLayout {
4178                         layoutWidth: fill
4179                         HorizontalLayout {
4180                             layoutWidth: fill
4181                             EditLine { id: edFind; layoutWidth: fill; alignment: vcenter }
4182                             Button { id: btnFindNext; text: EDIT_FIND_NEXT }
4183                             Button { id: btnFindPrev; text: EDIT_FIND_PREV }
4184                             VerticalLayout {
4185                                 VSpacer {}
4186                                 HorizontalLayout {
4187                                     ImageCheckButton { id: cbCaseSensitive; drawableId: "find_case_sensitive"; tooltipText: EDIT_FIND_CASE_SENSITIVE; styleId: TOOLBAR_BUTTON; alignment: vcenter }
4188                                     ImageCheckButton { id: cbWholeWords; drawableId: "find_whole_words"; tooltipText: EDIT_FIND_WHOLE_WORDS; styleId: TOOLBAR_BUTTON; alignment: vcenter }
4189                                     CheckBox { id: cbSelection; text: "Sel" }
4190                                 }
4191                                 VSpacer {}
4192                             }
4193                         }
4194                         HorizontalLayout {
4195                             id: replace
4196                             layoutWidth: fill;
4197                             EditLine { id: edReplace; layoutWidth: fill; alignment: vcenter }
4198                             Button { id: btnReplace; text: EDIT_REPLACE_NEXT }
4199                             Button { id: btnReplaceAndFind; text: EDIT_REPLACE_AND_FIND }
4200                             Button { id: btnReplaceAll; text: EDIT_REPLACE_ALL }
4201                         }
4202                     }
4203                     VerticalLayout {
4204                         VSpacer {}
4205                         ImageButton { id: btnClose; drawableId: close; styleId: BUTTON_TRANSPARENT }
4206                         VSpacer {}
4207                     }
4208                 }
4209             }, null, this);
4210         } catch (Exception e) {
4211             Log.e("Exception while parsing DML: ", e);
4212         }
4213         _editor = editor;
4214         _edFind = childById!EditLine("edFind");
4215         _edReplace = childById!EditLine("edReplace");
4216 
4217         if (initialText.length) {
4218             _edFind.text = initialText;
4219             _edReplace.text = initialText;
4220         }
4221 
4222         _edFind.editorAction.connect(&onFindEditorAction);
4223         _edFind.contentChange.connect(&onFindTextChange);
4224 
4225         //_edFind.keyEvent = &onEditorKeyEvent;
4226         //_edReplace.keyEvent = &onEditorKeyEvent;
4227 
4228         _btnFindNext = childById!Button("btnFindNext");
4229         _btnFindNext.click = &onButtonClick;
4230         _btnFindPrev = childById!Button("btnFindPrev");
4231         _btnFindPrev.click = &onButtonClick;
4232         _btnReplace = childById!Button("btnReplace");
4233         _btnReplace.click = &onButtonClick;
4234         _btnReplaceAndFind = childById!Button("btnReplaceAndFind");
4235         _btnReplaceAndFind.click = &onButtonClick;
4236         _btnReplaceAll = childById!Button("btnReplaceAll");
4237         _btnReplaceAll.click = &onButtonClick;
4238         _btnClose = childById!ImageButton("btnClose");
4239         _btnClose.click = &onButtonClick;
4240         _cbCaseSensitive = childById!ImageCheckButton("cbCaseSensitive");
4241         _cbWholeWords = childById!ImageCheckButton("cbWholeWords");
4242         _cbSelection =  childById!CheckBox("cbSelection");
4243         _cbCaseSensitive.checkChange = &onCaseSensitiveCheckChange;
4244         _cbWholeWords.checkChange = &onCaseSensitiveCheckChange;
4245         _cbSelection.checkChange = &onCaseSensitiveCheckChange;
4246         focusGroup = true;
4247         if (!replace)
4248             childById("replace").visibility = Visibility.Gone;
4249         //_edFind = new EditLine("edFind"
4250         dstring currentText = _edFind.text;
4251         Log.d("currentText=", currentText);
4252         setDirection(false);
4253         updateHighlight();
4254     }
4255     void activate() {
4256         _edFind.setFocus();
4257         dstring currentText = _edFind.text;
4258         Log.d("activate.currentText=", currentText);
4259         _edFind.setCaretPos(0, cast(int)currentText.length, true);
4260     }
4261 
4262     bool onButtonClick(Widget source) {
4263         switch (source.id) {
4264             case "btnFindNext":
4265                 findNext(false);
4266                 return true;
4267             case "btnFindPrev":
4268                 findNext(true);
4269                 return true;
4270             case "btnClose":
4271                 close();
4272                 return true;
4273             case "btnReplace":
4274                 replaceOne();
4275                 return true;
4276             case "btnReplaceAndFind":
4277                 replaceOne();
4278                 findNext(_backDirection);
4279                 return true;
4280             case "btnReplaceAll":
4281                 replaceAll();
4282                 return true;
4283             default:
4284                 return true;
4285         }
4286     }
4287 
4288     void close() {
4289         _editor.setTextToHighlight(null, 0);
4290         _editor.closeFindPanel();
4291     }
4292 
4293     override bool onKeyEvent(KeyEvent event) {
4294         if (event.keyCode == KeyCode.TAB)
4295             return super.onKeyEvent(event);
4296         if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.ESCAPE) {
4297             close();
4298             return true;
4299         }
4300         return true;
4301     }
4302 
4303     /// override to handle specific actions
4304     override bool handleAction(const Action a) {
4305         switch (a.id) {
4306             case EditorActions.FindNext:
4307                 findNext(false);
4308                 return true;
4309             case EditorActions.FindPrev:
4310                 findNext(true);
4311                 return true;
4312             default:
4313                 return false;
4314         }
4315     }
4316 
4317     protected bool _backDirection;
4318     void setDirection(bool back) {
4319         _backDirection = back;
4320         if (back) {
4321             _btnFindNext.resetState(State.Default);
4322             _btnFindPrev.setState(State.Default);
4323         } else {
4324             _btnFindNext.setState(State.Default);
4325             _btnFindPrev.resetState(State.Default);
4326         }
4327     }
4328 
4329     uint makeSearchFlags() {
4330         uint res = 0;
4331         if (_cbCaseSensitive.checked)
4332             res |= TextSearchFlag.CaseSensitive;
4333         if (_cbWholeWords.checked)
4334             res |= TextSearchFlag.WholeWords;
4335         if (_cbSelection.checked)
4336             res |= TextSearchFlag.SelectionOnly;
4337         return res;
4338     }
4339     bool findNext(bool back) {
4340         setDirection(back);
4341         dstring currentText = _edFind.text;
4342         Log.d("findNext text=", currentText, " back=", back);
4343         if (!currentText.length)
4344             return false;
4345         _editor.setTextToHighlight(currentText, makeSearchFlags);
4346         TextPosition pos = _editor.caretPos;
4347         bool res = _editor.findNextPattern(pos, currentText, makeSearchFlags, back ? -1 : 1);
4348         if (res) {
4349             _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)currentText.length));
4350             _editor.ensureCaretVisible();
4351             //_editor.setCaretPos(pos.line, pos.pos, true);
4352         }
4353         return res;
4354     }
4355 
4356     bool replaceOne() {
4357         dstring currentText = _edFind.text;
4358         dstring newText = _edReplace.text;
4359         Log.d("replaceOne text=", currentText, " back=", _backDirection, " newText=", newText);
4360         if (!currentText.length)
4361             return false;
4362         _editor.setTextToHighlight(currentText, makeSearchFlags);
4363         TextPosition pos = _editor.caretPos;
4364         bool res = _editor.findNextPattern(pos, currentText, makeSearchFlags, 0);
4365         if (res) {
4366             _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)currentText.length));
4367             _editor.replaceSelectionText(newText);
4368             _editor.selectionRange = TextRange(pos, TextPosition(pos.line, pos.pos + cast(int)newText.length));
4369             _editor.ensureCaretVisible();
4370             //_editor.setCaretPos(pos.line, pos.pos, true);
4371         }
4372         return res;
4373     }
4374 
4375     int replaceAll() {
4376         int count = 0;
4377         for(int i = 0; ; i++) {
4378             debug Log.d("replaceAll - calling replaceOne, iteration ", i);
4379             if (!replaceOne())
4380                 break;
4381             count++;
4382             TextPosition initialPosition = _editor.caretPos;
4383             debug Log.d("replaceAll - position is ", initialPosition);
4384             if (!findNext(_backDirection))
4385                 break;
4386             TextPosition newPosition = _editor.caretPos;
4387             debug Log.d("replaceAll - next position is ", newPosition);
4388             if (_backDirection && newPosition >= initialPosition)
4389                 break;
4390             if (!_backDirection && newPosition <= initialPosition)
4391                 break;
4392         }
4393         debug Log.d("replaceAll - done, replace count = ", count);
4394         _editor.ensureCaretVisible();
4395         return count;
4396     }
4397 
4398     void updateHighlight() {
4399         dstring currentText = _edFind.text;
4400         Log.d("onFindTextChange.currentText=", currentText);
4401         _editor.setTextToHighlight(currentText, makeSearchFlags);
4402     }
4403 
4404     void onFindTextChange(EditableContent source) {
4405         Log.d("onFindTextChange");
4406         updateHighlight();
4407     }
4408 
4409     bool onCaseSensitiveCheckChange(Widget source, bool checkValue) {
4410         updateHighlight();
4411         return true;
4412     }
4413 
4414     bool onFindEditorAction(const Action action) {
4415         Log.d("onFindEditorAction ", action);
4416         if (action.id == EditorActions.InsertNewLine) {
4417             findNext(_backDirection);
4418             return true;
4419         }
4420         return false;
4421     }
4422 }
4423 
4424 //import dlangui.widgets.metadata;
4425 //mixin(registerWidgets!(EditLine, EditBox, LogWidget)());