egfx: added more drawing primitives (circles, ellipses, non-filled rounded rect)
[chiroptera.git] / egui / editor.d
blobb862cf7dfe185ad44085e5ad38901ec3ee50a83a
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 egui.editor /*is aliced*/;
18 import arsd.simpledisplay;
20 import iv.alice;
21 import iv.cmdcon;
22 import iv.strex;
23 import iv.utfutil;
24 import iv.vfs;
26 import iv.egeditor.editor;
27 //import iv.egeditor.highlighters;
29 import egfx;
31 import egui.subwindows;
32 import egui.widgets;
33 import egui.dialogs;
36 // ////////////////////////////////////////////////////////////////////////// //
37 final class ChiTextMeter : EgTextMeter {
38 GxKerning twkern;
40 //int currofs; /// x offset for current char (i.e. the last char that was passed to `advance()` should be drawn with this offset)
41 //int currwdt; /// current line width (including, the last char that was passed to `advance()`), preferably without trailing empty space between chars
42 //int currheight; /// current text height; keep this in sync with the current state; `reset` should set it to "default text height"
44 /// this should reset text width iterator (and curr* fields); tabsize > 0: process tabs as... well... tabs ;-)
45 override void reset (int tabsize) nothrow {
46 twkern.reset(tabsize);
47 currheight = gxTextHeightUtf;
50 /// advance text width iterator, return x position for drawing next char
51 override void advance (dchar ch, in ref GapBuffer.HighState hs) nothrow {
52 twkern.fixWidthPre(ch);
53 currofs = twkern.currOfs;
54 currwdt = twkern.nextOfsNoSpacing;
57 /// finish text iterator; it should NOT reset curr* fields!
58 /// WARNING: EditorEngine tries to call this after each `reset()`, but user code may not
59 override void finish () nothrow {
60 //return twkern.finalWidth;
65 // ////////////////////////////////////////////////////////////////////////// //
66 final class TextEditor : EditorEngine {
67 enum TEDSingleOnly; // only for single-line mode
68 enum TEDMultiOnly; // only for multiline mode
69 enum TEDEditOnly; // only for non-readonly mode
70 enum TEDROOnly; // only for readonly mode
72 static struct TEDKey { string key; string help; bool hidden; } // UDA
74 static string TEDImplX(string key, string help, string code, size_t ln) () {
75 static assert(key.length > 0, "wtf?!");
76 static assert(code.length > 0, "wtf?!");
77 string res = "@TEDKey("~key.stringof~", "~help.stringof~") void _ted_";
78 int pos = 0;
79 while (pos < key.length) {
80 char ch = key[pos++];
81 if (key.length-pos > 0 && key[pos] == '-') {
82 if (ch == 'C' || ch == 'c') { ++pos; res ~= "Ctrl"; continue; }
83 if (ch == 'M' || ch == 'm') { ++pos; res ~= "Alt"; continue; }
84 if (ch == 'S' || ch == 's') { ++pos; res ~= "Shift"; continue; }
86 if (ch == '^') { res ~= "Ctrl"; continue; }
87 if (ch >= 'a' && ch <= 'z') ch -= 32;
88 if ((ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || ch == '_') res ~= ch; else res ~= '_';
90 res ~= ln.stringof;
91 res ~= " () {"~code~"}";
92 return res;
95 mixin template TEDImpl(string key, string help, string code, size_t ln=__LINE__) {
96 mixin(TEDImplX!(key, help, code, ln));
99 mixin template TEDImpl(string key, string code, size_t ln=__LINE__) {
100 mixin(TEDImplX!(key, "", code, ln));
103 protected:
104 KeyEvent[32] comboBuf;
105 int comboCount; // number of items in `comboBuf`
106 Widget pw;
108 ChiTextMeter chiTextMeter;
110 public:
111 this (Widget apw, int x0, int y0, int w, int h, bool asinglesine=false) {
112 pw = apw;
113 //coordsInPixels = true;
114 lineHeightPixels = gxTextHeightUtf;
115 chiTextMeter = new ChiTextMeter();
116 super(x0, y0, w, h, null, asinglesine);
117 textMeter = chiTextMeter;
118 utfuck = true;
119 visualtabs = true;
120 tabsize = 4; // spaces
123 final int lineQuoteLevel (int lidx) {
124 if (lidx < 0 || lidx >= lc.linecount) return 0;
125 int pos = lc.line2pos(lidx);
126 auto ts = gb.textsize;
127 if (pos >= ts) return 0;
128 if (gb[pos] != '>') return 0;
129 int count = 1;
130 ++pos;
131 while (pos < ts) {
132 char ch = gb[pos++];
133 if (ch == '>') ++count;
134 else if (ch == '\n') break;
135 else if (ch != ' ') break;
137 return count;
140 public override void drawCursor () {
141 if (pw.active) {
142 int lcx, lcy;
143 localCursorXY(&lcx, &lcy);
144 drawTextCursor(pw.parent.active, x0+lcx, y0+lcy);
148 // not here, 'cause this is done before text
149 public override void drawStatus () {}
151 public final paintStatusLine () {
152 import core.stdc.stdio : snprintf;
153 int sx = x0, sy = y0+height;
154 gxFillRect(sx, sy, width, gxTextHeightUtf, gxRGB!(255, 255, 255));
155 char[128] buf = void;
156 auto len = snprintf(buf.ptr, buf.length, "%04d:%04d %d", curx, cury, linecount);
157 gxDrawTextUtf(sx+2, sy, buf[0..len], gxRGB!(0, 0, 0));
160 public override void drawPage () {
161 fullDirty(); // HACK!
162 gxWithSavedClip{
163 gxClipRect.intersect(GxRect(x0, y0, width, height));
164 super.drawPage();
166 if (!singleline) paintStatusLine();
170 public override void drawLine (int lidx, int yofs, int xskip) {
171 int x = x0-xskip;
172 int y = y0+yofs;
173 auto pos = lc.line2pos(lidx);
174 auto lea = lc.line2pos(lidx+1);
175 auto ts = gb.textsize;
176 bool utfucked = utfuck;
177 immutable int bs = bstart, be = bend;
178 immutable ls = pos;
180 uint clr = gxRGB!(220, 220, 0);
181 enum markFgClr = gxRGB!(255, 255, 255);
182 enum markBgClr = gxRGB!(0, 160, 160);
184 if (!singleline) {
185 int qlevel = lineQuoteLevel(lidx);
186 if (qlevel) {
187 final switch (qlevel%2) {
188 case 0: clr = gxRGB!(128, 128, 0); break;
189 case 1: clr = gxRGB!( 0, 128, 128); break;
191 } else {
192 char[1024] abuf = void;
193 auto aname = getAttachName(abuf[], lidx);
194 if (aname.length) {
195 try {
196 import std.file : exists, isFile;
197 clr = (aname.exists && aname.isFile ? gxRGB!(0x6e, 0x00, 0xff) : gxRGB!(0xff, 0x00, 0x00));
198 } catch (Exception e) {}
203 bool checkMarking = false;
204 if (hasMarkedBlock) {
205 //conwriteln("bs=", bs, "; be=", be, "; pos=", pos, "; lea=", lea);
206 if (pos >= bs && lea <= be) {
207 // full line
208 gxFillRect(x0, y, winw, lineHeightPixels, markBgClr);
209 clr = markFgClr;
210 } else if (pos < be && lea > bs) {
211 // draw block background
212 auto tpos = pos;
213 int bx0 = x, bx1 = x;
214 chiTextMeter.twkern.reset(visualtabs ? tabsize : 0);
215 while (tpos < lea) {
216 immutable dchar dch = dcharAtAdvance(tpos);
217 if (!singleline && dch == '\n') break;
218 chiTextMeter.twkern.fixWidthPre(dch);
219 if (tpos > bs) break;
221 bx0 = bx1 = x+chiTextMeter.twkern.currOfs;
222 bool eolhit = false;
223 while (tpos < lea) {
224 if (tpos >= be) break;
225 immutable dchar dch = dcharAtAdvance(tpos);
226 if (!singleline && dch == '\n') { eolhit = (tpos < be); break; }
227 chiTextMeter.twkern.fixWidthPre(dch);
228 if (tpos > be) break;
230 bx1 = (eolhit ? x0+width : x+chiTextMeter.twkern.finalWidth);
231 gxFillRect(bx0, y, bx1-bx0+1, lineHeightPixels, markBgClr);
232 checkMarking = true;
235 if (singleline && hasMarkedBlock) checkMarking = true; // let it be
237 //twkern.reset(visualtabs ? tabsize : 0);
238 uint cc = clr;
240 int epos = pos;
241 if (singleline) {
242 epos = ts;
243 } else {
244 while (epos < ts) if (gb[epos++] == '\n') { --epos; break; }
247 //FIXME: tabsize
248 int wdt = gxDrawTextUtf(GxDrawTextOptions.Tab(tabsize), x, y, this[pos..epos], delegate (in ref state) nothrow @trusted {
249 return (checkMarking ? (pos+state.spos >= bs && pos+state.epos <= be ? markFgClr : clr) : clr);
251 if (!singleline) {
252 if (epos > pos && gb[epos-1] == ' ') gxDrawChar(x+wdt, y, '\u2248', gxRGB!(0, 90, 220));
256 while (pos < ts) {
257 if (checkMarking) cc = (pos >= bs && pos < be ? markFgClr : clr);
258 immutable dchar dch = dcharAtAdvance(pos);
259 if (!singleline && dch == '\n') {
260 // draw "can wrap" mark
261 if (pos-2 >= ls && gb[pos-2] == ' ') {
262 int xx = x+twkern.finalWidth+1;
263 gxDrawChar(xx, y, '\n', gxRGB!(0, 0, 220));
265 break;
267 if (dch == '\t' && visualtabs) {
268 int xx = x+twkern.fixWidthPre('\t');
269 gxHLine(xx, y+lineHeightPixels/2, twkern.tablength, gxRGB!(200, 0, 0));
270 } else {
271 gxDrawChar(x+twkern.fixWidthPre(dch), y, dch, cc);
277 // just clear line
278 // use `winXXX` vars to know window dimensions
279 public override void drawEmptyLine (int yofs) {
280 // nothing to do here
283 protected enum Ecc { None, Eaten, Combo }
285 // None: not valid
286 // Eaten: exact hit
287 // Combo: combo start
288 // comboBuf should contain comboCount keys!
289 protected final Ecc checkKeys (const(char)[] keys) {
290 foreach (immutable cidx; 0..comboCount+1) {
291 keys = keys.xstrip;
292 if (keys.length == 0) return Ecc.Combo;
293 usize kepos = 0;
294 while (kepos < keys.length && keys.ptr[kepos] > ' ') ++kepos;
295 if (comboBuf[cidx] != keys[0..kepos]) return Ecc.None;
296 keys = keys[kepos..$];
298 return (keys.xstrip.length ? Ecc.Combo : Ecc.Eaten);
301 // fuck! `(this ME)` trick doesn't work here
302 protected final Ecc doEditorCommandByUDA(ME=typeof(this)) (KeyEvent key) {
303 import std.traits;
304 bool possibleCombo = false;
305 // temporarily add current key to combo
306 comboBuf[comboCount] = key;
307 // check all known combos
308 foreach (string memn; __traits(allMembers, ME)) {
309 static if (is(typeof(&__traits(getMember, ME, memn)))) {
310 import std.meta : AliasSeq;
311 alias mx = AliasSeq!(__traits(getMember, ME, memn))[0];
312 static if (isCallable!mx && hasUDA!(mx, TEDKey)) {
313 // check modifiers
314 bool goodMode = true;
315 static if (hasUDA!(mx, TEDSingleOnly)) { if (!singleline) goodMode = false; }
316 static if (hasUDA!(mx, TEDMultiOnly)) { if (singleline) goodMode = false; }
317 static if (hasUDA!(mx, TEDEditOnly)) { if (readonly) goodMode = false; }
318 static if (hasUDA!(mx, TEDROOnly)) { if (!readonly) goodMode = false; }
319 if (goodMode) {
320 foreach (const TEDKey attr; getUDAs!(mx, TEDKey)) {
321 auto cc = checkKeys(attr.key);
322 if (cc == Ecc.Eaten) {
323 // hit
324 static if (is(ReturnType!mx == void)) {
325 comboCount = 0; // reset combo
326 mx();
327 return Ecc.Eaten;
328 } else {
329 if (mx()) {
330 comboCount = 0; // reset combo
331 return Ecc.Eaten;
334 } else if (cc == Ecc.Combo) {
335 possibleCombo = true;
342 // check if we can start/continue combo
343 if (possibleCombo) {
344 if (++comboCount < comboBuf.length-1) return Ecc.Combo;
346 // if we have combo prefix, eat key unconditionally
347 if (comboCount > 0) {
348 comboCount = 0; // reset combo, too long, or invalid, or none
349 return Ecc.Eaten;
351 return Ecc.None;
354 bool processKey (KeyEvent key) {
355 final switch (doEditorCommandByUDA(key)) {
356 case Ecc.None: break;
357 case Ecc.Combo:
358 case Ecc.Eaten:
359 return true;
362 return false;
365 bool processChar (dchar ch) {
366 if (ch < ' ' || ch == 127) return false;
367 if (ch == ' ') {
368 // check if we should reformat
369 auto llen = linelen(cury);
370 if (llen >= 76) {
371 if (curx == 0 || gb[curpos-1] != ' ') doPutChar(' ');
372 auto ocx = curx;
373 auto ocy = cury;
374 // normalize ocx
376 int rpos = lc.linestart(ocy);
377 int qcnt = 0;
378 if (gb[rpos] == '>') {
379 while (gb[rpos] == '>') { ++qcnt; ++rpos; }
380 if (gb[rpos] == ' ') { ++qcnt; ++rpos; }
382 //conwriteln("origcx=", ocx, "; ncx=", ocx-qcnt, "; qcnt=", qcnt);
383 if (ocx < qcnt) ocx = qcnt; else ocx -= qcnt;
385 reformatFromLine!true(cury);
386 // position cursor
387 while (ocy < lc.linecount) {
388 int rpos = lc.linestart(ocy);
389 llen = linelen(ocy);
390 //conwriteln(" 00: ocy=", ocy, "; llen=", llen, "; ocx=", ocx);
391 int qcnt = 0;
392 if (gb[rpos] == '>') {
393 while (gb[rpos] == '>') { --llen; ++qcnt; ++rpos; }
394 if (gb[rpos] == ' ') { --llen; ++qcnt; ++rpos; }
396 //conwriteln(" 01: ocy=", ocy, "; llen=", llen, "; ocx=", ocx, "; qcnt=", qcnt);
397 if (ocx <= llen) { ocx += qcnt; break; }
398 ocx -= llen;
399 ++ocy;
401 gotoXY(ocx, ocy);
402 return true;
405 doPutDChar(ch);
406 return true;
409 bool processClick (int x, int y, MouseEvent event) {
410 if (x < 0 || y < 0 || x >= winw || y >= winh) return false;
411 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
412 int tx, ty;
413 widget2text(x, y, tx, ty);
414 gotoXY(tx, ty);
415 return true;
417 return false;
420 final:
421 void processWordWith (scope char delegate (char ch) dg) {
422 if (dg is null) return;
423 bool undoAdded = false;
424 scope(exit) if (undoAdded) undoGroupEnd();
425 auto pos = curpos;
426 if (!isWordChar(gb[pos])) return;
427 // find word start
428 while (pos > 0 && isWordChar(gb[pos-1])) --pos;
429 while (pos < gb.textsize) {
430 auto ch = gb[pos];
431 if (!isWordChar(gb[pos])) break;
432 auto nc = dg(ch);
433 if (ch != nc) {
434 if (!undoAdded) { undoAdded = true; undoGroupStart(); }
435 replaceText!"none"(pos, 1, (&nc)[0..1]);
437 ++pos;
439 gotoPos(pos);
442 final:
443 @TEDMultiOnly mixin TEDImpl!("Up", q{ doUp(); });
444 @TEDMultiOnly mixin TEDImpl!("S-Up", q{ doUp(true); });
445 @TEDMultiOnly mixin TEDImpl!("C-Up", q{ doScrollUp(); });
446 @TEDMultiOnly mixin TEDImpl!("S-C-Up", q{ doScrollUp(true); });
448 @TEDMultiOnly mixin TEDImpl!("Down", q{ doDown(); });
449 @TEDMultiOnly mixin TEDImpl!("S-Down", q{ doDown(true); });
450 @TEDMultiOnly mixin TEDImpl!("C-Down", q{ doScrollDown(); });
451 @TEDMultiOnly mixin TEDImpl!("S-C-Down", q{ doScrollDown(true); });
453 mixin TEDImpl!("Left", q{ doLeft(); });
454 mixin TEDImpl!("S-Left", q{ doLeft(true); });
455 mixin TEDImpl!("C-Left", q{ doWordLeft(); });
456 mixin TEDImpl!("S-C-Left", q{ doWordLeft(true); });
458 mixin TEDImpl!("Right", q{ doRight(); });
459 mixin TEDImpl!("S-Right", q{ doRight(true); });
460 mixin TEDImpl!("C-Right", q{ doWordRight(); });
461 mixin TEDImpl!("S-C-Right", q{ doWordRight(true); });
463 @TEDMultiOnly mixin TEDImpl!("PageUp", q{ doPageUp(); });
464 @TEDMultiOnly mixin TEDImpl!("S-PageUp", q{ doPageUp(true); });
465 @TEDMultiOnly mixin TEDImpl!("C-PageUp", q{ doTextTop(); });
466 @TEDMultiOnly mixin TEDImpl!("S-C-PageUp", q{ doTextTop(true); });
468 @TEDMultiOnly mixin TEDImpl!("PageDown", q{ doPageDown(); });
469 @TEDMultiOnly mixin TEDImpl!("S-PageDown", q{ doPageDown(true); });
470 @TEDMultiOnly mixin TEDImpl!("C-PageDown", q{ doTextBottom(); });
471 @TEDMultiOnly mixin TEDImpl!("S-C-PageDown", q{ doTextBottom(true); });
473 mixin TEDImpl!("Home", q{ doHome(); });
474 mixin TEDImpl!("S-Home", q{ doHome(true, true); });
475 @TEDMultiOnly mixin TEDImpl!("C-Home", q{ doPageTop(); });
476 @TEDMultiOnly mixin TEDImpl!("S-C-Home", q{ doPageTop(true); });
478 mixin TEDImpl!("End", q{ doEnd(); });
479 mixin TEDImpl!("S-End", q{ doEnd(true); });
480 @TEDMultiOnly mixin TEDImpl!("C-End", q{ doPageBottom(); });
481 @TEDMultiOnly mixin TEDImpl!("S-C-End", q{ doPageBottom(true); });
483 @TEDEditOnly mixin TEDImpl!("Backspace", q{ doBackspace(); });
484 @TEDSingleOnly @TEDEditOnly mixin TEDImpl!("M-Backspace", "delete previous word", q{ doDeleteWord(); });
485 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("M-Backspace", "delete previous word or unindent", q{ doBackByIndent(); });
487 mixin TEDImpl!("Delete", q{
488 doDelete();
491 //mixin TEDImpl!("^Insert", "copy block to clipboard file, reset block mark", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); doBlockResetMark(); });
493 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("Enter", q{
494 auto ly = cury;
495 if (lineQuoteLevel(ly)) {
496 doLineSplit(false); // no autoindent
497 auto ls = lc.linestart(ly);
498 undoGroupStart();
499 scope(exit) undoGroupEnd();
500 while (gb[ls] == '>') {
501 ++ls;
502 insertText!"end"(curpos, ">");
504 if (gb[curpos] > ' ') insertText!"end"(curpos, " ");
505 } else {
506 doLineSplit();
509 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("M-Enter", "split line without autoindenting", q{ doLineSplit(false); });
512 mixin TEDImpl!("F3", "start/stop/reset block marking", q{ doToggleBlockMarkMode(); });
513 mixin TEDImpl!("C-F3", "reset block mark", q{ doBlockResetMark(); });
514 @TEDEditOnly mixin TEDImpl!("F5", "copy block", q{ doBlockCopy(); });
515 // mixin TEDImpl!("^F5", "copy block to clipboard file", q{ if (tempBlockFileName.length == 0) return; doBlockWrite(tempBlockFileName); });
516 //@TEDEditOnly mixin TEDImpl!("S-F5", "insert block from clipboard file", q{ if (tempBlockFileName.length == 0) return; waitingInF5 = true; });
517 @TEDEditOnly mixin TEDImpl!("F6", "move block", q{ doBlockMove(); });
518 @TEDEditOnly mixin TEDImpl!("F8", "delete block", q{ doBlockDelete(); });
520 mixin TEDImpl!("C-A", "move to line start", q{ doHome(); });
521 mixin TEDImpl!("C-E", "move to line end", q{ doEnd(); });
523 @TEDMultiOnly mixin TEDImpl!("M-I", "jump to previous bookmark", q{ doBookmarkJumpUp(); });
524 @TEDMultiOnly mixin TEDImpl!("M-J", "jump to next bookmark", q{ doBookmarkJumpDown(); });
525 @TEDMultiOnly mixin TEDImpl!("M-K", "toggle bookmark", q{ doBookmarkToggle(); });
527 @TEDEditOnly mixin TEDImpl!("M-C", "capitalize word", q{
528 bool first = true;
529 processWordWith((char ch) {
530 if (first) { first = false; ch = ch.toupper; }
531 return ch;
534 @TEDEditOnly mixin TEDImpl!("M-Q", "lowercase word", q{ processWordWith((char ch) => ch.tolower); });
535 @TEDEditOnly mixin TEDImpl!("M-U", "uppercase word", q{ processWordWith((char ch) => ch.toupper); });
537 @TEDMultiOnly mixin TEDImpl!("M-S-L", "force center current line", q{ makeCurLineVisibleCentered(true); });
538 @TEDEditOnly mixin TEDImpl!("C-U", "undo", q{ doUndo(); });
539 @TEDEditOnly mixin TEDImpl!("M-S-U", "redo", q{ doRedo(); });
540 @TEDEditOnly mixin TEDImpl!("C-W", "remove previous word", q{ doDeleteWord(); });
541 @TEDEditOnly mixin TEDImpl!("C-Y", "remove current line", q{ doKillLine(); });
543 //@TEDMultiOnly @TEDEditOnly mixin TEDImpl!("Tab", q{ doPutText(" "); });
544 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-Tab", "indent block", q{ doIndentBlock(); });
545 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-S-Tab", "unindent block", q{ doUnindentBlock(); });
547 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-I", "indent block", q{ doIndentBlock(); });
548 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-U", "unindent block", q{ doUnindentBlock(); });
549 @TEDEditOnly mixin TEDImpl!("C-K C-E", "clear from cursor to EOL", q{ doKillToEOL(); });
550 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K Tab", "indent block", q{ doIndentBlock(); });
551 // @TEDEditOnly mixin TEDImpl!("^K M-Tab", "untabify", q{ doUntabify(gb.tabsize ? gb.tabsize : 2); }); // alt+tab: untabify
552 // @TEDEditOnly mixin TEDImpl!("^K C-space", "remove trailing spaces", q{ doRemoveTailingSpaces(); });
553 // mixin TEDImpl!("C-K C-T", /*"toggle \"visual tabs\" mode",*/ q{ visualtabs = !visualtabs; });
555 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-B", q{ doSetBlockStart(); });
556 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-K", q{ doSetBlockEnd(); });
558 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-C", q{ doBlockCopy(); });
559 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-M", q{ doBlockMove(); });
560 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-Y", q{ doBlockDelete(); });
561 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K C-H", q{ doBlockResetMark(); });
562 // fuckin' vt100!
563 @TEDMultiOnly @TEDEditOnly mixin TEDImpl!("C-K Backspace", q{ doBlockResetMark(); });
565 @TEDEditOnly mixin TEDImpl!("C-Q Tab", q{ doPutChar('\t'); });
566 //mixin TEDImpl!("C-Q C-U", "toggle utfuck mode", q{ utfuck = !utfuck; }); // ^Q^U: switch utfuck mode
567 //mixin TEDImpl!("C-Q 1", "switch to koi8", q{ utfuck = false; codepage = CodePage.koi8u; fullDirty(); });
568 //mixin TEDImpl!("C-Q 2", "switch to cp1251", q{ utfuck = false; codepage = CodePage.cp1251; fullDirty(); });
569 //mixin TEDImpl!("C-Q 3", "switch to cp866", q{ utfuck = false; codepage = CodePage.cp866; fullDirty(); });
570 mixin TEDImpl!("C-Q C-B", "go to block start", q{ if (hasMarkedBlock) gotoPos!true(bstart); lastBGEnd = false; });
572 @TEDSingleOnly @TEDEditOnly mixin TEDImpl!("C-Q Enter", "intert LF", q{ doPutChar('\n'); });
573 @TEDSingleOnly @TEDEditOnly mixin TEDImpl!("C-Q M-Enter", "intert CR", q{ doPutChar('\r'); });
575 mixin TEDImpl!("C-Q C-K", "go to block end", q{ if (hasMarkedBlock) gotoPos!true(bend); lastBGEnd = true; });
577 @TEDMultiOnly @TEDROOnly mixin TEDImpl!("Space", q{ doPageDown(); });
578 @TEDMultiOnly @TEDROOnly mixin TEDImpl!("S-Space", q{ doPageUp(); });
580 final:
581 string[] extractAttaches () {
582 string[] res;
583 int lidx = 0;
584 lineloop: while (lidx < lc.linecount) {
585 // find "@@"
586 auto pos = lc.linestart(lidx);
587 //conwriteln("checking line ", lidx, ": '", gb[pos], "'");
588 while (pos < gb.textsize) {
589 char ch = gb[pos];
590 if (ch == '@' && gb[pos+1] == '@') { pos += 2; break; } // found
591 if (ch > ' ' || ch == '\n') { ++lidx; continue lineloop; } // next line
592 ++pos;
594 //conwriteln("found \"@@\" at line ", lidx);
595 // skip spaces after "@@"
596 while (pos < gb.textsize) {
597 char ch = gb[pos];
598 if (ch == '\n') { ++lidx; continue lineloop; } // next line
599 if (ch > ' ') break;
600 ++pos;
602 if (pos >= gb.textsize) break; // no more text
603 // extract file name
604 string fname;
605 while (pos < gb.textsize) {
606 char ch = gb[pos];
607 if (ch == '\n') break;
608 fname ~= ch;
609 ++pos;
611 if (fname.length) {
612 //conwriteln("got fname: '", fname, "'");
613 res ~= fname;
615 // remove this line
616 int ls = lc.linestart(lidx);
617 int le = lc.linestart(lidx+1);
618 if (ls < le) deleteText!"start"(ls, le-ls);
620 return res;
623 char[] getAttachName (char[] dest, int lidx) {
624 if (lidx < 0 || lidx >= lc.linecount) return null;
625 // find "@@"
626 auto pos = lc.linestart(lidx);
627 while (pos < gb.textsize) {
628 char ch = gb[pos];
629 if (ch == '@' && gb[pos+1] == '@') { pos += 2; break; } // found
630 if (ch > ' ' || ch == '\n') return null; // not found
631 ++pos;
633 // skip spaces after "@@"
634 while (pos < gb.textsize) {
635 char ch = gb[pos];
636 if (ch == '\n') return null; // not found
637 if (ch > ' ') break;
638 ++pos;
640 if (pos >= gb.textsize) return null; // not found
641 // extract file name
642 usize dpos = 0;
643 while (pos < gb.textsize) {
644 char ch = gb[pos];
645 if (ch == '\n') break;
646 if (dpos >= dest.length) return null;
647 dest.ptr[dpos++] = ch;
648 ++pos;
650 return dest.ptr[0..dpos];
653 // ah, who cares about speed?
654 void reformatFromLine(bool doUndoGroup) (int lidx) {
655 if (lidx < 0) lidx = 0;
656 if (lidx >= lc.linecount) return;
658 static if (doUndoGroup) {
659 bool undoGroupStarted = false;
660 scope(exit) if (undoGroupStarted) undoGroupEnd();
662 void startUndoGroup () {
663 if (!undoGroupStarted) {
664 undoGroupStarted = true;
665 undoGroupStart();
668 } else {
669 void startUndoGroup () {}
672 void normalizeQuoting (int lidx) {
673 while (lidx < lc.linecount) {
674 auto pos = lc.linestart(lidx);
675 if (gb[pos] == '>') {
676 bool lastWasSpace = false;
677 auto stpos = pos;
678 auto afterlastq = pos;
679 while (pos < gb.textsize) {
680 char ch = gb[pos];
681 if (ch == '\n') break;
682 if (ch == '>') { afterlastq = ++pos; continue; }
683 if (ch == ' ') { ++pos; continue; }
684 break;
686 pos = stpos;
687 while (pos < afterlastq) {
688 char ch = gb[pos];
689 assert(ch != '\n');
690 if (ch == ' ') { deleteText(pos, 1); --afterlastq; continue; } // remove space (thus normalizing quotes)
691 assert(ch == '>');
692 ++pos;
694 assert(pos == afterlastq);
695 if (pos < gb.textsize && gb[pos] > ' ') { startUndoGroup(); insertText!("none", false)(pos, " "); }
697 ++lidx;
701 // try to join two lines, if it is possible; return `true` if succeed
702 bool tryJoinLines (int lidx) {
703 assert(lidx >= 0);
704 if (lc.linecount == 1) return false; // nothing to do
705 if (lidx+1 >= lc.linecount) return false; // nothing to do
706 auto ql0 = lineQuoteLevel(lidx);
707 auto ql1 = lineQuoteLevel(lidx+1);
708 if (ql0 != ql1) return false; // different quote levels, can't join
709 auto ls = lc.linestart(lidx);
710 auto le = lc.lineend(lidx);
711 if (le-ls < 1) return false; // wtf?!
712 if (gb[le-1] != ' ') return false; // no trailing space -- can't join
713 // don't join if next line is empty one: this is prolly paragraph delimiter
714 if (le+1 >= gb.textsize || gb[le+1] == '\n') return false;
715 if (gb[le+1] == '>') {
716 int pp = le+1;
717 while (pp < gb.textsize) {
718 char ch = gb[pp];
719 if (ch != '>' && ch != ' ') break;
720 ++pp;
722 if (gb[pp-1] != ' ') return false;
724 // remove newline
725 startUndoGroup();
726 deleteText(le, 1);
727 // remove excessive spaces, if any
728 while (le > ls && gb[le-1] == ' ') --le;
729 assert(gb[le] == ' ');
730 ++le; // but leave one space
731 while (le < gb.textsize) {
732 if (gb[le] == '\n' || gb[le] > ' ') break;
733 deleteText(le, 1);
735 // remove quoting
736 if (ql0) {
737 assert(le < gb.textsize && gb[le] == '>');
738 while (le < gb.textsize) {
739 char ch = gb[le];
740 if (ch == '\n' || (ch != '>' && ch != ' ')) break;
741 deleteText(le, 1);
744 return true; // yay
747 // join lines; we'll split 'em later
748 for (int l = lidx; l < lc.linecount; ) if (!tryJoinLines(l)) ++l;
750 // make quoting consistent
751 normalizeQuoting(lidx);
753 void conwrlinerest (int pos) {
754 if (pos < 0 || pos >= gb.textsize) return;
755 while (pos < gb.textsize) {
756 char ch = gb[pos++];
757 if (ch == '\n') break;
758 conwrite(ch);
762 void conwrline (int lidx) {
763 if (lidx < 0 || lidx >= lc.linecount) return;
764 conwrlinerest(lc.linestart(lidx));
767 // now split the lines; all lines are joined, so we can only split
768 lineloop: while (lidx < lc.linecount) {
769 auto ls = lc.linestart(lidx);
770 auto le = lc.lineend(lidx);
771 // calculate line length without trailing spaces
772 auto llen = linelen(lidx);
774 auto pe = le;
775 while (pe > ls && gb[pe-1] <= ' ') { --pe; --llen; }
777 if (llen <= 76) { ++lidx; continue; } // nothing to do here
778 // need to wrap it
779 auto pos = ls;
780 int curlen = 0;
781 // skip quotes, if any
782 while (gb[pos] == '>') { ++curlen; ++pos; }
783 // skip leading spaces
784 while (gb[pos] != '\n' && gb[pos] <= ' ') { ++curlen; ++pos; }
785 if (pos >= gb.textsize || gb[pos] == '\n') { ++lidx; continue; } // wtf?!
786 int lwstart = -1;
787 bool inword = true;
788 while (pos < gb.textsize) {
789 immutable stpos = pos;
790 dchar ch = dcharAtAdvance(pos);
791 ++curlen;
792 if (inword) {
793 if (ch <= ' ') {
794 if (lwstart >= 0 && curlen > 76) {
795 // wrap
796 startUndoGroup();
797 insertText!("none", false)(lwstart, "\n");
798 ++lwstart;
799 if (gb[ls] == '>') {
800 while (gb[ls] == '>') {
801 insertText!("none", false)(lwstart, ">");
802 ++lwstart;
803 ++ls;
805 insertText!("none", false)(lwstart, " ");
807 ++lidx;
808 continue lineloop;
810 if (ch == '\n') break;
811 // not in word anymore
812 inword = false;
814 } else {
815 if (ch == '\n') break;
816 if (ch > ' ') { lwstart = stpos; inword = true; }
819 ++lidx;
825 // ////////////////////////////////////////////////////////////////////////// //
826 struct QuoteInfo {
827 int level; // quote level
828 int length; // quote prefix length, in chars
831 QuoteInfo calcQuote(T:const(char)[]) (T s) {
832 static if (is(T == typeof(null))) {
833 return QuoteInfo();
834 } else {
835 QuoteInfo qi;
836 if (s.length > 0 && s[0] == '>') {
837 while (qi.length < s.length) {
838 if (s[qi.length] != ' ') {
839 if (s[qi.length] != '>') break;
840 ++qi.level;
842 ++qi.length;
844 if (s.length-qi.length > 1 && s[qi.length] == ' ') ++qi.length;
846 return qi;
851 // ////////////////////////////////////////////////////////////////////////// //
852 final class EditorWidget : Widget {
853 TextEditor editor;
854 bool moveToBottom = true;
856 this (SubWindow aparent) {
857 super(aparent);
858 editor = new TextEditor(this, 0, 0, 10, 10);
861 void addText (const(char)[] s) {
862 editor.moveResize(x0, y0, (ww < 1 ? 1 : ww), (wh < 1 ? wh : 1));
863 editor.doPasteStart();
864 scope(exit) editor.doPasteEnd();
865 editor.doPutTextUtf(s);
866 //editor.doPutChar('\n');
869 void reformat () {
870 editor.clearAndDisableUndo();
871 scope(exit) editor.reinstantiateUndo();
872 editor.reformatFromLine!false(0);
873 editor.gotoXY(0, 0); // HACK
874 editor.textChanged = false;
877 string[] extractAttaches () {
878 return editor.extractAttaches();
881 private final void drawScrollBar () {
882 restoreClip(); // the easiest way again
883 gxDrawScrollBar(GxRect(x0, y0, 4, height), editor.linecount-1, editor.topline+editor.visibleLinesPerWindow-1);
886 protected override void doPaint () {
887 if (ww < 1 || wh < 1) return;
888 editor.moveResize(x0+5, y0, ww-5*2, wh-gxTextHeightUtf);
890 if (moveToBottom) { moveToBottom = false; editor.gotoXY(0, editor.linecount); } // HACK!
891 gxFillRect(x0, y0, ww, wh, gxRGB!(0, 0, 120));
892 editor.drawPage();
894 drawScrollBar();
897 // return `true` if event was eaten
898 override bool onKey (KeyEvent event) {
899 if (!active) return false;
900 if (event.pressed) {
901 if (editor.processKey(event)) return true;
902 if (event == "S-Insert") {
903 getClipboardText(vbwin, delegate (in char[] text) {
904 if (text.length) {
905 editor.doPasteStart();
906 scope(exit) editor.doPasteEnd();
907 editor.doPutTextUtf(text[]);
910 return true;
912 if (event == "C-Insert") {
913 auto mtr = editor.markedBlockRange();
914 if (mtr.length) {
915 char[] brng;
916 brng.reserve(mtr.length);
917 foreach (char ch; mtr) brng ~= ch;
918 if (brng.length > 0) {
919 setClipboardText(vbwin, cast(string)brng); // it is safe to cast here
920 setPrimarySelection(vbwin, cast(string)brng); // it is safe to cast here
922 editor.doBlockResetMark();
924 return true;
927 if (event == "M-Tab") {
928 char[1024] abuf = void;
929 auto attname = editor.getAttachName(abuf[], editor.cury);
930 if (attname.length && editor.curx >= editor.linelen(editor.cury)) {
931 auto cplist = buildAutoCompletion(attname);
932 auto atnlen = attname.length;
933 auto adg = delegate (string s) {
934 //conwriteln("attname=[", attname, "]; s=[", s, "]");
935 if (s.length <= atnlen /*|| s[0..attname.length] != attname*/) return;
936 editor.undoGroupStart();
937 scope(exit) editor.undoGroupEnd();
938 editor.doPutTextUtf(s[atnlen..$]);
940 if (cplist.length == 1) {
941 adg(cplist[0]);
942 } else {
943 auto acw = new SelectCompletionWindow(attname, cplist, true);
944 acw.onSelected = adg;
947 return true;
950 return super.onKey(event);
953 override bool onMouse (MouseEvent event) {
954 if (!active) return false;
955 int mx, my;
956 event.mouse2xy(mx, my);
957 if (mx >= x0 && my >= y0 && mx <= x1 && my <= y1) {
958 if (editor.processClick(mx-x0, my-y0, event)) return true;
960 return super.onMouse(event);
963 override bool onChar (dchar ch) {
964 if (!active) return false;
965 if (editor.processChar(ch)) return true;
966 return super.onChar(ch);
971 // ////////////////////////////////////////////////////////////////////////// //
972 final class LineEditWidget : Widget {
973 TextEditor editor;
974 string title;
975 public int titwdt = -1;
977 this (SubWindow aparent, string atitle=null, int atitwdt=-1) {
978 super(aparent);
979 parent.setupClientClip();
980 ww = gxClipRect.width;
981 wh = gxTextHeightUtf+2;
982 title = atitle;
983 titwdt = atitwdt;
984 if (titwdt < 0) titwdt = gxTextWidthUtf(atitle)+2;
985 editor = new TextEditor(this, 0, 0, ww, wh, true); // singleline
988 @property bool readonly () const nothrow { return editor.readonly; }
989 @property void readonly (bool v) nothrow { editor.readonly = v; }
991 @property bool killTextOnChar () const nothrow { return editor.killTextOnChar; }
992 @property void killTextOnChar (bool v) nothrow { editor.killTextOnChar = v; }
994 @property string str () {
995 if (editor.textsize == 0) return null;
996 char[] res;
997 res.reserve(editor.textsize);
998 foreach (char ch; editor[]) res ~= ch;
999 return cast(string)res; // it is safe to cast here
1002 @property void str (const(char)[] s) {
1003 editor.clearAndDisableUndo();
1004 scope(exit) editor.reinstantiateUndo();
1005 editor.clear();
1006 editor.doPutTextUtf(s);
1007 editor.textChanged = false;
1010 protected override void doPaint () {
1011 if (ww < 1 || wh < 1) return;
1013 int cx0 = x0;
1014 int cx1 = x1;
1015 int cy0 = y0;
1017 if (titwdt > 0) {
1018 gxDrawTextUtf(cx0+titwdt-gxTextWidthUtf(title)-1, cy0+1, title, gxRGB!(255, 255, 255));
1019 cx0 += titwdt;
1020 gxClipRect.intersect(GxRect(GxPoint(cx0, y0), GxPoint(x1, y1)));
1022 gxFillRect(cx0+1, cy0, ww-titwdt-2, wh, gxRGB!(0, 0, 0));
1023 gxClipRect.intersect(GxRect(GxPoint(cx0+2, y0), GxPoint(x1-2, y1)));
1024 cx0 += 2;
1025 cx1 -= 2;
1027 if (cx1 > cx0) {
1028 editor.moveResize(cx0, cy0+1, cx1-cx0+1, gxTextHeightUtf);
1029 editor.drawPage();
1033 // return `true` if event was eaten
1034 override bool onKey (KeyEvent event) {
1035 if (!active) return false;
1036 if (event.pressed) {
1037 if (editor.processKey(event)) return true;
1038 if (event == "S-Insert") {
1039 getClipboardText(vbwin, delegate (in char[] text) {
1040 if (text.length) {
1041 editor.doPasteStart();
1042 scope(exit) editor.doPasteEnd();
1043 editor.doPutTextUtf(text[]);
1046 return true;
1048 if (event == "C-Insert") {
1049 auto mtr = editor.markedBlockRange();
1050 if (mtr.length) {
1051 char[] brng;
1052 brng.reserve(mtr.length);
1053 foreach (char ch; mtr) brng ~= ch;
1054 if (brng.length > 0) {
1055 setClipboardText(vbwin, cast(string)brng); // it is safe to cast here
1056 setPrimarySelection(vbwin, cast(string)brng); // it is safe to cast here
1058 editor.doBlockResetMark();
1060 return true;
1063 return super.onKey(event);
1066 override bool onMouse (MouseEvent event) {
1067 if (!active) return false;
1068 int mx, my;
1069 event.mouse2xy(mx, my);
1070 if (mx >= x0 && my >= y0 && mx <= x1 && my <= y1) {
1071 if (editor.processClick(mx-x0, my-y0, event)) return true;
1073 return super.onMouse(event);
1076 override bool onChar (dchar ch) {
1077 if (!active) return false;
1078 if (editor.processChar(ch)) return true;
1079 return super.onChar(ch);