slightly better URL detection
[bioacid.git] / bioacid.d
blobe9ebdbbfca5f5cfffc0341a056dec6007d057a27
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 import core.memory : GC;
117 conwriteln("starting GC collection...");
118 GC.collect();
119 GC.minimize();
120 conwriteln("GC collection complete.");
121 })("gc_collect", "force GC collection cycle");
123 conRegFunc!(() {
124 if (sdmain !is null && !sdmain.closed) {
125 //conwriteln("act=", mainWindowActive, "; vis=", mainWindowVisible, "; realvis=", sdmain.visible);
126 if (!mainWindowVisible) {
127 // this strange code brings window to the current desktop it if was on a different one
128 sdmain.hide();
129 sdmain.show();
130 } else if (sdmain.visible) {
131 sdmain.hide();
132 } else {
133 sdmain.show();
135 flushGui();
137 })("win_toggle", "show/hide main window");
139 sdmain.addEventListener((GLConScreenRepaintEvent evt) {
140 if (sdmain.closed) return;
141 if (isQuitRequested) { sdmain.close(); return; }
142 sdmain.redrawOpenGlSceneNow();
145 sdmain.addEventListener((GLConDoConsoleCommandsEvent evt) {
146 glconProcessEventMessage();
149 sdmain.addEventListener((PopupCheckerEvent evt) {
150 popupCheckExpirations();
154 // ////////////////////////////////////////////////////////////////////// //
155 sdmain.onClosing = delegate () {
156 clist.forEachAccount(delegate (acc) { acc.forceOnline = false; });
157 popupKillAll();
158 if (nvg !is null) {
159 sdmain.setAsCurrentOpenGlContext();
160 scope(exit) { flushGui(); sdmain.releaseCurrentOpenGlContext(); }
161 svp.kill();
162 nvg.kill();
164 assert(nvg is null);
165 if (sdhint !is null) sdhint.close();
166 if (trayicon !is null) trayicon.close();
169 sdmain.closeQuery = delegate () {
170 concmd("quit");
171 glconPostDoConCommands!true();
174 // first time setup
175 sdmain.visibleForTheFirstTime = delegate () {
176 if (sdmain.width > 1 && optCListWidth < 0) optCListWidth = sdmain.width/5;
177 sdmain.setAsCurrentOpenGlContext(); // make this window active
178 scope(exit) sdmain.releaseCurrentOpenGlContext();
179 sdmain.vsync = false;
181 glconInit(sdmain.width, sdmain.height);
183 nvg = nvgCreateContext(NVGContextFlag.Antialias, NVGContextFlag.StencilStrokes, NVGContextFlag.FontNoAA);
184 if (nvg is null) assert(0, "cannot initialize NanoVG");
185 loadFonts();
187 try {
188 static immutable skullsPng = /*cast(immutable(ubyte)[])*/import("data/skulls.png");
189 //nvgSkullsImg = nvg.createImage("skulls.png", NVGImageFlags.NoFiltering|NVGImageFlags.RepeatX|NVGImageFlags.RepeatY);
190 auto xi = loadImageFromMemory(skullsPng[]);
191 scope(exit) delete xi;
192 //{ import core.stdc.stdio; printf("creating background image...\n"); }
193 nvgSkullsImg = nvg.createImageFromMemoryImage(xi, NVGImageFlags.NoFiltering, NVGImageFlags.RepeatX, NVGImageFlags.RepeatY);
194 //{ import core.stdc.stdio; printf("background image created\n"); }
195 if (!nvgSkullsImg.valid) assert(0, "cannot load background image");
196 } catch (Exception e) {
197 assert(0, "cannot load background image");
199 buildStatusImages();
201 prepareTrayIcon();
203 lay = new LayTextClass(laf, sdmain.width/2);
204 //conwriteln("wdt=", lay.width, "; text width=", lay.textWidth);
205 lastWindowWidth = sdmain.width;
207 clist = new CList();
208 loadAccount(accountNameToLoad, optAccountAllowCreate);
209 clist.onActivateContactCB = delegate (Contact ct) { doActivateContact(ct); };
210 addContactCommands();
212 sdmain.setMinSize(640, 480);
214 fixTrayIcon();
215 //sdmain.redrawOpenGlSceneNow();
218 sdmain.windowResized = delegate (int wdt, int hgt) {
219 if (sdmain.closed) return;
220 glconResize(wdt, hgt);
221 glconPostScreenRepaint/*Delayed*/();
222 //conwriteln("old=", lastWindowWidth, "; new=", wdt);
223 if (wdt > 1 && optCListWidth > 0 && lastWindowWidth > 0 && lastWindowWidth != wdt) {
224 immutable double frc = lastWindowWidth/optCListWidth;
225 optCListWidth = cast(int)(wdt/frc);
226 if (optCListWidth < 64) optCListWidth = 64;
227 lastWindowWidth = wdt;
229 lay.relayout(wdt);
233 int mouseX = -666, mouseY = -666;
236 sdmain.handleKeyEvent = delegate (KeyEvent event) {
237 if (sdmain.closed) return;
238 scope(exit) glconPostDoConCommands!true();
239 if (glconKeyEvent(event)) return;
241 auto acc = clist.mainAccount;
243 if (event == "D-Escape") {
244 if (sdmain !is null && !sdmain.closed && sdmain.visible) {
245 sdmain.hide();
246 flushGui();
248 return;
251 scope(exit) { if (event.pressed) glconPostScreenRepaint(); }
253 if (event == "D-C-Q") { concmd("quit"); return; }
254 if (event == "D-C-1") { acc.status = ContactStatus.Online; return; }
255 if (event == "D-C-2") { acc.status = ContactStatus.Away; return; }
256 if (event == "D-C-3") { acc.status = ContactStatus.Busy; return; }
257 if (event == "D-C-0") { acc.status = ContactStatus.Offline; return; }
259 if (event == "D-C-W") { doActivateContact(null); return; }
261 if (clist !is null) {
262 if (clist.onKey(event)) return;
266 if (event == "D-C-S-Enter") {
267 static PopupWindow.Kind kind = PopupWindow.Kind.Incoming;
268 //new PopupWindow(10, 10, "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
269 showPopup(kind, "TEST popup", "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
270 if (kind == PopupWindow.Kind.max) kind = PopupWindow.Kind.min; else ++kind;
271 return;
275 if (event == "D-Enter") {
276 auto text = currEdit.text.xstrip;
277 currEdit.clear();
278 if (text.length == 0) return;
280 // `null`: eol
281 string getWord () {
282 while (text.length && text[0] <= ' ') text = text[1..$];
283 if (text.length == 0) return null;
284 if (text[0] == '"' || text[0] == '\'') {
285 string res;
286 char ech = text[0];
287 text = text[1..$];
288 while (text.length && text[0] != ech) {
289 if (text[0] == '\\') {
290 text = text[1..$];
291 if (text.length == 0) break;
292 char ch = text[0];
293 switch (ch) {
294 case '\r': res ~= '\r'; break;
295 case '\n': res ~= '\n'; break;
296 case '\t': res ~= '\t'; break;
297 default: res ~= ch; break;
299 } else {
300 res ~= text[0];
302 text = text[1..$];
304 if (text.length) { assert(text[0] == ech); text = text[1..$]; }
305 return res;
306 } else {
307 auto ep = 0;
308 while (ep < text.length && text[ep] > ' ') ++ep;
309 auto res = text[0..ep];
310 text = text[ep..$];
311 return res;
315 if (activeContact !is null) {
316 auto otext = text;
317 if (text[0] == '/' && !text.startsWith("/me ") && !text.startsWith("/me\t")) {
318 text = text[1..$];
319 auto cmd = getWord();
320 if (cmd == "help") {
321 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false,
322 "/accept -- accept friend request\n"~
323 "/remove -- remove contact\n"~
324 "/kfd -- remove friend, block any further requests\n"~
325 "/status -- set account status\n"~
326 "/pubkey -- get contact public key\n"~
327 "/always -- always visible\n"~
328 "/normal -- normal visibility"~
329 "", systimeNow);
330 } else if (cmd == "accept") {
331 if (toxCoreAddFriend(activeContact.acc.toxpk, activeContact.info.pubkey)) {
332 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "accepted!", systimeNow);
333 activeContact.kind = ContactInfo.Kind.Friend;
334 activeContact.save();
335 } else {
336 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "ERROR accepting!", systimeNow);
338 } else if (cmd == "kfd") {
339 toxCoreRemoveFriend(activeContact.acc.toxpk, activeContact.info.pubkey);
340 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "KILL! FUCK! DIE!", systimeNow);
341 activeContact.kind = ContactInfo.Kind.KillFuckDie;
342 activeContact.save();
343 } else if (cmd == "remove") {
344 acc.removeContact(activeContact);
345 } else if (cmd == "friend") {
346 if (!isValidAddr(activeContact.info.fraddr)) { conwriteln("invalid address"); return; }
347 string msg = getWord();
348 if (msg.length == 0) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; please, specify message!"); return; }
349 if (msg.length > tox_max_friend_request_length()) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; message too long"); return; }
350 if (!acc.sendFriendRequest(activeContact.info.fraddr, msg)) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; error sending friend request"); return; }
351 } else if (cmd == "status") {
352 string msg = getWord();
353 if (msg.length == 0) { conwriteln("current: ", acc.info.statusmsg); return; }
354 if (!toxCoreSetStatusMessage(activeContact.acc.toxpk, msg)) { conwriteln("ERROR: cannot set status message"); return; }
355 activeContact.acc.info.statusmsg = msg;
356 activeContact.acc.save();
357 try { activeContact.acc.save(); } catch (Exception e) { conwriteln("ERROR saving account: ", e.msg); }
358 } else if (cmd == "pubkey") {
359 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, tox_hex(activeContact.info.pubkey), systimeNow);
360 } else if (cmd == "always") {
361 activeContact.showOffline = TriOption.Yes;
362 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "always visible", systimeNow);
363 } else if (cmd == "normal") {
364 activeContact.showOffline = TriOption.Default;
365 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "normal visibility", systimeNow);
366 } else {
367 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "wut?!", systimeNow);
369 } else {
370 activeContact.send(text);
372 } else {
373 // sysedit
374 if (acc !is null && text.length && text[0] == '/') {
375 text = text[1..$];
376 // get command
377 auto cmd = getWord();
378 switch (cmd) {
379 case "friend": // add new friend
380 string addr = getWord();
381 ToxAddr fraddr = decodeAddrStr(addr);
382 if (!isValidAddr(fraddr)) { conwriteln("invalid address: '", addr, "'"); return; }
383 string msg = getWord();
384 if (msg.length == 0) { conwriteln("address: '", tox_hex(fraddr), "'; please, specify message!"); return; }
385 if (msg.length > tox_max_friend_request_length()) { conwriteln("address: '", addr, "'; message too long"); return; }
386 //if (!toxCoreSendFriendRequest(acc.toxpk, fraddr, msg)) { conwriteln("address: '", addr, "'; error sending friend request");
387 if (!acc.sendFriendRequest(fraddr, msg)) { conwriteln("address: '", addr, "'; error sending friend request"); return; }
388 break;
389 case "status":
390 string msg = getWord();
391 if (msg.length == 0) { conwriteln("current: ", acc.info.statusmsg); return; }
392 if (!toxCoreSetStatusMessage(acc.toxpk, msg)) { conwriteln("ERROR: cannot set status message"); return; }
393 acc.info.statusmsg = msg;
394 activeContact.acc.save();
395 try { acc.save(); } catch (Exception e) { conwriteln("ERROR saving account: ", e.msg); }
396 break;
397 case "myaddr":
398 currEdit.text = acc.getAddress();
399 break;
400 case "help":
401 addTextToLog(null, null, LogFile.Msg.Kind.Notification, false,
402 "/friend addr msg -- send friend request\n"~
403 "/status -- set account status\n"~
404 "/myaddr -- put my address into the editor"~
405 "", systimeNow);
406 break;
407 default:
408 conwriteln("unknown command: '", cmd, "'");
409 break;
411 } else {
412 if (text.length) conwriteln("NOT A COMMAND: ", text);
415 return;
418 if (currEdit.onKey(event)) return;
421 int msLastPressX = -666, msLastPressY = -666;
422 uint msDoLogButton = 0; // =0: none; 1: left; 2: middle; 3: right
424 sdmain.handleMouseEvent = delegate (MouseEvent event) {
425 if (sdmain.closed) return;
426 scope(exit) glconPostDoConCommands!true();
427 if (isConsoleVisible) return;
428 mouseX = event.x;
429 mouseY = event.y;
431 // check for click in log
432 //FIXME: process it here, not in renderer
433 if (event == "LMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 1; glconPostScreenRepaint(); }
434 if (event == "MMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 2; glconPostScreenRepaint(); }
435 if (event == "RMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 3; glconPostScreenRepaint(); }
437 if (clist !is null) {
438 if (clist.onMouse(event)) return;
441 // log scroll
442 if (layWinHeight > 0) {
443 enum ScrollHeight = 32;
444 if (event == "WheelUp") {
445 layOffset += ScrollHeight;
446 if (layOffset > lay.textHeight-layWinHeight) layOffset = lay.textHeight-layWinHeight;
447 } else if (event == "WheelDown") {
448 layOffset -= ScrollHeight;
450 if (layOffset < 0) layOffset = 0;
453 // don't spam with repaint events
454 if (event.type != MouseEventType.motion) glconPostScreenRepaint();
457 sdmain.handleCharEvent = delegate (dchar ch) {
458 if (sdmain.closed) return;
459 scope(exit) glconPostDoConCommands!true();
460 if (glconCharEvent(ch)) return;
462 scope(exit) glconPostScreenRepaint();
464 if (currEdit.onChar(ch)) return;
467 // draw main screen
468 sdmain.redrawOpenGlScene = delegate () {
469 glconPostDoConCommands!true();
470 if (sdmain.closed) return;
471 sdmain.setAsCurrentOpenGlContext(); // make this window active
472 scope(exit) sdmain.releaseCurrentOpenGlContext();
473 glViewport(0, 0, sdmain.width, sdmain.height);
475 // draw main screen
476 scope(exit) glconDraw();
478 if (nvg is null) return;
480 //oglSetup2D(glconCtlWindow.width, glconCtlWindow.height);
481 glViewport(0, 0, sdmain.width, sdmain.height);
482 glMatrixMode(GL_MODELVIEW);
483 //conwriteln(glconCtlWindow.width, "x", glconCtlWindow.height);
485 glClearColor(0, 0, 0, 0);
486 glClear(glNVGClearFlags/*|GL_COLOR_BUFFER_BIT*/);
489 nvg.beginFrame(sdmain.width, sdmain.height);
490 scope(exit) nvg.endFrame();
492 if (clist !is null && optCListWidth > 0) {
493 // draw contact list
494 int cx = 1;
495 int cy = 1;
496 int wdt = optCListWidth;
497 int hgt = nvg.height-cy*2;
499 nvg.shapeAntiAlias = true;
500 nvg.nonZeroFill;
501 nvg.strokeWidth = 1;
504 nvg.save();
505 scope(exit) nvg.restore();
507 nvg.newPath();
508 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
510 int w, h;
511 nvg.imageSize(nvgSkullsImg, w, h);
512 nvg.fillPaint(nvg.imagePattern(0, 0, w, h, 0, nvgSkullsImg));
514 nvg.strokeColor(NVGColor("#f70"));
515 nvg.fill();
516 nvg.stroke();
518 // draw contact list
520 nvg.save();
521 scope(exit) nvg.restore();
522 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt-3*2, hgt-3*2);
523 clist.drawAt(nvg, cx+3, cy+3, wdt-3*2, hgt-3*2);
525 // draw scrollbar
526 if (clist.sbSize > 0) {
527 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);
532 nvg.save();
533 scope(exit) nvg.restore();
535 // draw chat log
536 cx += wdt+2;
537 wdt = nvg.width-cx-1;
538 auto baphHgt = hgt;
540 // calculate editor dimensions and draw editor
542 nvg.save();
543 scope(exit) nvg.restore();
545 currEdit.setWidth(wdt-3*2);
547 auto edheight = currEdit.calcHeight();
549 int edy = cy+hgt-edheight-3*2;
551 nvg.newPath();
552 nvg.roundedRect(cx+0.5f, edy+0.5f, wdt, edheight+3*2, 6);
553 nvg.fillColor(NVGColor.black);
554 nvg.strokeColor(NVGColor("#f70"));
555 nvg.fill();
556 nvg.stroke();
558 nvg.intersectScissor(cx+2.5f, edy+2.5f, wdt-3*2+2, edheight+2);
559 currEdit.draw(nvg, cx+3, edy+3);
561 hgt -= edheight+3*2;
564 nvg.newPath();
565 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
566 nvg.fillColor(NVGColor.black);
567 nvg.strokeColor(NVGColor("#f70"));
568 nvg.fill();
569 nvg.stroke();
571 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt-3*2, hgt-3*2);
572 version(all) {
573 immutable float scalex = (wdt-3*2-10*2)/BaphometDims;
574 immutable float scaley = (baphHgt-3*2-10*2)/BaphometDims;
575 immutable float scale = (scalex < scaley ? scalex : scaley)/1.5f;
576 immutable float sz = BaphometDims*scale;
577 nvg.strokeColor(NVGColor("#400"));
578 nvg.fillColor(NVGColor("#400"));
579 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);
582 immutable float sbx = cx+wdt-BND_SCROLLBAR_WIDTH-1.5f;
583 immutable float sby = cy+3.5f;
584 wdt -= BND_SCROLLBAR_WIDTH+1;
585 wdt -= 3*2;
586 hgt -= 3*2;
588 lay.relayout(wdt); // this is harmess if width wasn't changed
589 int ty = lay.textHeight-hgt+1-layOffset;
590 if (ty < 0) ty = 0;
591 layWinHeight = cast(int)hgt;
593 if (lay.textHeight > hgt) {
594 float h = lay.textHeight-hgt;
595 nvg.bndScrollBar(sbx, sby, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, ty/h, hgt/h);
596 } else {
597 nvg.bndScrollBar(sbx, sby, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);
600 nvg.intersectScissor(cx+2.5f, cy+2.5f, wdt+2, hgt+2);
601 nvg.drawLayouter(lay, ty, cx+3, cy+3, hgt);
603 if (msDoLogButton) {
604 uint btn = msDoLogButton;
605 msDoLogButton = 0;
606 msLastPressX -= cx+3;
607 msLastPressY -= cy+3;
608 if (msLastPressX >= 0 && msLastPressY >= 0 && msLastPressX < wdt && msLastPressY < hgt) {
609 //conwriteln("MOUSE: ", msLastPressX, " : ", msLastPressY, "; ty=", ty, "; thgt=", lay.textHeight);
610 auto widx = lay.wordAtXY(msLastPressX, ty+msLastPressY);
611 //conwriteln(" widx=", widx);
612 if (widx >= 0) {
613 auto w = lay.wordByIndex(widx);
614 conwriteln(" widx=", widx, "; <", lay.wordText(*w), ">; udata=", w.udata);
615 // lmb
616 if (btn == 1) {
617 string url = findUrlByWordIndex(widx);
618 if (url.length) {
619 //conwriteln("URL CLICK: <", url, ">");
620 openUrl(url);
623 // rmb
624 if (btn == 3 && w.objectIdx >= 0) {
625 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(w.objectIdx)) {
626 assert(activeContact !is null);
627 activeContact.removeFromResendQueue(maw.msgid, maw.digest, maw.time);
628 maw.msgid = -1;
629 // yeah, repaint
630 glconPostScreenRepaint();
641 flushGui();
642 MonoTime lastCollect = MonoTime.currTime;
643 sdmain.eventLoop(16000,
644 // pulser: process resend queues here
645 delegate () {
646 if (sdmain.closed || clist is null) return;
647 clist.forEachAccount(delegate (Account acc) { acc.processResendQueue(); });
649 immutable ctt = MonoTime.currTime;
650 if ((ctt-lastCollect).total!"minutes" >= 1) {
651 import core.memory : GC;
652 lastCollect = ctt;
653 GC.collect();
654 GC.minimize();
659 clist.forEachAccount(delegate (acc) { acc.forceOnline = false; });
660 clist.forEachAccount(delegate (Account acc) { acc.saveResendQueue(); acc.saveResendQueue(); });
661 toxCoreShutdownAll();
662 popupKillAll();
663 flushGui();
664 conProcessQueue(int.max/4);