1 // Written in the D programming language.
2 
3 /**
4 This module contains implementation of editors.
5 
6 
7 EditLine - single line editor.
8 
9 EditBox - multiline editor
10 
11 LogWidget - readonly text box for showing logs
12 
13 Synopsis:
14 
15 ----
16 import dlangui.widgets.editors;
17 
18 ----
19 
20 Copyright: Vadim Lopatin, 2014
21 License:   Boost License 1.0
22 Authors:   Vadim Lopatin, coolreader.org@gmail.com
23 */
24 module dlangui.widgets.editors;
25 
26 import dlangui.widgets.widget;
27 import dlangui.widgets.controls;
28 import dlangui.widgets.scroll;
29 import dlangui.widgets.layouts;
30 import dlangui.core.signals;
31 import dlangui.core.collections;
32 import dlangui.core.linestream;
33 import dlangui.platforms.common.platform;
34 import dlangui.widgets.menu;
35 import dlangui.widgets.popup;
36 import dlangui.graphics.colors;
37 public import dlangui.core.editable;
38 
39 import std.algorithm;
40 import dlangui.core.streams;
41 
42 /// Modified state change listener
43 interface ModifiedStateListener {
44     void onModifiedStateChange(Widget source, bool modified);
45 }
46 
47 /// Modified content listener
48 interface EditableContentChangeListener {
49     void onEditableContentChanged(EditableContent source);
50 }
51 
52 /// Editor action codes
53 enum EditorActions : int {
54     None = 0,
55     /// move cursor one char left
56     Left = 1000,
57     /// move cursor one char left with selection
58     SelectLeft,
59     /// move cursor one char right
60     Right,
61     /// move cursor one char right with selection
62     SelectRight,
63     /// move cursor one line up
64     Up,
65     /// move cursor one line up with selection
66     SelectUp,
67     /// move cursor one line down
68     Down,
69     /// move cursor one line down with selection
70     SelectDown,
71     /// move cursor one word left
72     WordLeft,
73     /// move cursor one word left with selection
74     SelectWordLeft,
75     /// move cursor one word right
76     WordRight,
77     /// move cursor one word right with selection
78     SelectWordRight,
79     /// move cursor one page up
80     PageUp,
81     /// move cursor one page up with selection
82     SelectPageUp,
83     /// move cursor one page down
84     PageDown,
85     /// move cursor one page down with selection
86     SelectPageDown,
87     /// move cursor to the beginning of page
88     PageBegin, 
89     /// move cursor to the beginning of page with selection
90     SelectPageBegin, 
91     /// move cursor to the end of page
92     PageEnd,   
93     /// move cursor to the end of page with selection
94     SelectPageEnd,   
95     /// move cursor to the beginning of line
96     LineBegin,
97     /// move cursor to the beginning of line with selection
98     SelectLineBegin,
99     /// move cursor to the end of line
100     LineEnd,
101     /// move cursor to the end of line with selection
102     SelectLineEnd,
103     /// move cursor to the beginning of document
104     DocumentBegin,
105     /// move cursor to the beginning of document with selection
106     SelectDocumentBegin,
107     /// move cursor to the end of document
108     DocumentEnd,
109     /// move cursor to the end of document with selection
110     SelectDocumentEnd,
111     /// delete char before cursor (backspace)
112     DelPrevChar, 
113     /// delete char after cursor (del key)
114     DelNextChar, 
115     /// delete word before cursor (ctrl + backspace)
116     DelPrevWord, 
117     /// delete char after cursor (ctrl + del key)
118     DelNextWord, 
119     
120     /// insert new line (Enter)
121     InsertNewLine,
122     /// insert new line before current position (Ctrl+Enter)
123     PrependNewLine,
124     /// insert new line after current position (Ctrl+Enter)
125     AppendNewLine,
126 
127     /// Turn On/Off replace mode
128     ToggleReplaceMode, 
129     
130     /// Copy selection to clipboard
131     Copy, 
132     /// Cut selection to clipboard
133     Cut, 
134     /// Paste selection from clipboard
135     Paste, 
136     /// Undo last change
137     Undo,
138     /// Redo last undoed change
139     Redo,
140     
141     /// Tab (e.g., Tab key to insert tab character or indent text)
142     Tab,
143     /// Tab (unindent text, or remove whitespace before cursor, usually Shift+Tab)
144     BackTab,
145     /// Indent text block or single line
146     Indent,
147     /// Unindent text
148     Unindent,
149 
150     /// Select whole content (usually, Ctrl+A)
151     SelectAll,
152     
153     // Scroll operations
154     
155     /// Scroll one line up (not changing cursor)
156     ScrollLineUp,
157     /// Scroll one line down (not changing cursor)
158     ScrollLineDown,
159     /// Scroll one page up (not changing cursor)
160     ScrollPageUp,
161     /// Scroll one page down (not changing cursor)
162     ScrollPageDown,
163     /// Scroll window left
164     ScrollLeft,
165     /// Scroll window right
166     ScrollRight,
167     
168     /// Zoom in editor font
169     ZoomIn,
170     /// Zoom out editor font
171     ZoomOut,
172     
173     /// Togle line comment
174     ToggleLineComment,
175     /// Toggle block comment
176     ToggleBlockComment,
177     /// Delete current line
178     DeleteLine,
179     /// Insert line
180     InsertLine,
181 
182     /// Toggle bookmark in current line
183     ToggleBookmark,
184     /// move cursor to next bookmark
185     GoToNextBookmark,
186     /// move cursor to previous bookmark
187     GoToPreviousBookmark,
188 
189     /// Find text
190     Find,
191     /// Replace text
192     Replace
193 }
194 
195 
196 void initStandardEditorActions() {
197     // register editor action names and ids
198     registerActionEnum!EditorActions();
199 }
200 
201 const Action ACTION_EDITOR_INSERT_NEW_LINE = (new Action(EditorActions.InsertNewLine, KeyCode.RETURN, 0)).addAccelerator(KeyCode.RETURN, KeyFlag.Shift);
202 const Action ACTION_EDITOR_PREPEND_NEW_LINE = (new Action(EditorActions.PrependNewLine, KeyCode.RETURN, KeyFlag.Control|KeyFlag.Shift));
203 const Action ACTION_EDITOR_APPEND_NEW_LINE = (new Action(EditorActions.AppendNewLine, KeyCode.RETURN, KeyFlag.Control));
204 const Action ACTION_EDITOR_DELETE_LINE = (new Action(EditorActions.DeleteLine, KeyCode.KEY_D, KeyFlag.Control)).addAccelerator(KeyCode.KEY_L, KeyFlag.Control);
205 const Action ACTION_EDITOR_TOGGLE_REPLACE_MODE = (new Action(EditorActions.ToggleReplaceMode, KeyCode.INS, 0));
206 const Action ACTION_EDITOR_SELECT_ALL = (new Action(EditorActions.SelectAll, KeyCode.KEY_A, KeyFlag.Control));
207 const Action ACTION_EDITOR_TOGGLE_LINE_COMMENT = (new Action(EditorActions.ToggleLineComment, KeyCode.KEY_DIVIDE, KeyFlag.Control));
208 const Action ACTION_EDITOR_TOGGLE_BLOCK_COMMENT = (new Action(EditorActions.ToggleBlockComment, KeyCode.KEY_DIVIDE, KeyFlag.Control | KeyFlag.Shift));
209 const Action ACTION_EDITOR_TOGGLE_BOOKMARK = (new Action(EditorActions.ToggleBookmark, "ACTION_EDITOR_TOGGLE_BOOKMARK"c, null, KeyCode.KEY_B, KeyFlag.Control | KeyFlag.Shift));
210 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));
211 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));
212 const Action ACTION_EDITOR_FIND = (new Action(EditorActions.Find, KeyCode.KEY_F, KeyFlag.Control));
213 const Action ACTION_EDITOR_REPLACE = (new Action(EditorActions.Replace, KeyCode.KEY_H, KeyFlag.Control));
214 
215 const Action[] STD_EDITOR_ACTIONS = [ACTION_EDITOR_INSERT_NEW_LINE, ACTION_EDITOR_PREPEND_NEW_LINE, 
216         ACTION_EDITOR_APPEND_NEW_LINE, ACTION_EDITOR_DELETE_LINE, ACTION_EDITOR_TOGGLE_REPLACE_MODE, 
217         ACTION_EDITOR_SELECT_ALL, ACTION_EDITOR_TOGGLE_LINE_COMMENT, ACTION_EDITOR_TOGGLE_BLOCK_COMMENT,
218         ACTION_EDITOR_TOGGLE_BOOKMARK, ACTION_EDITOR_GOTO_NEXT_BOOKMARK, ACTION_EDITOR_GOTO_PREVIOUS_BOOKMARK,
219 
220 ];
221 
222 /// base for all editor widgets
223 class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemActionHandler {
224     protected EditableContent _content;
225 
226     protected int _lineHeight;
227     protected Point _scrollPos;
228     protected bool _fixedFont;
229     protected int _spaceWidth;
230     protected int _leftPaneWidth; // left pane - can be used to show line numbers, collapse controls, bookmarks, breakpoints, custom icons
231 
232     protected int _minFontSize = -1; // disable zooming
233     protected int _maxFontSize = -1; // disable zooming
234 
235     protected bool _wantTabs = true;
236     protected bool _showLineNumbers = false; // show line numbers in left pane
237     protected bool _showModificationMarks = false; // show modification marks in left pane
238     protected bool _showIcons = false; // show icons in left pane
239     protected bool _showFolding = false; // show folding controls in left pane
240     protected int _lineNumbersWidth = 0;
241     protected int _modificationMarksWidth = 0;
242     protected int _iconsWidth = 0;
243     protected int _foldingWidth = 0;
244 
245     protected bool _selectAllWhenFocusedWithTab = false;
246     protected bool _deselectAllWhenUnfocused = false;
247 
248     protected bool _replaceMode;
249 
250     // TODO: move to styles
251     protected uint _selectionColorFocused = 0xB060A0FF;
252     protected uint _selectionColorNormal = 0xD060A0FF;
253     protected uint _leftPaneBackgroundColor = 0xF4F4F4;
254     protected uint _leftPaneBackgroundColor2 = 0xFFFFFF;
255     protected uint _leftPaneBackgroundColor3 = 0xF8F8F8;
256     protected uint _leftPaneLineNumberColor = 0x4060D0;
257     protected uint _leftPaneLineNumberColorEdited = 0xC0C000;
258     protected uint _leftPaneLineNumberColorSaved = 0x00C000;
259     protected uint _leftPaneLineNumberColorCurrentLine = 0xFFFFFFFF;
260     protected uint _leftPaneLineNumberBackgroundColorCurrentLine = 0xC08080FF;
261     protected uint _leftPaneLineNumberBackgroundColor = 0xF4F4F4;
262     protected uint _colorIconBreakpoint = 0xFF0000;
263     protected uint _colorIconBookmark = 0x0000FF;
264     protected uint _colorIconError = 0x80FF0000;
265 
266     protected uint _caretColor = 0x000000;
267     protected uint _caretColorReplace = 0x808080FF;
268     protected uint _matchingBracketHightlightColor = 0x60FFE0B0;
269 
270     protected uint _iconsPaneWidth = BACKEND_CONSOLE ? 1 : 16;
271     protected uint _foldingPaneWidth = BACKEND_CONSOLE ? 1 : 12;
272     protected uint _modificationMarksPaneWidth = BACKEND_CONSOLE ? 1 : 4;
273     /// when true, call measureVisibileText on next layout
274     protected bool _contentChanged = true;
275 
276     protected bool _copyCurrentLineWhenNoSelection = true;
277     /// when true allows copy / cut whole current line if there is no selection
278     @property bool copyCurrentLineWhenNoSelection() { return _copyCurrentLineWhenNoSelection; }
279     @property EditWidgetBase copyCurrentLineWhenNoSelection(bool flg) { _copyCurrentLineWhenNoSelection = flg; return this; }
280 
281     protected bool _showTabPositionMarks = false;
282     /// when true shows mark on tab positions in beginning of line
283     @property bool showTabPositionMarks() { return _showTabPositionMarks; }
284     @property EditWidgetBase showTabPositionMarks(bool flg) {
285         if (flg != _showTabPositionMarks) {
286             _showTabPositionMarks = flg;
287             invalidate();
288         }
289         return this;
290     }
291 
292     /// Modified state change listener (e.g. content has been saved, or first time modified after save)
293     Signal!ModifiedStateListener modifiedStateChange;
294 
295     /// Signal to emit when editor content is changed
296     Signal!EditableContentChangeListener contentChange;
297 
298     /// override to support modification of client rect after change, e.g. apply offset
299     override protected void handleClientRectLayout(ref Rect rc) {
300         updateLeftPaneWidth();
301         rc.left += _leftPaneWidth;
302     }
303 
304     /// override for multiline editors
305     protected int lineCount() {
306         return 1;
307     }
308 
309     protected bool _wordWrap;
310     /// true if word wrap mode is set
311     @property bool wordWrap() {
312         return _wordWrap;
313     }
314     /// true if word wrap mode is set
315     @property EditWidgetBase wordWrap(bool v) {
316         _wordWrap = v;
317         invalidate();
318         return this;
319     }
320 
321     void wrapLine(dstring line, int maxWidth) {
322         
323     }
324 
325     /// information about line span into several lines - in word wrap mode
326     protected LineSpan[] _span;
327 
328     /// override to add custom items on left panel
329     protected void updateLeftPaneWidth() {
330         import std.conv : to;
331         _iconsWidth = _showIcons ? _iconsPaneWidth : 0;
332         _foldingWidth = _showFolding ? _foldingPaneWidth : 0;
333         _modificationMarksWidth = _showModificationMarks && (BACKEND_GUI || !_showLineNumbers) ? _modificationMarksPaneWidth : 0;
334         _lineNumbersWidth = 0;
335         if (_showLineNumbers) {
336             dchar[] s = to!(dchar[])(lineCount + 1);
337             foreach(ref ch; s)
338                 ch = '9';
339             FontRef fnt = font;
340             Point sz = fnt.textSize(s);
341             _lineNumbersWidth = sz.x;
342         }
343         _leftPaneWidth = _lineNumbersWidth + _modificationMarksWidth + _foldingWidth + _iconsWidth;
344         if (_leftPaneWidth)
345             _leftPaneWidth += BACKEND_CONSOLE ? 1 : 3;
346     }
347 
348     protected void drawLeftPaneFolding(DrawBuf buf, Rect rc, int line) {
349         buf.fillRect(rc, _leftPaneBackgroundColor2);
350     }
351 
352     protected void drawLeftPaneIcon(DrawBuf buf, Rect rc, LineIcon icon) {
353         if (!icon)
354             return;
355         if (icon.type == LineIconType.error) {
356             buf.fillRect(rc, _colorIconError);
357         } else if (icon.type == LineIconType.bookmark) {
358             int dh = rc.height / 4;
359             rc.top += dh;
360             rc.bottom -= dh;
361             buf.fillRect(rc, _colorIconBookmark);
362         } else if (icon.type == LineIconType.breakpoint) {
363             int dh = rc.height / 8;
364             rc.top += dh;
365             rc.bottom -= dh;
366             int dw = rc.width / 4;
367             rc.left += dw;
368             rc.right -= dw;
369             buf.fillRect(rc, _colorIconBreakpoint);
370         }
371     }
372 
373     protected void drawLeftPaneIcons(DrawBuf buf, Rect rc, int line) {
374         buf.fillRect(rc, _leftPaneBackgroundColor3);
375         drawLeftPaneIcon(buf, rc, content.lineIcons.findByLineAndType(line, LineIconType.error));
376         drawLeftPaneIcon(buf, rc, content.lineIcons.findByLineAndType(line, LineIconType.bookmark));
377         drawLeftPaneIcon(buf, rc, content.lineIcons.findByLineAndType(line, LineIconType.breakpoint));
378     }
379 
380     protected void drawLeftPaneModificationMarks(DrawBuf buf, Rect rc, int line) {
381         if (line >= 0 && line < content.length) {
382             EditStateMark m = content.editMark(line);
383             if (m == EditStateMark.changed) {
384                 // modified, not saved
385                 buf.fillRect(rc, 0xFFD040);
386             } else if (m == EditStateMark.saved) {
387                 // modified, saved
388                 buf.fillRect(rc, 0x20C020);
389             }
390         }
391     }
392 
393     protected void drawLeftPaneLineNumbers(DrawBuf buf, Rect rc, int line) {
394         import std.conv : to;
395         uint bgcolor = _leftPaneLineNumberBackgroundColor;
396         if (line == _caretPos.line && !isFullyTransparentColor(_leftPaneLineNumberBackgroundColorCurrentLine))
397             bgcolor = _leftPaneLineNumberBackgroundColorCurrentLine;
398         buf.fillRect(rc, bgcolor);
399         if (line < 0)
400             return;
401         dstring s = to!dstring(line + 1);
402         FontRef fnt = font;
403         Point sz = fnt.textSize(s);
404         int x = rc.right - sz.x;
405         int y = rc.top + (rc.height - sz.y) / 2;
406         uint color = _leftPaneLineNumberColor;
407         if (line == _caretPos.line && !isFullyTransparentColor(_leftPaneLineNumberColorCurrentLine))
408             color = _leftPaneLineNumberColorCurrentLine;
409         if (line >= 0 && line < content.length) {
410             EditStateMark m = content.editMark(line);
411             if (m == EditStateMark.changed) {
412                 // modified, not saved
413                 color = _leftPaneLineNumberColorEdited;
414             } else if (m == EditStateMark.saved) {
415                 // modified, saved
416                 color = _leftPaneLineNumberColorSaved;
417             }
418         }
419         fnt.drawText(buf, x, y, s, color);
420     }
421 
422     protected bool onLeftPaneMouseClick(MouseEvent event) {
423         return false;
424     }
425 
426     protected bool handleLeftPaneFoldingMouseClick(MouseEvent event, Rect rc, int line) {
427         return true;
428     }
429 
430     protected bool handleLeftPaneModificationMarksMouseClick(MouseEvent event, Rect rc, int line) {
431         return true;
432     }
433 
434     protected bool handleLeftPaneLineNumbersMouseClick(MouseEvent event, Rect rc, int line) {
435         return true;
436     }
437 
438     protected MenuItem getLeftPaneIconsPopupMenu(int line) {
439         return null;
440     }
441 
442     protected bool handleLeftPaneIconsMouseClick(MouseEvent event, Rect rc, int line) {
443         if (event.button == MouseButton.Right) {
444             MenuItem menu = getLeftPaneIconsPopupMenu(line);
445             if (menu) {
446                 if (menu.openingSubmenu.assigned)
447                     if (!menu.openingSubmenu(_popupMenu))
448                         return true;
449                 menu.updateActionState(this);
450                 PopupMenu popupMenu = new PopupMenu(menu);
451                 popupMenu.menuItemAction = this;
452                 PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, event.x, event.y);
453                 popup.flags = PopupFlags.CloseOnClickOutside;
454             }
455             return true;
456         }
457         return true;
458     }
459 
460     protected bool handleLeftPaneMouseClick(MouseEvent event, Rect rc, int line) {
461         rc.right -= 3;
462         if (_foldingWidth) {
463             Rect rc2 = rc;
464             rc.right = rc2.left = rc2.right - _foldingWidth;
465             if (event.x >= rc2.left && event.x < rc2.right)
466                 return handleLeftPaneFoldingMouseClick(event, rc2, line);
467         }
468         if (_modificationMarksWidth) {
469             Rect rc2 = rc;
470             rc.right = rc2.left = rc2.right - _modificationMarksWidth;
471             if (event.x >= rc2.left && event.x < rc2.right)
472                 return handleLeftPaneModificationMarksMouseClick(event, rc2, line);
473         }
474         if (_lineNumbersWidth) {
475             Rect rc2 = rc;
476             rc.right = rc2.left = rc2.right - _lineNumbersWidth;
477             if (event.x >= rc2.left && event.x < rc2.right)
478                 return handleLeftPaneLineNumbersMouseClick(event, rc2, line);
479         }
480         if (_iconsWidth) {
481             Rect rc2 = rc;
482             rc.right = rc2.left = rc2.right - _iconsWidth;
483             if (event.x >= rc2.left && event.x < rc2.right)
484                 return handleLeftPaneIconsMouseClick(event, rc2, line);
485         }
486         return true;
487     }
488 
489     protected void drawLeftPane(DrawBuf buf, Rect rc, int line) {
490         // override for custom drawn left pane
491         buf.fillRect(rc, _leftPaneBackgroundColor);
492         //buf.fillRect(Rect(rc.right - 2, rc.top, rc.right - 1, rc.bottom), _leftPaneBackgroundColor2);
493         //buf.fillRect(Rect(rc.right - 1, rc.top, rc.right - 0, rc.bottom), _leftPaneBackgroundColor3);
494         rc.right -= BACKEND_CONSOLE ? 1 : 3;
495         if (_foldingWidth) {
496             Rect rc2 = rc;
497             rc.right = rc2.left = rc2.right - _foldingWidth;
498             drawLeftPaneFolding(buf, rc2, line);
499         }
500         if (_modificationMarksWidth) {
501             Rect rc2 = rc;
502             rc.right = rc2.left = rc2.right - _modificationMarksWidth;
503             drawLeftPaneModificationMarks(buf, rc2, line);
504         }
505         if (_lineNumbersWidth) {
506             Rect rc2 = rc;
507             rc.right = rc2.left = rc2.right - _lineNumbersWidth;
508             drawLeftPaneLineNumbers(buf, rc2, line);
509         }
510         if (_iconsWidth) {
511             Rect rc2 = rc;
512             rc.right = rc2.left = rc2.right - _iconsWidth;
513             drawLeftPaneIcons(buf, rc2, line);
514         }
515     }
516 
517     this(string ID, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) {
518         super(ID, hscrollbarMode, vscrollbarMode);
519         focusable = true;
520         acceleratorMap.add( [
521             new Action(EditorActions.Up, KeyCode.UP, 0),
522             new Action(EditorActions.SelectUp, KeyCode.UP, KeyFlag.Shift),
523             new Action(EditorActions.SelectUp, KeyCode.UP, KeyFlag.Control | KeyFlag.Shift),
524             new Action(EditorActions.Down, KeyCode.DOWN, 0),
525             new Action(EditorActions.SelectDown, KeyCode.DOWN, KeyFlag.Shift),
526             new Action(EditorActions.SelectDown, KeyCode.DOWN, KeyFlag.Control | KeyFlag.Shift),
527             new Action(EditorActions.Left, KeyCode.LEFT, 0),
528             new Action(EditorActions.SelectLeft, KeyCode.LEFT, KeyFlag.Shift),
529             new Action(EditorActions.Right, KeyCode.RIGHT, 0),
530             new Action(EditorActions.SelectRight, KeyCode.RIGHT, KeyFlag.Shift),
531             new Action(EditorActions.WordLeft, KeyCode.LEFT, KeyFlag.Control),
532             new Action(EditorActions.SelectWordLeft, KeyCode.LEFT, KeyFlag.Control | KeyFlag.Shift),
533             new Action(EditorActions.WordRight, KeyCode.RIGHT, KeyFlag.Control),
534             new Action(EditorActions.SelectWordRight, KeyCode.RIGHT, KeyFlag.Control | KeyFlag.Shift),
535             new Action(EditorActions.PageUp, KeyCode.PAGEUP, 0),
536             new Action(EditorActions.SelectPageUp, KeyCode.PAGEUP, KeyFlag.Shift),
537             new Action(EditorActions.PageDown, KeyCode.PAGEDOWN, 0),
538             new Action(EditorActions.SelectPageDown, KeyCode.PAGEDOWN, KeyFlag.Shift),
539             new Action(EditorActions.PageBegin, KeyCode.PAGEUP, KeyFlag.Control),
540             new Action(EditorActions.SelectPageBegin, KeyCode.PAGEUP, KeyFlag.Control | KeyFlag.Shift),
541             new Action(EditorActions.PageEnd, KeyCode.PAGEDOWN, KeyFlag.Control),
542             new Action(EditorActions.SelectPageEnd, KeyCode.PAGEDOWN, KeyFlag.Control | KeyFlag.Shift),
543             new Action(EditorActions.LineBegin, KeyCode.HOME, 0),
544             new Action(EditorActions.SelectLineBegin, KeyCode.HOME, KeyFlag.Shift),
545             new Action(EditorActions.LineEnd, KeyCode.END, 0),
546             new Action(EditorActions.SelectLineEnd, KeyCode.END, KeyFlag.Shift),
547             new Action(EditorActions.DocumentBegin, KeyCode.HOME, KeyFlag.Control),
548             new Action(EditorActions.SelectDocumentBegin, KeyCode.HOME, KeyFlag.Control | KeyFlag.Shift),
549             new Action(EditorActions.DocumentEnd, KeyCode.END, KeyFlag.Control),
550             new Action(EditorActions.SelectDocumentEnd, KeyCode.END, KeyFlag.Control | KeyFlag.Shift),
551 
552             new Action(EditorActions.ScrollLineUp, KeyCode.UP, KeyFlag.Control),
553             new Action(EditorActions.ScrollLineDown, KeyCode.DOWN, KeyFlag.Control),
554 
555             // Backspace/Del
556             new Action(EditorActions.DelPrevChar, KeyCode.BACK, 0),
557             new Action(EditorActions.DelNextChar, KeyCode.DEL, 0),
558             new Action(EditorActions.DelPrevWord, KeyCode.BACK, KeyFlag.Control),
559             new Action(EditorActions.DelNextWord, KeyCode.DEL, KeyFlag.Control),
560 
561             // Copy/Paste
562             new Action(EditorActions.Copy, KeyCode.KEY_C, KeyFlag.Control),
563             new Action(EditorActions.Copy, KeyCode.KEY_C, KeyFlag.Control|KeyFlag.Shift),
564             new Action(EditorActions.Copy, KeyCode.INS, KeyFlag.Control),
565             new Action(EditorActions.Cut, KeyCode.KEY_X, KeyFlag.Control),
566             new Action(EditorActions.Cut, KeyCode.KEY_X, KeyFlag.Control|KeyFlag.Shift),
567             new Action(EditorActions.Cut, KeyCode.DEL, KeyFlag.Shift),
568             new Action(EditorActions.Paste, KeyCode.KEY_V, KeyFlag.Control),
569             new Action(EditorActions.Paste, KeyCode.KEY_V, KeyFlag.Control|KeyFlag.Shift),
570             new Action(EditorActions.Paste, KeyCode.INS, KeyFlag.Shift),
571 
572             // Undo/Redo
573             new Action(EditorActions.Undo, KeyCode.KEY_Z, KeyFlag.Control),
574             new Action(EditorActions.Redo, KeyCode.KEY_Y, KeyFlag.Control),
575             new Action(EditorActions.Redo, KeyCode.KEY_Z, KeyFlag.Control|KeyFlag.Shift),
576 
577             new Action(EditorActions.Tab, KeyCode.TAB, 0),
578             new Action(EditorActions.BackTab, KeyCode.TAB, KeyFlag.Shift),
579 
580             new Action(EditorActions.Find, KeyCode.KEY_F, KeyFlag.Control),
581             new Action(EditorActions.Replace, KeyCode.KEY_H, KeyFlag.Control),
582         ]);
583         acceleratorMap.add(STD_EDITOR_ACTIONS);
584     }
585 
586     protected MenuItem _popupMenu;
587     @property MenuItem popupMenu() { return _popupMenu; }
588     @property EditWidgetBase popupMenu(MenuItem popupMenu) {
589         _popupMenu = popupMenu;
590         return this;
591     }
592 
593     /// 
594     override bool onMenuItemAction(const Action action) {
595         return dispatchAction(action);
596     }
597 
598     /// returns true if widget can show popup (e.g. by mouse right click at point x,y)
599     override bool canShowPopupMenu(int x, int y) {
600         if (_popupMenu is null)
601             return false;
602         if (_popupMenu.openingSubmenu.assigned)
603             if (!_popupMenu.openingSubmenu(_popupMenu))
604                 return false;
605         return true;
606     }
607 
608     /// returns true if widget is focusable and visible and enabled
609     override @property bool canFocus() {
610         // allow to focus even if not enabled
611         return focusable && visible;
612     }
613 
614     /// override to change popup menu items state
615     override bool isActionEnabled(const Action action) {
616         switch (action.id) with(EditorActions)
617         {
618             case Tab:
619             case BackTab:
620             case Indent:
621             case Unindent:
622                 return enabled;
623             case Copy:
624                 return _copyCurrentLineWhenNoSelection || !_selectionRange.empty;
625             case Cut:
626                 return enabled && (_copyCurrentLineWhenNoSelection || !_selectionRange.empty);
627             case Paste:
628                 return enabled && Platform.instance.getClipboardText().length > 0;
629             case Undo:
630                 return enabled && _content.hasUndo;
631             case Redo:
632                 return enabled && _content.hasRedo;
633             case ToggleBookmark:
634                 return _content.multiline;
635             case GoToNextBookmark:
636                 return _content.multiline && _content.lineIcons.hasBookmarks;
637             case GoToPreviousBookmark:
638                 return _content.multiline && _content.lineIcons.hasBookmarks;
639             case Replace:
640                 return _content.multiline && !readOnly;
641             case Find:
642                 return true;
643             default:
644                 return super.isActionEnabled(action);
645         }
646     }
647 
648     /// shows popup at (x,y)
649     override void showPopupMenu(int x, int y) {
650         /// if preparation signal handler assigned, call it; don't show popup if false is returned from handler
651         if (_popupMenu.openingSubmenu.assigned)
652             if (!_popupMenu.openingSubmenu(_popupMenu))
653                 return;
654         _popupMenu.updateActionState(this);
655         PopupMenu popupMenu = new PopupMenu(_popupMenu);
656         popupMenu.menuItemAction = this;
657         PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, x, y);
658         popup.flags = PopupFlags.CloseOnClickOutside;
659     }
660 
661     void onPopupMenuItem(MenuItem item) {
662         // TODO
663     }
664 
665     /// returns mouse cursor type for widget
666     override uint getCursorType(int x, int y) {
667         return x < _pos.left + _leftPaneWidth ? CursorType.Arrow : CursorType.IBeam;
668     }
669 
670     /// set bool property value, for ML loaders
671     mixin(generatePropertySettersMethodOverride("setBoolProperty", "bool",
672           "wantTabs", "showIcons", "showFolding", "showModificationMarks", "showLineNumbers", "readOnly", "replaceMode", "useSpacesForTabs", "copyCurrentLineWhenNoSelection", "showTabPositionMarks"));
673 
674     /// set int property value, for ML loaders
675     mixin(generatePropertySettersMethodOverride("setIntProperty", "int",
676           "tabSize"));
677 
678     /// when true, Tab / Shift+Tab presses are processed internally in widget (e.g. insert tab character) instead of focus change navigation.
679     @property bool wantTabs() {
680         return _wantTabs;
681     }
682 
683     /// sets tab size (in number of spaces)
684     @property EditWidgetBase wantTabs(bool wantTabs) {
685         _wantTabs = wantTabs;
686         return this;
687     }
688 
689     /// when true, show icons like bookmarks or breakpoints at the left
690     @property bool showIcons() {
691         return _showIcons;
692     }
693 
694     /// when true, show icons like bookmarks or breakpoints at the left
695     @property EditWidgetBase showIcons(bool flg) {
696         if (_showIcons != flg) {
697             _showIcons = flg;
698             updateLeftPaneWidth();
699             requestLayout();
700         }
701         return this;
702     }
703 
704     /// when true, show folding controls at the left
705     @property bool showFolding() {
706         return _showFolding;
707     }
708 
709     /// when true, show folding controls at the left
710     @property EditWidgetBase showFolding(bool flg) {
711         if (_showFolding != flg) {
712             _showFolding = flg;
713             updateLeftPaneWidth();
714             requestLayout();
715         }
716         return this;
717     }
718 
719     /// when true, show modification marks for lines (whether line is unchanged/modified/modified_saved)
720     @property bool showModificationMarks() {
721         return _showModificationMarks;
722     }
723 
724     /// when true, show modification marks for lines (whether line is unchanged/modified/modified_saved)
725     @property EditWidgetBase showModificationMarks(bool flg) {
726         if (_showModificationMarks != flg) {
727             _showModificationMarks = flg;
728             updateLeftPaneWidth();
729             requestLayout();
730         }
731         return this;
732     }
733 
734     /// when true, line numbers are shown
735     @property bool showLineNumbers() {
736         return _showLineNumbers;
737     }
738 
739     /// when true, line numbers are shown
740     @property EditWidgetBase showLineNumbers(bool flg) {
741         if (_showLineNumbers != flg) {
742             _showLineNumbers = flg;
743             updateLeftPaneWidth();
744             requestLayout();
745         }
746         return this;
747     }
748     
749     /// readonly flag (when true, user cannot change content of editor)
750     @property bool readOnly() {
751         return !enabled || _content.readOnly;
752     }
753 
754     /// sets readonly flag
755     @property EditWidgetBase readOnly(bool readOnly) {
756         enabled = !readOnly;
757         invalidate();
758         return this;
759     }
760 
761     /// replace mode flag (when true, entered character replaces character under cursor)
762     @property bool replaceMode() {
763         return _replaceMode;
764     }
765 
766     /// sets replace mode flag
767     @property EditWidgetBase replaceMode(bool replaceMode) {
768         _replaceMode = replaceMode;
769         invalidate();
770         return this;
771     }
772 
773     /// when true, spaces will be inserted instead of tabs
774     @property bool useSpacesForTabs() {
775         return _content.useSpacesForTabs;
776     }
777 
778     /// set new Tab key behavior flag: when true, spaces will be inserted instead of tabs
779     @property EditWidgetBase useSpacesForTabs(bool useSpacesForTabs) {
780         _content.useSpacesForTabs = useSpacesForTabs;
781         return this;
782     }
783 
784     /// returns tab size (in number of spaces)
785     @property int tabSize() {
786         return _content.tabSize;
787     }
788 
789     /// sets tab size (in number of spaces)
790     @property EditWidgetBase tabSize(int newTabSize) {
791         if (newTabSize < 1)
792             newTabSize = 1;
793         else if (newTabSize > 16)
794             newTabSize = 16;
795         if (newTabSize != tabSize) {
796             _content.tabSize = newTabSize;
797             requestLayout();
798         }
799         return this;
800     }
801 
802     /// true if smart indents are supported
803     @property bool supportsSmartIndents() { return _content.supportsSmartIndents; }
804     /// true if smart indents are enabled
805     @property bool smartIndents() { return _content.smartIndents; }
806     /// set smart indents enabled flag
807     @property EditWidgetBase smartIndents(bool enabled) { _content.smartIndents = enabled; return this; }
808 
809     /// true if smart indents are enabled
810     @property bool smartIndentsAfterPaste() { return _content.smartIndentsAfterPaste; }
811     /// set smart indents enabled flag
812     @property EditWidgetBase smartIndentsAfterPaste(bool enabled) { _content.smartIndentsAfterPaste = enabled; return this; }
813 
814 
815     /// editor content object
816     @property EditableContent content() {
817         return _content;
818     }
819 
820     /// when _ownContent is false, _content should not be destroyed in editor destructor
821     protected bool _ownContent = true;
822     /// set content object
823     @property EditWidgetBase content(EditableContent content) {
824         if (_content is content)
825             return this; // not changed
826         if (_content !is null) {
827             // disconnect old content
828             _content.contentChanged.disconnect(this);
829             if (_ownContent) {
830                 destroy(_content);
831             }
832         }
833         _content = content;
834         _ownContent = false;
835         _content.contentChanged.connect(this);
836         if (_content.readOnly)
837             enabled = false;
838         return this;
839     }
840 
841     /// free resources
842     ~this() {
843         if (_ownContent) {
844             destroy(_content);
845             _content = null;
846         }
847     }
848 
849     protected void updateMaxLineWidth() {
850     }
851 
852     protected void processSmartIndent(EditOperation operation) {
853         if (!supportsSmartIndents)
854             return;
855         if (!smartIndents && !smartIndentsAfterPaste)
856             return;
857         _content.syntaxSupport.applySmartIndent(operation, this);
858     }
859 
860     override void onContentChange(EditableContent content, EditOperation operation, ref TextRange rangeBefore, ref TextRange rangeAfter, Object source) {
861         //Log.d("onContentChange rangeBefore=", rangeBefore, " rangeAfter=", rangeAfter, " text=", operation.content);
862         _contentChanged = true;
863         if (source is this) {
864             if (operation.action == EditAction.ReplaceContent) {
865                 // fully replaced, e.g., loaded from file or text property is assigned
866                 _caretPos = rangeAfter.end;
867                 _selectionRange.start = _caretPos;
868                 _selectionRange.end = _caretPos;
869                 updateMaxLineWidth();
870                 measureVisibleText();
871                 ensureCaretVisible();
872                 correctCaretPos();
873                 requestLayout();
874                 requestActionsUpdate();
875             } else if (operation.action == EditAction.SaveContent) {
876                 // saved
877             } else {
878                 // modified
879                 _caretPos = rangeAfter.end;
880                 _selectionRange.start = _caretPos;
881                 _selectionRange.end = _caretPos;
882                 updateMaxLineWidth();
883                 measureVisibleText();
884                 ensureCaretVisible();
885                 requestActionsUpdate();
886                 processSmartIndent(operation);
887             }
888         } else {
889             updateMaxLineWidth();
890             measureVisibleText();
891             correctCaretPos();
892             requestLayout();
893             requestActionsUpdate();
894         }
895         invalidate();
896         if (modifiedStateChange.assigned) {
897             if (_lastReportedModifiedState != content.modified) {
898                 _lastReportedModifiedState = content.modified;
899                 modifiedStateChange(this, content.modified);
900                 requestActionsUpdate();
901             }
902         }
903         if (contentChange.assigned) {
904             contentChange(_content);
905         }
906         return;
907     }
908     protected bool _lastReportedModifiedState;
909 
910     /// get widget text
911     override @property dstring text() { return _content.text; }
912 
913     /// set text
914     override @property Widget text(dstring s) { 
915         _content.text = s;
916         requestLayout();
917         return this;
918     }
919 
920     /// set text
921     override @property Widget text(UIString s) { 
922         _content.text = s;
923         requestLayout();
924         return this;
925     }
926 
927     protected TextPosition _caretPos;
928     protected TextRange _selectionRange;
929 
930     abstract protected Rect textPosToClient(TextPosition p);
931 
932     abstract protected TextPosition clientToTextPos(Point pt);
933 
934     abstract protected void ensureCaretVisible(bool center = false);
935 
936     abstract protected Point measureVisibleText();
937 
938     protected int _caretBlingingInterval = 800;
939     protected ulong _caretTimerId;
940     protected bool _caretBlinkingPhase;
941     protected long _lastBlinkStartTs;
942 
943     protected void startCaretBlinking() {
944         if (window) {
945             static if (BACKEND_CONSOLE) {
946                 window.caretRect = caretRect;
947                 window.caretReplace = _replaceMode;
948             } else {
949                 long ts = currentTimeMillis;
950                 if (_caretTimerId) {
951                     if (_lastBlinkStartTs + _caretBlingingInterval / 4 > ts)
952                         return; // don't update timer too frequently
953                     cancelTimer(_caretTimerId);
954                 }
955                 _caretTimerId = setTimer(_caretBlingingInterval / 2);
956                 _lastBlinkStartTs = ts;
957                 _caretBlinkingPhase = false;
958                 invalidate();
959             }
960         }
961     }
962 
963     protected void stopCaretBlinking() {
964         if (window) {
965             static if (BACKEND_CONSOLE) {
966                 window.caretRect = Rect.init;
967             } else {
968                 if (_caretTimerId) {
969                     cancelTimer(_caretTimerId);
970                     _caretTimerId = 0;
971                 }
972             }
973         }
974     }
975 
976     /// handle timer; return true to repeat timer event after next interval, false cancel timer
977     override bool onTimer(ulong id) {
978         if (id == _caretTimerId) {
979             _caretBlinkingPhase = !_caretBlinkingPhase;
980             if (!_caretBlinkingPhase)
981                 _lastBlinkStartTs = currentTimeMillis;
982             invalidate();
983             //window.update(true);
984             bool res = focused;
985             if (!res)
986                 _caretTimerId = 0;
987             return res;
988         }
989         if (id == _hoverTimer) {
990             cancelHoverTimer();
991             onHoverTimeout(_hoverMousePosition, _hoverTextPosition);
992             return false;
993         }
994         return super.onTimer(id);
995     }
996 
997     /// override to handle focus changes
998     override protected void handleFocusChange(bool focused, bool receivedFocusFromKeyboard = false) {
999         if (focused)
1000             startCaretBlinking();
1001         else {
1002             stopCaretBlinking();
1003             cancelHoverTimer();
1004 
1005             if(_deselectAllWhenUnfocused) {
1006                 _selectionRange.start = _caretPos;
1007                 _selectionRange.end = _caretPos;
1008             }
1009         }
1010         if(focused && _selectAllWhenFocusedWithTab && receivedFocusFromKeyboard)
1011             handleAction(ACTION_EDITOR_SELECT_ALL);
1012         super.handleFocusChange(focused);
1013     }
1014 
1015     /// returns cursor rectangle
1016     protected Rect caretRect() {
1017         Rect caretRc = textPosToClient(_caretPos);
1018         if (_replaceMode) {
1019             dstring s = _content[_caretPos.line];
1020             if (_caretPos.pos < s.length) {
1021                 TextPosition nextPos = _caretPos;
1022                 nextPos.pos++;
1023                 Rect nextRect = textPosToClient(nextPos);
1024                 caretRc.right = nextRect.right;
1025             } else {
1026                 caretRc.right += _spaceWidth;
1027             }
1028         }
1029         caretRc.offset(_clientRect.left, _clientRect.top);
1030         return caretRc;
1031     }
1032 
1033     /// handle theme change: e.g. reload some themed resources
1034     override void onThemeChanged() {
1035         super.onThemeChanged();
1036         _caretColor = style.customColor("edit_caret");
1037         _caretColorReplace = style.customColor("edit_caret_replace");
1038         _selectionColorFocused = style.customColor("editor_selection_focused");
1039         _selectionColorNormal = style.customColor("editor_selection_normal");
1040         _leftPaneBackgroundColor = style.customColor("editor_left_pane_background");
1041         _leftPaneBackgroundColor2 = style.customColor("editor_left_pane_background2");
1042         _leftPaneBackgroundColor3 = style.customColor("editor_left_pane_background3");
1043         _leftPaneLineNumberColor = style.customColor("editor_left_pane_line_number_text");
1044         _leftPaneLineNumberColorEdited = style.customColor("editor_left_pane_line_number_text_edited", 0xC0C000);
1045         _leftPaneLineNumberColorSaved = style.customColor("editor_left_pane_line_number_text_saved", 0x00C000);
1046         _leftPaneLineNumberColorCurrentLine = style.customColor("editor_left_pane_line_number_text_current_line", 0xFFFFFFFF);
1047         _leftPaneLineNumberBackgroundColorCurrentLine = style.customColor("editor_left_pane_line_number_background_current_line", 0xC08080FF);
1048         _leftPaneLineNumberBackgroundColor = style.customColor("editor_left_pane_line_number_background");
1049         _colorIconBreakpoint = style.customColor("editor_left_pane_line_icon_color_breakpoint", 0xFF0000);
1050         _colorIconBookmark = style.customColor("editor_left_pane_line_icon_color_bookmark", 0x0000FF);
1051         _colorIconError = style.customColor("editor_left_pane_line_icon_color_error", 0x80FF0000);
1052         _matchingBracketHightlightColor = style.customColor("editor_matching_bracket_highlight");
1053     }
1054 
1055     /// draws caret
1056     protected void drawCaret(DrawBuf buf) {
1057         if (focused) {
1058             if (_caretBlinkingPhase) {
1059                 return;
1060             }
1061             // draw caret
1062             Rect caretRc = caretRect();
1063             if (caretRc.intersects(_clientRect)) {
1064                 //caretRc.left++;
1065                 if (_replaceMode && BACKEND_GUI)
1066                     buf.fillRect(caretRc, _caretColorReplace);
1067                 //buf.drawLine(Point(caretRc.left, caretRc.bottom), Point(caretRc.left, caretRc.top), _caretColor);
1068                 buf.fillRect(Rect(caretRc.left, caretRc.top, caretRc.left + 1, caretRc.bottom), _caretColor);
1069             }
1070         }
1071     }
1072 
1073     protected void updateFontProps() {
1074         FontRef font = font();
1075         _fixedFont = font.isFixed;
1076         _spaceWidth = font.spaceWidth;
1077         _lineHeight = font.height;
1078     }
1079 
1080     /// when cursor position or selection is out of content bounds, fix it to nearest valid position
1081     protected void correctCaretPos() {
1082         _content.correctPosition(_caretPos);
1083         _content.correctPosition(_selectionRange.start);
1084         _content.correctPosition(_selectionRange.end);
1085         if (_selectionRange.empty)
1086             _selectionRange = TextRange(_caretPos, _caretPos);
1087     }
1088 
1089 
1090     private int[] _lineWidthBuf;
1091     protected int calcLineWidth(dstring s) {
1092         int w = 0;
1093         if (_fixedFont) {
1094             int tabw = tabSize * _spaceWidth;
1095             // version optimized for fixed font
1096             for (int i = 0; i < s.length; i++) {
1097                 if (s[i] == '\t') {
1098                     w += _spaceWidth;
1099                     w = (w + tabw - 1) / tabw * tabw;
1100                 } else {
1101                     w += _spaceWidth;
1102                 }
1103             }
1104         } else {
1105             // variable pitch font
1106             if (_lineWidthBuf.length < s.length)
1107                 _lineWidthBuf.length = s.length;
1108             int charsMeasured = font.measureText(s, _lineWidthBuf, int.max);
1109             if (charsMeasured > 0)
1110                 w = _lineWidthBuf[charsMeasured - 1];
1111         }
1112         return w;
1113     }
1114 
1115     protected void updateSelectionAfterCursorMovement(TextPosition oldCaretPos, bool selecting) {
1116         if (selecting) {
1117             if (oldCaretPos == _selectionRange.start) {
1118                 if (_caretPos >= _selectionRange.end) {
1119                     _selectionRange.start = _selectionRange.end;
1120                     _selectionRange.end = _caretPos;
1121                 } else {
1122                     _selectionRange.start = _caretPos;
1123                 }
1124             } else if (oldCaretPos == _selectionRange.end) {
1125                 if (_caretPos < _selectionRange.start) {
1126                     _selectionRange.end = _selectionRange.start;
1127                     _selectionRange.start = _caretPos;
1128                 } else {
1129                     _selectionRange.end = _caretPos;
1130                 }
1131             } else {
1132                 if (oldCaretPos < _caretPos) {
1133                     // start selection forward
1134                     _selectionRange.start = oldCaretPos;
1135                     _selectionRange.end = _caretPos;
1136                 } else {
1137                     // start selection backward
1138                     _selectionRange.start = _caretPos;
1139                     _selectionRange.end = oldCaretPos;
1140                 }
1141             }
1142         } else {
1143             _selectionRange.start = _caretPos;
1144             _selectionRange.end = _caretPos;
1145         }
1146         invalidate();
1147         requestActionsUpdate();
1148     }
1149 
1150     protected void selectWordByMouse(int x, int y) {
1151         TextPosition oldCaretPos = _caretPos;
1152         TextPosition newPos = clientToTextPos(Point(x,y));
1153         TextRange r = content.wordBounds(newPos);
1154         if (r.start < r.end) {
1155             _selectionRange = r;
1156             _caretPos = r.end;
1157             invalidate();
1158             requestActionsUpdate();
1159         } else {
1160             _caretPos = newPos;
1161             updateSelectionAfterCursorMovement(oldCaretPos, false);
1162         }
1163     }
1164 
1165     protected void selectLineByMouse(int x, int y, bool onSameLineOnly = true) {
1166         TextPosition oldCaretPos = _caretPos;
1167         TextPosition newPos = clientToTextPos(Point(x,y));
1168         if (onSameLineOnly && newPos.line != oldCaretPos.line)
1169             return; // different lines
1170         TextRange r = content.lineRange(newPos.line);
1171         if (r.start < r.end) {
1172             _selectionRange = r;
1173             _caretPos = r.end;
1174             invalidate();
1175             requestActionsUpdate();
1176         } else {
1177             _caretPos = newPos;
1178             updateSelectionAfterCursorMovement(oldCaretPos, false);
1179         }
1180     }
1181 
1182     protected void updateCaretPositionByMouse(int x, int y, bool selecting) {
1183         TextPosition oldCaretPos = _caretPos;
1184         TextPosition newPos = clientToTextPos(Point(x,y));
1185         if (newPos != _caretPos) {
1186             _caretPos = newPos;
1187             updateSelectionAfterCursorMovement(oldCaretPos, selecting);
1188             invalidate();
1189         }
1190     }
1191 
1192     /// generate string of spaces, to reach next tab position
1193     protected dstring spacesForTab(int currentPos) {
1194         int newPos = (currentPos + tabSize + 1) / tabSize * tabSize;
1195         return "                "d[0..(newPos - currentPos)];
1196     }
1197 
1198     /// returns true if one or more lines selected fully
1199     protected bool multipleLinesSelected() {
1200         return _selectionRange.end.line > _selectionRange.start.line;
1201     }
1202 
1203     protected bool _camelCasePartsAsWords = true;
1204 
1205     protected bool removeSelectionTextIfSelected() {
1206         if (_selectionRange.empty)
1207             return false;
1208         // clear selection
1209         EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d]);
1210         _content.performOperation(op, this);
1211         ensureCaretVisible();
1212         return true;
1213     }
1214 
1215     /// returns current selection text (joined with LF when span over multiple lines)
1216     public dstring getSelectedText() {
1217         return getRangeText(_selectionRange);
1218     }
1219     
1220     /// returns text for specified range (joined with LF when span over multiple lines)
1221     public dstring getRangeText(TextRange range) {
1222         dstring selectionText = concatDStrings(_content.rangeText(range));
1223         return selectionText;
1224     }
1225 
1226     /// returns range for line with cursor
1227     @property public TextRange currentLineRange() {
1228         return _content.lineRange(_caretPos.line);
1229     }
1230 
1231     protected bool removeRangeText(TextRange range) {
1232         if (range.empty)
1233             return false;
1234         _selectionRange = range;
1235         _caretPos = _selectionRange.start;
1236         EditOperation op = new EditOperation(EditAction.Replace, range, [""d]);
1237         _content.performOperation(op, this);
1238         //_selectionRange.start = _caretPos;
1239         //_selectionRange.end = _caretPos;
1240         ensureCaretVisible();
1241         return true;
1242     }
1243 
1244     /// override to handle specific actions state (e.g. change enabled state for supported actions)
1245     override bool handleActionStateRequest(const Action a) {
1246         switch (a.id) with(EditorActions)
1247         {
1248             case ToggleBlockComment:
1249                 if (!_content.syntaxSupport || !_content.syntaxSupport.supportsToggleBlockComment)
1250                     a.state = ACTION_STATE_INVISIBLE;
1251                 else if (enabled && _content.syntaxSupport.canToggleBlockComment(_selectionRange))
1252                     a.state = ACTION_STATE_ENABLED;
1253                 else
1254                     a.state = ACTION_STATE_DISABLE;
1255                 return true;
1256             case ToggleLineComment:
1257                 if (!_content.syntaxSupport || !_content.syntaxSupport.supportsToggleLineComment)
1258                     a.state = ACTION_STATE_INVISIBLE;
1259                 else if (enabled && _content.syntaxSupport.canToggleLineComment(_selectionRange))
1260                     a.state = ACTION_STATE_ENABLED;
1261                 else
1262                     a.state = ACTION_STATE_DISABLE;
1263                 return true;
1264             case Copy:
1265             case Cut:
1266             case Paste:
1267             case Undo:
1268             case Redo:
1269             case Tab:
1270             case BackTab:
1271             case Indent:
1272             case Unindent:
1273                 if (isActionEnabled(a))
1274                     a.state = ACTION_STATE_ENABLED;
1275                 else
1276                     a.state = ACTION_STATE_DISABLE;
1277                 return true;
1278             default:
1279                 return super.handleActionStateRequest(a);
1280         }
1281     }
1282 
1283     override protected bool handleAction(const Action a) {
1284         TextPosition oldCaretPos = _caretPos;
1285         dstring currentLine = _content[_caretPos.line];
1286         switch (a.id) with(EditorActions)
1287         {
1288             case Left:
1289             case SelectLeft:
1290                 correctCaretPos();
1291                 if (_caretPos.pos > 0) {
1292                     _caretPos.pos--;
1293                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
1294                     ensureCaretVisible();
1295                 } else if (_caretPos.line > 0) {
1296                     _caretPos = _content.lineEnd(_caretPos.line - 1);
1297                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
1298                     ensureCaretVisible();
1299                 }
1300                 return true;
1301             case Right:
1302             case SelectRight:
1303                 correctCaretPos();
1304                 if (_caretPos.pos < currentLine.length) {
1305                     _caretPos.pos++;
1306                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
1307                     ensureCaretVisible();
1308                 } else if (_caretPos.line < _content.length && _content.multiline) {
1309                     _caretPos.pos = 0;
1310                     _caretPos.line++;
1311                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
1312                     ensureCaretVisible();
1313                 }
1314                 return true;
1315             case WordLeft:
1316             case SelectWordLeft:
1317                 {
1318                     TextPosition newpos = _content.moveByWord(_caretPos, -1, _camelCasePartsAsWords);
1319                     if (newpos != _caretPos) {
1320                         _caretPos = newpos;
1321                         updateSelectionAfterCursorMovement(oldCaretPos, a.id == EditorActions.SelectWordLeft);
1322                         ensureCaretVisible();
1323                     }
1324                 }
1325                 return true;
1326             case WordRight:
1327             case SelectWordRight:
1328                 {
1329                     TextPosition newpos = _content.moveByWord(_caretPos, 1, _camelCasePartsAsWords);
1330                     if (newpos != _caretPos) {
1331                         _caretPos = newpos;
1332                         updateSelectionAfterCursorMovement(oldCaretPos, a.id == EditorActions.SelectWordRight);
1333                         ensureCaretVisible();
1334                     }
1335                 }
1336                 return true;
1337             case DocumentBegin:
1338             case SelectDocumentBegin:
1339                 if (_caretPos.pos > 0 || _caretPos.line > 0) {
1340                     _caretPos.line = 0;
1341                     _caretPos.pos = 0;
1342                     ensureCaretVisible();
1343                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
1344                 }
1345                 return true;
1346             case LineBegin:
1347             case SelectLineBegin:
1348                 auto space = _content.getLineWhiteSpace(_caretPos.line);
1349                 if (_caretPos.pos > 0) {
1350                     if (_caretPos.pos > space.firstNonSpaceIndex && space.firstNonSpaceIndex > 0)
1351                         _caretPos.pos = space.firstNonSpaceIndex;
1352                     else
1353                         _caretPos.pos = 0;
1354                     ensureCaretVisible();
1355                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
1356                 } else {
1357                     // caret pos is 0
1358                     if (space.firstNonSpaceIndex > 0)
1359                         _caretPos.pos = space.firstNonSpaceIndex;
1360                     ensureCaretVisible();
1361                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
1362                 }
1363                 return true;
1364             case DocumentEnd:
1365             case SelectDocumentEnd:
1366                 if (_caretPos.line < _content.length - 1 || _caretPos.pos < _content[_content.length - 1].length) {
1367                     _caretPos.line = _content.length - 1;
1368                     _caretPos.pos = cast(int)_content[_content.length - 1].length;
1369                     ensureCaretVisible();
1370                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
1371                 }
1372                 return true;
1373             case LineEnd:
1374             case SelectLineEnd:
1375                 if (_caretPos.pos < currentLine.length) {
1376                     _caretPos.pos = cast(int)currentLine.length;
1377                     ensureCaretVisible();
1378                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
1379                 }
1380                 return true;
1381             case DelPrevWord:
1382                 if (readOnly)
1383                     return true;
1384                 correctCaretPos();
1385                 if (removeSelectionTextIfSelected()) // clear selection
1386                     return true;
1387                 TextPosition newpos = _content.moveByWord(_caretPos, -1, _camelCasePartsAsWords);
1388                 if (newpos < _caretPos)
1389                     removeRangeText(TextRange(newpos, _caretPos));
1390                 return true;
1391             case DelNextWord:
1392                 if (readOnly)
1393                     return true;
1394                 correctCaretPos();
1395                 if (removeSelectionTextIfSelected()) // clear selection
1396                     return true;
1397                 TextPosition newpos = _content.moveByWord(_caretPos, 1, _camelCasePartsAsWords);
1398                 if (newpos > _caretPos)
1399                     removeRangeText(TextRange(_caretPos, newpos));
1400                 return true;
1401             case DelPrevChar:
1402                 if (readOnly)
1403                     return true;
1404                 correctCaretPos();
1405                 if (removeSelectionTextIfSelected()) // clear selection
1406                     return true;
1407                 if (_caretPos.pos > 0) {
1408                     // delete prev char in current line
1409                     TextRange range = TextRange(_caretPos, _caretPos);
1410                     range.start.pos--;
1411                     removeRangeText(range);
1412                 } else if (_caretPos.line > 0) {
1413                     // merge with previous line
1414                     TextRange range = TextRange(_caretPos, _caretPos);
1415                     range.start = _content.lineEnd(range.start.line - 1);
1416                     removeRangeText(range);
1417                 }
1418                 return true;
1419             case DelNextChar:
1420                 if (readOnly)
1421                     return true;
1422                 correctCaretPos();
1423                 if (removeSelectionTextIfSelected()) // clear selection
1424                     return true;
1425                 if (_caretPos.pos < currentLine.length) {
1426                     // delete char in current line
1427                     TextRange range = TextRange(_caretPos, _caretPos);
1428                     range.end.pos++;
1429                     removeRangeText(range);
1430                 } else if (_caretPos.line < _content.length - 1) {
1431                     // merge with next line
1432                     TextRange range = TextRange(_caretPos, _caretPos);
1433                     range.end.line++;
1434                     range.end.pos = 0;
1435                     removeRangeText(range);
1436                 }
1437                 return true;
1438             case Copy:
1439             case Cut:
1440                 TextRange range = _selectionRange;
1441                 if (range.empty && _copyCurrentLineWhenNoSelection) {
1442                     range = currentLineRange;
1443                 }
1444                 if (!range.empty) {
1445                     dstring selectionText = getRangeText(range);
1446                     platform.setClipboardText(selectionText);
1447                     if (!readOnly && a.id == Cut) {
1448                         EditOperation op = new EditOperation(EditAction.Replace, range, [""d]);
1449                         _content.performOperation(op, this);
1450                     }
1451                 }
1452                 return true;
1453             case Paste:
1454                 {
1455                     if (readOnly)
1456                         return true;
1457                     dstring selectionText = platform.getClipboardText();
1458                     dstring[] lines;
1459                     if (_content.multiline) {
1460                         lines = splitDString(selectionText);
1461                     } else {
1462                         lines = [replaceEolsWithSpaces(selectionText)];
1463                     }
1464                     EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, lines);
1465                     _content.performOperation(op, this);
1466                 }
1467                 return true;
1468             case Undo:
1469                 {
1470                     if (readOnly)
1471                         return true;
1472                     _content.undo(this);
1473                 }
1474                 return true;
1475             case Redo:
1476                 {
1477                     if (readOnly)
1478                         return true;
1479                     _content.redo(this);
1480                 }
1481                 return true;
1482             case Indent:
1483                 indentRange(false);
1484                 return true;
1485             case Unindent:
1486                 indentRange(true);
1487                 return true;
1488             case Tab:
1489                 {
1490                     if (readOnly)
1491                         return true;
1492                     if (_selectionRange.empty) {
1493                         if (useSpacesForTabs) {
1494                             // insert one or more spaces to 
1495                             EditOperation op = new EditOperation(EditAction.Replace, TextRange(_caretPos, _caretPos), [spacesForTab(_caretPos.pos)]);
1496                             _content.performOperation(op, this);
1497                         } else {
1498                             // just insert tab character
1499                             EditOperation op = new EditOperation(EditAction.Replace, TextRange(_caretPos, _caretPos), ["\t"d]);
1500                             _content.performOperation(op, this);
1501                         }
1502                     } else {
1503                         if (multipleLinesSelected()) {
1504                             // indent range
1505                             return handleAction(new Action(EditorActions.Indent));
1506                         } else {
1507                             // insert tab
1508                             if (useSpacesForTabs) {
1509                                 // insert one or more spaces to 
1510                                 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [spacesForTab(_selectionRange.start.pos)]);
1511                                 _content.performOperation(op, this);
1512                             } else {
1513                                 // just insert tab character
1514                                 EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, ["\t"d]);
1515                                 _content.performOperation(op, this);
1516                             }
1517                         }
1518 
1519                     }
1520                 }
1521                 return true;
1522             case BackTab:
1523                 {
1524                     if (readOnly)
1525                         return true;
1526                     if (_selectionRange.empty) {
1527                         // remove spaces before caret
1528                         TextRange r = spaceBefore(_caretPos);
1529                         if (!r.empty) {
1530                             EditOperation op = new EditOperation(EditAction.Replace, r, [""d]);
1531                             _content.performOperation(op, this);
1532                         }
1533                     } else {
1534                         if (multipleLinesSelected()) {
1535                             // unindent range
1536                             return handleAction(new Action(EditorActions.Unindent));
1537                         } else {
1538                             // remove space before selection
1539                             TextRange r = spaceBefore(_selectionRange.start);
1540                             if (!r.empty) {
1541                                 int nchars = r.end.pos - r.start.pos;
1542                                 TextRange saveRange = _selectionRange;
1543                                 TextPosition saveCursor = _caretPos;
1544                                 EditOperation op = new EditOperation(EditAction.Replace, r, [""d]);
1545                                 _content.performOperation(op, this);
1546                                 if (saveCursor.line == saveRange.start.line)
1547                                     saveCursor.pos -= nchars;
1548                                 if (saveRange.end.line == saveRange.start.line)
1549                                     saveRange.end.pos -= nchars;
1550                                 saveRange.start.pos -= nchars;
1551                                 _selectionRange = saveRange;
1552                                 _caretPos = saveCursor;
1553                                 ensureCaretVisible();
1554                             }
1555                         }
1556                     }
1557                 }
1558                 return true;
1559             case ToggleReplaceMode:
1560                 replaceMode = !replaceMode;
1561                 return true;
1562             case SelectAll:
1563                 _selectionRange.start.line = 0;
1564                 _selectionRange.start.pos = 0;
1565                 _selectionRange.end = _content.lineEnd(_content.length - 1);
1566                 _caretPos = _selectionRange.end;
1567                 ensureCaretVisible();
1568                 requestActionsUpdate();
1569                 return true;
1570             case ToggleBookmark:
1571                 if (_content.multiline) {
1572                     int line = a.longParam >= 0 ? cast(int)a.longParam : _caretPos.line;
1573                     _content.lineIcons.toggleBookmark(line);
1574                     return true;
1575                 }
1576                 return false;
1577             case GoToNextBookmark:
1578             case GoToPreviousBookmark:
1579                 if (_content.multiline) {
1580                     LineIcon mark = _content.lineIcons.findNext(LineIconType.bookmark, _selectionRange.end.line, a.id == EditorActions.GoToNextBookmark ? 1 : -1);
1581                     if (mark) {
1582                         setCaretPos(mark.line, 0, true);
1583                         return true;
1584                     }
1585                 }
1586                 return false;
1587             default:
1588                 break;
1589         }
1590         return super.handleAction(a);
1591     }
1592 
1593     protected TextRange spaceBefore(TextPosition pos) {
1594         TextRange res = TextRange(pos, pos);
1595         dstring s = _content[pos.line];
1596         int x = 0;
1597         int start = -1;
1598         for (int i = 0; i < pos.pos; i++) {
1599             dchar ch = s[i];
1600             if (ch == ' ') {
1601                 if (start == -1 || (x % tabSize) == 0)
1602                     start = i;
1603                 x++;
1604             } else if (ch == '\t') {
1605                 if (start == -1 || (x % tabSize) == 0)
1606                     start = i;
1607                 x = (x + tabSize + 1) / tabSize * tabSize;
1608             } else {
1609                 x++;
1610                 start = -1;
1611             }
1612         }
1613         if (start != -1) {
1614             res.start.pos = start;
1615         }
1616         return res;
1617     }
1618 
1619     /// change line indent
1620     protected dstring indentLine(dstring src, bool back, TextPosition * cursorPos) {
1621         int firstNonSpace = -1;
1622         int x = 0;
1623         int unindentPos = -1;
1624         int cursor = cursorPos ? cursorPos.pos : 0;
1625         for (int i = 0; i < src.length; i++) {
1626             dchar ch = src[i];
1627             if (ch == ' ') {
1628                 x++;
1629             } else if (ch == '\t') {
1630                 x = (x + tabSize + 1) / tabSize * tabSize;
1631             } else {
1632                 firstNonSpace = i;
1633                 break;
1634             }
1635             if (x <= tabSize)
1636                 unindentPos = i + 1;
1637         }
1638         if (firstNonSpace == -1) // only spaces or empty line -- do not change it
1639             return src;
1640         if (back) {
1641             // unindent
1642             if (unindentPos == -1)
1643                 return src; // no change
1644             if (unindentPos == src.length) {
1645                 if (cursorPos)
1646                     cursorPos.pos = 0;
1647                 return ""d;
1648             }
1649             if (cursor >= unindentPos)
1650                 cursorPos.pos -= unindentPos;
1651             return src[unindentPos .. $].dup;
1652         } else {
1653             // indent
1654             if (useSpacesForTabs) {
1655                 if (cursor > 0)
1656                     cursorPos.pos += tabSize;
1657                 return spacesForTab(0) ~ src;
1658             } else {
1659                 if (cursor > 0)
1660                     cursorPos.pos++;
1661                 return "\t"d ~ src;
1662             }
1663         }
1664     }
1665 
1666     /// indent / unindent range
1667     protected void indentRange(bool back) {
1668         TextRange r = _selectionRange;
1669         r.start.pos = 0;
1670         if (r.end.pos > 0)
1671             r.end = _content.lineBegin(r.end.line + 1);
1672         if (r.end.line <= r.start.line)
1673             r = TextRange(_content.lineBegin(_caretPos.line), _content.lineBegin(_caretPos.line + 1));
1674         int lineCount = r.end.line - r.start.line;
1675         if (r.end.pos > 0)
1676             lineCount++;
1677         dstring[] newContent = new dstring[lineCount + 1];
1678         bool changed = false;
1679         for (int i = 0; i < lineCount; i++) {
1680             dstring srcline = _content.line(r.start.line + i);
1681             dstring dstline = indentLine(srcline, back, r.start.line + i == _caretPos.line ? &_caretPos : null);
1682             newContent[i] = dstline;
1683             if (dstline.length != srcline.length)
1684                 changed = true;
1685         }
1686         if (changed) {
1687             TextRange saveRange = r;
1688             TextPosition saveCursor = _caretPos;
1689             EditOperation op = new EditOperation(EditAction.Replace, r, newContent);
1690             _content.performOperation(op, this);
1691             _selectionRange = saveRange;
1692             _caretPos = saveCursor;
1693             ensureCaretVisible();
1694         }
1695     }
1696 
1697     /// map key to action
1698     override protected Action findKeyAction(uint keyCode, uint flags) {
1699         // don't handle tabs when disabled
1700         if (keyCode == KeyCode.TAB && (flags == 0 || flags == KeyFlag.Shift) && (!_wantTabs || readOnly))
1701             return null;
1702         return super.findKeyAction(keyCode, flags);
1703     }
1704 
1705     static bool isAZaz(dchar ch) {
1706         return (ch >= 'a' && ch <='z') || (ch >= 'A' && ch <='Z');
1707     }
1708 
1709     /// handle keys
1710     override bool onKeyEvent(KeyEvent event) {
1711         //Log.d("onKeyEvent ", event.action, " ", event.keyCode, " flags ", event.flags);
1712         if (focused) startCaretBlinking();
1713         cancelHoverTimer();
1714         bool ctrlOrAltPressed = !!(event.flags & KeyFlag.Control); //(event.flags & (KeyFlag.Control /* | KeyFlag.Alt */));
1715         //if (event.action == KeyAction.KeyDown && event.keyCode == KeyCode.SPACE && (event.flags & KeyFlag.Control)) {
1716         //    Log.d("Ctrl+Space pressed");
1717         //}
1718         if (event.action == KeyAction.Text && event.text.length && !ctrlOrAltPressed) {
1719             //Log.d("text entered: ", event.text);
1720             if (readOnly)
1721                 return true;
1722             if (!(!!(event.flags & KeyFlag.Alt) && event.text.length == 1 && isAZaz(event.text[0]))) { // filter out Alt+A..Z
1723                 if (replaceMode && _selectionRange.empty && _content[_caretPos.line].length >= _caretPos.pos + event.text.length) {
1724                     // replace next char(s)
1725                     TextRange range = _selectionRange;
1726                     range.end.pos += cast(int)event.text.length;
1727                     EditOperation op = new EditOperation(EditAction.Replace, range, [event.text]);
1728                     _content.performOperation(op, this);
1729                 } else {
1730                     EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [event.text]);
1731                     _content.performOperation(op, this);
1732                 }
1733                 if (focused) startCaretBlinking();
1734                 return true;
1735             }
1736         }
1737         //if (event.keyCode == KeyCode.SPACE && !readOnly) {
1738         //    return true;
1739         //}
1740         //if (event.keyCode == KeyCode.RETURN && !readOnly && !_content.multiline) {
1741         //    return true;
1742         //}
1743         bool res = super.onKeyEvent(event);
1744         if (focused) startCaretBlinking();
1745         return res;
1746     }
1747 
1748     /// Handle Ctrl + Left mouse click on text
1749     protected void onControlClick() {
1750         // override to do something useful on Ctrl + Left mouse click in text
1751     }
1752 
1753     protected TextPosition _hoverTextPosition;
1754     protected Point _hoverMousePosition;
1755     protected ulong _hoverTimer;
1756     protected long _hoverTimeoutMillis = 800;
1757 
1758     /// override to handle mouse hover timeout in text
1759     protected void onHoverTimeout(Point pt, TextPosition pos) {
1760         // override to do something useful on hover timeout
1761     }
1762 
1763     protected void onHover(Point pos) {
1764         if (_hoverMousePosition == pos)
1765             return;
1766         //Log.d("onHover ", pos);
1767         int x = pos.x - left - _leftPaneWidth;
1768         int y = pos.y - top;
1769         _hoverMousePosition = pos;
1770         _hoverTextPosition = clientToTextPos(Point(x, y));
1771         cancelHoverTimer();
1772         _hoverTimer = setTimer(_hoverTimeoutMillis);
1773     }
1774 
1775     protected void cancelHoverTimer() {
1776         if (_hoverTimer) {
1777             cancelTimer(_hoverTimer);
1778             _hoverTimer = 0;
1779         }
1780     }
1781 
1782     /// process mouse event; return true if event is processed by widget.
1783     override bool onMouseEvent(MouseEvent event) {
1784         //Log.d("onMouseEvent ", id, " ", event.action, "  (", event.x, ",", event.y, ")");
1785         // support onClick
1786         bool insideLeftPane = event.x < _clientRect.left && event.x >= _clientRect.left - _leftPaneWidth;
1787         if (event.action == MouseAction.ButtonDown && insideLeftPane) {
1788             setFocus();
1789             cancelHoverTimer();
1790             if (onLeftPaneMouseClick(event))
1791                 return true;
1792         }
1793         if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
1794             setFocus();
1795             cancelHoverTimer();
1796             if (event.tripleClick) {
1797                 selectLineByMouse(event.x - _clientRect.left, event.y - _clientRect.top);
1798             } else if (event.doubleClick) {
1799                 selectWordByMouse(event.x - _clientRect.left, event.y - _clientRect.top);
1800             } else {
1801                 auto doSelect = cast(bool)(event.keyFlags & MouseFlag.Shift);
1802                 updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, doSelect);
1803 
1804                 if (event.keyFlags == MouseFlag.Control)
1805                     onControlClick();
1806             }
1807             startCaretBlinking();
1808             invalidate();
1809             return true;
1810         }
1811         if (event.action == MouseAction.Move && (event.flags & MouseButton.Left) != 0) {
1812             updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, true);
1813             return true;
1814         }
1815         if (event.action == MouseAction.Move && event.flags == 0) {
1816             // hover
1817             if (focused && !insideLeftPane) {
1818                 onHover(event.pos);
1819             } else {
1820                 cancelHoverTimer();
1821             }
1822             return true;
1823         }
1824         if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) {
1825             cancelHoverTimer();
1826             return true;
1827         }
1828         if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) {
1829             cancelHoverTimer();
1830             return true;
1831         }
1832         if (event.action == MouseAction.FocusIn) {
1833             cancelHoverTimer();
1834             return true;
1835         }
1836         if (event.action == MouseAction.Wheel) {
1837             cancelHoverTimer();
1838             uint keyFlags = event.flags & (MouseFlag.Shift | MouseFlag.Control | MouseFlag.Alt);
1839             if (event.wheelDelta < 0) {
1840                 if (keyFlags == MouseFlag.Shift)
1841                     return handleAction(new Action(EditorActions.ScrollRight));
1842                 if (keyFlags == MouseFlag.Control)
1843                     return handleAction(new Action(EditorActions.ZoomOut));
1844                 return handleAction(new Action(EditorActions.ScrollLineDown));
1845             } else if (event.wheelDelta > 0) {
1846                 if (keyFlags == MouseFlag.Shift)
1847                     return handleAction(new Action(EditorActions.ScrollLeft));
1848                 if (keyFlags == MouseFlag.Control)
1849                     return handleAction(new Action(EditorActions.ZoomIn));
1850                 return handleAction(new Action(EditorActions.ScrollLineUp));
1851             }
1852         }
1853         cancelHoverTimer();
1854         return super.onMouseEvent(event);
1855     }
1856 
1857     /// returns caret position
1858     @property TextPosition caretPos() {
1859         return _caretPos;
1860     }
1861 
1862     /// change caret position and ensure it is visible
1863     void setCaretPos(int line, int column, bool makeVisible = true, bool center = false)
1864     {
1865         _caretPos = TextPosition(line,column);
1866         correctCaretPos();
1867         invalidate();
1868         if (makeVisible)
1869             ensureCaretVisible(center);
1870     }
1871 }
1872 
1873 interface EditorActionHandler {
1874     bool onEditorAction(const Action action);
1875 }
1876 
1877 /// single line editor
1878 class EditLine : EditWidgetBase {
1879 
1880     Signal!EditorActionHandler editorAction;
1881 
1882     /// empty parameter list constructor - for usage by factory
1883     this() {
1884         this(null);
1885     }
1886     /// create with ID parameter
1887     this(string ID, dstring initialContent = null) {
1888         super(ID, ScrollBarMode.Invisible, ScrollBarMode.Invisible);
1889         _content = new EditableContent(false);
1890         _content.contentChanged = this;
1891         _selectAllWhenFocusedWithTab = true;
1892         _deselectAllWhenUnfocused = true;
1893         wantTabs = false;
1894         styleId = STYLE_EDIT_LINE;
1895         text = initialContent;
1896         onThemeChanged();
1897     }
1898 
1899     protected dstring _measuredText;
1900     protected int[] _measuredTextWidths;
1901     protected Point _measuredTextSize;
1902 
1903     protected dchar _passwordChar = 0;
1904     /// password character - 0 for normal editor, some character, e.g. '*' to hide text by replacing all characters with this char
1905     @property dchar passwordChar() { return _passwordChar; }
1906     @property EditLine passwordChar(dchar ch) { 
1907         if (_passwordChar != ch) {
1908             _passwordChar = ch;
1909             requestLayout();
1910         }
1911         return this;
1912     }
1913 
1914     override protected Rect textPosToClient(TextPosition p) {
1915         Rect res;
1916         res.bottom = _clientRect.height;
1917         if (p.pos == 0)
1918             res.left = 0;
1919         else if (p.pos >= _measuredText.length)
1920             res.left = _measuredTextSize.x;
1921         else
1922             res.left = _measuredTextWidths[p.pos - 1];
1923         res.left -= _scrollPos.x;
1924         res.right = res.left + 1;
1925         return res;
1926     }
1927 
1928     override protected TextPosition clientToTextPos(Point pt) {
1929         pt.x += _scrollPos.x;
1930         TextPosition res;
1931         for (int i = 0; i < _measuredText.length; i++) {
1932             int x0 = i > 0 ? _measuredTextWidths[i - 1] : 0;
1933             int x1 = _measuredTextWidths[i];
1934             int mx = (x0 + x1) >> 1;
1935             if (pt.x <= mx) {
1936                 res.pos = i;
1937                 return res;
1938             }
1939         }
1940         res.pos = cast(int)_measuredText.length;
1941         return res;
1942     }
1943 
1944     override protected void ensureCaretVisible(bool center = false) {
1945         //_scrollPos
1946         Rect rc = textPosToClient(_caretPos);
1947         if (rc.left < 0) {
1948             // scroll left
1949             _scrollPos.x -= -rc.left + _clientRect.width / 10;
1950             if (_scrollPos.x < 0)
1951                 _scrollPos.x = 0;
1952             invalidate();
1953         } else if (rc.left >= _clientRect.width - 10) {
1954             // scroll right
1955             _scrollPos.x += (rc.left - _clientRect.width) + _spaceWidth * 4;
1956             invalidate();
1957         }
1958         updateScrollBars();
1959     }
1960 
1961     protected dstring applyPasswordChar(dstring s) {
1962         if (!_passwordChar || s.length == 0)
1963             return s;
1964         dchar[] ss = s.dup;
1965         foreach(ref ch; ss)
1966             ch = _passwordChar;
1967         return cast(dstring)ss;
1968     }
1969 
1970     override protected Point measureVisibleText() {
1971         FontRef font = font();
1972         //Point sz = font.textSize(text);
1973         _measuredText = applyPasswordChar(text);
1974         _measuredTextWidths.length = _measuredText.length;
1975         int charsMeasured = font.measureText(_measuredText, _measuredTextWidths, int.max, tabSize);
1976         _measuredTextSize.x = charsMeasured > 0 ? _measuredTextWidths[charsMeasured - 1]: 0;
1977         _measuredTextSize.y = font.height;
1978         return _measuredTextSize;
1979     }
1980 
1981     /// measure
1982     override void measure(int parentWidth, int parentHeight) { 
1983         updateFontProps();
1984         measureVisibleText();
1985         measuredContent(parentWidth, parentHeight, _measuredTextSize.x + _leftPaneWidth, _measuredTextSize.y);
1986     }
1987 
1988     override bool handleAction(const Action a) {
1989         switch (a.id) with(EditorActions)
1990         {
1991             case InsertNewLine:
1992             case PrependNewLine:
1993             case AppendNewLine:
1994                 if (editorAction.assigned) {
1995                     return editorAction(a);
1996                 }
1997                 break;
1998             case Up:
1999                 break;
2000             case Down:
2001                 break;
2002             case PageUp:
2003                 break;
2004             case PageDown:
2005                 break;
2006             default:
2007                 break;
2008         }
2009         return super.handleAction(a);
2010     }
2011 
2012 
2013     /// handle keys
2014     override bool onKeyEvent(KeyEvent event) {
2015         return super.onKeyEvent(event);
2016     }
2017 
2018     /// process mouse event; return true if event is processed by widget.
2019     override bool onMouseEvent(MouseEvent event) {
2020         return super.onMouseEvent(event);
2021     }
2022 
2023     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
2024     override void layout(Rect rc) {
2025         if (visibility == Visibility.Gone) {
2026             return;
2027         }
2028         _needLayout = false;
2029         Point sz = Point(rc.width, measuredHeight);
2030         applyAlign(rc, sz);
2031         _pos = rc;
2032         _clientRect = rc;
2033         applyMargins(_clientRect);
2034         applyPadding(_clientRect);
2035         if (_contentChanged) {
2036             measureVisibleText();
2037             _contentChanged = false;
2038         }
2039     }
2040 
2041 
2042     /// override to custom highlight of line background
2043     protected void drawLineBackground(DrawBuf buf, Rect lineRect, Rect visibleRect) {
2044         if (!_selectionRange.empty) {
2045             // line inside selection
2046             Rect startrc = textPosToClient(_selectionRange.start);
2047             Rect endrc = textPosToClient(_selectionRange.end);
2048             int startx = startrc.left + _clientRect.left;
2049             int endx = endrc.left + _clientRect.left;
2050             Rect rc = lineRect;
2051             rc.left = startx;
2052             rc.right = endx;
2053             if (!rc.empty) {
2054                 // draw selection rect for line
2055                 buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal);
2056             }
2057             if (_leftPaneWidth > 0) {
2058                 Rect leftPaneRect = visibleRect;
2059                 leftPaneRect.right = leftPaneRect.left;
2060                 leftPaneRect.left -= _leftPaneWidth;
2061                 drawLeftPane(buf, leftPaneRect, 0);
2062             }
2063         }
2064     }
2065 
2066     /// draw content
2067     override void onDraw(DrawBuf buf) {
2068         if (visibility != Visibility.Visible)
2069             return;
2070         super.onDraw(buf);
2071         Rect rc = _pos;
2072         applyMargins(rc);
2073         applyPadding(rc);
2074         auto saver = ClipRectSaver(buf, rc, alpha);
2075         FontRef font = font();
2076         dstring txt = applyPasswordChar(text);
2077         Point sz = font.textSize(txt);
2078         //applyAlign(rc, sz);
2079         Rect lineRect = _clientRect;
2080         lineRect.left = _clientRect.left - _scrollPos.x;
2081         lineRect.right = lineRect.left + calcLineWidth(txt);
2082         Rect visibleRect = lineRect;
2083         visibleRect.left = _clientRect.left;
2084         visibleRect.right = _clientRect.right;
2085         drawLineBackground(buf, lineRect, visibleRect);
2086         font.drawText(buf, rc.left - _scrollPos.x, rc.top, txt, textColor, tabSize);
2087 
2088         drawCaret(buf);
2089     }
2090 }
2091 
2092 
2093 
2094 /// multiline editor
2095 class EditBox : EditWidgetBase {
2096     /// empty parameter list constructor - for usage by factory
2097     this() {
2098         this(null);
2099     }
2100     /// create with ID parameter
2101     this(string ID, dstring initialContent = null, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) {
2102         super(ID, hscrollbarMode, vscrollbarMode);
2103         _content = new EditableContent(true); // multiline
2104         _content.contentChanged = this;
2105         styleId = STYLE_EDIT_BOX;
2106         text = initialContent;
2107         acceleratorMap.add( [
2108             // zoom
2109             new Action(EditorActions.ZoomIn, KeyCode.ADD, KeyFlag.Control),
2110             new Action(EditorActions.ZoomOut, KeyCode.SUB, KeyFlag.Control),
2111         ]);
2112         onThemeChanged();
2113     }
2114 
2115     ~this() {
2116         if (_findPanel) {
2117             destroy(_findPanel);
2118             _findPanel = null;
2119         }
2120     }
2121 
2122     protected int _firstVisibleLine;
2123 
2124     protected int _maxLineWidth;
2125     protected int _numVisibleLines;             // number of lines visible in client area
2126     protected dstring[] _visibleLines;          // text for visible lines
2127     protected int[][] _visibleLinesMeasurement; // char positions for visible lines
2128     protected int[] _visibleLinesWidths; // width (in pixels) of visible lines
2129     protected CustomCharProps[][] _visibleLinesHighlights;
2130     protected CustomCharProps[][] _visibleLinesHighlightsBuf;
2131 
2132     override protected int lineCount() {
2133         return _content.length;
2134     }
2135 
2136     override protected void updateMaxLineWidth() {
2137         // find max line width. TODO: optimize!!!
2138         int maxw;
2139         int[] buf;
2140         for (int i = 0; i < _content.length; i++) {
2141             dstring s = _content[i];
2142             int w = calcLineWidth(s);
2143             if (maxw < w)
2144                 maxw = w;
2145         }
2146         _maxLineWidth = maxw;
2147     }
2148 
2149     @property int minFontSize() {
2150         return _minFontSize;
2151     }
2152     @property EditBox minFontSize(int size) {
2153         _minFontSize = size;
2154         return this;
2155     }
2156 
2157     @property int maxFontSize() {
2158         return _maxFontSize;
2159     }
2160 
2161     @property EditBox maxFontSize(int size) {
2162         _maxFontSize = size;
2163         return this;
2164     }
2165 
2166     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
2167     override void layout(Rect rc) {
2168         if (visibility == Visibility.Gone) {
2169             return;
2170         }
2171         if (rc != _pos)
2172             _contentChanged = true;
2173         Rect contentRc = rc;
2174         int findPanelHeight;
2175         if (_findPanel && _findPanel.visibility != Visibility.Gone) {
2176             _findPanel.measure(rc.width, rc.height);
2177             findPanelHeight = _findPanel.measuredHeight;
2178             _findPanel.layout(Rect(rc.left, rc.bottom - findPanelHeight, rc.right, rc.bottom));
2179             contentRc.bottom -= findPanelHeight;
2180         }
2181         
2182         super.layout(contentRc);
2183         if (_contentChanged) {
2184             measureVisibleText();
2185             _contentChanged = false;
2186         }
2187 
2188         _pos = rc;
2189     }
2190 
2191     override protected Point measureVisibleText() {
2192         Point sz;
2193         FontRef font = font();
2194         _lineHeight = font.height;
2195         _numVisibleLines = (_clientRect.height + _lineHeight - 1) / _lineHeight;
2196         if (_firstVisibleLine >= _content.length) {
2197             _firstVisibleLine = _content.length - _numVisibleLines + 1;
2198             if (_firstVisibleLine < 0)
2199                 _firstVisibleLine = 0;
2200             _caretPos.line = _content.length - 1;
2201             _caretPos.pos = 0;
2202         }
2203         if (_numVisibleLines < 1)
2204             _numVisibleLines = 1;
2205         if (_firstVisibleLine + _numVisibleLines > _content.length)
2206             _numVisibleLines = _content.length - _firstVisibleLine;
2207         if (_numVisibleLines < 1)
2208             _numVisibleLines = 1;
2209         _visibleLines.length = _numVisibleLines;
2210         if (_visibleLinesMeasurement.length < _numVisibleLines)
2211             _visibleLinesMeasurement.length = _numVisibleLines;
2212         if (_visibleLinesWidths.length < _numVisibleLines)
2213             _visibleLinesWidths.length = _numVisibleLines;
2214         if (_visibleLinesHighlights.length < _numVisibleLines) {
2215             _visibleLinesHighlights.length = _numVisibleLines;
2216             _visibleLinesHighlightsBuf.length = _numVisibleLines;
2217         }
2218         for (int i = 0; i < _numVisibleLines; i++) {
2219             _visibleLines[i] = _content[_firstVisibleLine + i];
2220             size_t len = _visibleLines[i].length;
2221             if (_visibleLinesMeasurement[i].length < len)
2222                 _visibleLinesMeasurement[i].length = len;
2223             if (_visibleLinesHighlightsBuf[i].length < len)
2224                 _visibleLinesHighlightsBuf[i].length = len;
2225             _visibleLinesHighlights[i] = handleCustomLineHighlight(_firstVisibleLine + i, _visibleLines[i], _visibleLinesHighlightsBuf[i]);
2226             int charsMeasured = font.measureText(_visibleLines[i], _visibleLinesMeasurement[i], int.max, tabSize);
2227             _visibleLinesWidths[i] = charsMeasured > 0 ? _visibleLinesMeasurement[i][charsMeasured - 1] : 0;
2228             if (sz.x < _visibleLinesWidths[i])
2229                 sz.x = _visibleLinesWidths[i]; // width - max from visible lines
2230         }
2231         sz.x = _maxLineWidth;
2232         sz.y = _lineHeight * _content.length; // height - for all lines
2233         return sz;
2234     }
2235 
2236     /// update horizontal scrollbar widget position
2237     override protected void updateHScrollBar() {
2238         _hscrollbar.setRange(0, _maxLineWidth + _clientRect.width / 4);
2239         _hscrollbar.pageSize = _clientRect.width;
2240         _hscrollbar.position = _scrollPos.x;
2241     }
2242 
2243     /// update verticat scrollbar widget position
2244     override protected void updateVScrollBar() {
2245         int visibleLines = _lineHeight ? _clientRect.height / _lineHeight : 1; // fully visible lines
2246         if (visibleLines < 1)
2247             visibleLines = 1;
2248         _vscrollbar.setRange(0, _content.length - 1);
2249         _vscrollbar.pageSize = visibleLines;
2250         _vscrollbar.position = _firstVisibleLine;
2251     }
2252 
2253     /// process horizontal scrollbar event
2254     override bool onHScroll(ScrollEvent event) {
2255         if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) {
2256             if (_scrollPos.x != event.position) {
2257                 _scrollPos.x = event.position;
2258                 invalidate();
2259             }
2260         } else if (event.action == ScrollAction.PageUp) {
2261             dispatchAction(new Action(EditorActions.ScrollLeft));
2262         } else if (event.action == ScrollAction.PageDown) {
2263             dispatchAction(new Action(EditorActions.ScrollRight));
2264         } else if (event.action == ScrollAction.LineUp) {
2265             dispatchAction(new Action(EditorActions.ScrollLeft));
2266         } else if (event.action == ScrollAction.LineDown) {
2267             dispatchAction(new Action(EditorActions.ScrollRight));
2268         }
2269         return true;
2270     }
2271 
2272     /// process vertical scrollbar event
2273     override bool onVScroll(ScrollEvent event) {
2274         if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) {
2275             if (_firstVisibleLine != event.position) {
2276                 _firstVisibleLine = event.position;
2277                 measureVisibleText();
2278                 invalidate();
2279             }
2280         } else if (event.action == ScrollAction.PageUp) {
2281             dispatchAction(new Action(EditorActions.ScrollPageUp));
2282         } else if (event.action == ScrollAction.PageDown) {
2283             dispatchAction(new Action(EditorActions.ScrollPageDown));
2284         } else if (event.action == ScrollAction.LineUp) {
2285             dispatchAction(new Action(EditorActions.ScrollLineUp));
2286         } else if (event.action == ScrollAction.LineDown) {
2287             dispatchAction(new Action(EditorActions.ScrollLineDown));
2288         }
2289         return true;
2290     }
2291 
2292     protected bool _enableScrollAfterText = true;
2293     override protected void ensureCaretVisible(bool center = false) {
2294         if (_caretPos.line >= _content.length)
2295             _caretPos.line = _content.length - 1;
2296         if (_caretPos.line < 0)
2297             _caretPos.line = 0;
2298         int visibleLines = _lineHeight > 0 ? _clientRect.height / _lineHeight : 1; // fully visible lines
2299         if (visibleLines < 1)
2300             visibleLines = 1;
2301         int maxFirstVisibleLine = _content.length - 1;
2302         if (!_enableScrollAfterText)
2303             maxFirstVisibleLine = _content.length - visibleLines;
2304         if (maxFirstVisibleLine < 0)
2305             maxFirstVisibleLine = 0;
2306 
2307         if (_caretPos.line < _firstVisibleLine) {
2308             _firstVisibleLine = _caretPos.line;
2309             if (center) {
2310                 _firstVisibleLine -= visibleLines / 2;
2311                 if (_firstVisibleLine < 0)
2312                     _firstVisibleLine = 0;
2313             }
2314             if (_firstVisibleLine > maxFirstVisibleLine)
2315                 _firstVisibleLine = maxFirstVisibleLine;
2316             measureVisibleText();
2317             invalidate();
2318         } else if (_caretPos.line >= _firstVisibleLine + visibleLines) {
2319             _firstVisibleLine = _caretPos.line - visibleLines + 1;
2320             if (center)
2321                 _firstVisibleLine += visibleLines / 2;
2322             if (_firstVisibleLine > maxFirstVisibleLine)
2323                 _firstVisibleLine = maxFirstVisibleLine;
2324             if (_firstVisibleLine < 0)
2325                 _firstVisibleLine = 0;
2326             measureVisibleText();
2327             invalidate();
2328         } else if (_firstVisibleLine > maxFirstVisibleLine) {
2329             _firstVisibleLine = maxFirstVisibleLine;
2330             if (_firstVisibleLine < 0)
2331                 _firstVisibleLine = 0;
2332             measureVisibleText();
2333             invalidate();
2334         }
2335         //_scrollPos
2336         Rect rc = textPosToClient(_caretPos);
2337         if (rc.left < 0) {
2338             // scroll left
2339             _scrollPos.x -= -rc.left + _clientRect.width / 4;
2340             if (_scrollPos.x < 0)
2341                 _scrollPos.x = 0;
2342             invalidate();
2343         } else if (rc.left >= _clientRect.width - 10) {
2344             // scroll right
2345             _scrollPos.x += (rc.left - _clientRect.width) + _clientRect.width / 4;
2346             invalidate();
2347         }
2348         updateScrollBars();
2349     }
2350 
2351     override protected Rect textPosToClient(TextPosition p) {
2352         Rect res;
2353         int lineIndex = p.line - _firstVisibleLine;
2354         res.top = lineIndex * _lineHeight;
2355         res.bottom = res.top + _lineHeight;
2356         // if visible
2357         if (lineIndex >= 0 && lineIndex < _visibleLines.length) {
2358             if (p.pos == 0)
2359                 res.left = 0;
2360             else if (p.pos >= _visibleLinesMeasurement[lineIndex].length)
2361                 res.left = _visibleLinesWidths[lineIndex];
2362             else
2363                 res.left = _visibleLinesMeasurement[lineIndex][p.pos - 1];
2364         }
2365         res.left -= _scrollPos.x;
2366         res.right = res.left + 1;
2367         return res;
2368     }
2369 
2370     override protected TextPosition clientToTextPos(Point pt) {
2371         TextPosition res;
2372         pt.x += _scrollPos.x;
2373         int lineIndex = pt.y / _lineHeight;
2374         if (lineIndex < 0)
2375             lineIndex = 0;
2376         if (lineIndex < _visibleLines.length) {
2377             res.line = lineIndex + _firstVisibleLine;
2378             int len = cast(int)_visibleLines[lineIndex].length;
2379             for (int i = 0; i < len; i++) {
2380                 int x0 = i > 0 ? _visibleLinesMeasurement[lineIndex][i - 1] : 0;
2381                 int x1 = _visibleLinesMeasurement[lineIndex][i];
2382                 int mx = (x0 + x1) >> 1;
2383                 if (pt.x <= mx) {
2384                     res.pos = i;
2385                     return res;
2386                 }
2387             }
2388             res.pos = cast(int)_visibleLines[lineIndex].length;
2389         } else if (_visibleLines.length > 0) {
2390             res.line = _firstVisibleLine + cast(int)_visibleLines.length - 1;
2391             res.pos = cast(int)_visibleLines[$ - 1].length;
2392         } else {
2393             res.line = 0;
2394             res.pos = 0;
2395         }
2396         return res;
2397     }
2398 
2399     override protected bool handleAction(const Action a) {
2400         TextPosition oldCaretPos = _caretPos;
2401         dstring currentLine = _content[_caretPos.line];
2402         switch (a.id) with(EditorActions)
2403         {
2404             case PrependNewLine:
2405                 if (!readOnly) {
2406                     correctCaretPos();
2407                     _caretPos.pos = 0;
2408                     EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]);
2409                     _content.performOperation(op, this);
2410                 }
2411                 return true;
2412             case InsertNewLine:
2413                 if (!readOnly) {
2414                     correctCaretPos();
2415                     EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]);
2416                     _content.performOperation(op, this);
2417                 }
2418                 return true;
2419             case Up:
2420             case SelectUp:
2421                 if (_caretPos.line > 0) {
2422                     _caretPos.line--;
2423                     correctCaretPos();
2424                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
2425                     ensureCaretVisible();
2426                 }
2427                 return true;
2428             case Down:
2429             case SelectDown:
2430                 if (_caretPos.line < _content.length - 1) {
2431                     _caretPos.line++;
2432                     correctCaretPos();
2433                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
2434                     ensureCaretVisible();
2435                 }
2436                 return true;
2437             case PageBegin:
2438             case SelectPageBegin:
2439                 {
2440                     ensureCaretVisible();
2441                     _caretPos.line = _firstVisibleLine;
2442                     correctCaretPos();
2443                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
2444                 }
2445                 return true;
2446             case PageEnd:
2447             case SelectPageEnd:
2448                 {
2449                     ensureCaretVisible();
2450                     int fullLines = _clientRect.height / _lineHeight;
2451                     int newpos = _firstVisibleLine + fullLines - 1;
2452                     if (newpos >= _content.length)
2453                         newpos = _content.length - 1;
2454                     _caretPos.line = newpos;
2455                     correctCaretPos();
2456                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
2457                 }
2458                 return true;
2459             case PageUp:
2460             case SelectPageUp:
2461                 {
2462                     ensureCaretVisible();
2463                     int fullLines = _clientRect.height / _lineHeight;
2464                     int newpos = _firstVisibleLine - fullLines;
2465                     if (newpos < 0) {
2466                         _firstVisibleLine = 0;
2467                         _caretPos.line = 0;
2468                     } else {
2469                         int delta = _firstVisibleLine - newpos;
2470                         _firstVisibleLine = newpos;
2471                         _caretPos.line -= delta;
2472                     }
2473                     correctCaretPos();
2474                     measureVisibleText();
2475                     updateScrollBars();
2476                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
2477                 }
2478                 return true;
2479             case PageDown:
2480             case SelectPageDown:
2481                 {
2482                     ensureCaretVisible();
2483                     int fullLines = _clientRect.height / _lineHeight;
2484                     int newpos = _firstVisibleLine + fullLines;
2485                     if (newpos >= _content.length) {
2486                         _caretPos.line = _content.length - 1;
2487                     } else {
2488                         int delta = newpos - _firstVisibleLine;
2489                         _firstVisibleLine = newpos;
2490                         _caretPos.line += delta;
2491                     }
2492                     correctCaretPos();
2493                     measureVisibleText();
2494                     updateScrollBars();
2495                     updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
2496                 }
2497                 return true;
2498             case ScrollLeft:
2499                 {
2500                     if (_scrollPos.x > 0) {
2501                         int newpos = _scrollPos.x - _spaceWidth * 4;
2502                         if (newpos < 0)
2503                             newpos = 0;
2504                         _scrollPos.x = newpos;
2505                         updateScrollBars();
2506                         invalidate();
2507                     }
2508                 }
2509                 return true;
2510             case ScrollRight:
2511                 {
2512                     if (_scrollPos.x < _maxLineWidth - _clientRect.width) {
2513                         int newpos = _scrollPos.x + _spaceWidth * 4;
2514                         if (newpos > _maxLineWidth - _clientRect.width)
2515                             newpos = _maxLineWidth - _clientRect.width;
2516                         _scrollPos.x = newpos;
2517                         updateScrollBars();
2518                         invalidate();
2519                     }
2520                 }
2521                 return true;
2522             case ScrollLineUp:
2523                 {
2524                     if (_firstVisibleLine > 0) {
2525                         _firstVisibleLine -= 3;
2526                         if (_firstVisibleLine < 0)
2527                             _firstVisibleLine = 0;
2528                         measureVisibleText();
2529                         updateScrollBars();
2530                         invalidate();
2531                     }
2532                 }
2533                 return true;
2534             case ScrollPageUp:
2535                 {
2536                     int fullLines = _clientRect.height / _lineHeight;
2537                     if (_firstVisibleLine > 0) {
2538                         _firstVisibleLine -= fullLines * 3 / 4;
2539                         if (_firstVisibleLine < 0)
2540                             _firstVisibleLine = 0;
2541                         measureVisibleText();
2542                         updateScrollBars();
2543                         invalidate();
2544                     }
2545                 }
2546                 return true;
2547             case ScrollLineDown:
2548                 {
2549                     int fullLines = _clientRect.height / _lineHeight;
2550                     if (_firstVisibleLine + fullLines < _content.length) {
2551                         _firstVisibleLine += 3;
2552                         if (_firstVisibleLine > _content.length - fullLines)
2553                             _firstVisibleLine = _content.length - fullLines;
2554                         if (_firstVisibleLine < 0)
2555                             _firstVisibleLine = 0;
2556                         measureVisibleText();
2557                         updateScrollBars();
2558                         invalidate();
2559                     }
2560                 }
2561                 return true;
2562             case ScrollPageDown:
2563                 {
2564                     int fullLines = _clientRect.height / _lineHeight;
2565                     if (_firstVisibleLine + fullLines < _content.length) {
2566                         _firstVisibleLine += fullLines * 3 / 4;
2567                         if (_firstVisibleLine > _content.length - fullLines)
2568                             _firstVisibleLine = _content.length - fullLines;
2569                         if (_firstVisibleLine < 0)
2570                             _firstVisibleLine = 0;
2571                         measureVisibleText();
2572                         updateScrollBars();
2573                         invalidate();
2574                     }
2575                 }
2576                 return true;
2577             case ZoomOut:
2578             case ZoomIn:
2579                 {
2580                     int dir = a.id == ZoomIn ? 1 : -1;
2581                     if (_minFontSize < _maxFontSize && _minFontSize > 0 && _maxFontSize > 0) {
2582                         int currentFontSize = fontSize;
2583                         int increment = currentFontSize >= 30 ? 2 : 1;
2584                         int newFontSize = currentFontSize + increment * dir; //* 110 / 100;
2585                         if (newFontSize > 30)
2586                             newFontSize &= 0xFFFE;
2587                         if (currentFontSize != newFontSize && newFontSize <= _maxFontSize && newFontSize >= _minFontSize) {
2588                             Log.i("Font size in editor ", id, " zoomed to ", newFontSize);
2589                             fontSize = cast(ushort)newFontSize;
2590                             updateFontProps();
2591                             measureVisibleText();
2592                             updateScrollBars();
2593                             invalidate();
2594                         }
2595                     }
2596                 }
2597                 return true;
2598             case ToggleBlockComment:
2599                 if (!readOnly && _content.syntaxSupport && _content.syntaxSupport.supportsToggleBlockComment && _content.syntaxSupport.canToggleBlockComment(_selectionRange))
2600                     _content.syntaxSupport.toggleBlockComment(_selectionRange, this);
2601                 return true;
2602             case ToggleLineComment:
2603                 if (!readOnly && _content.syntaxSupport && _content.syntaxSupport.supportsToggleLineComment && _content.syntaxSupport.canToggleLineComment(_selectionRange))
2604                     _content.syntaxSupport.toggleLineComment(_selectionRange, this);
2605                 return true;
2606             case AppendNewLine:
2607                 if (!readOnly) {
2608                     correctCaretPos();
2609                     TextPosition p = _content.lineEnd(_caretPos.line);
2610                     TextRange r = TextRange(p, p);
2611                     EditOperation op = new EditOperation(EditAction.Replace, r, [""d, ""d]);
2612                     _content.performOperation(op, this);
2613                     _caretPos = oldCaretPos;
2614                 }
2615                 return true;
2616             case DeleteLine:
2617                 if (!readOnly) {
2618                     correctCaretPos();
2619                     EditOperation op = new EditOperation(EditAction.Replace, _content.lineRange(_caretPos.line), [""d]);
2620                     _content.performOperation(op, this);
2621                 }
2622                 return true;
2623             case Find:
2624                 createFindPanel(false, false);
2625                 return true;
2626             case Replace:
2627                 createFindPanel(false, true);
2628                 return true;
2629             default:
2630                 break;
2631         }
2632         return super.handleAction(a);
2633     }
2634 
2635     /// calculate full content size in pixels
2636     override Point fullContentSize() {
2637         Point textSz = measureVisibleText();
2638         int maxy = _lineHeight * 5; // limit measured height
2639         if (textSz.y > maxy)
2640             textSz.y = maxy;
2641         return textSz;
2642     }
2643 
2644     /// measure
2645     override void measure(int parentWidth, int parentHeight) { 
2646         if (visibility == Visibility.Gone) {
2647             return;
2648         }
2649         updateFontProps();
2650         updateMaxLineWidth();
2651         int findPanelHeight;
2652         if (_findPanel) {
2653             _findPanel.measure(parentWidth, parentHeight);
2654             findPanelHeight = _findPanel.measuredHeight;
2655             if (parentHeight != SIZE_UNSPECIFIED)
2656                 parentHeight -= findPanelHeight;
2657         }
2658 
2659         super.measure(parentWidth, parentHeight);
2660         // do we need to add vsbwidth, hsbheight ???
2661         //measuredContent(parentWidth, parentHeight, textSz.x + vsbwidth, textSz.y + hsbheight);
2662     }
2663 
2664 
2665     protected void highlightLineRange(DrawBuf buf, Rect lineRect, uint color, TextRange r) {
2666         Rect startrc = textPosToClient(r.start);
2667         Rect endrc = textPosToClient(r.end);
2668         Rect rc = lineRect;
2669         rc.left = _clientRect.left + startrc.left;
2670         rc.right = _clientRect.left + endrc.right;
2671         if (!rc.empty) {
2672             // draw selection rect for matching bracket
2673             buf.fillRect(rc, color);
2674         }
2675     }
2676 
2677     /// override to custom highlight of line background
2678     protected void drawLineBackground(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) {
2679         // highlight odd lines
2680         //if ((lineIndex & 1))
2681         //    buf.fillRect(visibleRect, 0xF4808080);
2682 
2683         if (!_selectionRange.empty && _selectionRange.start.line <= lineIndex && _selectionRange.end.line >= lineIndex) {
2684             // line inside selection
2685             Rect startrc = textPosToClient(_selectionRange.start);
2686             Rect endrc = textPosToClient(_selectionRange.end);
2687             int startx = lineIndex == _selectionRange.start.line ? startrc.left + _clientRect.left : lineRect.left;
2688             int endx = lineIndex == _selectionRange.end.line ? endrc.left + _clientRect.left : lineRect.right + _spaceWidth;
2689             Rect rc = lineRect;
2690             rc.left = startx;
2691             rc.right = endx;
2692             if (!rc.empty) {
2693                 // draw selection rect for line
2694                 buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal);
2695             }
2696         }
2697 
2698         if (_matchingBraces.start.line == lineIndex)  {
2699             TextRange r = TextRange(_matchingBraces.start, _matchingBraces.start.offset(1));
2700             highlightLineRange(buf, lineRect, _matchingBracketHightlightColor, r);
2701         }
2702         if (_matchingBraces.end.line == lineIndex)  {
2703             TextRange r = TextRange(_matchingBraces.end, _matchingBraces.end.offset(1));
2704             highlightLineRange(buf, lineRect, _matchingBracketHightlightColor, r);
2705         }
2706 
2707         // frame around current line
2708         if (focused && lineIndex == _caretPos.line && _selectionRange.singleLine && _selectionRange.start.line == _caretPos.line) {
2709             buf.drawFrame(visibleRect, 0xA0808080, Rect(1,1,1,1));
2710         }
2711 
2712     }
2713 
2714     override protected void drawExtendedArea(DrawBuf buf) {
2715         if (_leftPaneWidth <= 0)
2716             return;
2717         Rect rc = _clientRect;
2718 
2719         FontRef font = font();
2720         int i = _firstVisibleLine;
2721         int lc = lineCount;
2722         for (;;) {
2723             Rect lineRect = rc;
2724             lineRect.left = _clientRect.left - _leftPaneWidth;
2725             lineRect.right = _clientRect.left;
2726             lineRect.bottom = lineRect.top + _lineHeight;
2727             if (lineRect.top >= _clientRect.bottom)
2728                 break;
2729             drawLeftPane(buf, lineRect, i < lc ? i : -1);
2730             i++;
2731             rc.top += _lineHeight;
2732         }
2733     }
2734 
2735 
2736     protected CustomCharProps[ubyte] _tokenHighlightColors;
2737 
2738     /// set highlight options for particular token category
2739     void setTokenHightlightColor(ubyte tokenCategory, uint color, bool underline = false, bool strikeThrough = false) {
2740          _tokenHighlightColors[tokenCategory] = CustomCharProps(color, underline, strikeThrough);
2741     }
2742     /// clear highlight colors
2743     void clearTokenHightlightColors() {
2744         destroy(_tokenHighlightColors);
2745     }
2746 
2747     /** 
2748         Custom text color and style highlight (using text highlight) support.
2749 
2750         Return null if no syntax highlight required for line.
2751      */
2752     protected CustomCharProps[] handleCustomLineHighlight(int line, dstring txt, ref CustomCharProps[] buf) {
2753         if (!_tokenHighlightColors)
2754             return null; // no highlight colors set
2755         TokenPropString tokenProps = _content.lineTokenProps(line);
2756         if (tokenProps.length > 0) {
2757             bool hasNonzeroTokens = false;
2758             foreach(t; tokenProps)
2759                 if (t) {
2760                     hasNonzeroTokens = true;
2761                     break;
2762                 }
2763             if (!hasNonzeroTokens)
2764                 return null; // all characters are of unknown token type (or white space)
2765             if (buf.length < tokenProps.length)
2766                 buf.length = tokenProps.length;
2767             CustomCharProps[] colors = buf[0..tokenProps.length]; //new CustomCharProps[tokenProps.length];
2768             for (int i = 0; i < tokenProps.length; i++) {
2769                 ubyte p = tokenProps[i];
2770                 if (p in _tokenHighlightColors)
2771                     colors[i] = _tokenHighlightColors[p];
2772                 else if ((p & TOKEN_CATEGORY_MASK) in _tokenHighlightColors)
2773                     colors[i] = _tokenHighlightColors[(p & TOKEN_CATEGORY_MASK)];
2774                 else
2775                     colors[i].color = textColor;
2776                 if (isFullyTransparentColor(colors[i].color))
2777                     colors[i].color = textColor;
2778             }
2779             return colors;
2780         }
2781         return null;
2782     }
2783 
2784     TextRange _matchingBraces;
2785 
2786     bool _showWhiteSpaceMarks;
2787     /// when true, show marks for tabs and spaces at beginning and end of line, and tabs inside line
2788     @property bool showWhiteSpaceMarks() const { return _showWhiteSpaceMarks; }
2789     @property void showWhiteSpaceMarks(bool show) { 
2790         if (_showWhiteSpaceMarks != show) {
2791             _showWhiteSpaceMarks = show; 
2792             invalidate();
2793         }
2794     }
2795 
2796     /// find max tab mark column position for line
2797     protected int findMaxTabMarkColumn(int lineIndex) {
2798         if (lineIndex < 0 || lineIndex >= content.length)
2799             return -1;
2800         int maxSpace = -1;
2801         auto space = content.getLineWhiteSpace(lineIndex);
2802         maxSpace = space.firstNonSpaceColumn;
2803         if (maxSpace >= 0)
2804             return maxSpace;
2805         for(int i = lineIndex - 1; i >= 0; i--) {
2806             space = content.getLineWhiteSpace(i);
2807             if (!space.empty) {
2808                 maxSpace = space.firstNonSpaceColumn;
2809                 break;
2810             }
2811         }
2812         for(int i = lineIndex + 1; i < content.length; i++) {
2813             space = content.getLineWhiteSpace(i);
2814             if (!space.empty) {
2815                 if (maxSpace < 0 || maxSpace < space.firstNonSpaceColumn)
2816                     maxSpace = space.firstNonSpaceColumn;
2817                 break;
2818             }
2819         }
2820         return maxSpace;
2821     }
2822 
2823     void drawTabPositionMarks(DrawBuf buf, ref FontRef font, int lineIndex, Rect lineRect) {
2824         int maxCol = findMaxTabMarkColumn(lineIndex);
2825         if (maxCol > 0) {
2826             int spaceWidth = font.charWidth(' ');
2827             Rect rc = lineRect;
2828             uint color = addAlpha(textColor, 0xC0);
2829             for (int i = 0; i < maxCol; i += tabSize) {
2830                 rc.left = lineRect.left + i * spaceWidth;
2831                 rc.right = rc.left + 1;
2832                 buf.fillRectPattern(rc, color, PatternType.dotted);
2833             }
2834         }
2835     }
2836 
2837     void drawWhiteSpaceMarks(DrawBuf buf, ref FontRef font, dstring txt, int tabSize, Rect lineRect, Rect visibleRect) {
2838         // _showTabPositionMarks
2839         // _showWhiteSpaceMarks
2840         int firstNonSpace = -1;
2841         int lastNonSpace = -1;
2842         bool hasTabs = false;
2843         for(int i = 0; i < txt.length; i++) {
2844             if (txt[i] == '\t') {
2845                 hasTabs = true;
2846             } else if (txt[i] != ' ') {
2847                 if (firstNonSpace == -1)
2848                     firstNonSpace = i;
2849                 lastNonSpace = i + 1;
2850             }
2851         }
2852         if (firstNonSpace <= 0 && lastNonSpace >= txt.length && !hasTabs)
2853             return;
2854         uint color = addAlpha(textColor, 0xC0);
2855         static int[] textSizeBuffer;
2856         int charsMeasured = font.measureText(txt, textSizeBuffer, MAX_WIDTH_UNSPECIFIED, tabSize, 0, 0);
2857         int ts = tabSize;
2858         if (ts < 1)
2859             ts = 1;
2860         if (ts > 8)
2861             ts = 8;
2862         int spaceIndex = 0;
2863         for (int i = 0; i < txt.length && i < charsMeasured; i++) {
2864             dchar ch = txt[i];
2865             bool outsideText = (i < firstNonSpace || i >= lastNonSpace);
2866             if ((ch == ' ' && outsideText) || ch == '\t') {
2867                 Rect rc = lineRect;
2868                 rc.left = lineRect.left + (i > 0 ? textSizeBuffer[i - 1] : 0);
2869                 rc.right = lineRect.left + textSizeBuffer[i];
2870                 int h = rc.height;
2871                 if (rc.intersects(visibleRect)) {
2872                     // draw space mark
2873                     if (ch == ' ') {
2874                         // space
2875                         int sz = h / 6;
2876                         if (sz < 1)
2877                             sz = 1;
2878                         rc.top += h / 2 - sz / 2;
2879                         rc.bottom = rc.top + sz;
2880                         rc.left += rc.width / 2 - sz / 2;
2881                         rc.right = rc.left + sz;
2882                         buf.fillRect(rc, color);
2883                     } else if (ch == '\t') {
2884                         // tab
2885                         Point p1 = Point(rc.left + 1, rc.top + h / 2);
2886                         Point p2 = p1;
2887                         p2.x = rc.right - 1;
2888                         int sz = h / 4;
2889                         if (sz < 2)
2890                             sz = 2;
2891                         if (sz > p2.x - p1.x)
2892                             sz = p2.x - p1.x;
2893                         buf.drawLine(p1, p2, color);
2894                         buf.drawLine(p2, Point(p2.x - sz, p2.y - sz), color);
2895                         buf.drawLine(p2, Point(p2.x - sz, p2.y + sz), color);
2896                     }
2897                 }
2898             }
2899         }
2900     }
2901 
2902     override protected void drawClient(DrawBuf buf) {
2903         // update matched braces
2904         if (!content.findMatchedBraces(_caretPos, _matchingBraces)) {
2905             _matchingBraces.start.line = -1;
2906             _matchingBraces.end.line = -1;
2907         }
2908 
2909         Rect rc = _clientRect;
2910 
2911         FontRef font = font();
2912         for (int i = 0; i < _visibleLines.length; i++) {
2913             dstring txt = _visibleLines[i];
2914             Rect lineRect = rc;
2915             lineRect.left = _clientRect.left - _scrollPos.x;
2916             lineRect.right = lineRect.left + calcLineWidth(_content[_firstVisibleLine + i]);
2917             lineRect.top = _clientRect.top + i * _lineHeight;
2918             lineRect.bottom = lineRect.top + _lineHeight;
2919             Rect visibleRect = lineRect;
2920             visibleRect.left = _clientRect.left;
2921             visibleRect.right = _clientRect.right;
2922             drawLineBackground(buf, _firstVisibleLine + i, lineRect, visibleRect);
2923             if (_showTabPositionMarks)
2924                 drawTabPositionMarks(buf, font, _firstVisibleLine + i, lineRect);
2925             if (!txt.length)
2926                 continue;
2927             if (_showWhiteSpaceMarks)
2928                 drawWhiteSpaceMarks(buf, font, txt, tabSize, lineRect, visibleRect);
2929             if (_leftPaneWidth > 0) {
2930                 Rect leftPaneRect = visibleRect;
2931                 leftPaneRect.right = leftPaneRect.left;
2932                 leftPaneRect.left -= _leftPaneWidth;
2933                 drawLeftPane(buf, leftPaneRect, 0);
2934             }
2935             if (txt.length > 0) {
2936                 CustomCharProps[] highlight = _visibleLinesHighlights[i];
2937                 if (highlight)
2938                     font.drawColoredText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, highlight, tabSize);
2939                 else
2940                     font.drawText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, textColor, tabSize);
2941             }
2942         }
2943 
2944         drawCaret(buf);
2945     }
2946 
2947     protected override bool onLeftPaneMouseClick(MouseEvent event) {
2948         if (_leftPaneWidth <= 0)
2949             return false;
2950         Rect rc = _clientRect;
2951         FontRef font = font();
2952         int i = _firstVisibleLine;
2953         int lc = lineCount;
2954         for (;;) {
2955             Rect lineRect = rc;
2956             lineRect.left = _clientRect.left - _leftPaneWidth;
2957             lineRect.right = _clientRect.left;
2958             lineRect.bottom = lineRect.top + _lineHeight;
2959             if (lineRect.top >= _clientRect.bottom)
2960                 break;
2961             if (event.y >= lineRect.top && event.y < lineRect.bottom) {
2962                 return handleLeftPaneMouseClick(event, lineRect, i);
2963             }
2964             i++;
2965             rc.top += _lineHeight;
2966         }
2967         return false;
2968     }
2969 
2970     override protected MenuItem getLeftPaneIconsPopupMenu(int line) {
2971         MenuItem menu = new MenuItem();
2972         Action toggleBookmarkAction = ACTION_EDITOR_TOGGLE_BOOKMARK.clone();
2973         toggleBookmarkAction.longParam = line;
2974         toggleBookmarkAction.objectParam = this;
2975         MenuItem item = menu.add(toggleBookmarkAction);
2976         return menu;
2977     }
2978 
2979     protected FindPanel _findPanel;
2980 
2981     /// create find panel
2982     protected void createFindPanel(bool selectionOnly, bool replaceMode) {
2983         _findPanel = new FindPanel(selectionOnly, replaceMode);
2984         addChild(_findPanel);
2985         requestLayout();
2986     }
2987 
2988     /// close find panel
2989     protected void closeFindPanel() {
2990         if (_findPanel) {
2991             removeChild(_findPanel);
2992             destroy(_findPanel);
2993             _findPanel = null;
2994             requestLayout();
2995         }
2996     }
2997 
2998     /// Draw widget at its position to buffer
2999     override void onDraw(DrawBuf buf) {
3000         if (visibility != Visibility.Visible)
3001             return;
3002         super.onDraw(buf);
3003         if (_findPanel && _findPanel.visibility == Visibility.Visible) {
3004             _findPanel.onDraw(buf);
3005         }
3006     }
3007 }
3008 
3009 /// Read only edit box for displaying logs with lines append operation
3010 class LogWidget : EditBox {
3011 
3012     protected int  _maxLines;
3013     /// max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit
3014     @property int maxLines() { return _maxLines; }
3015     /// set max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit
3016     @property void maxLines(int n) { _maxLines = n; }
3017 
3018     protected bool _scrollLock;
3019     /// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction)
3020     @property bool scrollLock() { return _scrollLock; }
3021     /// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction)
3022     @property void scrollLock(bool flg) { _scrollLock = flg; }
3023 
3024     this() {
3025         this(null);
3026     }
3027 
3028     this(string ID) {
3029         super(ID);
3030         _scrollLock = true;
3031         _enableScrollAfterText = false;
3032         enabled = false;
3033         fontSize = makePointSize(9);
3034         //fontFace = "Consolas,Lucida Console,Courier New";
3035         fontFace = "Menlo,Consolas,DejaVuSansMono,DejaVu Sans Mono,Lucida Sans Typewriter,Courier New,Lucida Console";
3036         fontFamily = FontFamily.MonoSpace;
3037         minFontSize(pointsToPixels(6)).maxFontSize(pointsToPixels(32)); // allow font zoom with Ctrl + MouseWheel
3038         onThemeChanged();
3039     }
3040 
3041     /// append lines to the end of text
3042     void appendText(dstring text) {
3043         import std.array : split;
3044         if (text.length == 0)
3045             return;
3046         dstring[] lines = text.split("\n");
3047         //lines ~= ""d; // append new line after last line
3048         content.appendLines(lines);
3049         if (_maxLines > 0 && lineCount > _maxLines) {
3050             TextRange range;
3051             range.end.line = lineCount - _maxLines;
3052             EditOperation op = new EditOperation(EditAction.Replace, range, [""d]);
3053             _content.performOperation(op, this);
3054             _contentChanged = true;
3055         }
3056         updateScrollBars();
3057         if (_scrollLock) {
3058             _caretPos = lastLineBegin();
3059             ensureCaretVisible();
3060         }
3061     }
3062 
3063     TextPosition lastLineBegin() {
3064         TextPosition res;
3065         if (_content.length == 0)
3066             return res;
3067         if (_content.lineLength(_content.length - 1) == 0 && _content.length > 1)
3068             res.line = _content.length - 2;
3069         else
3070             res.line = _content.length - 1;
3071         return res;
3072     }
3073 
3074     /// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
3075     override void layout(Rect rc) {
3076         if (visibility == Visibility.Gone) {
3077             return;
3078         }
3079         super.layout(rc);
3080         if (_scrollLock) {
3081             measureVisibleText();
3082             _caretPos = lastLineBegin();
3083             ensureCaretVisible();
3084         }
3085     }
3086 
3087 }
3088 
3089 class FindPanel : HorizontalLayout {
3090     protected EditLine _edFind;
3091     protected EditLine _edReplace;
3092     protected ImageCheckButton _cbCaseSensitive;
3093     protected ImageCheckButton _cbWholeWords;
3094     protected CheckBox _cbSelection;
3095     protected Button _btnFindNext;
3096     protected Button _btnFindPrev;
3097     protected Button _btnReplace;
3098     protected Button _btnReplaceAll;
3099     this(bool selectionOnly, bool replace) {
3100         import dlangui.dml.parser;
3101         try {
3102             parseML(q{
3103                 {
3104                     layoutWidth: fill
3105                     VerticalLayout {
3106                         layoutWidth: fill
3107                         HorizontalLayout {
3108                             layoutWidth: fill
3109                             EditLine { id: edFind; layoutWidth: fill; alignment: vcenter }
3110                             Button { id: btnFindNext; text: EDIT_FIND_NEXT }
3111                             Button { id: btnFindPrev; text: EDIT_FIND_PREV }
3112                         }
3113                         HorizontalLayout {
3114                             id: replace
3115                             layoutWidth: fill;
3116                             EditLine { id: edReplace; layoutWidth: fill; alignment: vcenter }
3117                             Button { id: btnReplace; text: EDIT_REPLACE_NEXT }
3118                             Button { id: btnReplaceAll; text: EDIT_REPLACE_ALL }
3119                         }
3120                     }
3121                     VerticalLayout {
3122                         HorizontalLayout {
3123                             ImageCheckButton { id: cbCaseSensitive; drawableId: "find_case_sensitive"; tooltipText: EDIT_FIND_CASE_SENSITIVE; styleId: TOOLBAR_BUTTON; alignment: vcenter }
3124                             ImageCheckButton { id: cbWholeWords; drawableId: "find_whole_words"; tooltipText: EDIT_FIND_WHOLE_WORDS; styleId: TOOLBAR_BUTTON; alignment: vcenter }
3125                             CheckBox { id: cbSelection; text: "Sel" }
3126                         }
3127                         VSpacer {}
3128                     }
3129                     VerticalLayout {
3130                         ImageButton { id: btnClose; drawableId: "close"; styleId: BUTTON_TRANSPARENT }
3131                         VSpacer {}
3132                     }
3133                 }
3134             }, null, this);
3135         } catch (Exception e) {
3136             Log.e("Exception while parsing DML: ", e);
3137         }
3138         _edFind = childById!EditLine("edFind");
3139         _edReplace = childById!EditLine("edReplace");
3140         _btnFindNext = childById!Button("btnFindNext");
3141         _btnFindPrev = childById!Button("btnFindPrev");
3142         _btnReplace = childById!Button("btnReplace");
3143         _btnReplaceAll = childById!Button("btnReplaceAll");
3144         _cbCaseSensitive = childById!ImageCheckButton("cbCaseSensitive");
3145         _cbWholeWords = childById!ImageCheckButton("cbWholeWords");
3146         _cbSelection =  childById!CheckBox("cbSelection");
3147         if (!replace)
3148             childById("replace").visibility = Visibility.Gone;
3149         //_edFind = new EditLine("edFind"
3150     }
3151 }
3152 
3153 //import dlangui.widgets.metadata;
3154 //mixin(registerWidgets!(EditLine, EditBox, LogWidget)());