dlangui/src/dlangui/widgets/editors.d

2922 lines
106 KiB
D

// Written in the D programming language.
/**
This module contains implementation of editors.
EditLine - single line editor.
EditBox - multiline editor
LogWidget - readonly text box for showing logs
Synopsis:
----
import dlangui.widgets.editors;
----
Copyright: Vadim Lopatin, 2014
License: Boost License 1.0
Authors: Vadim Lopatin, coolreader.org@gmail.com
*/
module dlangui.widgets.editors;
import dlangui.widgets.widget;
import dlangui.widgets.controls;
import dlangui.widgets.scroll;
import dlangui.core.signals;
import dlangui.core.collections;
import dlangui.core.linestream;
import dlangui.platforms.common.platform;
import dlangui.widgets.menu;
import dlangui.widgets.popup;
private import dlangui.graphics.colors;
import std.algorithm;
import std.stream;
immutable dchar EOL = '\n';
const ubyte TOKEN_CATEGORY_SHIFT = 4;
const ubyte TOKEN_CATEGORY_MASK = 0xF0; // token category 0..15
const ubyte TOKEN_SUBCATEGORY_MASK = 0x0F; // token subcategory 0..15
const ubyte TOKEN_UNKNOWN = 0;
/*
Bit mask:
7654 3210
cccc ssss
| |
| \ ssss = token subcategory
|
\ cccc = token category
*/
/// token category for syntax highlight
enum TokenCategory : ubyte {
WhiteSpace = (0 << TOKEN_CATEGORY_SHIFT),
WhiteSpace_Space = (0 << TOKEN_CATEGORY_SHIFT) | 1,
WhiteSpace_Tab = (0 << TOKEN_CATEGORY_SHIFT) | 2,
Comment = (1 << TOKEN_CATEGORY_SHIFT),
Comment_SingleLine = (1 << TOKEN_CATEGORY_SHIFT) | 1, // single line comment
Comment_SingleLineDoc = (1 << TOKEN_CATEGORY_SHIFT) | 2,// documentation in single line comment
Comment_MultyLine = (1 << TOKEN_CATEGORY_SHIFT) | 3, // multiline coment
Comment_MultyLineDoc = (1 << TOKEN_CATEGORY_SHIFT) | 4, // documentation in multiline comment
Comment_Documentation = (1 << TOKEN_CATEGORY_SHIFT) | 5,// documentation comment
Identifier = (2 << TOKEN_CATEGORY_SHIFT), // identifier (exact subcategory is unknown)
Identifier_Class = (2 << TOKEN_CATEGORY_SHIFT) | 1, // class name
Identifier_Struct = (2 << TOKEN_CATEGORY_SHIFT) | 2, // struct name
Identifier_Local = (2 << TOKEN_CATEGORY_SHIFT) | 3, // local variable
Identifier_Member = (2 << TOKEN_CATEGORY_SHIFT) | 4, // struct or class member
Identifier_Deprecated = (2 << TOKEN_CATEGORY_SHIFT) | 15, // usage of this identifier is deprecated
/// string literal
String = (3 << TOKEN_CATEGORY_SHIFT),
/// character literal
Character = (4 << TOKEN_CATEGORY_SHIFT),
/// integer literal
Integer = (5 << TOKEN_CATEGORY_SHIFT),
/// floating point number literal
Float = (6 << TOKEN_CATEGORY_SHIFT),
/// keyword
Keyword = (7 << TOKEN_CATEGORY_SHIFT),
/// operator
Op = (8 << TOKEN_CATEGORY_SHIFT),
// add more here
//....
/// error - unparsed character sequence
Error = (15 << TOKEN_CATEGORY_SHIFT),
/// invalid token - generic
Error_InvalidToken = (15 << TOKEN_CATEGORY_SHIFT) | 1,
/// invalid number token - error occured while parsing number
Error_InvalidNumber = (15 << TOKEN_CATEGORY_SHIFT) | 2,
/// invalid string token - error occured while parsing string
Error_InvalidString = (15 << TOKEN_CATEGORY_SHIFT) | 3,
/// invalid identifier token - error occured while parsing identifier
Error_InvalidIdentifier = (15 << TOKEN_CATEGORY_SHIFT) | 4,
/// invalid comment token - error occured while parsing comment
Error_InvalidComment = (15 << TOKEN_CATEGORY_SHIFT) | 4,
}
/// Editor action codes
enum EditorActions : int {
None = 0,
/// move cursor one char left
Left = 1000,
/// move cursor one char left with selection
SelectLeft,
/// move cursor one char right
Right,
/// move cursor one char right with selection
SelectRight,
/// move cursor one line up
Up,
/// move cursor one line up with selection
SelectUp,
/// move cursor one line down
Down,
/// move cursor one line down with selection
SelectDown,
/// move cursor one word left
WordLeft,
/// move cursor one word left with selection
SelectWordLeft,
/// move cursor one word right
WordRight,
/// move cursor one word right with selection
SelectWordRight,
/// move cursor one page up
PageUp,
/// move cursor one page up with selection
SelectPageUp,
/// move cursor one page down
PageDown,
/// move cursor one page down with selection
SelectPageDown,
/// move cursor to the beginning of page
PageBegin,
/// move cursor to the beginning of page with selection
SelectPageBegin,
/// move cursor to the end of page
PageEnd,
/// move cursor to the end of page with selection
SelectPageEnd,
/// move cursor to the beginning of line
LineBegin,
/// move cursor to the beginning of line with selection
SelectLineBegin,
/// move cursor to the end of line
LineEnd,
/// move cursor to the end of line with selection
SelectLineEnd,
/// move cursor to the beginning of document
DocumentBegin,
/// move cursor to the beginning of document with selection
SelectDocumentBegin,
/// move cursor to the end of document
DocumentEnd,
/// move cursor to the end of document with selection
SelectDocumentEnd,
/// delete char before cursor (backspace)
DelPrevChar,
/// delete char after cursor (del key)
DelNextChar,
/// delete word before cursor (ctrl + backspace)
DelPrevWord,
/// delete char after cursor (ctrl + del key)
DelNextWord,
/// insert new line (Enter)
InsertNewLine,
/// insert new line after current position (Ctrl+Enter)
PrependNewLine,
/// Turn On/Off replace mode
ToggleReplaceMode,
/// Copy selection to clipboard
Copy,
/// Cut selection to clipboard
Cut,
/// Paste selection from clipboard
Paste,
/// Undo last change
Undo,
/// Redo last undoed change
Redo,
/// Tab (e.g., Tab key to insert tab character or indent text)
Tab,
/// Tab (unindent text, or remove whitespace before cursor, usually Shift+Tab)
BackTab,
/// Select whole content (usually, Ctrl+A)
SelectAll,
// Scroll operations
/// Scroll one line up (not changing cursor)
ScrollLineUp,
/// Scroll one line down (not changing cursor)
ScrollLineDown,
/// Scroll one page up (not changing cursor)
ScrollPageUp,
/// Scroll one page down (not changing cursor)
ScrollPageDown,
/// Scroll window left
ScrollLeft,
/// Scroll window right
ScrollRight,
/// Zoom in editor font
ZoomIn,
/// Zoom out editor font
ZoomOut,
}
/// split dstring by delimiters
dstring[] splitDString(dstring source, dchar delimiter = EOL) {
int start = 0;
dstring[] res;
dchar lastchar;
for (int i = 0; i <= source.length; i++) {
if (i == source.length || source[i] == delimiter) {
if (i >= start) {
dchar prevchar = i > 1 && i > start + 1 ? source[i - 1] : 0;
int end = i;
if (delimiter == EOL && prevchar == '\r') // windows CR/LF
end--;
dstring line = i > start ? cast(dstring)(source[start .. end].dup) : ""d;
res ~= line;
}
start = i + 1;
}
}
return res;
}
version (Windows) {
immutable dstring SYSTEM_DEFAULT_EOL = "\r\n";
} else {
immutable dstring SYSTEM_DEFAULT_EOL = "\n";
}
/// concat strings from array using delimiter
dstring concatDStrings(dstring[] lines, dstring delimiter = SYSTEM_DEFAULT_EOL) {
dchar[] buf;
foreach(line; lines) {
if (buf.length)
buf ~= delimiter;
buf ~= line;
}
return cast(dstring)buf;
}
/// replace end of lines with spaces
dstring replaceEolsWithSpaces(dstring source) {
dchar[] buf;
dchar lastch;
foreach(ch; source) {
if (ch == '\r') {
buf ~= ' ';
} else if (ch == '\n') {
if (lastch != '\r')
buf ~= ' ';
} else {
buf ~= ch;
}
lastch = ch;
}
return cast(dstring)buf;
}
/// text content position
struct TextPosition {
/// line number, zero based
int line;
/// character position in line (0 == before first character)
int pos;
/// compares two positions
int opCmp(ref const TextPosition v) const {
if (line < v.line)
return -1;
if (line > v.line)
return 1;
if (pos < v.pos)
return -1;
if (pos > v.pos)
return 1;
return 0;
}
}
/// text content range
struct TextRange {
TextPosition start;
TextPosition end;
/// returns true if range is empty
@property bool empty() const {
return end <= start;
}
/// returns true if start and end located at the same line
@property bool singleLine() const {
return end.line == start.line;
}
/// returns count of lines in range
@property int lines() const {
return end.line - start.line + 1;
}
}
/// action performed with editable contents
enum EditAction {
/// insert content into specified position (range.start)
//Insert,
/// delete content in range
//Delete,
/// replace range content with new content
Replace,
/// replace whole content
ReplaceContent,
}
/// edit operation details for EditableContent
class EditOperation {
protected EditAction _action;
/// action performed
@property EditAction action() { return _action; }
protected TextRange _range;
/// source range to replace with new content
@property ref TextRange range() { return _range; }
protected TextRange _newRange;
/// new range after operation applied
@property ref TextRange newRange() { return _newRange; }
@property void newRange(TextRange range) { _newRange = range; }
/// new content for range (if required for this action)
protected dstring[] _content;
@property ref dstring[] content() { return _content; }
/// old content for range
protected dstring[] _oldContent;
@property ref dstring[] oldContent() { return _oldContent; }
@property void oldContent(dstring[] content) { _oldContent = content; }
this(EditAction action) {
_action = action;
}
this(EditAction action, TextPosition pos, dstring text) {
this(action, TextRange(pos, pos), text);
}
this(EditAction action, TextRange range, dstring text) {
_action = action;
_range = range;
_content.length = 1;
_content[0] = text.dup;
}
this(EditAction action, TextRange range, dstring[] text) {
_action = action;
_range = range;
_content.length = text.length;
for(int i = 0; i < text.length; i++)
_content[i] = text[i].dup;
//_content = text;
}
/// try to merge two operations (simple entering of characters in the same line), return true if succeded
bool merge(EditOperation op) {
if (_range.start.line != op._range.start.line) // both ops whould be on the same line
return false;
if (_content.length != 1 || op._content.length != 1) // both ops should operate the same line
return false;
// appending of single character
if (_range.empty && op._range.empty && op._content[0].length == 1 && _newRange.end.pos == op._range.start.pos) {
_content[0] ~= op._content[0];
_newRange.end.pos++;
return true;
}
// removing single character
if (_newRange.empty && op._newRange.empty && op._oldContent[0].length == 1) {
if (_newRange.end.pos == op.range.end.pos) {
// removed char before
_range.start.pos--;
_newRange.start.pos--;
_newRange.end.pos--;
_oldContent[0] = (op._oldContent[0].dup ~ _oldContent[0].dup).dup;
return true;
} else if (_newRange.end.pos == op._range.start.pos) {
// removed char after
_range.end.pos++;
_oldContent[0] = (_oldContent[0].dup ~ op._oldContent[0].dup).dup;
return true;
}
}
return false;
}
}
/// Undo/Redo buffer
class UndoBuffer {
protected Collection!EditOperation _undoList;
protected Collection!EditOperation _redoList;
/// returns true if buffer contains any undo items
@property bool hasUndo() {
return !_undoList.empty;
}
/// returns true if buffer contains any redo items
@property bool hasRedo() {
return !_redoList.empty;
}
/// adds undo operation
void saveForUndo(EditOperation op) {
_redoList.clear();
if (!_undoList.empty) {
if (_undoList.back.merge(op)) {
return; // merged - no need to add new operation
}
}
_undoList.pushBack(op);
}
/// returns operation to be undone (put it to redo), null if no undo ops available
EditOperation undo() {
if (!hasUndo)
return null; // no undo operations
EditOperation res = _undoList.popBack();
_redoList.pushBack(res);
return res;
}
/// returns operation to be redone (put it to undo), null if no undo ops available
EditOperation redo() {
if (!hasRedo)
return null; // no undo operations
EditOperation res = _redoList.popBack();
_undoList.pushBack(res);
return res;
}
/// clears both undo and redo buffers
void clear() {
_undoList.clear();
_redoList.clear();
}
}
/// Editable Content change listener
interface EditableContentListener {
void onContentChange(EditableContent content, EditOperation operation, ref TextRange rangeBefore, ref TextRange rangeAfter, Object source);
}
alias TokenPropString = ubyte[];
/// interface for custom syntax highlight
interface SyntaxHighlighter {
/// categorize characters in content by token types
void updateHighlight(dstring[] lines, TokenPropString[] props, int changeStartLine, int changeEndLine);
}
/// editable plain text (singleline/multiline)
class EditableContent {
this(bool multiline) {
_multiline = multiline;
_lines.length = 1; // initial state: single empty line
_undoBuffer = new UndoBuffer();
}
protected UndoBuffer _undoBuffer;
protected SyntaxHighlighter _syntaxHighlighter;
@property SyntaxHighlighter syntaxHighlighter() {
return _syntaxHighlighter;
}
@property EditableContent syntaxHighlighter(SyntaxHighlighter syntaxHighlighter) {
_syntaxHighlighter = syntaxHighlighter;
updateTokenProps(0, cast(int)_lines.length);
return this;
}
/// returns true if content has syntax highlight handler set
@property bool hasSyntaxHighlight() {
return _syntaxHighlighter !is null;
}
protected bool _readOnly;
@property bool readOnly() {
return _readOnly;
}
@property void readOnly(bool readOnly) {
_readOnly = readOnly;
}
/// listeners for edit operations
Signal!EditableContentListener contentChangeListeners;
protected bool _multiline;
/// returns true if miltyline content is supported
@property bool multiline() { return _multiline; }
protected dstring[] _lines;
protected TokenPropString[] _tokenProps;
/// returns all lines concatenated delimited by '\n'
@property dstring text() {
if (_lines.length == 0)
return "";
if (_lines.length == 1)
return _lines[0];
// concat lines
dchar[] buf;
foreach(item;_lines) {
if (buf.length)
buf ~= EOL;
buf ~= item;
}
return cast(dstring)buf;
}
/// append one or more lines at end
void appendLines(dstring[] lines...) {
TextRange rangeBefore;
rangeBefore.start = rangeBefore.end = lineEnd(_lines.length ? cast(int)_lines.length - 1 : 0);
EditOperation op = new EditOperation(EditAction.Replace, rangeBefore, lines);
performOperation(op, this);
}
/// call listener to say that whole content is replaced e.g. by loading from file
void notifyContentReplaced() {
TextRange rangeBefore;
TextRange rangeAfter;
// notify about content change
handleContentChange(new EditOperation(EditAction.ReplaceContent), rangeBefore, rangeAfter, this);
}
protected void updateTokenProps(int startLine, int endLine) {
clearTokenProps(startLine, endLine);
if (_syntaxHighlighter) {
_syntaxHighlighter.updateHighlight(_lines, _tokenProps, startLine, endLine);
}
}
/// set props arrays size equal to text line sizes, bit fill with unknown token
protected void clearTokenProps(int startLine, int endLine) {
for (int i = startLine; i < endLine; i++) {
if (hasSyntaxHighlight) {
int len = cast(int)_lines[i].length;
_tokenProps[i].length = len;
for (int j = 0; j < len; j++)
_tokenProps[i][j] = TOKEN_UNKNOWN;
} else {
_tokenProps[i] = null; // no token props
}
}
}
/// replace whole text with another content
@property EditableContent text(dstring newContent) {
clearUndo();
_lines.length = 0;
if (_multiline) {
_lines = splitDString(newContent);
_tokenProps.length = _lines.length;
updateTokenProps(0, cast(int)_lines.length);
} else {
_lines.length = 1;
_lines[0] = replaceEolsWithSpaces(newContent);
_tokenProps.length = 1;
updateTokenProps(0, cast(int)_lines.length);
}
notifyContentReplaced();
return this;
}
/// clear content
void clear() {
clearUndo();
_lines.length = 0;
}
/// returns line text
@property int length() { return cast(int)_lines.length; }
dstring opIndex(int index) {
return line(index);
}
/// returns line text by index, "" if index is out of bounds
dstring line(int index) {
return index >= 0 && index < _lines.length ? _lines[index] : ""d;
}
/// returns line token properties one item per character
TokenPropString lineTokenProps(int index) {
return index >= 0 && index < _tokenProps.length ? _tokenProps[index] : null;
}
/// returns text position for end of line lineIndex
TextPosition lineEnd(int lineIndex) {
return TextPosition(lineIndex, lineLength(lineIndex));
}
/// returns position before first non-space character of line, returns 0 position if no non-space chars
TextPosition firstNonSpace(int lineIndex) {
dstring s = line(lineIndex);
for (int i = 0; i < s.length; i++)
if (s[i] != ' ' && s[i] != '\t')
return TextPosition(lineIndex, i);
return TextPosition(lineIndex, 0);
}
/// returns position after last non-space character of line, returns 0 position if no non-space chars on line
TextPosition lastNonSpace(int lineIndex) {
dstring s = line(lineIndex);
for (int i = cast(int)s.length - 1; i >= 0; i--)
if (s[i] != ' ' && s[i] != '\t')
return TextPosition(lineIndex, i + 1);
return TextPosition(lineIndex, 0);
}
/// returns text position for end of line lineIndex
int lineLength(int lineIndex) {
return lineIndex >= 0 && lineIndex < _lines.length ? cast(int)_lines[lineIndex].length : 0;
}
/// returns maximum length of line
int maxLineLength() {
int m = 0;
foreach(s; _lines)
if (m < s.length)
m = cast(int)s.length;
return m;
}
void handleContentChange(EditOperation op, ref TextRange rangeBefore, ref TextRange rangeAfter, Object source) {
// update highlight if necessary
updateTokenProps(rangeAfter.start.line, rangeAfter.end.line + 1);
// call listeners
contentChangeListeners(this, op, rangeBefore, rangeAfter, source);
}
/// return text for specified range
dstring[] rangeText(TextRange range) {
dstring[] res;
if (range.empty) {
res ~= ""d;
return res;
}
for (int lineIndex = range.start.line; lineIndex <= range.end.line; lineIndex++) {
dstring lineText = line(lineIndex);
dstring lineFragment = lineText;
int startchar = 0;
int endchar = cast(int)lineText.length;
if (lineIndex == range.start.line)
startchar = range.start.pos;
if (lineIndex == range.end.line)
endchar = range.end.pos;
if (endchar > lineText.length)
endchar = cast(int)lineText.length;
if (endchar <= startchar)
lineFragment = ""d;
else if (startchar != 0 || endchar != lineText.length)
lineFragment = lineText[startchar .. endchar].dup;
res ~= lineFragment;
}
return res;
}
/// when position is out of content bounds, fix it to nearest valid position
void correctPosition(ref TextPosition position) {
if (position.line >= length) {
position.line = length - 1;
position.pos = lineLength(position.line);
}
if (position.line < 0) {
position.line = 0;
position.pos = 0;
}
int currentLineLength = lineLength(position.line);
if (position.pos > currentLineLength)
position.pos = currentLineLength;
if (position.pos < 0)
position.pos = 0;
}
/// when range positions is out of content bounds, fix it to nearest valid position
void correctRange(ref TextRange range) {
correctPosition(range.start);
correctPosition(range.end);
}
/// removes removedCount lines starting from start
protected void removeLines(int start, int removedCount) {
int end = start + removedCount;
assert(removedCount > 0 && start >= 0 && end > 0 && start < _lines.length && end <= _lines.length);
for (int i = start; i < _lines.length - removedCount; i++) {
_lines[i] = _lines[i + removedCount];
_tokenProps[i] = _tokenProps[i + removedCount];
}
for (int i = cast(int)_lines.length - removedCount; i < _lines.length; i++) {
_lines[i] = null; // free unused line references
_tokenProps[i] = null; // free unused line references
}
_lines.length -= removedCount;
_tokenProps.length = _lines.length;
}
/// inserts count empty lines at specified position
protected void insertLines(int start, int count) {
assert(count > 0);
_lines.length += count;
_tokenProps.length = _lines.length;
for (int i = cast(int)_lines.length - 1; i >= start + count; i--) {
_lines[i] = _lines[i - count];
_tokenProps[i] = _tokenProps[i - count];
}
for (int i = start; i < start + count; i++) {
_lines[i] = ""d;
_tokenProps[i] = null;
}
}
/// inserts or removes lines, removes text in range
protected void replaceRange(TextRange before, TextRange after, dstring[] newContent) {
dstring firstLineBefore = line(before.start.line);
dstring lastLineBefore = before.singleLine ? firstLineBefore : line(before.end.line);
dstring firstLineHead = before.start.pos > 0 && before.start.pos <= firstLineBefore.length ? firstLineBefore[0..before.start.pos] : ""d;
dstring lastLineTail = before.end.pos >= 0 && before.end.pos < lastLineBefore.length ? lastLineBefore[before.end.pos .. $] : ""d;
int linesBefore = before.lines;
int linesAfter = after.lines;
if (linesBefore < linesAfter) {
// add more lines
insertLines(before.start.line + 1, linesAfter - linesBefore);
} else if (linesBefore > linesAfter) {
// remove extra lines
removeLines(before.start.line + 1, linesBefore - linesAfter);
}
for (int i = after.start.line; i <= after.end.line; i++) {
dstring newline = newContent[i - after.start.line];
if (i == after.start.line && i == after.end.line) {
dchar[] buf;
buf ~= firstLineHead;
buf ~= newline;
buf ~= lastLineTail;
//Log.d("merging lines ", firstLineHead, " ", newline, " ", lastLineTail);
_lines[i] = cast(dstring)buf;
clearTokenProps(i, i + 1);
//Log.d("merge result: ", _lines[i]);
} else if (i == after.start.line) {
dchar[] buf;
buf ~= firstLineHead;
buf ~= newline;
_lines[i] = cast(dstring)buf;
clearTokenProps(i, i + 1);
} else if (i == after.end.line) {
dchar[] buf;
buf ~= newline;
buf ~= lastLineTail;
_lines[i] = cast(dstring)buf;
clearTokenProps(i, i + 1);
} else {
_lines[i] = newline; // no dup needed
clearTokenProps(i, i + 1);
}
}
}
static bool isDigit(dchar ch) pure nothrow {
return ch >= '0' && ch <= '9';
}
static bool isAlpha(dchar ch) pure nothrow {
return isLowerAlpha(ch) || isUpperAlpha(ch);
}
static bool isAlNum(dchar ch) pure nothrow {
return isDigit(ch) || isAlpha(ch);
}
static bool isLowerAlpha(dchar ch) pure nothrow {
return (ch >= 'a' && ch <= 'z') || (ch == '_');
}
static bool isUpperAlpha(dchar ch) pure nothrow {
return (ch >= 'A' && ch <= 'Z');
}
static bool isPunct(dchar ch) pure nothrow {
switch(ch) {
case '.':
case ',':
case ';':
case '?':
case '!':
return true;
default:
return false;
}
}
static bool isBracket(dchar ch) pure nothrow {
switch(ch) {
case '(':
case ')':
case '[':
case ']':
case '{':
case '}':
return true;
default:
return false;
}
}
static bool isWordBound(dchar thischar, dchar nextchar) {
return (isAlNum(thischar) && !isAlNum(nextchar))
|| (isPunct(thischar) && !isPunct(nextchar))
|| (isBracket(thischar) && !isBracket(nextchar))
|| (thischar != ' ' && nextchar == ' ');
}
/// change text position to nearest word bound (direction < 0 - back, > 0 - forward)
TextPosition moveByWord(TextPosition p, int direction, bool camelCasePartsAsWords) {
correctPosition(p);
TextPosition firstns = firstNonSpace(p.line); // before first non space
TextPosition lastns = lastNonSpace(p.line); // after last non space
int linelen = lineLength(p.line); // line length
if (direction < 0) {
// back
if (p.pos <= 0) {
// beginning of line - move to prev line
if (p.line > 0)
p = lastNonSpace(p.line - 1);
} else if (p.pos <= firstns.pos) { // before first nonspace
// to beginning of line
p.pos = 0;
} else {
dstring txt = line(p.line);
int found = -1;
for (int i = p.pos - 1; i > 0; i--) {
// check if position i + 1 is after word end
dchar thischar = i >= 0 && i < linelen ? txt[i] : ' ';
if (thischar == '\t')
thischar = ' ';
dchar nextchar = i - 1 >= 0 && i - 1 < linelen ? txt[i - 1] : ' ';
if (nextchar == '\t')
nextchar = ' ';
if (isWordBound(thischar, nextchar)
|| (camelCasePartsAsWords && isUpperAlpha(thischar) && isLowerAlpha(nextchar))) {
found = i;
break;
}
}
if (found >= 0)
p.pos = found;
else
p.pos = 0;
}
} else if (direction > 0) {
// forward
if (p.pos >= linelen) {
// last position of line
if (p.line < length - 1)
p = firstNonSpace(p.line + 1);
} else if (p.pos >= lastns.pos) { // before first nonspace
// to beginning of line
p.pos = linelen;
} else {
dstring txt = line(p.line);
int found = -1;
for (int i = p.pos; i < linelen; i++) {
// check if position i + 1 is after word end
dchar thischar = txt[i];
if (thischar == '\t')
thischar = ' ';
dchar nextchar = i < linelen - 1 ? txt[i + 1] : ' ';
if (nextchar == '\t')
nextchar = ' ';
if (isWordBound(thischar, nextchar)
|| (camelCasePartsAsWords && isLowerAlpha(thischar) && isUpperAlpha(nextchar))) {
found = i + 1;
break;
}
}
if (found >= 0)
p.pos = found;
else
p.pos = linelen;
}
}
return p;
}
/// edit content
bool performOperation(EditOperation op, Object source) {
if (_readOnly)
throw new Exception("content is readonly");
if (op.action == EditAction.Replace) {
TextRange rangeBefore = op.range;
assert(rangeBefore.start <= rangeBefore.end);
//correctRange(rangeBefore);
dstring[] oldcontent = rangeText(rangeBefore);
dstring[] newcontent = op.content;
if (newcontent.length == 0)
newcontent ~= ""d;
TextRange rangeAfter = op.range;
rangeAfter.end = rangeAfter.start;
if (newcontent.length > 1) {
// different lines
rangeAfter.end.line = rangeAfter.start.line + cast(int)newcontent.length - 1;
rangeAfter.end.pos = cast(int)newcontent[$ - 1].length;
} else {
// same line
rangeAfter.end.pos = rangeAfter.start.pos + cast(int)newcontent[0].length;
}
assert(rangeAfter.start <= rangeAfter.end);
op.newRange = rangeAfter;
op.oldContent = oldcontent;
replaceRange(rangeBefore, rangeAfter, newcontent);
handleContentChange(op, rangeBefore, rangeAfter, source);
_undoBuffer.saveForUndo(op);
return true;
}
return false;
}
/// return true if there is at least one operation in undo buffer
@property bool hasUndo() {
return _undoBuffer.hasUndo;
}
/// return true if there is at least one operation in redo buffer
@property bool hasRedo() {
return _undoBuffer.hasRedo;
}
/// undoes last change
bool undo() {
if (!hasUndo)
return false;
if (_readOnly)
throw new Exception("content is readonly");
EditOperation op = _undoBuffer.undo();
TextRange rangeBefore = op.newRange;
dstring[] oldcontent = op.content;
dstring[] newcontent = op.oldContent;
TextRange rangeAfter = op.range;
//Log.d("Undoing op rangeBefore=", rangeBefore, " contentBefore=`", oldcontent, "` rangeAfter=", rangeAfter, " contentAfter=`", newcontent, "`");
replaceRange(rangeBefore, rangeAfter, newcontent);
handleContentChange(op, rangeBefore, rangeAfter, this);
return true;
}
/// redoes last undone change
bool redo() {
if (!hasUndo)
return false;
if (_readOnly)
throw new Exception("content is readonly");
EditOperation op = _undoBuffer.redo();
TextRange rangeBefore = op.range;
dstring[] oldcontent = op.oldContent;
dstring[] newcontent = op.content;
TextRange rangeAfter = op.newRange;
//Log.d("Redoing op rangeBefore=", rangeBefore, " contentBefore=`", oldcontent, "` rangeAfter=", rangeAfter, " contentAfter=`", newcontent, "`");
replaceRange(rangeBefore, rangeAfter, newcontent);
handleContentChange(op, rangeBefore, rangeAfter, this);
return true;
}
/// clear undo/redp history
void clearUndo() {
_undoBuffer.clear();
}
protected string _filename;
protected TextFileFormat _format;
/// file used to load editor content
@property string filename() {
return _filename;
}
/// load content form input stream
bool load(InputStream f, string fname = null) {
import dlangui.core.linestream;
clear();
_filename = fname;
_format = TextFileFormat.init;
try {
LineStream lines = LineStream.create(f, fname);
for (;;) {
dchar[] s = lines.readLine();
if (s is null)
break;
int pos = cast(int)(_lines.length++);
_tokenProps.length = _lines.length;
_lines[pos] = s.dup;
clearTokenProps(pos, pos + 1);
}
if (lines.errorCode != 0) {
clear();
Log.e("Error ", lines.errorCode, " ", lines.errorMessage, " -- at line ", lines.errorLine, " position ", lines.errorPos);
notifyContentReplaced();
return false;
}
// EOF
_format = lines.textFormat;
notifyContentReplaced();
return true;
} catch (Exception e) {
Log.e("Exception while trying to read file ", fname, " ", e.toString);
clear();
notifyContentReplaced();
return false;
}
}
/// load content from file
bool load(string filename) {
clear();
try {
std.stream.File f = new std.stream.File(filename);
scope(exit) { f.close(); }
return load(f, filename);
} catch (Exception e) {
Log.e("Exception while trying to read file ", filename, " ", e.toString);
clear();
return false;
}
}
/// save to output stream in specified format
bool save(OutputStream stream, string filename, TextFileFormat format) {
if (!filename)
filename = _filename;
_format = format;
import dlangui.core.linestream;
try {
OutputLineStream writer = new OutputLineStream(stream, filename, format);
scope(exit) { writer.close(); }
for (int i = 0; i < _lines.length; i++) {
writer.writeLine(_lines[i]);
}
// EOF
return true;
} catch (Exception e) {
Log.e("Exception while trying to write file ", filename, " ", e.toString);
return false;
}
}
/// save to output stream in current format
bool save(OutputStream stream, string filename) {
return save(stream, filename, _format);
}
/// save to file in specified format
bool save(string filename, TextFileFormat format) {
if (!filename)
filename = _filename;
try {
std.stream.File f = new std.stream.File(filename, FileMode.OutNew);
scope(exit) { f.close(); }
return save(f, filename, format);
} catch (Exception e) {
Log.e("Exception while trying to save file ", filename, " ", e.toString);
return false;
}
}
/// save to file in current format
bool save(string filename = null) {
return save(filename, _format);
}
}
/// base for all editor widgets
class EditWidgetBase : ScrollWidgetBase, EditableContentListener, MenuItemActionHandler {
protected EditableContent _content;
protected int _lineHeight;
protected Point _scrollPos;
protected bool _fixedFont;
protected int _spaceWidth;
protected int _tabSize = 4;
protected int _leftPaneWidth; // left pane - can be used to show line numbers, collapse controls, bookmarks, breakpoints, custom icons
protected int _minFontSize = -1; // disable zooming
protected int _maxFontSize = -1; // disable zooming
protected bool _wantTabs = true;
protected bool _useSpacesForTabs = false;
protected bool _showLineNumbers = false; // show line numbers in left pane
protected bool _showModificationMarks = false; // show modification marks in left pane
protected bool _showIcons = false; // show icons in left pane
protected bool _showFolding = false; // show folding controls in left pane
protected int _lineNumbersWidth = 0;
protected int _modificationMarksWidth = 0;
protected int _iconsWidth = 0;
protected int _foldingWidth = 0;
protected bool _replaceMode;
// TODO: move to styles
protected uint _selectionColorFocused = 0xB060A0FF;
protected uint _selectionColorNormal = 0xD060A0FF;
protected uint _leftPaneBackgroundColor = 0xE0E0E0;
protected uint _leftPaneBackgroundColor2 = 0xFFFFFF;
protected uint _leftPaneBackgroundColor3 = 0xC0C0C0;
protected uint _leftPaneLineNumberColor = 0x4060D0;
protected uint _leftPaneLineNumberBackgroundColor = 0xF0F0F0;
protected uint _iconsPaneWidth = 16;
protected uint _foldingPaneWidth = 16;
protected uint _modificationMarksPaneWidth = 8;
/// override to support modification of client rect after change, e.g. apply offset
override protected void handleClientRectLayout(ref Rect rc) {
updateLeftPaneWidth();
rc.left += _leftPaneWidth;
}
/// override for multiline editors
protected int lineCount() {
return 1;
}
/// override to add custom items on left panel
protected void updateLeftPaneWidth() {
_iconsWidth = _showIcons ? _iconsPaneWidth : 0;
_foldingWidth = _showFolding ? _foldingPaneWidth : 0;
_modificationMarksWidth = _showModificationMarks ? _modificationMarksPaneWidth : 0;
_lineNumbersWidth = 0;
if (_showLineNumbers) {
dchar[] s = to!(dchar[])(lineCount + 1);
foreach(ref ch; s)
ch = '9';
FontRef fnt = font;
Point sz = fnt.textSize(s);
_lineNumbersWidth = sz.x;
}
_leftPaneWidth = _lineNumbersWidth + _modificationMarksWidth + _foldingWidth + _iconsWidth;
if (_leftPaneWidth)
_leftPaneWidth += 3;
}
protected void drawLeftPaneFolding(DrawBuf buf, Rect rc, int line) {
}
protected void drawLeftPaneIcons(DrawBuf buf, Rect rc, int line) {
}
protected void drawLeftPaneModificationMarks(DrawBuf buf, Rect rc, int line) {
}
protected void drawLeftPaneLineNumbers(DrawBuf buf, Rect rc, int line) {
buf.fillRect(rc, _leftPaneLineNumberBackgroundColor);
if (line < 0)
return;
dstring s = to!dstring(line + 1);
FontRef fnt = font;
Point sz = fnt.textSize(s);
int x = rc.right - sz.x;
int y = rc.top + (rc.height - sz.y) / 2;
fnt.drawText(buf, x, y, s, _leftPaneLineNumberColor);
}
protected void drawLeftPane(DrawBuf buf, Rect rc, int line) {
// override for custom drawn left pane
buf.fillRect(rc, _leftPaneBackgroundColor);
buf.fillRect(Rect(rc.right - 2, rc.top, rc.right - 1, rc.bottom), _leftPaneBackgroundColor2);
buf.fillRect(Rect(rc.right - 1, rc.top, rc.right - 0, rc.bottom), _leftPaneBackgroundColor3);
rc.right -= 3;
if (_foldingWidth) {
Rect rc2 = rc;
rc.right = rc2.left = rc2.right - _foldingWidth;
drawLeftPaneFolding(buf, rc2, line);
}
if (_lineNumbersWidth) {
Rect rc2 = rc;
rc.right = rc2.left = rc2.right - _lineNumbersWidth;
drawLeftPaneLineNumbers(buf, rc2, line);
}
if (_modificationMarksWidth) {
Rect rc2 = rc;
rc.right = rc2.left = rc2.right - _modificationMarksWidth;
drawLeftPaneModificationMarks(buf, rc2, line);
}
if (_iconsWidth) {
Rect rc2 = rc;
rc.right = rc2.left = rc2.right - _iconsWidth;
drawLeftPaneIcons(buf, rc2, line);
}
}
this(string ID, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) {
super(ID, hscrollbarMode, vscrollbarMode);
focusable = true;
acceleratorMap.add( [
new Action(EditorActions.Up, KeyCode.UP, 0),
new Action(EditorActions.SelectUp, KeyCode.UP, KeyFlag.Shift),
new Action(EditorActions.Down, KeyCode.DOWN, 0),
new Action(EditorActions.SelectDown, KeyCode.DOWN, KeyFlag.Shift),
new Action(EditorActions.Left, KeyCode.LEFT, 0),
new Action(EditorActions.SelectLeft, KeyCode.LEFT, KeyFlag.Shift),
new Action(EditorActions.Right, KeyCode.RIGHT, 0),
new Action(EditorActions.SelectRight, KeyCode.RIGHT, KeyFlag.Shift),
new Action(EditorActions.WordLeft, KeyCode.LEFT, KeyFlag.Control),
new Action(EditorActions.SelectWordLeft, KeyCode.LEFT, KeyFlag.Control | KeyFlag.Shift),
new Action(EditorActions.WordRight, KeyCode.RIGHT, KeyFlag.Control),
new Action(EditorActions.SelectWordRight, KeyCode.RIGHT, KeyFlag.Control | KeyFlag.Shift),
new Action(EditorActions.PageUp, KeyCode.PAGEUP, 0),
new Action(EditorActions.SelectPageUp, KeyCode.PAGEUP, KeyFlag.Shift),
new Action(EditorActions.PageDown, KeyCode.PAGEDOWN, 0),
new Action(EditorActions.SelectPageDown, KeyCode.PAGEDOWN, KeyFlag.Shift),
new Action(EditorActions.PageBegin, KeyCode.PAGEUP, KeyFlag.Control),
new Action(EditorActions.SelectPageBegin, KeyCode.PAGEUP, KeyFlag.Control | KeyFlag.Shift),
new Action(EditorActions.PageEnd, KeyCode.PAGEDOWN, KeyFlag.Control),
new Action(EditorActions.SelectPageEnd, KeyCode.PAGEDOWN, KeyFlag.Control | KeyFlag.Shift),
new Action(EditorActions.LineBegin, KeyCode.HOME, 0),
new Action(EditorActions.SelectLineBegin, KeyCode.HOME, KeyFlag.Shift),
new Action(EditorActions.LineEnd, KeyCode.END, 0),
new Action(EditorActions.SelectLineEnd, KeyCode.END, KeyFlag.Shift),
new Action(EditorActions.DocumentBegin, KeyCode.HOME, KeyFlag.Control),
new Action(EditorActions.SelectDocumentBegin, KeyCode.HOME, KeyFlag.Control | KeyFlag.Shift),
new Action(EditorActions.DocumentEnd, KeyCode.END, KeyFlag.Control),
new Action(EditorActions.SelectDocumentEnd, KeyCode.END, KeyFlag.Control | KeyFlag.Shift),
new Action(EditorActions.ScrollLineUp, KeyCode.UP, KeyFlag.Control),
new Action(EditorActions.ScrollLineDown, KeyCode.DOWN, KeyFlag.Control),
new Action(EditorActions.InsertNewLine, KeyCode.RETURN, 0),
new Action(EditorActions.InsertNewLine, KeyCode.RETURN, KeyFlag.Shift),
new Action(EditorActions.PrependNewLine, KeyCode.RETURN, KeyFlag.Control),
// Backspace/Del
new Action(EditorActions.DelPrevChar, KeyCode.BACK, 0),
new Action(EditorActions.DelNextChar, KeyCode.DEL, 0),
new Action(EditorActions.DelPrevWord, KeyCode.BACK, KeyFlag.Control),
new Action(EditorActions.DelNextWord, KeyCode.DEL, KeyFlag.Control),
// Copy/Paste
new Action(EditorActions.Copy, KeyCode.KEY_C, KeyFlag.Control),
new Action(EditorActions.Copy, KeyCode.KEY_C, KeyFlag.Control|KeyFlag.Shift),
new Action(EditorActions.Copy, KeyCode.INS, KeyFlag.Control),
new Action(EditorActions.Cut, KeyCode.KEY_X, KeyFlag.Control),
new Action(EditorActions.Cut, KeyCode.KEY_X, KeyFlag.Control|KeyFlag.Shift),
new Action(EditorActions.Cut, KeyCode.DEL, KeyFlag.Shift),
new Action(EditorActions.Paste, KeyCode.KEY_V, KeyFlag.Control),
new Action(EditorActions.Paste, KeyCode.KEY_V, KeyFlag.Control|KeyFlag.Shift),
new Action(EditorActions.Paste, KeyCode.INS, KeyFlag.Shift),
// Undo/Redo
new Action(EditorActions.Undo, KeyCode.KEY_Z, KeyFlag.Control),
new Action(EditorActions.Redo, KeyCode.KEY_Y, KeyFlag.Control),
new Action(EditorActions.Redo, KeyCode.KEY_Z, KeyFlag.Control|KeyFlag.Shift),
new Action(EditorActions.Tab, KeyCode.TAB, 0),
new Action(EditorActions.BackTab, KeyCode.TAB, KeyFlag.Shift),
new Action(EditorActions.ToggleReplaceMode, KeyCode.INS, 0),
new Action(EditorActions.SelectAll, KeyCode.KEY_A, KeyFlag.Control),
]);
}
protected MenuItem _popupMenu;
@property MenuItem popupMenu() { return _popupMenu; }
@property EditWidgetBase popupMenu(MenuItem popupMenu) {
_popupMenu = popupMenu;
return this;
}
///
override bool onMenuItemAction(const Action action) {
return dispatchAction(action);
}
/// returns true if widget can show popup (e.g. by mouse right click at point x,y)
override bool canShowPopupMenu(int x, int y) {
if (_popupMenu is null)
return false;
if (_popupMenu.onBeforeOpeningSubmenu.assigned)
if (!_popupMenu.onBeforeOpeningSubmenu(_popupMenu))
return false;
return true;
}
/// returns true if widget is focusable and visible and enabled
override @property bool canFocus() {
// allow to focus even if not enabled
return focusable && visible;
}
/// override to change popup menu items state
override bool isActionEnabled(const Action action) {
switch (action.id) {
case EditorActions.Copy:
case EditorActions.Cut:
return !_selectionRange.empty;
case EditorActions.Paste:
return Platform.instance.getClipboardText().length > 0;
case EditorActions.Undo:
return _content.hasUndo;
case EditorActions.Redo:
return _content.hasRedo;
default:
return super.isActionEnabled(action);
}
}
/// shows popup at (x,y)
override void showPopupMenu(int x, int y) {
/// if preparation signal handler assigned, call it; don't show popup if false is returned from handler
if (_popupMenu.onBeforeOpeningSubmenu.assigned)
if (!_popupMenu.onBeforeOpeningSubmenu(_popupMenu))
return;
for (int i = 0; i < _popupMenu.subitemCount; i++) {
MenuItem item = _popupMenu.subitem(i);
if (item.action && isActionEnabled(item.action)) {
item.enabled = true;
} else {
item.enabled = false;
}
}
PopupMenu popupMenu = new PopupMenu(_popupMenu);
popupMenu.onMenuItemActionListener = this;
PopupWidget popup = window.showPopup(popupMenu, this, PopupAlign.Point | PopupAlign.Right, x, y);
popup.flags = PopupFlags.CloseOnClickOutside;
}
void onPopupMenuItem(MenuItem item) {
// TODO
}
/// returns mouse cursor type for widget
override uint getCursorType(int x, int y) {
return CursorType.IBeam;
}
/// when true, Tab / Shift+Tab presses are processed internally in widget (e.g. insert tab character) instead of focus change navigation.
@property bool wantTabs() {
return _wantTabs;
}
/// sets tab size (in number of spaces)
@property EditWidgetBase wantTabs(bool wantTabs) {
_wantTabs = wantTabs;
return this;
}
/// when true, line numbers are shown
@property bool showLineNumbers() {
return _showLineNumbers;
}
/// when true, line numbers are shown
@property EditWidgetBase showLineNumbers(bool flg) {
if (_showLineNumbers != flg) {
_showLineNumbers = flg;
updateLeftPaneWidth();
requestLayout();
}
return this;
}
/// readonly flag (when true, user cannot change content of editor)
@property bool readOnly() {
return !enabled || _content.readOnly;
}
/// sets readonly flag
@property EditWidgetBase readOnly(bool readOnly) {
enabled = !readOnly;
invalidate();
return this;
}
/// replace mode flag (when true, entered character replaces character under cursor)
@property bool replaceMode() {
return _replaceMode;
}
/// sets replace mode flag
@property EditWidgetBase replaceMode(bool replaceMode) {
_replaceMode = replaceMode;
invalidate();
return this;
}
/// when true, spaces will be inserted instead of tabs
@property bool useSpacesForTabs() {
return _useSpacesForTabs;
}
/// set new Tab key behavior flag: when true, spaces will be inserted instead of tabs
@property EditWidgetBase useSpacesForTabs(bool useSpacesForTabs) {
_useSpacesForTabs = useSpacesForTabs;
return this;
}
/// returns tab size (in number of spaces)
@property int tabSize() {
return _tabSize;
}
/// sets tab size (in number of spaces)
@property EditWidgetBase tabSize(int newTabSize) {
if (newTabSize < 1)
newTabSize = 1;
else if (newTabSize > 16)
newTabSize = 16;
if (newTabSize != _tabSize) {
_tabSize = newTabSize;
requestLayout();
}
return this;
}
/// editor content object
@property EditableContent content() {
return _content;
}
/// when _ownContent is false, _content should not be destroyed in editor destructor
protected bool _ownContent = true;
/// set content object
@property EditWidgetBase content(EditableContent content) {
if (_content is content)
return this; // not changed
if (_content !is null) {
// disconnect old content
_content.contentChangeListeners.disconnect(this);
if (_ownContent) {
destroy(_content);
}
}
_content = content;
_ownContent = false;
_content.contentChangeListeners.connect(this);
if (_content.readOnly)
enabled = false;
return this;
}
/// free resources
~this() {
if (_ownContent) {
destroy(_content);
_content = null;
}
}
protected void updateMaxLineWidth() {
}
override void onContentChange(EditableContent content, EditOperation operation, ref TextRange rangeBefore, ref TextRange rangeAfter, Object source) {
Log.d("onContentChange rangeBefore=", rangeBefore, " rangeAfter=", rangeAfter, " text=", operation.content);
updateMaxLineWidth();
measureVisibleText();
if (source is this) {
if (operation.action == EditAction.ReplaceContent) {
// loaded from file
_caretPos = rangeAfter.end;
_selectionRange.start = _caretPos;
_selectionRange.end = _caretPos;
ensureCaretVisible();
correctCaretPos();
requestLayout();
} else {
_caretPos = rangeAfter.end;
_selectionRange.start = _caretPos;
_selectionRange.end = _caretPos;
ensureCaretVisible();
}
} else {
correctCaretPos();
// TODO: do something better (e.g. take into account ranges when correcting)
}
invalidate();
return;
}
/// get widget text
override @property dstring text() { return _content.text; }
/// set text
override @property Widget text(dstring s) {
_content.text = s;
requestLayout();
return this;
}
/// set text
override @property Widget text(UIString s) {
_content.text = s;
requestLayout();
return this;
}
protected TextPosition _caretPos;
protected TextRange _selectionRange;
abstract protected Rect textPosToClient(TextPosition p);
abstract protected TextPosition clientToTextPos(Point pt);
abstract protected void ensureCaretVisible();
abstract protected Point measureVisibleText();
/// returns cursor rectangle
protected Rect caretRect() {
Rect caretRc = textPosToClient(_caretPos);
if (_replaceMode) {
dstring s = _content[_caretPos.line];
if (_caretPos.pos < s.length) {
TextPosition nextPos = _caretPos;
nextPos.pos++;
Rect nextRect = textPosToClient(nextPos);
caretRc.right = nextRect.right;
} else {
caretRc.right += _spaceWidth;
}
}
caretRc.offset(_clientRect.left, _clientRect.top);
return caretRc;
}
/// draws caret
protected void drawCaret(DrawBuf buf) {
if (focused) {
// draw caret
Rect caretRc = caretRect();
if (caretRc.intersects(_clientRect)) {
Rect rc1 = caretRc;
rc1.right = rc1.left + 1;
caretRc.left++;
if (_replaceMode)
buf.fillRect(caretRc, 0x808080FF);
buf.fillRect(rc1, 0x000000);
}
}
}
protected void updateFontProps() {
FontRef font = font();
_fixedFont = font.isFixed;
_spaceWidth = font.spaceWidth;
_lineHeight = font.height;
}
/// when cursor position or selection is out of content bounds, fix it to nearest valid position
protected void correctCaretPos() {
_content.correctPosition(_caretPos);
_content.correctPosition(_selectionRange.start);
_content.correctPosition(_selectionRange.end);
if (_selectionRange.empty)
_selectionRange = TextRange(_caretPos, _caretPos);
}
private int[] _lineWidthBuf;
protected int calcLineWidth(dstring s) {
int w = 0;
if (_fixedFont) {
int tabw = _tabSize * _spaceWidth;
// version optimized for fixed font
for (int i = 0; i < s.length; i++) {
if (s[i] == '\t') {
w += _spaceWidth;
w = (w + tabw - 1) / tabw * tabw;
} else {
w += _spaceWidth;
}
}
} else {
// variable pitch font
if (_lineWidthBuf.length < s.length)
_lineWidthBuf.length = s.length;
int charsMeasured = font.measureText(s, _lineWidthBuf, int.max);
if (charsMeasured > 0)
w = _lineWidthBuf[charsMeasured - 1];
}
return w;
}
protected void updateSelectionAfterCursorMovement(TextPosition oldCaretPos, bool selecting) {
if (selecting) {
if (oldCaretPos == _selectionRange.start) {
if (_caretPos >= _selectionRange.end) {
_selectionRange.start = _selectionRange.end;
_selectionRange.end = _caretPos;
} else {
_selectionRange.start = _caretPos;
}
} else if (oldCaretPos == _selectionRange.end) {
if (_caretPos < _selectionRange.start) {
_selectionRange.end = _selectionRange.start;
_selectionRange.start = _caretPos;
} else {
_selectionRange.end = _caretPos;
}
} else {
_selectionRange.start = _caretPos;
_selectionRange.end = _caretPos;
}
} else {
_selectionRange.start = _caretPos;
_selectionRange.end = _caretPos;
}
invalidate();
}
protected void updateCaretPositionByMouse(int x, int y, bool selecting) {
TextPosition oldCaretPos = _caretPos;
TextPosition newPos = clientToTextPos(Point(x,y));
if (newPos != _caretPos) {
_caretPos = newPos;
updateSelectionAfterCursorMovement(oldCaretPos, selecting);
invalidate();
}
}
/// generate string of spaces, to reach next tab position
protected dstring spacesForTab(int currentPos) {
int newPos = (currentPos + tabSize + 1) / tabSize * tabSize;
return " "d[0..(newPos - currentPos)];
}
/// returns true if one or more lines selected fully
protected bool wholeLinesSelected() {
return _selectionRange.end.line > _selectionRange.start.line
&& _selectionRange.end.pos == 0
&& _selectionRange.start.pos == 0;
}
protected bool _camelCasePartsAsWords = true;
protected bool removeSelectionTextIfSelected() {
if (_selectionRange.empty)
return false;
// clear selection
EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d]);
_content.performOperation(op, this);
ensureCaretVisible();
return true;
}
protected bool removeRangeText(TextRange range) {
if (range.empty)
return false;
_selectionRange = range;
_caretPos = _selectionRange.start;
EditOperation op = new EditOperation(EditAction.Replace, range, [""d]);
_content.performOperation(op, this);
//_selectionRange.start = _caretPos;
//_selectionRange.end = _caretPos;
ensureCaretVisible();
return true;
}
override protected bool handleAction(const Action a) {
TextPosition oldCaretPos = _caretPos;
dstring currentLine = _content[_caretPos.line];
switch (a.id) {
case EditorActions.Left:
case EditorActions.SelectLeft:
correctCaretPos();
if (_caretPos.pos > 0) {
_caretPos.pos--;
updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
ensureCaretVisible();
} else if (_caretPos.line > 0) {
_caretPos = _content.lineEnd(_caretPos.line - 1);
updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
ensureCaretVisible();
}
return true;
case EditorActions.Right:
case EditorActions.SelectRight:
correctCaretPos();
if (_caretPos.pos < currentLine.length) {
_caretPos.pos++;
updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
ensureCaretVisible();
} else if (_caretPos.line < _content.length) {
_caretPos.pos = 0;
_caretPos.line++;
updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
ensureCaretVisible();
}
return true;
case EditorActions.WordLeft:
case EditorActions.SelectWordLeft:
{
TextPosition newpos = _content.moveByWord(_caretPos, -1, _camelCasePartsAsWords);
if (newpos != _caretPos) {
_caretPos = newpos;
updateSelectionAfterCursorMovement(oldCaretPos, a.id == EditorActions.SelectWordLeft);
ensureCaretVisible();
}
}
return true;
case EditorActions.WordRight:
case EditorActions.SelectWordRight:
{
TextPosition newpos = _content.moveByWord(_caretPos, 1, _camelCasePartsAsWords);
if (newpos != _caretPos) {
_caretPos = newpos;
updateSelectionAfterCursorMovement(oldCaretPos, a.id == EditorActions.SelectWordRight);
ensureCaretVisible();
}
}
return true;
case EditorActions.DocumentBegin:
case EditorActions.SelectDocumentBegin:
if (_caretPos.pos > 0 || _caretPos.line > 0) {
_caretPos.line = 0;
_caretPos.pos = 0;
ensureCaretVisible();
updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
}
return true;
case EditorActions.LineBegin:
case EditorActions.SelectLineBegin:
if (_caretPos.pos > 0) {
_caretPos.pos = 0;
ensureCaretVisible();
updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
}
return true;
case EditorActions.DocumentEnd:
case EditorActions.SelectDocumentEnd:
if (_caretPos.line < _content.length - 1 || _caretPos.pos < _content[_content.length - 1].length) {
_caretPos.line = _content.length - 1;
_caretPos.pos = cast(int)_content[_content.length - 1].length;
ensureCaretVisible();
updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
}
return true;
case EditorActions.LineEnd:
case EditorActions.SelectLineEnd:
if (_caretPos.pos < currentLine.length) {
_caretPos.pos = cast(int)currentLine.length;
ensureCaretVisible();
updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
}
return true;
case EditorActions.DelPrevWord:
if (readOnly)
return true;
correctCaretPos();
if (removeSelectionTextIfSelected()) // clear selection
return true;
TextPosition newpos = _content.moveByWord(_caretPos, -1, _camelCasePartsAsWords);
if (newpos < _caretPos)
removeRangeText(TextRange(newpos, _caretPos));
return true;
case EditorActions.DelNextWord:
if (readOnly)
return true;
correctCaretPos();
if (removeSelectionTextIfSelected()) // clear selection
return true;
TextPosition newpos = _content.moveByWord(_caretPos, 1, _camelCasePartsAsWords);
if (newpos > _caretPos)
removeRangeText(TextRange(_caretPos, newpos));
return true;
case EditorActions.DelPrevChar:
if (readOnly)
return true;
correctCaretPos();
if (removeSelectionTextIfSelected()) // clear selection
return true;
if (_caretPos.pos > 0) {
// delete prev char in current line
TextRange range = TextRange(_caretPos, _caretPos);
range.start.pos--;
removeRangeText(range);
} else if (_caretPos.line > 0) {
// merge with previous line
TextRange range = TextRange(_caretPos, _caretPos);
range.start = _content.lineEnd(range.start.line - 1);
removeRangeText(range);
}
return true;
case EditorActions.DelNextChar:
if (readOnly)
return true;
correctCaretPos();
if (removeSelectionTextIfSelected()) // clear selection
return true;
if (_caretPos.pos < currentLine.length) {
// delete char in current line
TextRange range = TextRange(_caretPos, _caretPos);
range.end.pos++;
removeRangeText(range);
} else if (_caretPos.line < _content.length - 1) {
// merge with next line
TextRange range = TextRange(_caretPos, _caretPos);
range.end.line++;
range.end.pos = 0;
removeRangeText(range);
}
return true;
case EditorActions.Copy:
if (!_selectionRange.empty) {
dstring selectionText = concatDStrings(_content.rangeText(_selectionRange));
platform.setClipboardText(selectionText);
}
return true;
case EditorActions.Cut:
if (!_selectionRange.empty) {
dstring selectionText = concatDStrings(_content.rangeText(_selectionRange));
platform.setClipboardText(selectionText);
if (readOnly)
return true;
EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d]);
_content.performOperation(op, this);
}
return true;
case EditorActions.Paste:
{
if (readOnly)
return true;
dstring selectionText = platform.getClipboardText();
dstring[] lines;
if (_content.multiline) {
lines = splitDString(selectionText);
} else {
lines = [replaceEolsWithSpaces(selectionText)];
}
EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, lines);
_content.performOperation(op, this);
}
return true;
case EditorActions.Undo:
{
if (readOnly)
return true;
_content.undo();
}
return true;
case EditorActions.Redo:
{
if (readOnly)
return true;
_content.redo();
}
return true;
case EditorActions.Tab:
{
if (readOnly)
return true;
if (_selectionRange.empty) {
if (_useSpacesForTabs) {
// insert one or more spaces to
EditOperation op = new EditOperation(EditAction.Replace, TextRange(_caretPos, _caretPos), [spacesForTab(_caretPos.pos)]);
_content.performOperation(op, this);
} else {
// just insert tab character
EditOperation op = new EditOperation(EditAction.Replace, TextRange(_caretPos, _caretPos), ["\t"d]);
_content.performOperation(op, this);
}
} else {
if (wholeLinesSelected()) {
// indent range
indentRange(false);
} else {
// insert tab
if (_useSpacesForTabs) {
// insert one or more spaces to
EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [spacesForTab(_selectionRange.start.pos)]);
_content.performOperation(op, this);
} else {
// just insert tab character
EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, ["\t"d]);
_content.performOperation(op, this);
}
}
}
}
return true;
case EditorActions.BackTab:
{
if (readOnly)
return true;
if (_selectionRange.empty) {
// remove spaces before caret
TextRange r = spaceBefore(_caretPos);
if (!r.empty) {
EditOperation op = new EditOperation(EditAction.Replace, r, [""d]);
_content.performOperation(op, this);
}
} else {
if (wholeLinesSelected()) {
// unindent range
indentRange(true);
} else {
// remove space before selection
TextRange r = spaceBefore(_selectionRange.start);
if (!r.empty) {
int nchars = r.end.pos - r.start.pos;
TextRange saveRange = _selectionRange;
TextPosition saveCursor = _caretPos;
EditOperation op = new EditOperation(EditAction.Replace, r, [""d]);
_content.performOperation(op, this);
if (saveCursor.line == saveRange.start.line)
saveCursor.pos -= nchars;
if (saveRange.end.line == saveRange.start.line)
saveRange.end.pos -= nchars;
saveRange.start.pos -= nchars;
_selectionRange = saveRange;
_caretPos = saveCursor;
ensureCaretVisible();
}
}
}
}
return true;
case EditorActions.ToggleReplaceMode:
replaceMode = !replaceMode;
return true;
case EditorActions.SelectAll:
_selectionRange.start.line = 0;
_selectionRange.start.pos = 0;
_selectionRange.end = _content.lineEnd(_content.length - 1);
_caretPos = _selectionRange.end;
ensureCaretVisible();
return true;
default:
break;
}
return super.handleAction(a);
}
protected TextRange spaceBefore(TextPosition pos) {
TextRange res = TextRange(pos, pos);
dstring s = _content[pos.line];
int x = 0;
int start = -1;
for (int i = 0; i < pos.pos; i++) {
dchar ch = s[i];
if (ch == ' ') {
if (start == -1 || (x % tabSize) == 0)
start = i;
x++;
} else if (ch == '\t') {
if (start == -1 || (x % tabSize) == 0)
start = i;
x = (x + tabSize + 1) / tabSize * tabSize;
} else {
x++;
start = -1;
}
}
if (start != -1) {
res.start.pos = start;
}
return res;
}
/// change line indent
protected dstring indentLine(dstring src, bool back) {
int firstNonSpace = -1;
int x = 0;
int unindentPos = -1;
for (int i = 0; i < src.length; i++) {
dchar ch = src[i];
if (ch == ' ') {
x++;
} else if (ch == '\t') {
x = (x + tabSize + 1) / tabSize * tabSize;
} else {
firstNonSpace = i;
break;
}
if (x <= tabSize)
unindentPos = i + 1;
}
if (firstNonSpace == -1) // only spaces or empty line -- do not change it
return src;
if (back) {
// unindent
if (unindentPos == -1)
return src; // no change
if (unindentPos == src.length)
return ""d;
return src[unindentPos .. $].dup;
} else {
// indent
if (_useSpacesForTabs) {
return spacesForTab(0) ~ src;
} else {
return "\t"d ~ src;
}
}
}
/// indent / unindent range
protected void indentRange(bool back) {
int lineCount = _selectionRange.end.line - _selectionRange.start.line;
dstring[] newContent = new dstring[lineCount + 1];
bool changed = false;
for (int i = 0; i < lineCount; i++) {
dstring srcline = _content.line(_selectionRange.start.line + i);
dstring dstline = indentLine(srcline, back);
newContent[i] = dstline;
if (dstline.length != srcline.length)
changed = true;
}
if (changed) {
TextRange saveRange = _selectionRange;
TextPosition saveCursor = _caretPos;
EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, newContent);
_content.performOperation(op, this);
_selectionRange = saveRange;
_caretPos = saveCursor;
ensureCaretVisible();
}
}
/// map key to action
override protected Action findKeyAction(uint keyCode, uint flags) {
// don't handle tabs when disabled
if (keyCode == KeyCode.TAB && (flags == 0 || flags == KeyFlag.Shift) && (!_wantTabs || readOnly))
return null;
return super.findKeyAction(keyCode, flags);
}
/// handle keys
override bool onKeyEvent(KeyEvent event) {
if (event.action == KeyAction.Text && event.text.length && !(event.flags & (KeyFlag.Control | KeyFlag.Alt))) {
Log.d("text entered: ", event.text);
if (readOnly)
return true;
dchar ch = event.text[0];
if (replaceMode && _selectionRange.empty && _content[_caretPos.line].length >= _caretPos.pos + event.text.length) {
// replace next char(s)
TextRange range = _selectionRange;
range.end.pos += cast(int)event.text.length;
EditOperation op = new EditOperation(EditAction.Replace, range, [event.text]);
_content.performOperation(op, this);
} else {
EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [event.text]);
_content.performOperation(op, this);
}
return true;
}
return super.onKeyEvent(event);
}
/// process mouse event; return true if event is processed by widget.
override bool onMouseEvent(MouseEvent event) {
//Log.d("onMouseEvent ", id, " ", event.action, " (", event.x, ",", event.y, ")");
// support onClick
if (event.action == MouseAction.ButtonDown && event.button == MouseButton.Left) {
setFocus();
updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, false);
invalidate();
return true;
}
if (event.action == MouseAction.Move && (event.flags & MouseButton.Left) != 0) {
updateCaretPositionByMouse(event.x - _clientRect.left, event.y - _clientRect.top, true);
return true;
}
if (event.action == MouseAction.ButtonUp && event.button == MouseButton.Left) {
return true;
}
if (event.action == MouseAction.FocusOut || event.action == MouseAction.Cancel) {
return true;
}
if (event.action == MouseAction.FocusIn) {
return true;
}
if (event.action == MouseAction.Wheel) {
uint keyFlags = event.flags & (MouseFlag.Shift | MouseFlag.Control | MouseFlag.Alt);
if (event.wheelDelta < 0) {
if (keyFlags == MouseFlag.Shift)
return handleAction(new Action(EditorActions.ScrollRight));
if (keyFlags == MouseFlag.Control)
return handleAction(new Action(EditorActions.ZoomOut));
return handleAction(new Action(EditorActions.ScrollLineDown));
} else if (event.wheelDelta > 0) {
if (keyFlags == MouseFlag.Shift)
return handleAction(new Action(EditorActions.ScrollLeft));
if (keyFlags == MouseFlag.Control)
return handleAction(new Action(EditorActions.ZoomIn));
return handleAction(new Action(EditorActions.ScrollLineUp));
}
}
return super.onMouseEvent(event);
}
}
interface EditorActionHandler {
bool onEditorAction(const Action action);
}
/// single line editor
class EditLine : EditWidgetBase {
Signal!EditorActionHandler editorActionListener;
/// empty parameter list constructor - for usage by factory
this() {
this(null);
}
/// create with ID parameter
this(string ID, dstring initialContent = null) {
super(ID, ScrollBarMode.Invisible, ScrollBarMode.Invisible);
_content = new EditableContent(false);
_content.contentChangeListeners = this;
wantTabs = false;
styleId = STYLE_EDIT_LINE;
text = initialContent;
}
protected dstring _measuredText;
protected int[] _measuredTextWidths;
protected Point _measuredTextSize;
override protected Rect textPosToClient(TextPosition p) {
Rect res;
res.bottom = _clientRect.height;
if (p.pos == 0)
res.left = 0;
else if (p.pos >= _measuredText.length)
res.left = _measuredTextSize.x;
else
res.left = _measuredTextWidths[p.pos - 1];
res.left -= _scrollPos.x;
res.right = res.left + 1;
return res;
}
override protected TextPosition clientToTextPos(Point pt) {
pt.x += _scrollPos.x;
TextPosition res;
for (int i = 0; i < _measuredText.length; i++) {
int x0 = i > 0 ? _measuredTextWidths[i - 1] : 0;
int x1 = _measuredTextWidths[i];
int mx = (x0 + x1) >> 1;
if (pt.x < mx) {
res.pos = i;
return res;
}
}
res.pos = cast(int)_measuredText.length;
return res;
}
override protected void ensureCaretVisible() {
//_scrollPos
Rect rc = textPosToClient(_caretPos);
if (rc.left < 0) {
// scroll left
_scrollPos.x -= -rc.left + _clientRect.width / 10;
if (_scrollPos.x < 0)
_scrollPos.x = 0;
invalidate();
} else if (rc.left >= _clientRect.width - 10) {
// scroll right
_scrollPos.x += (rc.left - _clientRect.width) + _spaceWidth * 4;
invalidate();
}
updateScrollBars();
}
override protected Point measureVisibleText() {
FontRef font = font();
//Point sz = font.textSize(text);
_measuredText = text;
_measuredTextWidths.length = _measuredText.length;
int charsMeasured = font.measureText(_measuredText, _measuredTextWidths, int.max, tabSize);
_measuredTextSize.x = charsMeasured > 0 ? _measuredTextWidths[charsMeasured - 1]: 0;
_measuredTextSize.y = font.height;
return _measuredTextSize;
}
/// measure
override void measure(int parentWidth, int parentHeight) {
updateFontProps();
measureVisibleText();
measuredContent(parentWidth, parentHeight, _measuredTextSize.x + _leftPaneWidth, _measuredTextSize.y);
}
override protected bool handleAction(const Action a) {
switch (a.id) {
case EditorActions.InsertNewLine:
case EditorActions.PrependNewLine:
if (editorActionListener.assigned) {
return editorActionListener(a);
}
break;
case EditorActions.Up:
break;
case EditorActions.Down:
break;
case EditorActions.PageUp:
break;
case EditorActions.PageDown:
break;
default:
break;
}
return super.handleAction(a);
}
/// handle keys
override bool onKeyEvent(KeyEvent event) {
return super.onKeyEvent(event);
}
/// process mouse event; return true if event is processed by widget.
override bool onMouseEvent(MouseEvent event) {
return super.onMouseEvent(event);
}
/// Set widget rectangle to specified value and layout widget contents. (Step 2 of two phase layout).
override void layout(Rect rc) {
if (visibility == Visibility.Gone) {
return;
}
_needLayout = false;
Point sz = Point(rc.width, measuredHeight);
applyAlign(rc, sz);
_pos = rc;
_clientRect = rc;
applyMargins(_clientRect);
applyPadding(_clientRect);
}
/// override to custom highlight of line background
protected void drawLineBackground(DrawBuf buf, Rect lineRect, Rect visibleRect) {
if (!_selectionRange.empty) {
// line inside selection
Rect startrc = textPosToClient(_selectionRange.start);
Rect endrc = textPosToClient(_selectionRange.end);
int startx = startrc.left + _clientRect.left;
int endx = endrc.left + _clientRect.left;
Rect rc = lineRect;
rc.left = startx;
rc.right = endx;
if (!rc.empty) {
// draw selection rect for line
buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal);
}
if (_leftPaneWidth > 0) {
Rect leftPaneRect = visibleRect;
leftPaneRect.right = leftPaneRect.left;
leftPaneRect.left -= _leftPaneWidth;
drawLeftPane(buf, leftPaneRect, 0);
}
}
}
/// draw content
override void onDraw(DrawBuf buf) {
if (visibility != Visibility.Visible)
return;
super.onDraw(buf);
Rect rc = _pos;
applyMargins(rc);
applyPadding(rc);
auto saver = ClipRectSaver(buf, rc, alpha);
FontRef font = font();
dstring txt = text;
Point sz = font.textSize(txt);
//applyAlign(rc, sz);
Rect lineRect = _clientRect;
lineRect.left = _clientRect.left - _scrollPos.x;
lineRect.right = lineRect.left + calcLineWidth(txt);
Rect visibleRect = lineRect;
visibleRect.left = _clientRect.left;
visibleRect.right = _clientRect.right;
drawLineBackground(buf, lineRect, visibleRect);
font.drawText(buf, rc.left - _scrollPos.x, rc.top, txt, textColor, tabSize);
drawCaret(buf);
}
}
/// single line editor
class EditBox : EditWidgetBase {
/// empty parameter list constructor - for usage by factory
this() {
this(null);
}
/// create with ID parameter
this(string ID, dstring initialContent = null, ScrollBarMode hscrollbarMode = ScrollBarMode.Visible, ScrollBarMode vscrollbarMode = ScrollBarMode.Visible) {
super(ID, hscrollbarMode, vscrollbarMode);
_content = new EditableContent(true); // multiline
_content.contentChangeListeners = this;
styleId = STYLE_EDIT_BOX;
text = initialContent;
acceleratorMap.add( [
// zoom
new Action(EditorActions.ZoomIn, KeyCode.ADD, KeyFlag.Control),
new Action(EditorActions.ZoomOut, KeyCode.SUB, KeyFlag.Control),
]);
}
protected int _firstVisibleLine;
protected int _maxLineWidth;
protected int _numVisibleLines; // number of lines visible in client area
protected dstring[] _visibleLines; // text for visible lines
protected int[][] _visibleLinesMeasurement; // char positions for visible lines
protected int[] _visibleLinesWidths; // width (in pixels) of visible lines
protected CustomCharProps[][] _visibleLinesHighlights;
override protected int lineCount() {
return _content.length;
}
override protected void updateMaxLineWidth() {
// find max line width. TODO: optimize!!!
int maxw;
int[] buf;
for (int i = 0; i < _content.length; i++) {
dstring s = _content[i];
int w = calcLineWidth(s);
if (maxw < w)
maxw = w;
}
_maxLineWidth = maxw;
}
@property int minFontSize() {
return _minFontSize;
}
@property EditBox minFontSize(int size) {
_minFontSize = size;
return this;
}
@property int maxFontSize() {
return _maxFontSize;
}
@property EditBox maxFontSize(int size) {
_maxFontSize = size;
return this;
}
override protected Point measureVisibleText() {
Point sz;
FontRef font = font();
_lineHeight = font.height;
_numVisibleLines = (_clientRect.height + _lineHeight - 1) / _lineHeight;
if (_firstVisibleLine + _numVisibleLines > _content.length)
_numVisibleLines = _content.length - _firstVisibleLine;
_visibleLines.length = _numVisibleLines;
_visibleLinesMeasurement.length = _numVisibleLines;
_visibleLinesWidths.length = _numVisibleLines;
_visibleLinesHighlights.length = _numVisibleLines;
for (int i = 0; i < _numVisibleLines; i++) {
_visibleLines[i] = _content[_firstVisibleLine + i];
_visibleLinesMeasurement[i].length = _visibleLines[i].length;
_visibleLinesHighlights[i] = handleCustomLineHighlight(_firstVisibleLine + i, _visibleLines[i]);
int charsMeasured = font.measureText(_visibleLines[i], _visibleLinesMeasurement[i], int.max, tabSize);
_visibleLinesWidths[i] = charsMeasured > 0 ? _visibleLinesMeasurement[i][charsMeasured - 1] : 0;
if (sz.x < _visibleLinesWidths[i])
sz.x = _visibleLinesWidths[i]; // width - max from visible lines
}
sz.x = _maxLineWidth;
sz.y = _lineHeight * _content.length; // height - for all lines
return sz;
}
/// update horizontal scrollbar widget position
override protected void updateHScrollBar() {
_hscrollbar.setRange(0, _maxLineWidth + _clientRect.width / 4);
_hscrollbar.pageSize = _clientRect.width;
_hscrollbar.position = _scrollPos.x;
}
/// update verticat scrollbar widget position
override protected void updateVScrollBar() {
int visibleLines = _clientRect.height / _lineHeight; // fully visible lines
if (visibleLines < 1)
visibleLines = 1;
_vscrollbar.setRange(0, _content.length - 1);
_vscrollbar.pageSize = visibleLines;
_vscrollbar.position = _firstVisibleLine;
}
/// process horizontal scrollbar event
override bool onHScroll(ScrollEvent event) {
if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) {
if (_scrollPos.x != event.position) {
_scrollPos.x = event.position;
invalidate();
}
} else if (event.action == ScrollAction.PageUp) {
dispatchAction(new Action(EditorActions.ScrollLeft));
} else if (event.action == ScrollAction.PageDown) {
dispatchAction(new Action(EditorActions.ScrollRight));
} else if (event.action == ScrollAction.LineUp) {
dispatchAction(new Action(EditorActions.ScrollLeft));
} else if (event.action == ScrollAction.LineDown) {
dispatchAction(new Action(EditorActions.ScrollRight));
}
return true;
}
/// process vertical scrollbar event
override bool onVScroll(ScrollEvent event) {
if (event.action == ScrollAction.SliderMoved || event.action == ScrollAction.SliderReleased) {
if (_firstVisibleLine != event.position) {
_firstVisibleLine = event.position;
measureVisibleText();
invalidate();
}
} else if (event.action == ScrollAction.PageUp) {
dispatchAction(new Action(EditorActions.ScrollPageUp));
} else if (event.action == ScrollAction.PageDown) {
dispatchAction(new Action(EditorActions.ScrollPageDown));
} else if (event.action == ScrollAction.LineUp) {
dispatchAction(new Action(EditorActions.ScrollLineUp));
} else if (event.action == ScrollAction.LineDown) {
dispatchAction(new Action(EditorActions.ScrollLineDown));
}
return true;
}
override protected void ensureCaretVisible() {
if (_caretPos.line >= _content.length)
_caretPos.line = _content.length - 1;
if (_caretPos.line < 0)
_caretPos.line = 0;
int visibleLines = _clientRect.height / _lineHeight; // fully visible lines
if (visibleLines < 1)
visibleLines = 1;
if (_caretPos.line < _firstVisibleLine) {
_firstVisibleLine = _caretPos.line;
measureVisibleText();
invalidate();
} else if (_caretPos.line >= _firstVisibleLine + visibleLines) {
_firstVisibleLine = _caretPos.line - visibleLines + 1;
if (_firstVisibleLine < 0)
_firstVisibleLine = 0;
measureVisibleText();
invalidate();
}
//_scrollPos
Rect rc = textPosToClient(_caretPos);
if (rc.left < 0) {
// scroll left
_scrollPos.x -= -rc.left + _clientRect.width / 4;
if (_scrollPos.x < 0)
_scrollPos.x = 0;
invalidate();
} else if (rc.left >= _clientRect.width - 10) {
// scroll right
_scrollPos.x += (rc.left - _clientRect.width) + _clientRect.width / 4;
invalidate();
}
updateScrollBars();
}
override protected Rect textPosToClient(TextPosition p) {
Rect res;
int lineIndex = p.line - _firstVisibleLine;
res.top = lineIndex * _lineHeight;
res.bottom = res.top + _lineHeight;
if (lineIndex >=0 && lineIndex < _visibleLines.length) {
if (p.pos == 0)
res.left = 0;
else if (p.pos >= _visibleLinesMeasurement[lineIndex].length)
res.left = _visibleLinesWidths[lineIndex];
else
res.left = _visibleLinesMeasurement[lineIndex][p.pos - 1];
}
res.left -= _scrollPos.x;
res.right = res.left + 1;
return res;
}
override protected TextPosition clientToTextPos(Point pt) {
TextPosition res;
pt.x += _scrollPos.x;
int lineIndex = pt.y / _lineHeight;
if (lineIndex < 0)
lineIndex = 0;
if (lineIndex < _visibleLines.length) {
res.line = lineIndex + _firstVisibleLine;
for (int i = 0; i < _visibleLinesMeasurement[lineIndex].length; i++) {
int x0 = i > 0 ? _visibleLinesMeasurement[lineIndex][i - 1] : 0;
int x1 = _visibleLinesMeasurement[lineIndex][i];
int mx = (x0 + x1) >> 1;
if (pt.x < mx) {
res.pos = i;
return res;
}
}
res.pos = cast(int)_visibleLines[lineIndex].length;
} else if (_visibleLines.length > 0) {
res.line = _firstVisibleLine + cast(int)_visibleLines.length - 1;
res.pos = cast(int)_visibleLines[$ - 1].length;
} else {
res.line = 0;
res.pos = 0;
}
return res;
}
override protected bool handleAction(const Action a) {
TextPosition oldCaretPos = _caretPos;
dstring currentLine = _content[_caretPos.line];
switch (a.id) {
case EditorActions.PrependNewLine:
{
correctCaretPos();
_caretPos.pos = 0;
EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]);
_content.performOperation(op, this);
}
return true;
case EditorActions.InsertNewLine:
{
correctCaretPos();
EditOperation op = new EditOperation(EditAction.Replace, _selectionRange, [""d, ""d]);
_content.performOperation(op, this);
}
return true;
case EditorActions.Up:
case EditorActions.SelectUp:
if (_caretPos.line > 0) {
_caretPos.line--;
updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
ensureCaretVisible();
}
return true;
case EditorActions.Down:
case EditorActions.SelectDown:
if (_caretPos.line < _content.length - 1) {
_caretPos.line++;
updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
ensureCaretVisible();
}
return true;
case EditorActions.PageBegin:
case EditorActions.SelectPageBegin:
{
ensureCaretVisible();
_caretPos.line = _firstVisibleLine;
updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
}
return true;
case EditorActions.PageEnd:
case EditorActions.SelectPageEnd:
{
ensureCaretVisible();
int fullLines = _clientRect.height / _lineHeight;
int newpos = _firstVisibleLine + fullLines - 1;
if (newpos >= _content.length)
newpos = _content.length - 1;
_caretPos.line = newpos;
updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
}
return true;
case EditorActions.PageUp:
case EditorActions.SelectPageUp:
{
ensureCaretVisible();
int fullLines = _clientRect.height / _lineHeight;
int newpos = _firstVisibleLine - fullLines;
if (newpos < 0) {
_firstVisibleLine = 0;
_caretPos.line = 0;
} else {
int delta = _firstVisibleLine - newpos;
_firstVisibleLine = newpos;
_caretPos.line -= delta;
}
measureVisibleText();
updateScrollBars();
updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
}
return true;
case EditorActions.PageDown:
case EditorActions.SelectPageDown:
{
ensureCaretVisible();
int fullLines = _clientRect.height / _lineHeight;
int newpos = _firstVisibleLine + fullLines;
if (newpos >= _content.length) {
_caretPos.line = _content.length - 1;
} else {
int delta = newpos - _firstVisibleLine;
_firstVisibleLine = newpos;
_caretPos.line += delta;
}
measureVisibleText();
updateScrollBars();
updateSelectionAfterCursorMovement(oldCaretPos, (a.id & 1) != 0);
}
return true;
case EditorActions.ScrollLeft:
{
if (_scrollPos.x > 0) {
int newpos = _scrollPos.x - _spaceWidth * 4;
if (newpos < 0)
newpos = 0;
_scrollPos.x = newpos;
updateScrollBars();
invalidate();
}
}
return true;
case EditorActions.ScrollRight:
{
if (_scrollPos.x < _maxLineWidth - _clientRect.width) {
int newpos = _scrollPos.x + _spaceWidth * 4;
if (newpos > _maxLineWidth - _clientRect.width)
newpos = _maxLineWidth - _clientRect.width;
_scrollPos.x = newpos;
updateScrollBars();
invalidate();
}
}
return true;
case EditorActions.ScrollLineUp:
{
if (_firstVisibleLine > 0) {
_firstVisibleLine -= 3;
if (_firstVisibleLine < 0)
_firstVisibleLine = 0;
measureVisibleText();
updateScrollBars();
invalidate();
}
}
return true;
case EditorActions.ScrollPageUp:
{
int fullLines = _clientRect.height / _lineHeight;
if (_firstVisibleLine > 0) {
_firstVisibleLine -= fullLines * 3 / 4;
if (_firstVisibleLine < 0)
_firstVisibleLine = 0;
measureVisibleText();
updateScrollBars();
invalidate();
}
}
return true;
case EditorActions.ScrollLineDown:
{
int fullLines = _clientRect.height / _lineHeight;
if (_firstVisibleLine + fullLines < _content.length) {
_firstVisibleLine += 3;
if (_firstVisibleLine > _content.length - fullLines)
_firstVisibleLine = _content.length - fullLines;
if (_firstVisibleLine < 0)
_firstVisibleLine = 0;
measureVisibleText();
updateScrollBars();
invalidate();
}
}
return true;
case EditorActions.ScrollPageDown:
{
int fullLines = _clientRect.height / _lineHeight;
if (_firstVisibleLine + fullLines < _content.length) {
_firstVisibleLine += fullLines * 3 / 4;
if (_firstVisibleLine > _content.length - fullLines)
_firstVisibleLine = _content.length - fullLines;
if (_firstVisibleLine < 0)
_firstVisibleLine = 0;
measureVisibleText();
updateScrollBars();
invalidate();
}
}
return true;
case EditorActions.ZoomIn:
{
if (_minFontSize < _maxFontSize && _minFontSize >= 9 && _maxFontSize >= 9) {
int currentFontSize = fontSize;
int increment = currentFontSize >= 30 ? 2 : 1;
int newFontSize = currentFontSize + increment; //* 110 / 100;
if (currentFontSize != newFontSize && newFontSize <= _maxFontSize) {
fontSize = cast(ushort)newFontSize;
updateFontProps();
measureVisibleText();
updateScrollBars();
invalidate();
}
}
}
return true;
case EditorActions.ZoomOut:
{
if (_minFontSize < _maxFontSize && _minFontSize >= 9 && _maxFontSize >= 9) {
int currentFontSize = fontSize;
int increment = currentFontSize >= 30 ? 2 : 1;
int newFontSize = currentFontSize - increment; //* 100 / 110;
if (currentFontSize != newFontSize && newFontSize >= _minFontSize) {
fontSize = cast(ushort)newFontSize;
updateFontProps();
measureVisibleText();
updateScrollBars();
invalidate();
}
}
}
return true;
default:
break;
}
return super.handleAction(a);
}
/// calculate full content size in pixels
override Point fullContentSize() {
Point textSz = measureVisibleText();
int maxy = _lineHeight * 5; // limit measured height
if (textSz.y > maxy)
textSz.y = maxy;
return textSz;
}
/// measure
override void measure(int parentWidth, int parentHeight) {
if (visibility == Visibility.Gone) {
return;
}
updateFontProps();
updateMaxLineWidth();
super.measure(parentWidth, parentHeight);
// do we need to add vsbwidth, hsbheight ???
//measuredContent(parentWidth, parentHeight, textSz.x + vsbwidth, textSz.y + hsbheight);
}
/// override to custom highlight of line background
protected void drawLineBackground(DrawBuf buf, int lineIndex, Rect lineRect, Rect visibleRect) {
// highlight odd lines
//if ((lineIndex & 1))
// buf.fillRect(visibleRect, 0xF4808080);
if (!_selectionRange.empty && _selectionRange.start.line <= lineIndex && _selectionRange.end.line >= lineIndex) {
// line inside selection
Rect startrc = textPosToClient(_selectionRange.start);
Rect endrc = textPosToClient(_selectionRange.end);
int startx = lineIndex == _selectionRange.start.line ? startrc.left + _clientRect.left : lineRect.left;
int endx = lineIndex == _selectionRange.end.line ? endrc.left + _clientRect.left : lineRect.right + _spaceWidth;
Rect rc = lineRect;
rc.left = startx;
rc.right = endx;
if (!rc.empty) {
// draw selection rect for line
buf.fillRect(rc, focused ? _selectionColorFocused : _selectionColorNormal);
}
}
// frame around current line
if (focused && lineIndex == _caretPos.line && _selectionRange.singleLine && _selectionRange.start.line == _caretPos.line) {
buf.drawFrame(visibleRect, 0xA0808080, Rect(1,1,1,1));
}
}
override protected void drawExtendedArea(DrawBuf buf) {
if (_leftPaneWidth <= 0)
return;
Rect rc = _clientRect;
FontRef font = font();
int i = _firstVisibleLine;
int lc = lineCount;
for (;;) {
Rect lineRect = rc;
lineRect.left = _clientRect.left - _leftPaneWidth;
lineRect.right = _clientRect.left;
lineRect.bottom = lineRect.top + _lineHeight;
if (lineRect.top >= _clientRect.bottom)
break;
drawLeftPane(buf, lineRect, i < lc ? i : -1);
i++;
rc.top += _lineHeight;
}
}
protected CustomCharProps[ubyte] _tokenHighlightColors;
/// set highlight options for particular token category
void setTokenHightlightColor(ubyte tokenCategory, uint color, bool underline = false, bool strikeThrough = false) {
_tokenHighlightColors[tokenCategory] = CustomCharProps(color, underline, strikeThrough);
}
/// clear highlight colors
void clearTokenHightlightColors() {
destroy(_tokenHighlightColors);
}
/**
Custom text color and style highlight (using text highlight) support.
Return null if no syntax highlight required for line.
*/
protected CustomCharProps[] handleCustomLineHighlight(int line, dstring txt) {
if (!_tokenHighlightColors)
return null; // no highlight colors set
TokenPropString tokenProps = _content.lineTokenProps(line);
if (tokenProps.length > 0) {
bool hasNonzeroTokens = false;
foreach(t; tokenProps)
if (t) {
hasNonzeroTokens = true;
break;
}
if (!hasNonzeroTokens)
return null; // all characters are of unknown token type (or white space)
CustomCharProps[] colors = new CustomCharProps[tokenProps.length];
for (int i = 0; i < tokenProps.length; i++) {
ubyte p = tokenProps[i];
if (p in _tokenHighlightColors)
colors[i] = _tokenHighlightColors[p];
else if ((p & TOKEN_CATEGORY_MASK) in _tokenHighlightColors)
colors[i] = _tokenHighlightColors[(p & TOKEN_CATEGORY_MASK)];
else
colors[i].color = textColor;
if (isFullyTransparentColor(colors[i].color))
colors[i].color = textColor;
}
return colors;
}
return null;
}
override protected void drawClient(DrawBuf buf) {
Rect rc = _clientRect;
FontRef font = font();
for (int i = 0; i < _visibleLines.length; i++) {
dstring txt = _visibleLines[i];
Rect lineRect = rc;
lineRect.left = _clientRect.left - _scrollPos.x;
lineRect.right = lineRect.left + calcLineWidth(_content[_firstVisibleLine + i]);
lineRect.top = _clientRect.top + i * _lineHeight;
lineRect.bottom = lineRect.top + _lineHeight;
Rect visibleRect = lineRect;
visibleRect.left = _clientRect.left;
visibleRect.right = _clientRect.right;
drawLineBackground(buf, _firstVisibleLine + i, lineRect, visibleRect);
if (_leftPaneWidth > 0) {
Rect leftPaneRect = visibleRect;
leftPaneRect.right = leftPaneRect.left;
leftPaneRect.left -= _leftPaneWidth;
drawLeftPane(buf, leftPaneRect, 0);
}
if (txt.length > 0) {
CustomCharProps[] highlight = _visibleLinesHighlights[i];
if (highlight)
font.drawColoredText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, highlight, tabSize);
else
font.drawText(buf, rc.left - _scrollPos.x, rc.top + i * _lineHeight, txt, textColor, tabSize);
}
}
drawCaret(buf);
}
}
/// Read only edit box for displaying logs with lines append operation
class LogWidget : EditBox {
protected int _maxLines;
/// max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit
@property int maxLines() { return _maxLines; }
/// set max lines to show (when appended more than max lines, older lines will be truncated), 0 means no limit
@property void maxLines(int n) { _maxLines = n; }
protected bool _scrollLock;
/// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction)
@property bool scrollLock() { return _scrollLock; }
/// when true, automatically scrolls down when new lines are appended (usually being reset by scrollbar interaction)
@property void scrollLock(bool flg) { _scrollLock = flg; }
this(string ID) {
super(ID);
_scrollLock = true;
enabled = false;
fontSize = 12;
fontFace = "Consolas,Lucida Console,Courier New";
fontFamily = FontFamily.MonoSpace;
}
/// append lines to the end of text
void appendLines(dstring[] lines...) {
lines ~= ""d; // append new line after last line
content.appendLines(lines);
if (_maxLines > 0 && lineCount > _maxLines) {
TextRange range;
range.end.line = lineCount - _maxLines;
EditOperation op = new EditOperation(EditAction.Replace, range, [""d]);
_content.performOperation(op, this);
}
updateScrollBars();
if (_scrollLock) {
_caretPos = TextPosition(lineCount > 0 ? lineCount - 1 : 0, 0);
ensureCaretVisible();
}
}
}