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