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