better quoting
[bioacid.git] / bioacid.d
blob0a3b3f4caaed6d476d9c2690dc2de4d1ebbc8d3a
1 /* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
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 bioacid is aliced;
18 import std.datetime;
20 import arsd.color;
21 import arsd.image;
22 import arsd.simpledisplay;
24 import iv.cmdcon;
25 import iv.cmdcon.gl;
26 import iv.gxx;
27 import iv.meta;
28 import iv.nanovega;
29 import iv.nanovega.blendish;
30 import iv.nanovega.textlayouter;
31 import iv.strex;
32 import iv.tox;
33 import iv.sdpyutil;
34 import iv.unarray;
35 import iv.utfutil;
36 import iv.vfs.io;
38 version(sfnt_test) import iv.nanovega.simplefont;
40 import accdb;
41 import accobj;
42 import fonts;
43 import popups;
44 import icondata;
45 import notifyicon;
46 import toxproto;
48 import tkmain;
49 import tklog;
50 import tkclist;
51 import tkminiedit;
54 // ////////////////////////////////////////////////////////////////////////// //
55 //__gshared string accountNameToLoad = "_fakeacc";
56 __gshared string accountNameToLoad = "";
57 __gshared string globalHotkey = "M-H-F";
58 __gshared bool optAccountAllowCreate = false;
59 __gshared mouseStartMarking = false;
62 //==========================================================================
64 // quoteText
66 //==========================================================================
67 string quoteText (const(char)[] text, int wrapwidth=73, const(char)[] pfx=">") {
68 text = text.xstripright;
69 if (text.length == 0) return null;
70 string res;
72 string currline;
73 string currword;
74 int wordwidth = 0;
75 int currwidth = 0;
76 bool wasnewline = true;
78 Utf8DecoderFast dc;
80 void flushWord (bool force) {
81 if (currwidth && currwidth+wordwidth+1 > wrapwidth) {
82 if (res.length) res ~= "\n";
83 res ~= pfx;
84 res ~= currline;
85 currline = null;
86 currwidth = 0;
88 currline ~= " ";
89 currline ~= currword;
90 currwidth += wordwidth+1;
91 if (force) {
92 if (res.length) res ~= "\n";
93 res ~= pfx;
94 res ~= currline;
95 currline = null;
96 currwidth = 0;
98 currword = null;
99 wordwidth = 0;
102 void addChar (char ch) {
103 if (!dc.decodeSafe(cast(ubyte)ch)) return;
104 if (dc.codepoint <= 32) {
105 if (wordwidth || dc.codepoint == 10) {
106 flushWord(dc.codepoint == 10);
107 } else {
108 // just in case
109 currword = null;
110 wordwidth = 0;
112 } else {
113 if (dc.codepoint == lay.SoftHyphenCh) return;
114 if (dc.codepoint == lay.NBHyphenCh) return;
115 if (dc.codepoint == lay.HyphenPointCh) return;
116 //if (dc.codepoint == lay.HyphenCh) return;
117 if (dc.codepoint == lay.WordJoinerCh) return;
118 if (dc.codepoint == lay.ZWSpaceCh) return;
119 if (dc.codepoint == lay.ZWNBSpaceCh) return;
120 char[4] buf;
121 int ulen = utf8Encode(buf[], dc.codepoint);
122 if (ulen > 0) {
123 currword ~= buf[0..ulen];
124 wordwidth += 1;
129 //conwriteln(text);
130 foreach (char ch; text) addChar(ch);
131 if (wordwidth) flushWord(false);
132 if (currwidth) {
133 if (res.length) res ~= "\n";
134 res ~= pfx;
135 res ~= currline;
137 if (res.length) res ~= "\n";
138 return res;
142 //==========================================================================
144 // main
146 //==========================================================================
147 void main (string[] args) {
148 if (!TOX_VERSION_IS_ABI_COMPATIBLE()) assert(0, "invalid ToxCore library version");
150 conRegVar!accountNameToLoad("starting_account", "account to load");
151 conRegVar!optAccountAllowCreate("account_allow_creating", "create new account if BioAcid can't load it");
152 conRegVar!optShowOffline("show_offline", "always show offline contacts?");
153 conRegVar!optHRLastSeen("hr_lastseen", "human-readable lastseen?");
155 //conwriteln("account '", acc.info.nick, "' loaded, ", acc.contacts.length, " contacts, ", acc.groups.length, " groups");
157 //glconShowKey = "M-Grave";
158 glconSetAndSealFPS(0); // draw-on-demand
160 conProcessQueue(256*1024); // load config
161 conProcessArgs!true(args);
162 conProcessQueue(256*1024);
164 if (accountNameToLoad.length == 0) assert(0, "no account to load");
166 //setOpenGLContextVersion(3, 2); // up to GLSL 150
167 setOpenGLContextVersion(2, 0); // it's enough
169 loadAllFonts();
172 NVGPathSet svp = null;
174 //glconRunGLWindowResizeable(800, 600, "BioAcid", "BIOACID");
175 sdpyWindowClass = "BIOACID";
176 sdmain = new SimpleWindow(800, 600, "BioAcid", OpenGlOptions.yes, Resizability.allowResizing);
177 glconCtlWindow = sdmain;
179 setupToxCoreSender();
180 setupToxEventListener(sdmain);
182 sdmain.visibilityChanged = delegate (bool vis) { mainWindowVisible = vis; fixUnreadIndicators(); };
183 sdmain.onFocusChange = delegate (bool focused) { mainWindowActive = focused; /*conwriteln("focus=", focused);*/ fixUnreadIndicators(); };
185 MiniEdit sysedit = new MiniEdit();
187 MiniEdit currEdit () { return (activeContact !is null ? activeContact.edit : sysedit); }
189 try {
190 if (globalHotkey.length > 0) {
191 GlobalHotkeyManager.register(globalHotkey, delegate () { concmd("win_toggle"); glconPostDoConCommands!true(); });
193 } catch (Exception e) {
194 conwriteln("ERROR registering hotkey!");
197 conRegFunc!(() {
198 if (sdmain !is null) sdmain.close();
199 })("quit", "quit BioAcid");
201 conRegFunc!(() {
202 import core.memory : GC;
203 conwriteln("starting GC collection...");
204 GC.collect();
205 GC.minimize();
206 conwriteln("GC collection complete.");
207 })("gc_collect", "force GC collection cycle");
209 conRegFunc!(() {
210 if (sdmain !is null && !sdmain.closed) {
211 //conwriteln("act=", mainWindowActive, "; vis=", mainWindowVisible, "; realvis=", sdmain.visible);
212 if (!mainWindowVisible) {
213 // this strange code brings window to the current desktop it if was on a different one
214 sdmain.hide();
215 sdmain.show();
216 } else if (sdmain.visible) {
217 sdmain.hide();
218 } else {
219 sdmain.show();
221 flushGui();
223 })("win_toggle", "show/hide main window");
225 sdmain.addEventListener((GLConScreenRepaintEvent evt) {
226 if (sdmain.closed) return;
227 if (isQuitRequested) { sdmain.close(); return; }
228 sdmain.redrawOpenGlSceneNow();
231 sdmain.addEventListener((GLConDoConsoleCommandsEvent evt) {
232 glconProcessEventMessage();
235 sdmain.addEventListener((PopupCheckerEvent evt) {
236 popupCheckExpirations();
240 // ////////////////////////////////////////////////////////////////////// //
241 sdmain.onClosing = delegate () {
242 clist.forEachAccount(delegate (acc) { acc.forceOnline = false; });
243 popupKillAll();
244 if (nvg !is null) {
245 sdmain.setAsCurrentOpenGlContext();
246 scope(exit) { flushGui(); sdmain.releaseCurrentOpenGlContext(); }
247 svp.kill();
248 nvg.kill();
250 assert(nvg is null);
251 if (sdhint !is null) sdhint.close();
252 if (trayicon !is null) trayicon.close();
255 sdmain.closeQuery = delegate () {
256 concmd("quit");
257 glconPostDoConCommands!true();
260 // first time setup
261 sdmain.visibleForTheFirstTime = delegate () {
262 if (sdmain.width > 1 && optCListWidth < 0) optCListWidth = sdmain.width/5;
263 sdmain.setAsCurrentOpenGlContext(); // make this window active
264 scope(exit) sdmain.releaseCurrentOpenGlContext();
265 sdmain.vsync = false;
267 glconInit(sdmain.width, sdmain.height);
269 nvg = nvgCreateContext(NVGContextFlag.Antialias, NVGContextFlag.StencilStrokes, NVGContextFlag.FontNoAA);
270 if (nvg is null) assert(0, "cannot initialize NanoVG");
271 loadFonts();
273 try {
274 static immutable skullsPng = /*cast(immutable(ubyte)[])*/import("data/skulls.png");
275 //nvgSkullsImg = nvg.createImage("skulls.png", NVGImageFlags.NoFiltering|NVGImageFlags.RepeatX|NVGImageFlags.RepeatY);
276 auto xi = loadImageFromMemory(skullsPng[]);
277 scope(exit) delete xi;
278 //{ import core.stdc.stdio; printf("creating background image...\n"); }
279 nvgSkullsImg = nvg.createImageFromMemoryImage(xi, NVGImageFlags.NoFiltering, NVGImageFlags.RepeatX, NVGImageFlags.RepeatY);
280 //{ import core.stdc.stdio; printf("background image created\n"); }
281 if (!nvgSkullsImg.valid) assert(0, "cannot load background image");
282 } catch (Exception e) {
283 assert(0, "cannot load background image");
285 buildStatusImages();
287 prepareTrayIcon();
289 lay = new LayTextClass(laf, sdmain.width/2);
290 //conwriteln("wdt=", lay.width, "; text width=", lay.textWidth);
291 lastWindowWidth = sdmain.width;
293 clist = new CList();
294 loadAccount(accountNameToLoad, optAccountAllowCreate);
295 clist.onActivateContactCB = delegate (Contact ct) { doActivateContact(ct); };
296 addContactCommands();
298 sdmain.setMinSize(640, 480);
300 fixTrayIcon();
301 //sdmain.redrawOpenGlSceneNow();
304 sdmain.windowResized = delegate (int wdt, int hgt) {
305 if (sdmain.closed) return;
306 glconResize(wdt, hgt);
307 glconPostScreenRepaint/*Delayed*/();
308 //conwriteln("old=", lastWindowWidth, "; new=", wdt);
309 if (wdt > 1 && optCListWidth > 0 && lastWindowWidth > 0 && lastWindowWidth != wdt) {
310 immutable double frc = lastWindowWidth/optCListWidth;
311 optCListWidth = cast(int)(wdt/frc);
312 if (optCListWidth < 64) optCListWidth = 64;
313 lastWindowWidth = wdt;
315 lay.relayout(wdt);
319 int mouseX = -666, mouseY = -666;
322 sdmain.handleKeyEvent = delegate (KeyEvent event) {
323 if (sdmain.closed) return;
324 scope(exit) glconPostDoConCommands!true();
325 if (glconKeyEvent(event)) return;
327 auto acc = clist.mainAccount;
329 if (event == "D-Escape") {
330 if (sdmain !is null && !sdmain.closed && sdmain.visible) {
331 sdmain.hide();
332 flushGui();
334 return;
337 scope(exit) { if (event.pressed) glconPostScreenRepaint(); }
339 if (event == "D-C-X") { concmd("quit"); return; }
340 if (event == "D-C-1") { acc.status = ContactStatus.Online; return; }
341 if (event == "D-C-2") { acc.status = ContactStatus.Away; return; }
342 if (event == "D-C-3") { acc.status = ContactStatus.Busy; return; }
343 if (event == "D-C-0") { acc.status = ContactStatus.Offline; return; }
345 if (event == "D-C-W") { doActivateContact(null); return; }
347 if (event == "D-C-C" || event == "D-C-Insert") {
348 string text = lay.getMarkedText().xstripright;
349 if (text.length) {
350 setClipboardText(sdmain, text);
351 setPrimarySelection(sdmain, text);
352 setSecondarySelection(sdmain, text);
356 if (event == "D-M-Q") {
357 string text = quoteText(lay.getMarkedText());
358 if (text.length) {
359 auto etext = currEdit.text;
360 if (etext.length && etext[$-1] != '\n') currEdit.addText("\n");
361 currEdit.addText(text);
365 if (clist !is null) {
366 if (clist.onKey(event)) return;
370 if (event == "D-C-S-Enter") {
371 static PopupWindow.Kind kind = PopupWindow.Kind.Incoming;
372 //new PopupWindow(10, 10, "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
373 showPopup(kind, "TEST popup", "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
374 if (kind == PopupWindow.Kind.max) kind = PopupWindow.Kind.min; else ++kind;
375 return;
379 if (event == "D-Enter") {
380 auto text = currEdit.text.xstrip;
381 currEdit.clear();
382 if (text.length == 0) return;
384 // `null`: eol
385 string getWord () {
386 while (text.length && text[0] <= ' ') text = text[1..$];
387 if (text.length == 0) return null;
388 if (text[0] == '"' || text[0] == '\'') {
389 string res;
390 char ech = text[0];
391 text = text[1..$];
392 while (text.length && text[0] != ech) {
393 if (text[0] == '\\') {
394 text = text[1..$];
395 if (text.length == 0) break;
396 char ch = text[0];
397 switch (ch) {
398 case '\r': res ~= '\r'; break;
399 case '\n': res ~= '\n'; break;
400 case '\t': res ~= '\t'; break;
401 default: res ~= ch; break;
403 } else {
404 res ~= text[0];
406 text = text[1..$];
408 if (text.length) { assert(text[0] == ech); text = text[1..$]; }
409 return res;
410 } else {
411 auto ep = 0;
412 while (ep < text.length && text[ep] > ' ') ++ep;
413 auto res = text[0..ep];
414 text = text[ep..$];
415 return res;
419 if (activeContact !is null) {
420 auto otext = text;
421 if (text[0] == '/' && !text.startsWith("/me ") && !text.startsWith("/me\t")) {
422 text = text[1..$];
423 auto cmd = getWord();
424 if (cmd == "help") {
425 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false,
426 "/accept -- accept friend request\n"~
427 "/remove -- remove contact\n"~
428 "/kfd -- remove friend, block any further requests\n"~
429 "/status -- set account status\n"~
430 "/pubkey -- get contact public key\n"~
431 "/always -- always visible\n"~
432 "/normal -- normal visibility"~
433 "", systimeNow);
434 } else if (cmd == "accept") {
435 if (toxCoreAddFriend(activeContact.acc.toxpk, activeContact.info.pubkey)) {
436 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "accepted!", systimeNow);
437 activeContact.kind = ContactInfo.Kind.Friend;
438 activeContact.save();
439 } else {
440 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "ERROR accepting!", systimeNow);
442 } else if (cmd == "kfd") {
443 toxCoreRemoveFriend(activeContact.acc.toxpk, activeContact.info.pubkey);
444 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "KILL! FUCK! DIE!", systimeNow);
445 activeContact.kind = ContactInfo.Kind.KillFuckDie;
446 activeContact.save();
447 } else if (cmd == "remove") {
448 acc.removeContact(activeContact);
449 } else if (cmd == "friend") {
450 if (!isValidAddr(activeContact.info.fraddr)) { conwriteln("invalid address"); return; }
451 string msg = getWord();
452 if (msg.length == 0) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; please, specify message!"); return; }
453 if (msg.length > tox_max_friend_request_length()) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; message too long"); return; }
454 if (!acc.sendFriendRequest(activeContact.info.fraddr, msg)) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; error sending friend request"); return; }
455 } else if (cmd == "status") {
456 string msg = getWord();
457 if (msg.length == 0) { conwriteln("current: ", acc.info.statusmsg); return; }
458 if (!toxCoreSetStatusMessage(activeContact.acc.toxpk, msg)) { conwriteln("ERROR: cannot set status message"); return; }
459 activeContact.acc.info.statusmsg = msg;
460 activeContact.acc.save();
461 try { activeContact.acc.save(); } catch (Exception e) { conwriteln("ERROR saving account: ", e.msg); }
462 } else if (cmd == "pubkey") {
463 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, tox_hex(activeContact.info.pubkey), systimeNow);
464 } else if (cmd == "always") {
465 activeContact.showOffline = TriOption.Yes;
466 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "always visible", systimeNow);
467 } else if (cmd == "normal") {
468 activeContact.showOffline = TriOption.Default;
469 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "normal visibility", systimeNow);
470 } else {
471 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "wut?!", systimeNow);
473 } else {
474 activeContact.send(text);
476 } else {
477 // sysedit
478 if (acc !is null && text.length && text[0] == '/') {
479 text = text[1..$];
480 // get command
481 auto cmd = getWord();
482 switch (cmd) {
483 case "friend": // add new friend
484 string addr = getWord();
485 ToxAddr fraddr = decodeAddrStr(addr);
486 if (!isValidAddr(fraddr)) { conwriteln("invalid address: '", addr, "'"); return; }
487 string msg = getWord();
488 if (msg.length == 0) { conwriteln("address: '", tox_hex(fraddr), "'; please, specify message!"); return; }
489 if (msg.length > tox_max_friend_request_length()) { conwriteln("address: '", addr, "'; message too long"); return; }
490 //if (!toxCoreSendFriendRequest(acc.toxpk, fraddr, msg)) { conwriteln("address: '", addr, "'; error sending friend request");
491 if (!acc.sendFriendRequest(fraddr, msg)) { conwriteln("address: '", addr, "'; error sending friend request"); return; }
492 break;
493 case "status":
494 string msg = getWord();
495 if (msg.length == 0) { conwriteln("current: ", acc.info.statusmsg); return; }
496 if (!toxCoreSetStatusMessage(acc.toxpk, msg)) { conwriteln("ERROR: cannot set status message"); return; }
497 acc.info.statusmsg = msg;
498 activeContact.acc.save();
499 try { acc.save(); } catch (Exception e) { conwriteln("ERROR saving account: ", e.msg); }
500 break;
501 case "myaddr":
502 currEdit.text = acc.getAddress();
503 break;
504 case "help":
505 addTextToLog(null, null, LogFile.Msg.Kind.Notification, false,
506 "/friend addr msg -- send friend request\n"~
507 "/status -- set account status\n"~
508 "/myaddr -- put my address into the editor"~
509 "", systimeNow);
510 break;
511 default:
512 conwriteln("unknown command: '", cmd, "'");
513 break;
515 } else {
516 if (text.length) conwriteln("NOT A COMMAND: ", text);
519 return;
522 if (currEdit.onKey(event)) return;
525 int msLastPressX = -666, msLastPressY = -666;
526 int msDoLogButton = 0; // =0: none; 1: left; 2: middle; 3: right; negative: release
528 sdmain.handleMouseEvent = delegate (MouseEvent event) {
529 if (sdmain.closed) return;
530 scope(exit) glconPostDoConCommands!true();
531 if (isConsoleVisible) return;
532 mouseX = event.x;
533 mouseY = event.y;
535 // check for click in log
536 //FIXME: process it here, not in renderer
537 if (event == "LMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 1; glconPostScreenRepaint(); }
538 if (event == "MMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 2; glconPostScreenRepaint(); }
539 if (event == "RMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 3; glconPostScreenRepaint(); }
540 if (event == "LMB-Up") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = -1; glconPostScreenRepaint(); }
541 if (event == "MMB-Up") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = -2; glconPostScreenRepaint(); }
542 if (event == "RMB-Up") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = -3; glconPostScreenRepaint(); }
544 if (clist !is null) {
545 if (clist.onMouse(event)) {
546 mouseStartMarking = false;
547 if (lay.hasMark()) {
548 lay.resetMarks();
549 glconPostScreenRepaint();
551 return;
555 // log scroll
556 if (layWinHeight > 0) {
557 enum ScrollHeight = 32;
558 if (event == "WheelUp") {
559 layOffset += ScrollHeight;
560 if (layOffset > lay.textHeight-layWinHeight) layOffset = lay.textHeight-layWinHeight;
561 } else if (event == "WheelDown") {
562 layOffset -= ScrollHeight;
564 if (layOffset < 0) layOffset = 0;
567 // don't spam with repaint events
568 if (event.type != MouseEventType.motion) {
569 glconPostScreenRepaint();
570 } else {
571 if (mouseStartMarking) {
572 msDoLogButton = 666;
573 msLastPressX = mouseX;
574 msLastPressY = mouseY;
575 glconPostScreenRepaint();
580 sdmain.handleCharEvent = delegate (dchar ch) {
581 if (sdmain.closed) return;
582 scope(exit) glconPostDoConCommands!true();
583 if (glconCharEvent(ch)) return;
585 scope(exit) glconPostScreenRepaint();
587 if (currEdit.onChar(ch)) return;
590 // draw main screen
591 sdmain.redrawOpenGlScene = delegate () {
592 glconPostDoConCommands!true();
593 if (sdmain.closed) return;
594 sdmain.setAsCurrentOpenGlContext(); // make this window active
595 scope(exit) sdmain.releaseCurrentOpenGlContext();
596 glViewport(0, 0, sdmain.width, sdmain.height);
598 // draw main screen
599 scope(exit) glconDraw();
601 if (nvg is null) return;
603 //oglSetup2D(glconCtlWindow.width, glconCtlWindow.height);
604 glViewport(0, 0, sdmain.width, sdmain.height);
605 glMatrixMode(GL_MODELVIEW);
606 //conwriteln(glconCtlWindow.width, "x", glconCtlWindow.height);
608 glClearColor(0, 0, 0, 0);
609 glClear(glNVGClearFlags/*|GL_COLOR_BUFFER_BIT*/);
612 nvg.beginFrame(sdmain.width, sdmain.height);
613 scope(exit) nvg.endFrame();
615 if (clist !is null && optCListWidth > 0) {
616 // draw contact list
617 int cx = 1;
618 int cy = 1;
619 int wdt = optCListWidth;
620 int hgt = nvg.height-cy*2;
622 nvg.shapeAntiAlias = true;
623 nvg.nonZeroFill;
624 nvg.strokeWidth = 1;
627 nvg.save();
628 scope(exit) nvg.restore();
630 nvg.newPath();
631 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
633 int w, h;
634 nvg.imageSize(nvgSkullsImg, w, h);
635 nvg.fillPaint(nvg.imagePattern(0, 0, w, h, 0, nvgSkullsImg));
637 nvg.strokeColor(NVGColor("#f70"));
638 nvg.fill();
639 nvg.stroke();
641 // draw contact list
643 nvg.save();
644 scope(exit) nvg.restore();
645 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt-3*2, hgt-3*2);
646 clist.drawAt(nvg, cx+3, cy+3, wdt-3*2, hgt-3*2);
648 // draw scrollbar
649 if (clist.sbSize > 0) {
650 nvg.bndScrollBar(cx+3+(wdt-3*2)+3-BND_SCROLLBAR_WIDTH+0.5f, cy+1+0.5f, BND_SCROLLBAR_WIDTH, hgt-2, BND_DEFAULT, clist.sbPosition, clist.sbSize);
655 nvg.save();
656 scope(exit) nvg.restore();
658 // draw chat log
659 cx += wdt+2;
660 wdt = nvg.width-cx-1;
661 auto baphHgt = hgt;
663 // calculate editor dimensions and draw editor
665 nvg.save();
666 scope(exit) nvg.restore();
668 currEdit.setWidth(wdt-3*2);
670 auto edheight = currEdit.calcHeight();
672 int edy = cy+hgt-edheight-3*2;
674 nvg.newPath();
675 nvg.roundedRect(cx+0.5f, edy+0.5f, wdt, edheight+3*2, 6);
676 nvg.fillColor(NVGColor.black);
677 nvg.strokeColor(NVGColor("#f70"));
678 nvg.fill();
679 nvg.stroke();
681 nvg.intersectScissor(cx+2.5f, edy+2.5f, wdt-3*2+2, edheight+2);
682 currEdit.draw(nvg, cx+3, edy+3);
684 hgt -= edheight+3*2;
687 nvg.newPath();
688 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
689 nvg.fillColor(NVGColor.black);
690 nvg.strokeColor(NVGColor("#f70"));
691 nvg.fill();
692 nvg.stroke();
694 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt-3*2, hgt-3*2);
695 version(all) {
696 immutable float scalex = (wdt-3*2-10*2)/BaphometDims;
697 immutable float scaley = (baphHgt-3*2-10*2)/BaphometDims;
698 immutable float scale = (scalex < scaley ? scalex : scaley)/1.5f;
699 immutable float sz = BaphometDims*scale;
700 nvg.strokeColor(NVGColor("#400"));
701 nvg.fillColor(NVGColor("#400"));
702 nvg.renderBaphomet(cx+10.5f+(wdt-3*2-10*2)/2-sz/2, cy+10.5f+(baphHgt-3*2-10*2)/2-sz/2, scale, scale);
705 immutable float sbx = cx+wdt-BND_SCROLLBAR_WIDTH-1.5f;
706 immutable float sby = cy+3.5f;
707 wdt -= BND_SCROLLBAR_WIDTH+1;
708 wdt -= 3*2;
709 hgt -= 3*2;
711 lay.relayout(wdt); // this is harmess if width wasn't changed
712 int ty = lay.textHeight-hgt+1-layOffset;
713 if (ty < 0) ty = 0;
714 layWinHeight = cast(int)hgt;
716 if (lay.textHeight > hgt) {
717 float h = lay.textHeight-hgt;
718 nvg.bndScrollBar(sbx, sby, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, ty/h, hgt/h);
719 } else {
720 nvg.bndScrollBar(sbx, sby, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);
723 nvg.intersectScissor(cx+2.5f, cy+2.5f, wdt+2, hgt+2);
724 nvg.drawLayouter(lay, ty, cx+3, cy+3, hgt);
726 //conwriteln("msDoLogButton=", msDoLogButton, "; mouseStartMarking=", mouseStartMarking);
727 if (msDoLogButton) {
728 immutable int btn = msDoLogButton;
729 msDoLogButton = 0;
730 immutable int mx = msLastPressX-(cx+3);
731 immutable int my = msLastPressY-(cy+3);
732 if (mx >= 0 && my >= 0 && mx < wdt && my < hgt) {
733 //conwriteln("MOUSE: ", mx, " : ", my, "; ty=", ty, "; thgt=", lay.textHeight);
734 auto widx = lay.wordAtXY(mx, ty+my);
735 //conwriteln(" widx=", widx);
736 if (widx >= 0) {
737 auto w = lay.wordByIndex(widx);
738 //conwriteln(" widx=", widx, "; <", lay.wordText(*w), ">; udata=", w.udata);
739 // lmb
740 if (btn == 1) {
741 string url = findUrlByWordIndex(widx);
742 if (!mouseStartMarking && url.length) {
743 //conwriteln("URL CLICK: <", url, ">");
744 openUrl(url);
745 mouseStartMarking = false;
746 } else {
747 // mark text
748 if (!mouseStartMarking) {
749 //conwriteln("MARK START!");
750 mouseStartMarking = true;
751 lay.setMark(lay.MarkType.Both, mx, ty+my);
752 glconPostScreenRepaint();
756 // rmb
757 if (btn == 3 && w.objectIdx >= 0) {
758 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(w.objectIdx)) {
759 assert(activeContact !is null);
760 activeContact.removeFromResendQueue(maw.msgid, maw.digest, maw.time);
761 maw.msgid = -1;
762 // yeah, repaint
763 glconPostScreenRepaint();
768 // continue marking
769 if (mouseStartMarking && btn == 666) {
770 //conwriteln("MARK CONTINUE!");
771 //conwriteln(" before: ", lay.getMarkWordFirst, ":", lay.getMarkWordCount);
772 lay.setMark(lay.MarkType.End, mx, ty+my);
773 //conwriteln(" after: ", lay.getMarkWordFirst, ":", lay.getMarkWordCount);
774 glconPostScreenRepaint();
776 // releases
777 if (btn == -1) {
778 if (mouseStartMarking) {
779 mouseStartMarking = false;
780 //conwriteln("MARK END!");
789 flushGui();
790 MonoTime lastCollect = MonoTime.currTime;
791 sdmain.eventLoop(16000,
792 // pulser: process resend queues here
793 delegate () {
794 if (sdmain.closed || clist is null) return;
795 clist.forEachAccount(delegate (Account acc) { acc.processResendQueue(); });
797 immutable ctt = MonoTime.currTime;
798 if ((ctt-lastCollect).total!"minutes" >= 1) {
799 import core.memory : GC;
800 lastCollect = ctt;
801 GC.collect();
802 GC.minimize();
807 clist.forEachAccount(delegate (acc) { acc.forceOnline = false; });
808 clist.forEachAccount(delegate (Account acc) { acc.saveResendQueue(); acc.saveResendQueue(); });
809 toxCoreShutdownAll();
810 popupKillAll();
811 flushGui();
812 conProcessQueue(int.max/4);