better url detection
[bioacid.git] / bioacid.d
blob5c4f8d7e26f99e461071488a99f3581462ba0b15
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;
61 void main (string[] args) {
62 if (!TOX_VERSION_IS_ABI_COMPATIBLE()) assert(0, "invalid ToxCore library version");
64 conRegVar!accountNameToLoad("starting_account", "account to load");
65 conRegVar!optAccountAllowCreate("account_allow_creating", "create new account if BioAcid can't load it");
66 conRegVar!optShowOffline("show_offline", "always show offline contacts?");
67 conRegVar!optHRLastSeen("hr_lastseen", "human-readable lastseen?");
69 //conwriteln("account '", acc.info.nick, "' loaded, ", acc.contacts.length, " contacts, ", acc.groups.length, " groups");
71 //glconShowKey = "M-Grave";
72 glconSetAndSealFPS(0); // draw-on-demand
74 conProcessQueue(256*1024); // load config
75 conProcessArgs!true(args);
76 conProcessQueue(256*1024);
78 if (accountNameToLoad.length == 0) assert(0, "no account to load");
80 //setOpenGLContextVersion(3, 2); // up to GLSL 150
81 setOpenGLContextVersion(2, 0); // it's enough
83 loadAllFonts();
86 NVGPathSet svp = null;
88 //glconRunGLWindowResizeable(800, 600, "BioAcid", "BIOACID");
89 sdpyWindowClass = "BIOACID";
90 sdmain = new SimpleWindow(800, 600, "BioAcid", OpenGlOptions.yes, Resizability.allowResizing);
91 glconCtlWindow = sdmain;
93 setupToxCoreSender();
94 setupToxEventListener(sdmain);
96 sdmain.visibilityChanged = delegate (bool vis) { mainWindowVisible = vis; fixUnreadIndicators(); };
97 sdmain.onFocusChange = delegate (bool focused) { mainWindowActive = focused; /*conwriteln("focus=", focused);*/ fixUnreadIndicators(); };
99 MiniEdit sysedit = new MiniEdit();
101 MiniEdit currEdit () { return (activeContact !is null ? activeContact.edit : sysedit); }
103 try {
104 if (globalHotkey.length > 0) {
105 GlobalHotkeyManager.register(globalHotkey, delegate () { concmd("win_toggle"); glconPostDoConCommands!true(); });
107 } catch (Exception e) {
108 conwriteln("ERROR registering hotkey!");
111 conRegFunc!(() {
112 if (sdmain !is null) sdmain.close();
113 })("quit", "quit BioAcid");
115 conRegFunc!(() {
116 if (sdmain !is null && !sdmain.closed) {
117 //conwriteln("act=", mainWindowActive, "; vis=", mainWindowVisible, "; realvis=", sdmain.visible);
118 if (!mainWindowVisible) {
119 // this strange code brings window to the current desktop it if was on a different one
120 sdmain.hide();
121 sdmain.show();
122 } else if (sdmain.visible) {
123 sdmain.hide();
124 } else {
125 sdmain.show();
127 flushGui();
129 })("win_toggle", "show/hide main window");
131 sdmain.addEventListener((GLConScreenRepaintEvent evt) {
132 if (sdmain.closed) return;
133 if (isQuitRequested) { sdmain.close(); return; }
134 sdmain.redrawOpenGlSceneNow();
137 sdmain.addEventListener((GLConDoConsoleCommandsEvent evt) {
138 glconProcessEventMessage();
141 sdmain.addEventListener((PopupCheckerEvent evt) {
142 popupCheckExpirations();
146 // ////////////////////////////////////////////////////////////////////// //
147 sdmain.onClosing = delegate () {
148 clist.forEachAccount(delegate (acc) { acc.forceOnline = false; });
149 popupKillAll();
150 if (nvg !is null) {
151 sdmain.setAsCurrentOpenGlContext();
152 scope(exit) { flushGui(); sdmain.releaseCurrentOpenGlContext(); }
153 svp.kill();
154 nvg.kill();
156 assert(nvg is null);
157 if (sdhint !is null) sdhint.close();
158 if (trayicon !is null) trayicon.close();
161 sdmain.closeQuery = delegate () {
162 concmd("quit");
163 glconPostDoConCommands!true();
166 // first time setup
167 sdmain.visibleForTheFirstTime = delegate () {
168 if (sdmain.width > 1 && optCListWidth < 0) optCListWidth = sdmain.width/5;
169 sdmain.setAsCurrentOpenGlContext(); // make this window active
170 scope(exit) sdmain.releaseCurrentOpenGlContext();
171 sdmain.vsync = false;
173 glconInit(sdmain.width, sdmain.height);
175 nvg = nvgCreateContext(NVGContextFlag.Antialias, NVGContextFlag.StencilStrokes, NVGContextFlag.FontNoAA);
176 if (nvg is null) assert(0, "cannot initialize NanoVG");
177 loadFonts();
179 try {
180 static immutable skullsPng = /*cast(immutable(ubyte)[])*/import("data/skulls.png");
181 //nvgSkullsImg = nvg.createImage("skulls.png", NVGImageFlags.NoFiltering|NVGImageFlags.RepeatX|NVGImageFlags.RepeatY);
182 auto xi = loadImageFromMemory(skullsPng[]);
183 scope(exit) delete xi;
184 //{ import core.stdc.stdio; printf("creating background image...\n"); }
185 nvgSkullsImg = nvg.createImageFromMemoryImage(xi, NVGImageFlags.NoFiltering, NVGImageFlags.RepeatX, NVGImageFlags.RepeatY);
186 //{ import core.stdc.stdio; printf("background image created\n"); }
187 if (!nvgSkullsImg.valid) assert(0, "cannot load background image");
188 } catch (Exception e) {
189 assert(0, "cannot load background image");
191 buildStatusImages();
193 prepareTrayIcon();
195 lay = new LayTextClass(laf, sdmain.width/2);
196 //conwriteln("wdt=", lay.width, "; text width=", lay.textWidth);
197 lastWindowWidth = sdmain.width;
199 clist = new CList();
200 loadAccount(accountNameToLoad, optAccountAllowCreate);
201 clist.onActivateContactCB = delegate (Contact ct) { doActivateContact(ct); };
202 addContactCommands();
204 sdmain.setMinSize(640, 480);
206 fixTrayIcon();
207 //sdmain.redrawOpenGlSceneNow();
210 sdmain.windowResized = delegate (int wdt, int hgt) {
211 if (sdmain.closed) return;
212 glconResize(wdt, hgt);
213 glconPostScreenRepaint/*Delayed*/();
214 //conwriteln("old=", lastWindowWidth, "; new=", wdt);
215 if (wdt > 1 && optCListWidth > 0 && lastWindowWidth > 0 && lastWindowWidth != wdt) {
216 immutable double frc = lastWindowWidth/optCListWidth;
217 optCListWidth = cast(int)(wdt/frc);
218 if (optCListWidth < 64) optCListWidth = 64;
219 lastWindowWidth = wdt;
221 lay.relayout(wdt);
225 int mouseX = -666, mouseY = -666;
228 sdmain.handleKeyEvent = delegate (KeyEvent event) {
229 if (sdmain.closed) return;
230 scope(exit) glconPostDoConCommands!true();
231 if (glconKeyEvent(event)) return;
233 auto acc = clist.mainAccount;
235 if (event == "D-Escape") {
236 if (sdmain !is null && !sdmain.closed && sdmain.visible) {
237 sdmain.hide();
238 flushGui();
240 return;
243 scope(exit) { if (event.pressed) glconPostScreenRepaint(); }
245 if (event == "D-C-Q") { concmd("quit"); return; }
246 if (event == "D-C-1") { acc.status = ContactStatus.Online; return; }
247 if (event == "D-C-2") { acc.status = ContactStatus.Away; return; }
248 if (event == "D-C-3") { acc.status = ContactStatus.Busy; return; }
249 if (event == "D-C-0") { acc.status = ContactStatus.Offline; return; }
251 if (event == "D-C-W") { doActivateContact(null); return; }
253 if (clist !is null) {
254 if (clist.onKey(event)) return;
258 if (event == "D-C-S-Enter") {
259 static PopupWindow.Kind kind = PopupWindow.Kind.Incoming;
260 //new PopupWindow(10, 10, "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
261 showPopup(kind, "TEST popup", "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
262 if (kind == PopupWindow.Kind.max) kind = PopupWindow.Kind.min; else ++kind;
263 return;
267 if (event == "D-Enter") {
268 auto text = currEdit.text.xstrip;
269 currEdit.clear();
270 if (text.length == 0) return;
272 // `null`: eol
273 string getWord () {
274 while (text.length && text[0] <= ' ') text = text[1..$];
275 if (text.length == 0) return null;
276 if (text[0] == '"' || text[0] == '\'') {
277 string res;
278 char ech = text[0];
279 text = text[1..$];
280 while (text.length && text[0] != ech) {
281 if (text[0] == '\\') {
282 text = text[1..$];
283 if (text.length == 0) break;
284 char ch = text[0];
285 switch (ch) {
286 case '\r': res ~= '\r'; break;
287 case '\n': res ~= '\n'; break;
288 case '\t': res ~= '\t'; break;
289 default: res ~= ch; break;
291 } else {
292 res ~= text[0];
294 text = text[1..$];
296 if (text.length) { assert(text[0] == ech); text = text[1..$]; }
297 return res;
298 } else {
299 auto ep = 0;
300 while (ep < text.length && text[ep] > ' ') ++ep;
301 auto res = text[0..ep];
302 text = text[ep..$];
303 return res;
307 if (activeContact !is null) {
308 auto otext = text;
309 if (text[0] == '/' && !text.startsWith("/me ") && !text.startsWith("/me\t")) {
310 text = text[1..$];
311 auto cmd = getWord();
312 if (cmd == "help") {
313 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false,
314 "/accept -- accept friend request\n"~
315 "/remove -- remove contact\n"~
316 "/kfd -- remove friend, block any further requests\n"~
317 "/status -- set account status\n"~
318 "/pubkey -- get contact public key\n"~
319 "/always -- always visible\n"~
320 "/normal -- normal visibility"~
321 "", systimeNow);
322 } else if (cmd == "accept") {
323 if (toxCoreAddFriend(activeContact.acc.toxpk, activeContact.info.pubkey)) {
324 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "accepted!", systimeNow);
325 activeContact.kind = ContactInfo.Kind.Friend;
326 activeContact.save();
327 } else {
328 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "ERROR accepting!", systimeNow);
330 } else if (cmd == "kfd") {
331 toxCoreRemoveFriend(activeContact.acc.toxpk, activeContact.info.pubkey);
332 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "KILL! FUCK! DIE!", systimeNow);
333 activeContact.kind = ContactInfo.Kind.KillFuckDie;
334 activeContact.save();
335 } else if (cmd == "remove") {
336 acc.removeContact(activeContact);
337 } else if (cmd == "friend") {
338 if (!isValidAddr(activeContact.info.fraddr)) { conwriteln("invalid address"); return; }
339 string msg = getWord();
340 if (msg.length == 0) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; please, specify message!"); return; }
341 if (msg.length > tox_max_friend_request_length()) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; message too long"); return; }
342 if (!acc.sendFriendRequest(activeContact.info.fraddr, msg)) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; error sending friend request"); return; }
343 } else if (cmd == "status") {
344 string msg = getWord();
345 if (msg.length == 0) { conwriteln("current: ", acc.info.statusmsg); return; }
346 if (!toxCoreSetStatusMessage(activeContact.acc.toxpk, msg)) { conwriteln("ERROR: cannot set status message"); return; }
347 activeContact.acc.info.statusmsg = msg;
348 activeContact.acc.save();
349 try { activeContact.acc.save(); } catch (Exception e) { conwriteln("ERROR saving account: ", e.msg); }
350 } else if (cmd == "pubkey") {
351 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, tox_hex(activeContact.info.pubkey), systimeNow);
352 } else if (cmd == "always") {
353 activeContact.showOffline = TriOption.Yes;
354 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "always visible", systimeNow);
355 } else if (cmd == "normal") {
356 activeContact.showOffline = TriOption.Default;
357 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "normal visibility", systimeNow);
358 } else {
359 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "wut?!", systimeNow);
361 } else {
362 activeContact.send(text);
364 } else {
365 // sysedit
366 if (acc !is null && text.length && text[0] == '/') {
367 text = text[1..$];
368 // get command
369 auto cmd = getWord();
370 switch (cmd) {
371 case "friend": // add new friend
372 string addr = getWord();
373 ToxAddr fraddr = decodeAddrStr(addr);
374 if (!isValidAddr(fraddr)) { conwriteln("invalid address: '", addr, "'"); return; }
375 string msg = getWord();
376 if (msg.length == 0) { conwriteln("address: '", tox_hex(fraddr), "'; please, specify message!"); return; }
377 if (msg.length > tox_max_friend_request_length()) { conwriteln("address: '", addr, "'; message too long"); return; }
378 //if (!toxCoreSendFriendRequest(acc.toxpk, fraddr, msg)) { conwriteln("address: '", addr, "'; error sending friend request");
379 if (!acc.sendFriendRequest(fraddr, msg)) { conwriteln("address: '", addr, "'; error sending friend request"); return; }
380 break;
381 case "status":
382 string msg = getWord();
383 if (msg.length == 0) { conwriteln("current: ", acc.info.statusmsg); return; }
384 if (!toxCoreSetStatusMessage(acc.toxpk, msg)) { conwriteln("ERROR: cannot set status message"); return; }
385 acc.info.statusmsg = msg;
386 activeContact.acc.save();
387 try { acc.save(); } catch (Exception e) { conwriteln("ERROR saving account: ", e.msg); }
388 break;
389 case "myaddr":
390 currEdit.text = acc.getAddress();
391 break;
392 case "help":
393 addTextToLog(null, null, LogFile.Msg.Kind.Notification, false,
394 "/friend addr msg -- send friend request\n"~
395 "/status -- set account status\n"~
396 "/myaddr -- put my address into the editor"~
397 "", systimeNow);
398 break;
399 default:
400 conwriteln("unknown command: '", cmd, "'");
401 break;
403 } else {
404 if (text.length) conwriteln("NOT A COMMAND: ", text);
407 return;
410 if (currEdit.onKey(event)) return;
413 int msLastPressX = -666, msLastPressY = -666;
414 uint msDoLogButton = 0; // =0: none; 1: left; 2: middle; 3: right
416 sdmain.handleMouseEvent = delegate (MouseEvent event) {
417 if (sdmain.closed) return;
418 scope(exit) glconPostDoConCommands!true();
419 if (isConsoleVisible) return;
420 mouseX = event.x;
421 mouseY = event.y;
423 // check for click in log
424 //FIXME: process it here, not in renderer
425 if (event == "LMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 1; glconPostScreenRepaint(); }
426 if (event == "MMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 2; glconPostScreenRepaint(); }
427 if (event == "RMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 3; glconPostScreenRepaint(); }
429 if (clist !is null) {
430 if (clist.onMouse(event)) return;
433 // log scroll
434 if (layWinHeight > 0) {
435 enum ScrollHeight = 32;
436 if (event == "WheelUp") {
437 layOffset += ScrollHeight;
438 if (layOffset > lay.textHeight-layWinHeight) layOffset = lay.textHeight-layWinHeight;
439 } else if (event == "WheelDown") {
440 layOffset -= ScrollHeight;
442 if (layOffset < 0) layOffset = 0;
445 // don't spam with repaint events
446 if (event.type != MouseEventType.motion) glconPostScreenRepaint();
449 sdmain.handleCharEvent = delegate (dchar ch) {
450 if (sdmain.closed) return;
451 scope(exit) glconPostDoConCommands!true();
452 if (glconCharEvent(ch)) return;
454 scope(exit) glconPostScreenRepaint();
456 if (currEdit.onChar(ch)) return;
459 // draw main screen
460 sdmain.redrawOpenGlScene = delegate () {
461 glconPostDoConCommands!true();
462 if (sdmain.closed) return;
463 sdmain.setAsCurrentOpenGlContext(); // make this window active
464 scope(exit) sdmain.releaseCurrentOpenGlContext();
465 glViewport(0, 0, sdmain.width, sdmain.height);
467 // draw main screen
468 scope(exit) glconDraw();
470 if (nvg is null) return;
472 //oglSetup2D(glconCtlWindow.width, glconCtlWindow.height);
473 glViewport(0, 0, sdmain.width, sdmain.height);
474 glMatrixMode(GL_MODELVIEW);
475 //conwriteln(glconCtlWindow.width, "x", glconCtlWindow.height);
477 glClearColor(0, 0, 0, 0);
478 glClear(glNVGClearFlags/*|GL_COLOR_BUFFER_BIT*/);
481 nvg.beginFrame(sdmain.width, sdmain.height);
482 scope(exit) nvg.endFrame();
484 if (clist !is null && optCListWidth > 0) {
485 // draw contact list
486 int cx = 1;
487 int cy = 1;
488 int wdt = optCListWidth;
489 int hgt = nvg.height-cy*2;
491 nvg.shapeAntiAlias = true;
492 nvg.nonZeroFill;
493 nvg.strokeWidth = 1;
496 nvg.save();
497 scope(exit) nvg.restore();
499 nvg.newPath();
500 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
502 int w, h;
503 nvg.imageSize(nvgSkullsImg, w, h);
504 nvg.fillPaint(nvg.imagePattern(0, 0, w, h, 0, nvgSkullsImg));
506 nvg.strokeColor(NVGColor("#f70"));
507 nvg.fill();
508 nvg.stroke();
510 // draw contact list
512 nvg.save();
513 scope(exit) nvg.restore();
514 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt-3*2, hgt-3*2);
515 clist.drawAt(nvg, cx+3, cy+3, wdt-3*2, hgt-3*2);
517 // draw scrollbar
518 if (clist.sbSize > 0) {
519 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);
524 nvg.save();
525 scope(exit) nvg.restore();
527 // draw chat log
528 cx += wdt+2;
529 wdt = nvg.width-cx-1;
530 auto baphHgt = hgt;
532 // calculate editor dimensions and draw editor
534 nvg.save();
535 scope(exit) nvg.restore();
537 currEdit.setWidth(wdt-3*2);
539 auto edheight = currEdit.calcHeight();
541 int edy = cy+hgt-edheight-3*2;
543 nvg.newPath();
544 nvg.roundedRect(cx+0.5f, edy+0.5f, wdt, edheight+3*2, 6);
545 nvg.fillColor(NVGColor.black);
546 nvg.strokeColor(NVGColor("#f70"));
547 nvg.fill();
548 nvg.stroke();
550 nvg.intersectScissor(cx+2.5f, edy+2.5f, wdt-3*2+2, edheight+2);
551 currEdit.draw(nvg, cx+3, edy+3);
553 hgt -= edheight+3*2;
556 nvg.newPath();
557 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
558 nvg.fillColor(NVGColor.black);
559 nvg.strokeColor(NVGColor("#f70"));
560 nvg.fill();
561 nvg.stroke();
563 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt-3*2, hgt-3*2);
564 version(all) {
565 immutable float scalex = (wdt-3*2-10*2)/BaphometDims;
566 immutable float scaley = (baphHgt-3*2-10*2)/BaphometDims;
567 immutable float scale = (scalex < scaley ? scalex : scaley)/1.5f;
568 immutable float sz = BaphometDims*scale;
569 nvg.strokeColor(NVGColor("#400"));
570 nvg.fillColor(NVGColor("#400"));
571 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);
574 immutable float sbx = cx+wdt-BND_SCROLLBAR_WIDTH-1.5f;
575 immutable float sby = cy+3.5f;
576 wdt -= BND_SCROLLBAR_WIDTH+1;
577 wdt -= 3*2;
578 hgt -= 3*2;
580 lay.relayout(wdt); // this is harmess if width wasn't changed
581 int ty = lay.textHeight-hgt+1-layOffset;
582 if (ty < 0) ty = 0;
583 layWinHeight = cast(int)hgt;
585 if (lay.textHeight > hgt) {
586 float h = lay.textHeight-hgt;
587 nvg.bndScrollBar(sbx, sby, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, ty/h, hgt/h);
588 } else {
589 nvg.bndScrollBar(sbx, sby, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);
592 nvg.intersectScissor(cx+2.5f, cy+2.5f, wdt+2, hgt+2);
593 nvg.drawLayouter(lay, ty, cx+3, cy+3, hgt);
595 if (msDoLogButton) {
596 uint btn = msDoLogButton;
597 msDoLogButton = 0;
598 msLastPressX -= cx+3;
599 msLastPressY -= cy+3;
600 if (msLastPressX >= 0 && msLastPressY >= 0 && msLastPressX < wdt && msLastPressY < hgt) {
601 //conwriteln("MOUSE: ", msLastPressX, " : ", msLastPressY, "; ty=", ty, "; thgt=", lay.textHeight);
602 auto widx = lay.wordAtXY(msLastPressX, ty+msLastPressY);
603 //conwriteln(" widx=", widx);
604 if (widx >= 0) {
605 auto w = lay.wordByIndex(widx);
606 conwriteln(" widx=", widx, "; <", lay.wordText(*w), ">; udata=", w.udata);
607 // lmb
608 if (btn == 1) {
609 string url = findUrlByWordIndex(widx);
610 if (url.length) {
611 //conwriteln("URL CLICK: <", url, ">");
612 openUrl(url);
615 // rmb
616 if (btn == 3 && w.objectIdx >= 0) {
617 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(w.objectIdx)) {
618 assert(activeContact !is null);
619 activeContact.removeFromResendQueue(maw.msgid, maw.digest, maw.time);
620 maw.msgid = -1;
621 // yeah, repaint
622 glconPostScreenRepaint();
633 flushGui();
634 sdmain.eventLoop(16000,
635 // pulser: process resend queues here
636 delegate () {
637 if (sdmain.closed || clist is null) return;
638 clist.forEachAccount(delegate (Account acc) { acc.processResendQueue(); });
641 clist.forEachAccount(delegate (acc) { acc.forceOnline = false; });
642 clist.forEachAccount(delegate (Account acc) { acc.saveResendQueue(); acc.saveResendQueue(); });
643 toxCoreShutdownAll();
644 popupKillAll();
645 flushGui();
646 conProcessQueue(int.max/4);