1 /* Invisible Vector Library
2 * simple FlexBox-based TUI engine
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 module iv
.egra
.gui
.widgets
.editors
/*is aliced*/;
18 import arsd
.simpledisplay
;
27 import iv
.egeditor
.editor
;
28 //import iv.egeditor.highlighters;
32 import iv
.egra
.gui
.subwindows
;
33 import iv
.egra
.gui
.widgets
.base
;
34 import iv
.egra
.gui
.dialogs
;
37 // ////////////////////////////////////////////////////////////////////////// //
38 final class EgraEdTextMeter
: EgTextMeter
{
41 //int currofs; /// x offset for current char (i.e. the last char that was passed to `advance()` should be drawn with this offset)
42 //int currwdt; /// current line width (including, the last char that was passed to `advance()`), preferably without trailing empty space between chars
43 //int currheight; /// current text height; keep this in sync with the current state; `reset` should set it to "default text height"
45 /// this should reset text width iterator (and curr* fields); tabsize > 0: process tabs as... well... tabs ;-)
46 override void reset (int tabsize
) nothrow {
47 twkern
.reset(tabsize
, egraFontSize
);
48 currheight
= gxTextHeightUtf
;
51 /// advance text width iterator, return x position for drawing next char
52 override void advance (dchar ch
, in ref GapBuffer
.HighState hs
) nothrow {
53 twkern
.fixWidthPre(ch
);
54 currofs
= twkern
.currOfs
;
55 currwdt
= twkern
.nextOfsNoSpacing
;
58 /// finish text iterator; it should NOT reset curr* fields!
59 /// WARNING: EditorEngine tries to call this after each `reset()`, but user code may not
60 override void finish () nothrow {
61 //return twkern.finalWidth;
66 // ////////////////////////////////////////////////////////////////////////// //
67 final class TextEditor
: EditorEngine
{
68 enum TEDSingleOnly
; // only for single-line mode
69 enum TEDMultiOnly
; // only for multiline mode
70 enum TEDEditOnly
; // only for non-readonly mode
71 enum TEDROOnly
; // only for readonly mode
73 static struct TEDKey
{ string key
; string help
; bool hidden
; } // UDA
75 static string
TEDImplX(string key
, string help
, string code
, size_t ln
) () {
76 static assert(key
.length
> 0, "wtf?!");
77 static assert(code
.length
> 0, "wtf?!");
78 string res
= "@TEDKey("~key
.stringof
~", "~help
.stringof
~") void _ted_";
80 while (pos
< key
.length
) {
82 if (key
.length
-pos
> 0 && key
[pos
] == '-') {
83 if (ch
== 'C' || ch
== 'c') { ++pos
; res
~= "Ctrl"; continue; }
84 if (ch
== 'M' || ch
== 'm') { ++pos
; res
~= "Alt"; continue; }
85 if (ch
== 'S' || ch
== 's') { ++pos
; res
~= "Shift"; continue; }
87 if (ch
== '^') { res
~= "Ctrl"; continue; }
88 if (ch
>= 'a' && ch
<= 'z') ch
-= 32;
89 if ((ch
>= '0' && ch
<= '9') ||
(ch
>= 'A' && ch
<= 'Z') || ch
== '_') res
~= ch
; else res
~= '_';
92 res
~= " () {"~code
~"}";
96 mixin template TEDImpl(string key
, string help
, string code
, size_t ln
=__LINE__
) {
97 mixin(TEDImplX
!(key
, help
, code
, ln
));
100 mixin template TEDImpl(string key
, string code
, size_t ln
=__LINE__
) {
101 mixin(TEDImplX
!(key
, "", code
, ln
));
105 KeyEvent
[32] comboBuf
;
106 int comboCount
; // number of items in `comboBuf`
109 EgraEdTextMeter chiTextMeter
;
112 this (Widget apw
, int x0
, int y0
, int w
, int h
, bool asinglesine
=false) {
114 //coordsInPixels = true;
115 lineHeightPixels
= gxTextHeightUtf
;
116 chiTextMeter
= new EgraEdTextMeter();
117 super(x0
, y0
, w
, h
, null, asinglesine
);
118 textMeter
= chiTextMeter
;
121 tabsize
= 4; // spaces
124 final int lineQuoteLevel (int lidx
) {
125 if (lidx
< 0 || lidx
>= lc
.linecount
) return 0;
126 int pos
= lc
.line2pos(lidx
);
127 auto ts
= gb
.textsize
;
128 if (pos
>= ts
) return 0;
129 if (gb
[pos
] != '>') return 0;
134 if (ch
== '>') ++count
;
135 else if (ch
== '\n') break;
136 else if (ch
!= ' ') break;
141 public override void drawCursor () {
144 localCursorXY(&lcx
, &lcy
);
145 //drawTextCursor(/*pw.parent.isFocused*/true, x0+lcx, y0+lcy);
146 pw
.drawTextCursor(x0
+lcx
, y0
+lcy
, gxTextHeightUtf
);
150 // not here, 'cause this is done before text
151 public override void drawStatus () {}
153 public final paintStatusLine () {
154 import core
.stdc
.stdio
: snprintf
;
155 int sx
= x0
, sy
= y0
+height
;
156 gxFillRect(sx
, sy
, width
, gxTextHeightUtf
, pw
.getColor("status-back"));
157 char[128] buf
= void;
158 auto len
= snprintf(buf
.ptr
, buf
.length
, "%04d:%04d %d", curx
, cury
, linecount
);
159 gxDrawTextUtf(sx
+2, sy
, buf
[0..len
], pw
.getColor("status-text"));
162 public override void drawPage () {
163 fullDirty(); // HACK!
165 gxClipRect
.intersect(GxRect(x0
, y0
, width
, height
));
168 if (!singleline
) paintStatusLine();
172 public override void drawLine (int lidx
, int yofs
, int xskip
) {
175 auto pos
= lc
.line2pos(lidx
);
176 auto lea = lc
.line2pos(lidx
+1);
177 auto ts
= gb
.textsize
;
178 bool utfucked
= utfuck
;
179 immutable int bs
= bstart
, be
= bend
;
182 uint clr
= pw
.getColor("text");
183 immutable uint markFgClr
= pw
.getColor("mark-text");
184 immutable uint markBgClr
= pw
.getColor("mark-back");
187 int qlevel
= lineQuoteLevel(lidx
);
189 final switch (qlevel
%2) {
190 case 0: clr
= pw
.getColor("quote0-text"); break;
191 case 1: clr
= pw
.getColor("quote1-text"); break;
194 char[1024] abuf
= void;
195 auto aname
= getAttachName(abuf
[], lidx
);
198 import std
.file
: exists
, isFile
;
199 clr
= (aname
.exists
&& aname
.isFile ? pw
.getColor("attach-file-text") : pw
.getColor("attach-bad-text"));
200 } catch (Exception e
) {}
205 bool checkMarking
= false;
206 if (hasMarkedBlock
) {
207 //conwriteln("bs=", bs, "; be=", be, "; pos=", pos, "; lea=", lea);
208 if (pos
>= bs
&& lea <= be
) {
210 gxFillRect(x0
, y
, winw
, lineHeightPixels
, markBgClr
);
212 } else if (pos
< be
&& lea > bs
) {
213 // draw block background
215 int bx0
= x
, bx1
= x
;
216 chiTextMeter
.twkern
.reset((visualtabs ? tabsize
: 0), egraFontSize
);
218 immutable dchar dch
= dcharAtAdvance(tpos
);
219 if (!singleline
&& dch
== '\n') break;
220 chiTextMeter
.twkern
.fixWidthPre(dch
);
221 if (tpos
> bs
) break;
223 bx0
= bx1
= x
+chiTextMeter
.twkern
.currOfs
;
226 if (tpos
>= be
) break;
227 immutable dchar dch
= dcharAtAdvance(tpos
);
228 if (!singleline
&& dch
== '\n') { eolhit
= (tpos
< be
); break; }
229 chiTextMeter
.twkern
.fixWidthPre(dch
);
230 if (tpos
> be
) break;
232 bx1
= (eolhit ? x0
+width
: x
+chiTextMeter
.twkern
.finalWidth
);
233 gxFillRect(bx0
, y
, bx1
-bx0
+1, lineHeightPixels
, markBgClr
);
237 if (singleline
&& hasMarkedBlock
) checkMarking
= true; // let it be
239 //twkern.reset(visualtabs ? tabsize : 0);
246 while (epos
< ts
) if (gb
[epos
++] == '\n') { --epos
; break; }
250 int wdt
= gxDrawTextUtfOpt(GxDrawTextOptions
.Tab(tabsize
), x
, y
, this[pos
..epos
], delegate (in ref state
) nothrow @trusted {
251 return (checkMarking ?
(pos
+state
.spos
>= bs
&& pos
+state
.epos
<= be ? markFgClr
: clr
) : clr
);
254 if (epos
> pos
&& gb
[epos
-1] == ' ') {
255 gxDrawChar(x
+wdt
, y
, '\u2248', pw
.getColor("wrap-mark-text"));
261 if (checkMarking) cc = (pos >= bs && pos < be ? markFgClr : clr);
262 immutable dchar dch = dcharAtAdvance(pos);
263 if (!singleline && dch == '\n') {
264 // draw "can wrap" mark
265 if (pos-2 >= ls && gb[pos-2] == ' ') {
266 int xx = x+twkern.finalWidth+1;
267 gxDrawChar(xx, y, '\n', gxRGB!(0, 0, 220));
271 if (dch == '\t' && visualtabs) {
272 int xx = x+twkern.fixWidthPre('\t');
273 gxHLine(xx, y+lineHeightPixels/2, twkern.tablength, gxRGB!(200, 0, 0));
275 gxDrawChar(x+twkern.fixWidthPre(dch), y, dch, cc);
282 // use `winXXX` vars to know window dimensions
283 public override void drawEmptyLine (int yofs
) {
284 // nothing to do here
287 protected enum Ecc
{ None
, Eaten
, Combo
}
291 // Combo: combo start
292 // comboBuf should contain comboCount keys!
293 protected final Ecc
checkKeys (const(char)[] keys
) {
294 foreach (immutable cidx
; 0..comboCount
+1) {
296 if (keys
.length
== 0) return Ecc
.Combo
;
298 while (kepos
< keys
.length
&& keys
.ptr
[kepos
] > ' ') ++kepos
;
299 if (comboBuf
[cidx
] != keys
[0..kepos
]) return Ecc
.None
;
300 keys
= keys
[kepos
..$];
302 return (keys
.xstrip
.length ? Ecc
.Combo
: Ecc
.Eaten
);
305 // fuck! `(this ME)` trick doesn't work here
306 protected final Ecc
doEditorCommandByUDA(ME
=typeof(this)) (KeyEvent key
) {
308 bool possibleCombo
= false;
309 // temporarily add current key to combo
310 comboBuf
[comboCount
] = key
;
311 // check all known combos
312 foreach (string memn
; __traits(allMembers
, ME
)) {
313 static if (is(typeof(&__traits(getMember
, ME
, memn
)))) {
314 import std
.meta
: AliasSeq
;
315 alias mx
= AliasSeq
!(__traits(getMember
, ME
, memn
))[0];
316 static if (isCallable
!mx
&& hasUDA
!(mx
, TEDKey
)) {
318 bool goodMode
= true;
319 static if (hasUDA
!(mx
, TEDSingleOnly
)) { if (!singleline
) goodMode
= false; }
320 static if (hasUDA
!(mx
, TEDMultiOnly
)) { if (singleline
) goodMode
= false; }
321 static if (hasUDA
!(mx
, TEDEditOnly
)) { if (readonly
) goodMode
= false; }
322 static if (hasUDA
!(mx
, TEDROOnly
)) { if (!readonly
) goodMode
= false; }
324 foreach (const TEDKey attr
; getUDAs
!(mx
, TEDKey
)) {
325 auto cc
= checkKeys(attr
.key
);
326 if (cc
== Ecc
.Eaten
) {
328 static if (is(ReturnType
!mx
== void)) {
329 comboCount
= 0; // reset combo
334 comboCount
= 0; // reset combo
338 } else if (cc
== Ecc
.Combo
) {
339 possibleCombo
= true;
346 // check if we can start/continue combo
348 if (++comboCount
< comboBuf
.length
-1) return Ecc
.Combo
;
350 // if we have combo prefix, eat key unconditionally
351 if (comboCount
> 0) {
352 comboCount
= 0; // reset combo, too long, or invalid, or none
358 bool processKey (KeyEvent key
) {
359 final switch (doEditorCommandByUDA(key
)) {
360 case Ecc
.None
: break;
369 bool processChar (dchar ch
) {
370 if (ch
< ' ' || ch
== 127) return false;
372 // check if we should reformat
373 auto llen
= linelen(cury
);
375 if (curx
== 0 || gb
[curpos
-1] != ' ') doPutChar(' ');
380 int rpos
= lc
.linestart(ocy
);
382 if (gb
[rpos
] == '>') {
383 while (gb
[rpos
] == '>') { ++qcnt
; ++rpos
; }
384 if (gb
[rpos
] == ' ') { ++qcnt
; ++rpos
; }
386 //conwriteln("origcx=", ocx, "; ncx=", ocx-qcnt, "; qcnt=", qcnt);
387 if (ocx
< qcnt
) ocx
= qcnt
; else ocx
-= qcnt
;
389 reformatFromLine
!true(cury
);
391 while (ocy
< lc
.linecount
) {
392 int rpos
= lc
.linestart(ocy
);
394 //conwriteln(" 00: ocy=", ocy, "; llen=", llen, "; ocx=", ocx);
396 if (gb
[rpos
] == '>') {
397 while (gb
[rpos
] == '>') { --llen
; ++qcnt
; ++rpos
; }
398 if (gb
[rpos
] == ' ') { --llen
; ++qcnt
; ++rpos
; }
400 //conwriteln(" 01: ocy=", ocy, "; llen=", llen, "; ocx=", ocx, "; qcnt=", qcnt);
401 if (ocx
<= llen
) { ocx
+= qcnt
; break; }
413 bool processClick (int x
, int y
, MouseEvent event
) {
414 if (x
< 0 || y
< 0 || x
>= winw || y
>= winh
) return false;
415 if (event
.type
== MouseEventType
.buttonPressed
&& event
.button
== MouseButton
.left
) {
417 widget2text(x
, y
, tx
, ty
);
425 void processWordWith (scope char delegate (char ch
) dg
) {
426 if (dg
is null) return;
427 bool undoAdded
= false;
428 scope(exit
) if (undoAdded
) undoGroupEnd();
430 if (!isWordChar(gb
[pos
])) return;
432 while (pos
> 0 && isWordChar(gb
[pos
-1])) --pos
;
433 while (pos
< gb
.textsize
) {
435 if (!isWordChar(gb
[pos
])) break;
438 if (!undoAdded
) { undoAdded
= true; undoGroupStart(); }
439 replaceText
!"none"(pos
, 1, (&nc
)[0..1]);
447 @TEDMultiOnly mixin TEDImpl
!("Up", q
{ doUp(); });
448 @TEDMultiOnly mixin TEDImpl
!("S-Up", q
{ doUp(true); });
449 @TEDMultiOnly mixin TEDImpl
!("C-Up", q
{ doScrollUp(); });
450 @TEDMultiOnly mixin TEDImpl
!("S-C-Up", q
{ doScrollUp(true); });
452 @TEDMultiOnly mixin TEDImpl
!("Down", q
{ doDown(); });
453 @TEDMultiOnly mixin TEDImpl
!("S-Down", q
{ doDown(true); });
454 @TEDMultiOnly mixin TEDImpl
!("C-Down", q
{ doScrollDown(); });
455 @TEDMultiOnly mixin TEDImpl
!("S-C-Down", q
{ doScrollDown(true); });
457 mixin TEDImpl
!("Left", q
{ doLeft(); });
458 mixin TEDImpl
!("S-Left", q
{ doLeft(true); });
459 mixin TEDImpl
!("C-Left", q
{ doWordLeft(); });
460 mixin TEDImpl
!("S-C-Left", q
{ doWordLeft(true); });
462 mixin TEDImpl
!("Right", q
{ doRight(); });
463 mixin TEDImpl
!("S-Right", q
{ doRight(true); });
464 mixin TEDImpl
!("C-Right", q
{ doWordRight(); });
465 mixin TEDImpl
!("S-C-Right", q
{ doWordRight(true); });
467 @TEDMultiOnly mixin TEDImpl
!("PageUp", q
{ doPageUp(); });
468 @TEDMultiOnly mixin TEDImpl
!("S-PageUp", q
{ doPageUp(true); });
469 @TEDMultiOnly mixin TEDImpl
!("C-PageUp", q
{ doTextTop(); });
470 @TEDMultiOnly mixin TEDImpl
!("S-C-PageUp", q
{ doTextTop(true); });
472 @TEDMultiOnly mixin TEDImpl
!("PageDown", q
{ doPageDown(); });
473 @TEDMultiOnly mixin TEDImpl
!("S-PageDown", q
{ doPageDown(true); });
474 @TEDMultiOnly mixin TEDImpl
!("C-PageDown", q
{ doTextBottom(); });
475 @TEDMultiOnly mixin TEDImpl
!("S-C-PageDown", q
{ doTextBottom(true); });
477 mixin TEDImpl
!("Home", q
{ doHome(); });
478 mixin TEDImpl
!("S-Home", q
{ doHome(true, true); });
479 @TEDMultiOnly mixin TEDImpl
!("C-Home", q
{ doPageTop(); });
480 @TEDMultiOnly mixin TEDImpl
!("S-C-Home", q
{ doPageTop(true); });
482 mixin TEDImpl
!("End", q
{ doEnd(); });
483 mixin TEDImpl
!("S-End", q
{ doEnd(true); });
484 @TEDMultiOnly mixin TEDImpl
!("C-End", q
{ doPageBottom(); });
485 @TEDMultiOnly mixin TEDImpl
!("S-C-End", q
{ doPageBottom(true); });
487 @TEDEditOnly mixin TEDImpl
!("Backspace", q
{ doBackspace(); });
488 @TEDSingleOnly @TEDEditOnly mixin TEDImpl
!("M-Backspace", "delete previous word", q
{ doDeleteWord(); });
489 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("M-Backspace", "delete previous word or unindent", q
{ doBackByIndent(); });
491 mixin TEDImpl
!("Delete", q
{
495 //mixin TEDImpl!("^Insert", "copy block to clipboard file, reset block mark", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); doBlockResetMark(); });
497 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("Enter", q
{
499 if (lineQuoteLevel(ly
)) {
500 doLineSplit(false); // no autoindent
501 auto ls
= lc
.linestart(ly
);
503 scope(exit
) undoGroupEnd();
504 while (gb
[ls
] == '>') {
506 insertText
!"end"(curpos
, ">");
508 if (gb
[curpos
] > ' ') insertText
!"end"(curpos
, " ");
513 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("M-Enter", "split line without autoindenting", q
{ doLineSplit(false); });
516 mixin TEDImpl
!("F3", "start/stop/reset block marking", q
{ doToggleBlockMarkMode(); });
517 mixin TEDImpl
!("C-F3", "reset block mark", q
{ doBlockResetMark(); });
518 @TEDEditOnly mixin TEDImpl
!("F5", "copy block", q
{ doBlockCopy(); });
519 // mixin TEDImpl!("^F5", "copy block to clipboard file", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); });
520 //@TEDEditOnly mixin TEDImpl!("S-F5", "insert block from clipboard file", q{ if (tempBlockFileName.length == 0) return; waitingInF5 = true; });
521 @TEDEditOnly mixin TEDImpl
!("F6", "move block", q
{ doBlockMove(); });
522 @TEDEditOnly mixin TEDImpl
!("F8", "delete block", q
{ doBlockDelete(); });
524 mixin TEDImpl
!("C-A", "move to line start", q
{ doHome(); });
525 mixin TEDImpl
!("C-E", "move to line end", q
{ doEnd(); });
527 @TEDMultiOnly mixin TEDImpl
!("M-I", "jump to previous bookmark", q
{ doBookmarkJumpUp(); });
528 @TEDMultiOnly mixin TEDImpl
!("M-J", "jump to next bookmark", q
{ doBookmarkJumpDown(); });
529 @TEDMultiOnly mixin TEDImpl
!("M-K", "toggle bookmark", q
{ doBookmarkToggle(); });
531 @TEDEditOnly mixin TEDImpl
!("M-C", "capitalize word", q
{
533 processWordWith((char ch
) {
534 if (first
) { first
= false; ch
= ch
.toupper
; }
538 @TEDEditOnly mixin TEDImpl
!("M-Q", "lowercase word", q
{ processWordWith((char ch
) => ch
.tolower
); });
539 @TEDEditOnly mixin TEDImpl
!("M-U", "uppercase word", q
{ processWordWith((char ch
) => ch
.toupper
); });
541 @TEDMultiOnly mixin TEDImpl
!("M-S-L", "force center current line", q
{ makeCurLineVisibleCentered(true); });
542 @TEDEditOnly mixin TEDImpl
!("C-U", "undo", q
{ doUndo(); });
543 @TEDEditOnly mixin TEDImpl
!("M-S-U", "redo", q
{ doRedo(); });
544 @TEDEditOnly mixin TEDImpl
!("C-W", "remove previous word", q
{ doDeleteWord(); });
545 @TEDEditOnly mixin TEDImpl
!("C-Y", "remove current line", q
{ doKillLine(); });
547 //@TEDMultiOnly @TEDEditOnly mixin TEDImpl!("Tab", q{ doPutText(" "); });
548 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-Tab", "indent block", q
{ doIndentBlock(); });
549 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-S-Tab", "unindent block", q
{ doUnindentBlock(); });
551 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-I", "indent block", q
{ doIndentBlock(); });
552 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-U", "unindent block", q
{ doUnindentBlock(); });
553 @TEDEditOnly mixin TEDImpl
!("C-K C-E", "clear from cursor to EOL", q
{ doKillToEOL(); });
554 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K Tab", "indent block", q
{ doIndentBlock(); });
555 // @TEDEditOnly mixin TEDImpl!("^K M-Tab", "untabify", q{ doUntabify(gb.tabsize ? gb.tabsize : 2); }); // alt+tab: untabify
556 // @TEDEditOnly mixin TEDImpl!("^K C-space", "remove trailing spaces", q{ doRemoveTailingSpaces(); });
557 // mixin TEDImpl!("C-K C-T", /*"toggle \"visual tabs\" mode",*/ q{ visualtabs = !visualtabs; });
559 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-B", q
{ doSetBlockStart(); });
560 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-K", q
{ doSetBlockEnd(); });
562 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-C", q
{ doBlockCopy(); });
563 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-M", q
{ doBlockMove(); });
564 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-Y", q
{ doBlockDelete(); });
565 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K C-H", q
{ doBlockResetMark(); });
567 @TEDMultiOnly @TEDEditOnly mixin TEDImpl
!("C-K Backspace", q
{ doBlockResetMark(); });
569 @TEDEditOnly mixin TEDImpl
!("C-Q Tab", q
{ doPutChar('\t'); });
570 //mixin TEDImpl!("C-Q C-U", "toggle utfuck mode", q{ utfuck = !utfuck; }); // ^Q^U: switch utfuck mode
571 //mixin TEDImpl!("C-Q 1", "switch to koi8", q{ utfuck = false; codepage = CodePage.koi8u; fullDirty(); });
572 //mixin TEDImpl!("C-Q 2", "switch to cp1251", q{ utfuck = false; codepage = CodePage.cp1251; fullDirty(); });
573 //mixin TEDImpl!("C-Q 3", "switch to cp866", q{ utfuck = false; codepage = CodePage.cp866; fullDirty(); });
574 mixin TEDImpl
!("C-Q C-B", "go to block start", q
{ if (hasMarkedBlock
) gotoPos
!true(bstart
); lastBGEnd
= false; });
576 @TEDSingleOnly @TEDEditOnly mixin TEDImpl
!("C-Q Enter", "intert LF", q
{ doPutChar('\n'); });
577 @TEDSingleOnly @TEDEditOnly mixin TEDImpl
!("C-Q M-Enter", "intert CR", q
{ doPutChar('\r'); });
579 mixin TEDImpl
!("C-Q C-K", "go to block end", q
{ if (hasMarkedBlock
) gotoPos
!true(bend
); lastBGEnd
= true; });
581 @TEDMultiOnly @TEDROOnly mixin TEDImpl
!("Space", q
{ doPageDown(); });
582 @TEDMultiOnly @TEDROOnly mixin TEDImpl
!("S-Space", q
{ doPageUp(); });
585 dynstring
[] extractAttaches () {
588 lineloop
: while (lidx
< lc
.linecount
) {
590 auto pos
= lc
.linestart(lidx
);
591 //conwriteln("checking line ", lidx, ": '", gb[pos], "'");
592 while (pos
< gb
.textsize
) {
594 if (ch
== '@' && gb
[pos
+1] == '@') { pos
+= 2; break; } // found
595 if (ch
> ' ' || ch
== '\n') { ++lidx
; continue lineloop
; } // next line
598 //conwriteln("found \"@@\" at line ", lidx);
599 // skip spaces after "@@"
600 while (pos
< gb
.textsize
) {
602 if (ch
== '\n') { ++lidx
; continue lineloop
; } // next line
606 if (pos
>= gb
.textsize
) break; // no more text
609 while (pos
< gb
.textsize
) {
611 if (ch
== '\n') break;
616 //conwriteln("got fname: '", fname, "'");
620 int ls
= lc
.linestart(lidx
);
621 int le
= lc
.linestart(lidx
+1);
622 if (ls
< le
) deleteText
!"start"(ls
, le
-ls
);
627 char[] getAttachName (char[] dest
, int lidx
) {
628 if (lidx
< 0 || lidx
>= lc
.linecount
) return null;
630 auto pos
= lc
.linestart(lidx
);
631 while (pos
< gb
.textsize
) {
633 if (ch
== '@' && gb
[pos
+1] == '@') { pos
+= 2; break; } // found
634 if (ch
> ' ' || ch
== '\n') return null; // not found
637 // skip spaces after "@@"
638 while (pos
< gb
.textsize
) {
640 if (ch
== '\n') return null; // not found
644 if (pos
>= gb
.textsize
) return null; // not found
647 while (pos
< gb
.textsize
) {
649 if (ch
== '\n') break;
650 if (dpos
>= dest
.length
) return null;
651 dest
.ptr
[dpos
++] = ch
;
654 return dest
.ptr
[0..dpos
];
657 // ah, who cares about speed?
658 void reformatFromLine(bool doUndoGroup
) (int lidx
) {
659 if (lidx
< 0) lidx
= 0;
660 if (lidx
>= lc
.linecount
) return;
662 static if (doUndoGroup
) {
663 bool undoGroupStarted
= false;
664 scope(exit
) if (undoGroupStarted
) undoGroupEnd();
666 void startUndoGroup () {
667 if (!undoGroupStarted
) {
668 undoGroupStarted
= true;
673 void startUndoGroup () {}
676 void normalizeQuoting (int lidx
) {
677 while (lidx
< lc
.linecount
) {
678 auto pos
= lc
.linestart(lidx
);
679 if (gb
[pos
] == '>') {
680 bool lastWasSpace
= false;
682 auto afterlastq
= pos
;
683 while (pos
< gb
.textsize
) {
685 if (ch
== '\n') break;
686 if (ch
== '>') { afterlastq
= ++pos
; continue; }
687 if (ch
== ' ') { ++pos
; continue; }
691 while (pos
< afterlastq
) {
694 if (ch
== ' ') { deleteText(pos
, 1); --afterlastq
; continue; } // remove space (thus normalizing quotes)
698 assert(pos
== afterlastq
);
699 if (pos
< gb
.textsize
&& gb
[pos
] > ' ') { startUndoGroup(); insertText
!("none", false)(pos
, " "); }
705 // try to join two lines, if it is possible; return `true` if succeed
706 bool tryJoinLines (int lidx
) {
708 if (lc
.linecount
== 1) return false; // nothing to do
709 if (lidx
+1 >= lc
.linecount
) return false; // nothing to do
710 auto ql0
= lineQuoteLevel(lidx
);
711 auto ql1
= lineQuoteLevel(lidx
+1);
712 if (ql0
!= ql1
) return false; // different quote levels, can't join
713 auto ls
= lc
.linestart(lidx
);
714 auto le
= lc
.lineend(lidx
);
715 if (le
-ls
< 1) return false; // wtf?!
716 if (gb
[le
-1] != ' ') return false; // no trailing space -- can't join
717 // don't join if next line is empty one: this is prolly paragraph delimiter
718 if (le
+1 >= gb
.textsize || gb
[le
+1] == '\n') return false;
719 if (gb
[le
+1] == '>') {
721 while (pp
< gb
.textsize
) {
723 if (ch
!= '>' && ch
!= ' ') break;
726 if (gb
[pp
-1] != ' ') return false;
731 // remove excessive spaces, if any
732 while (le
> ls
&& gb
[le
-1] == ' ') --le
;
733 assert(gb
[le
] == ' ');
734 ++le
; // but leave one space
735 while (le
< gb
.textsize
) {
736 if (gb
[le
] == '\n' || gb
[le
] > ' ') break;
741 assert(le
< gb
.textsize
&& gb
[le
] == '>');
742 while (le
< gb
.textsize
) {
744 if (ch
== '\n' ||
(ch
!= '>' && ch
!= ' ')) break;
751 // join lines; we'll split 'em later
752 for (int l
= lidx
; l
< lc
.linecount
; ) if (!tryJoinLines(l
)) ++l
;
754 // make quoting consistent
755 normalizeQuoting(lidx
);
757 void conwrlinerest (int pos
) {
758 if (pos
< 0 || pos
>= gb
.textsize
) return;
759 while (pos
< gb
.textsize
) {
761 if (ch
== '\n') break;
766 void conwrline (int lidx
) {
767 if (lidx
< 0 || lidx
>= lc
.linecount
) return;
768 conwrlinerest(lc
.linestart(lidx
));
771 // now split the lines; all lines are joined, so we can only split
772 lineloop
: while (lidx
< lc
.linecount
) {
773 auto ls
= lc
.linestart(lidx
);
774 auto le
= lc
.lineend(lidx
);
775 // calculate line length without trailing spaces
776 auto llen
= linelen(lidx
);
779 while (pe
> ls
&& gb
[pe
-1] <= ' ') { --pe
; --llen
; }
781 if (llen
<= 76) { ++lidx
; continue; } // nothing to do here
785 // skip quotes, if any
786 while (gb
[pos
] == '>') { ++curlen
; ++pos
; }
787 // skip leading spaces
788 while (gb
[pos
] != '\n' && gb
[pos
] <= ' ') { ++curlen
; ++pos
; }
789 if (pos
>= gb
.textsize || gb
[pos
] == '\n') { ++lidx
; continue; } // wtf?!
792 while (pos
< gb
.textsize
) {
793 immutable stpos
= pos
;
794 dchar ch
= dcharAtAdvance(pos
);
798 if (lwstart
>= 0 && curlen
> 76) {
801 insertText
!("none", false)(lwstart
, "\n");
804 while (gb
[ls
] == '>') {
805 insertText
!("none", false)(lwstart
, ">");
809 insertText
!("none", false)(lwstart
, " ");
814 if (ch
== '\n') break;
815 // not in word anymore
819 if (ch
== '\n') break;
820 if (ch
> ' ') { lwstart
= stpos
; inword
= true; }
829 // ////////////////////////////////////////////////////////////////////////// //
831 int level
; // quote level
832 int length
; // quote prefix length, in chars
835 QuoteInfo
calcQuote(T
:const(char)[]) (T s
) {
836 static if (is(T
== typeof(null))) {
840 if (s
.length
> 0 && s
[0] == '>') {
841 while (qi
.length
< s
.length
) {
842 if (s
[qi
.length
] != ' ') {
843 if (s
[qi
.length
] != '>') break;
848 if (s
.length
-qi
.length
> 1 && s
[qi
.length
] == ' ') ++qi
.length
;
855 // ////////////////////////////////////////////////////////////////////////// //
856 final class EditorWidget
: Widget
{
858 bool moveToBottom
= true;
860 this (Widget aparent
) {
863 editor
= new TextEditor(this, 0, 0, 10, 10);
867 assert(creatorCurrentParent
!is null);
868 this(creatorCurrentParent
);
871 void addText (const(char)[] s
) {
872 immutable GxRect grect
= globalRect
;
873 editor
.moveResize(grect
.x0
, grect
.y0
, (grect
.width
< 1 ?
1 : grect
.width
), (grect
.height
< 1 ? grect
.height
: 1));
874 editor
.doPasteStart();
875 scope(exit
) editor
.doPasteEnd();
876 editor
.doPutTextUtf(s
);
877 //editor.doPutChar('\n');
880 dynstring
getText () {
882 if (editor
.textsize
== 0) return res
;
883 res
.reserve(editor
.textsize
);
884 foreach (char ch
; editor
[]) res
~= ch
;
889 editor
.clearAndDisableUndo();
890 scope(exit
) editor
.reinstantiateUndo();
891 editor
.reformatFromLine
!false(0);
892 editor
.gotoXY(0, 0); // HACK
893 editor
.textChanged
= false;
896 dynstring
[] extractAttaches () {
897 return editor
.extractAttaches();
900 private final void drawScrollBar () {
901 //restoreClip(); // the easiest way again
902 immutable GxRect grect
= globalRect
;
903 gxDrawScrollBar(GxRect(grect
.x0
, grect
.y0
, 4, height
), editor
.linecount
-1, editor
.topline
+editor
.visibleLinesPerWindow
-1);
906 protected override void doPaint (GxRect grect
) {
907 if (width
< 1 || height
< 1) return;
908 editor
.moveResize(grect
.x0
+5, grect
.y0
, grect
.width
-5*2, grect
.height
-gxTextHeightUtf
);
910 if (moveToBottom
) { moveToBottom
= false; editor
.gotoXY(0, editor
.linecount
); } // HACK!
911 gxFillRect(grect
, getColor("back"));
919 // return `true` if event was eaten
920 override bool onKey (KeyEvent event
) {
921 if (!isFocused
) return super.onKey(event
);
923 if (editor
.processKey(event
)) {
927 if (event
== "S-Insert") {
928 getClipboardText(vbwin
, delegate (in char[] text
) {
930 editor
.doPasteStart();
931 scope(exit
) editor
.doPasteEnd();
932 editor
.doPutTextUtf(text
[]);
938 if (event
== "C-Insert") {
939 auto mtr
= editor
.markedBlockRange();
942 brng
.reserve(mtr
.length
);
943 foreach (char ch
; mtr
) brng
~= ch
;
944 if (brng
.length
> 0) {
945 setClipboardText(vbwin
, cast(string
)brng
); // it is safe to cast here
946 setPrimarySelection(vbwin
, cast(string
)brng
); // it is safe to cast here
948 editor
.doBlockResetMark();
954 if (event
== "M-Tab") {
955 char[1024] abuf
= void;
956 auto attname
= editor
.getAttachName(abuf
[], editor
.cury
);
957 if (attname
.length
&& editor
.curx
>= editor
.linelen(editor
.cury
)) {
958 auto cplist
= buildAutoCompletion(attname
);
959 auto atnlen
= attname
.length
;
960 auto adg
= delegate (dynstring s
) {
961 //conwriteln("attname=[", attname, "]; s=[", s, "]");
962 if (s
.length
<= atnlen
/*|| s[0..attname.length] != attname*/) return;
963 editor
.undoGroupStart();
964 scope(exit
) editor
.undoGroupEnd();
965 editor
.doPutTextUtf(s
[atnlen
..$]);
968 if (cplist
.length
== 1) {
971 auto acw
= new SelectCompletionWindow(attname
, cplist
, true);
972 acw
.onSelected
= adg
;
978 return super.onKey(event
);
981 override bool onMouse (MouseEvent event
) {
982 if (!isFocused
) return super.onMouse(event
);
983 if (GxPoint(event
.x
, event
.y
).inside(rect
.size
)) {
984 if (editor
.processClick(event
.x
, event
.y
, event
)) {
989 return super.onMouse(event
);
992 override bool onChar (dchar ch
) {
993 if (isFocused
&& editor
.processChar(ch
)) {
997 return super.onChar(ch
);
1002 // ////////////////////////////////////////////////////////////////////////// //
1003 final class LineEditWidget
: Widget
{
1006 this (Widget aparent
) {
1009 if (width
== 0) width
= 96;
1010 height
= gxTextHeightUtf
+2;
1011 editor
= new TextEditor(this, 0, 0, width
, height
, true); // singleline
1015 assert(creatorCurrentParent
!is null);
1016 this(creatorCurrentParent
);
1019 @property bool readonly () const nothrow { return editor
.readonly
; }
1020 @property void readonly (bool v
) nothrow { editor
.readonly
= v
; }
1022 @property bool killTextOnChar () const nothrow { return editor
.killTextOnChar
; }
1023 @property void killTextOnChar (bool v
) nothrow { editor
.killTextOnChar
= v
; }
1025 @property dynstring
str () {
1027 if (editor
.textsize
== 0) return res
;
1028 res
.reserve(editor
.textsize
);
1029 foreach (char ch
; editor
[]) res
~= ch
;
1033 @property void str (const(char)[] s
) {
1034 editor
.clearAndDisableUndo();
1035 scope(exit
) editor
.reinstantiateUndo();
1037 editor
.doPutTextUtf(s
);
1038 editor
.textChanged
= false;
1041 protected override void doPaint (GxRect grect
) {
1042 if (width
< 1 || height
< 1) return;
1043 gxFillRect(grect
, getColor("back"));
1044 grect
.shrinkBy(0, 1);
1045 if (grect
.width
>= 12) grect
.shrinkBy(2, 0);
1047 editor
.moveResize(grect
.x0
, grect
.y0
, grect
.width
, gxTextHeightUtf
);
1052 // return `true` if event was eaten
1053 override bool onKey (KeyEvent event
) {
1054 if (!isFocused
) return super.onKey(event
);
1055 if (event
.pressed
) {
1056 if (editor
.processKey(event
)) {
1060 if (event
== "S-Insert") {
1061 getClipboardText(vbwin
, delegate (in char[] text
) {
1063 editor
.doPasteStart();
1064 scope(exit
) editor
.doPasteEnd();
1065 editor
.doPutTextUtf(text
[]);
1071 if (event
== "C-Insert") {
1072 auto mtr
= editor
.markedBlockRange();
1075 brng
.reserve(mtr
.length
);
1076 foreach (char ch
; mtr
) brng
~= ch
;
1077 if (brng
.length
> 0) {
1078 setClipboardText(vbwin
, cast(string
)brng
); // it is safe to cast here
1079 setPrimarySelection(vbwin
, cast(string
)brng
); // it is safe to cast here
1081 editor
.doBlockResetMark();
1087 return super.onKey(event
);
1090 override bool onMouse (MouseEvent event
) {
1091 if (!isFocused
) return super.onMouse(event
);
1092 if (GxPoint(event
.x
, event
.y
).inside(rect
.size
)) {
1093 if (editor
.processClick(event
.x
, event
.y
, event
)) {
1098 return super.onMouse(event
);
1101 override bool onChar (dchar ch
) {
1102 if (isFocused
&& editor
.processChar(ch
)) {
1106 return super.onChar(ch
);