C-3 to set "busy" status
[bioacid.git] / bioacid.d
blob30042d362b4d2e7061157f44fe4277b352eea93b
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, either version 3 of the License, or
7 * (at your option) any later version.
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 module bioacid is aliced;
19 import std.datetime;
21 import arsd.color;
22 import arsd.image;
23 import arsd.simpledisplay;
25 import iv.cmdcon;
26 import iv.cmdcon.gl;
27 import iv.gxx;
28 import iv.meta;
29 import iv.nanovega;
30 import iv.nanovega.blendish;
31 import iv.nanovega.textlayouter;
32 import iv.strex;
33 import iv.tox;
34 import iv.txtser;
35 import iv.sdpyutil;
36 import iv.unarray;
37 import iv.utfutil;
38 import iv.vfs.io;
40 version(sfnt_test) import iv.nanovega.simplefont;
42 import accdb;
43 import accobj;
44 import fonts;
45 import popups;
46 import icondata;
47 import notifyicon;
48 import toxproto;
50 import tkmain;
51 import tklog;
52 import tkclist;
53 import tkminiedit;
56 // ////////////////////////////////////////////////////////////////////////// //
57 //__gshared string accountNameToLoad = "_fakeacc";
58 __gshared string accountNameToLoad = "";
59 __gshared string globalHotkey = "M-H-F";
62 void main (string[] args) {
63 if (!TOX_VERSION_IS_ABI_COMPATIBLE()) assert(0, "invalid ToxCore library version");
65 conRegVar!accountNameToLoad("starting_account", "account to load");
66 conRegVar!optShowOffline("show_offline", "always show offline contacts?");
68 //conwriteln("account '", acc.info.nick, "' loaded, ", acc.contacts.length, " contacts, ", acc.groups.length, " groups");
70 //glconShowKey = "M-Grave";
71 glconSetAndSealFPS(0); // draw-on-demand
73 conProcessQueue(256*1024); // load config
74 conProcessArgs!true(args);
75 conProcessQueue(256*1024);
77 if (accountNameToLoad.length == 0) assert(0, "no account to load");
79 //setOpenGLContextVersion(3, 2); // up to GLSL 150
80 setOpenGLContextVersion(2, 0); // it's enough
82 loadAllFonts();
85 NVGPathSet svp = null;
87 //glconRunGLWindowResizeable(800, 600, "BioAcid", "BIOACID");
88 sdpyWindowClass = "BIOACID";
89 sdmain = new SimpleWindow(800, 600, "BioAcid", OpenGlOptions.yes, Resizability.allowResizing);
90 glconCtlWindow = sdmain;
92 setupToxCoreSender();
93 setupToxEventListener(sdmain);
95 sdmain.visibilityChanged = delegate (bool vis) { mainWindowVisible = vis; fixUnreadIndicators(); };
96 sdmain.onFocusChange = delegate (bool focused) { mainWindowActive = focused; /*conwriteln("focus=", focused);*/ fixUnreadIndicators(); };
98 MiniEdit sysedit = new MiniEdit();
100 MiniEdit currEdit () { return (activeContact !is null ? activeContact.edit : sysedit); }
102 try {
103 if (globalHotkey.length > 0) {
104 GlobalHotkeyManager.register(globalHotkey, delegate () { concmd("win_toggle"); glconPostDoConCommands!true(); });
106 } catch (Exception e) {
107 conwriteln("ERROR registering hotkey!");
110 conRegFunc!(() {
111 if (sdmain !is null) sdmain.close();
112 })("quit", "quit BioAcid");
114 conRegFunc!(() {
115 if (sdmain !is null && !sdmain.closed) {
116 //conwriteln("act=", mainWindowActive, "; vis=", mainWindowVisible, "; realvis=", sdmain.visible);
117 if (!mainWindowVisible) {
118 // this strange code brings window to the current desktop it if was on a different one
119 sdmain.hide();
120 sdmain.show();
121 } else if (sdmain.visible) {
122 sdmain.hide();
123 } else {
124 sdmain.show();
126 flushGui();
128 })("win_toggle", "show/hide main window");
130 sdmain.addEventListener((GLConScreenRepaintEvent evt) {
131 if (sdmain.closed) return;
132 if (isQuitRequested) { sdmain.close(); return; }
133 sdmain.redrawOpenGlSceneNow();
136 sdmain.addEventListener((GLConDoConsoleCommandsEvent evt) {
137 glconProcessEventMessage();
140 sdmain.addEventListener((PopupCheckerEvent evt) {
141 popupCheckExpirations();
145 // ////////////////////////////////////////////////////////////////////// //
146 sdmain.onClosing = delegate () {
147 clist.forEachAccount(delegate (acc) { acc.forceOnline = false; });
148 popupKillAll();
149 if (nvg !is null) {
150 sdmain.setAsCurrentOpenGlContext();
151 scope(exit) { flushGui(); sdmain.releaseCurrentOpenGlContext(); }
152 svp.kill();
153 nvg.kill();
155 assert(nvg is null);
156 if (sdhint !is null) sdhint.close();
157 if (trayicon !is null) trayicon.close();
160 sdmain.closeQuery = delegate () {
161 concmd("quit");
162 glconPostDoConCommands!true();
165 // first time setup
166 sdmain.visibleForTheFirstTime = delegate () {
167 if (sdmain.width > 1 && optCListWidth < 0) optCListWidth = sdmain.width/5;
168 sdmain.setAsCurrentOpenGlContext(); // make this window active
169 scope(exit) sdmain.releaseCurrentOpenGlContext();
170 sdmain.vsync = false;
172 glconInit(sdmain.width, sdmain.height);
174 nvg = nvgCreateContext(NVGContextFlag.Antialias, NVGContextFlag.StencilStrokes, NVGContextFlag.FontNoAA);
175 if (nvg is null) assert(0, "cannot initialize NanoVG");
176 loadFonts(nvg);
178 try {
179 static immutable skullsPng = /*cast(immutable(ubyte)[])*/import("data/skulls.png");
180 //nvgSkullsImg = nvg.createImage("skulls.png", NVGImageFlags.NoFiltering|NVGImageFlags.RepeatX|NVGImageFlags.RepeatY);
181 auto xi = loadImageFromMemory(skullsPng[]);
182 scope(exit) delete xi;
183 //{ import core.stdc.stdio; printf("creating background image...\n"); }
184 nvgSkullsImg = nvg.createImageFromMemoryImage(xi, NVGImageFlags.NoFiltering, NVGImageFlags.RepeatX, NVGImageFlags.RepeatY);
185 //{ import core.stdc.stdio; printf("background image created\n"); }
186 if (!nvgSkullsImg.valid) assert(0, "cannot load background image");
187 } catch (Exception e) {
188 assert(0, "cannot load background image");
190 buildStatusImages();
192 prepareTrayIcon();
194 lay = new LayTextClass(laf, sdmain.width/2);
195 //conwriteln("wdt=", lay.width, "; text width=", lay.textWidth);
196 lastWindowWidth = sdmain.width;
198 clist = new CList();
199 loadAccount(accountNameToLoad);
200 //clist.buildAccount(acc);
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 if (event == "D-C-Q") { concmd("quit"); return; }
244 if (event == "D-C-1") { acc.status = ContactStatus.Online; return; }
245 if (event == "D-C-2") { acc.status = ContactStatus.Away; return; }
246 if (event == "D-C-3") { acc.status = ContactStatus.Busy; return; }
247 if (event == "D-C-0") { acc.status = ContactStatus.Offline; return; }
249 if (event == "D-C-W") { doActivateContact(null); return; }
251 if (clist !is null) {
252 sdmain.setAsCurrentOpenGlContext(); // make this window active
253 scope(exit) sdmain.releaseCurrentOpenGlContext();
255 bool inFrame = nvg.inFrame;
256 if (!inFrame) nvg.beginFrame(sdmain.width, sdmain.height);
257 scope(exit) if (!inFrame) nvg.endFrame();
260 nvg.save();
261 scope(exit) nvg.restore();
262 if (clist.onKey(event)) return;
266 if (event == "D-C-S-Enter") {
267 static PopupWindow.Kind kind;
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;
274 if (event == "D-Enter") {
275 auto text = currEdit.text.xstrip;
276 currEdit.clear();
277 if (text.length == 0) { glconPostScreenRepaint(); return; }
279 // `null`: eol
280 string getWord () {
281 while (text.length && text[0] <= ' ') text = text[1..$];
282 if (text.length == 0) return null;
283 if (text[0] == '"' || text[0] == '\'') {
284 string res;
285 char ech = text[0];
286 text = text[1..$];
287 while (text.length && text[0] != ech) {
288 if (text[0] == '\\') {
289 text = text[1..$];
290 if (text.length == 0) break;
291 char ch = text[0];
292 switch (ch) {
293 case '\r': res ~= '\r'; break;
294 case '\n': res ~= '\n'; break;
295 case '\t': res ~= '\t'; break;
296 default: res ~= ch; break;
298 } else {
299 res ~= text[0];
301 text = text[1..$];
303 if (text.length) { assert(text[0] == ech); text = text[1..$]; }
304 return res;
305 } else {
306 auto ep = 0;
307 while (ep < text.length && text[ep] > ' ') ++ep;
308 auto res = text[0..ep];
309 text = text[ep..$];
310 return res;
314 if (activeContact !is null) {
315 auto otext = text;
316 if (text[0] == '/' && !text.startsWith("/me ") && !text.startsWith("/me\t")) {
317 text = text[1..$];
318 auto cmd = getWord();
319 if (cmd == "accept") {
320 if (toxCoreAddFriend(activeContact.acc.toxpk, activeContact.info.pubkey)) {
321 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "accepted!", systimeNow);
322 activeContact.kind = ContactInfo.Kind.Friend;
323 activeContact.save();
324 } else {
325 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "ERROR accepting!", systimeNow);
327 } else if (cmd == "kfd") {
328 toxCoreRemoveFriend(activeContact.acc.toxpk, activeContact.info.pubkey);
329 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "KILL! FUCK! DIE!", systimeNow);
330 activeContact.kind = ContactInfo.Kind.KillFuckDie;
331 activeContact.save();
332 } else if (cmd == "friend") {
333 if (!isValidAddr(activeContact.info.fraddr)) { conwriteln("invalid address"); return; }
334 string msg = getWord();
335 if (msg.length == 0) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; please, specify message!"); return; }
336 if (msg.length > tox_max_friend_request_length()) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; message too long"); return; }
337 if (!acc.sendFriendRequest(activeContact.info.fraddr, msg)) { conwriteln("address: '", tox_hex(activeContact.info.fraddr), "'; error sending friend request"); return; }
338 } else if (cmd == "status") {
339 string msg = getWord();
340 if (msg.length == 0) { conwriteln("message?"); return; }
341 if (!toxCoreSetStatusMessage(activeContact.acc.toxpk, msg)) { conwriteln("ERROR: cannot set status message"); return; }
342 activeContact.acc.info.statusmsg = msg;
343 try { activeContact.acc.save(); } catch (Exception e) { conwriteln("ERROR saving account: ", e.msg); }
344 } else {
345 addTextToLog(activeContact.acc, activeContact, LogFile.Msg.Kind.Notification, false, "wut?!", systimeNow);
347 } else {
348 activeContact.send(text);
350 } else {
351 // sysedit
352 if (acc !is null && text.length && text[0] == '/') {
353 text = text[1..$];
354 // get command
355 auto cmd = getWord();
356 switch (cmd) {
357 case "friend": // add new friend
358 string addr = getWord();
359 ToxAddr fraddr = decodeAddrStr(addr);
360 if (!isValidAddr(fraddr)) { conwriteln("invalid address: '", addr, "'"); return; }
361 string msg = getWord();
362 if (msg.length == 0) { conwriteln("address: '", addr, "'; please, specify message!"); return; }
363 if (msg.length > tox_max_friend_request_length()) { conwriteln("address: '", addr, "'; message too long"); return; }
364 //if (!toxCoreSendFriendRequest(acc.toxpk, fraddr, msg)) { conwriteln("address: '", addr, "'; error sending friend request");
365 if (!acc.sendFriendRequest(fraddr, msg)) { conwriteln("address: '", addr, "'; error sending friend request"); return; }
366 break;
367 case "status":
368 string msg = getWord();
369 if (msg.length == 0) { conwriteln("message?"); return; }
370 if (!toxCoreSetStatusMessage(acc.toxpk, msg)) { conwriteln("ERROR: cannot set status message"); return; }
371 acc.info.statusmsg = msg;
372 try { acc.save(); } catch (Exception e) { conwriteln("ERROR saving account: ", e.msg); }
373 break;
374 default:
375 conwriteln("unknown command: '", cmd, "'");
376 break;
378 } else {
379 if (text.length) conwriteln("NOT A COMMAND: ", text);
382 glconPostScreenRepaint();
383 return;
386 if (currEdit.onKey(event)) return;
389 int msLastPressX = -666, msLastPressY = -666;
390 uint msDoLogButton = 0; // =0: none; 1: left; 2: middle; 3: right
392 sdmain.handleMouseEvent = delegate (MouseEvent event) {
393 if (sdmain.closed) return;
394 scope(exit) glconPostDoConCommands!true();
395 if (isConsoleVisible) return;
396 mouseX = event.x;
397 mouseY = event.y;
399 // check for click in log
400 //FIXME: process it here, not in renderer
401 if (event == "LMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 1; glconPostScreenRepaint(); }
402 if (event == "MMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 2; glconPostScreenRepaint(); }
403 if (event == "RMB-Down") { msLastPressX = mouseX; msLastPressY = mouseY; msDoLogButton = 3; glconPostScreenRepaint(); }
405 if (clist !is null) {
406 sdmain.setAsCurrentOpenGlContext(); // make this window active
407 scope(exit) sdmain.releaseCurrentOpenGlContext();
409 bool inFrame = nvg.inFrame;
410 if (!inFrame) nvg.beginFrame(sdmain.width, sdmain.height);
411 scope(exit) if (!inFrame) nvg.endFrame();
414 nvg.save();
415 scope(exit) nvg.restore();
416 if (clist.onMouse(event)) return;
420 // log scroll
421 if (layWinHeight > 0) {
422 enum ScrollHeight = 32;
423 if (event == "WheelUp") {
424 layOffset += ScrollHeight;
425 if (layOffset > lay.textHeight-layWinHeight) layOffset = lay.textHeight-layWinHeight;
426 } else if (event == "WheelDown") {
427 layOffset -= ScrollHeight;
429 if (layOffset < 0) layOffset = 0;
432 // don't spam with repaint events
433 if (event.type != MouseEventType.motion) glconPostScreenRepaint();
436 sdmain.handleCharEvent = delegate (dchar ch) {
437 if (sdmain.closed) return;
438 scope(exit) glconPostDoConCommands!true();
439 if (glconCharEvent(ch)) return;
441 if (currEdit.onChar(ch)) return;
444 // draw main screen
445 sdmain.redrawOpenGlScene = delegate () {
446 glconPostDoConCommands!true();
447 if (sdmain.closed) return;
448 sdmain.setAsCurrentOpenGlContext(); // make this window active
449 scope(exit) sdmain.releaseCurrentOpenGlContext();
451 // draw main screen
452 scope(exit) glconDraw();
454 if (nvg is null) return;
456 //oglSetup2D(glconCtlWindow.width, glconCtlWindow.height);
457 glViewport(0, 0, sdmain.width, sdmain.height);
458 glMatrixMode(GL_MODELVIEW);
459 //conwriteln(glconCtlWindow.width, "x", glconCtlWindow.height);
461 glClearColor(0, 0, 0, 0);
462 glClear(glNVGClearFlags/*|GL_COLOR_BUFFER_BIT*/);
465 nvg.beginFrame(sdmain.width, sdmain.height);
466 scope(exit) nvg.endFrame();
468 if (clist !is null && optCListWidth > 0) {
469 // draw contact list
470 int cx = 1;
471 int cy = 1;
472 int wdt = optCListWidth;
473 int hgt = nvg.height-cy*2;
475 nvg.shapeAntiAlias = true;
476 nvg.nonZeroFill;
477 nvg.strokeWidth = 1;
480 nvg.save();
481 scope(exit) nvg.restore();
483 nvg.newPath();
484 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
486 int w, h;
487 nvg.imageSize(nvgSkullsImg, w, h);
488 nvg.fillPaint(nvg.imagePattern(0, 0, w, h, 0, nvgSkullsImg));
490 nvg.strokeColor(NVGColor("#f70"));
491 nvg.fill();
492 nvg.stroke();
494 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt-3*2, hgt-3*2);
496 clist.drawAt(cx+3, cy+3, wdt-3*2, hgt-3*2);
500 nvg.save();
501 scope(exit) nvg.restore();
503 //nvg.transform(NVGMatrix.init);
505 auto xf = nvg.currTransform;
506 nvg.currTransform = xf;
509 // draw chat log
510 cx += wdt+2;
511 wdt = nvg.width-cx-1;
512 auto baphHgt = hgt;
514 // calculate editor dimensions and draw editor
516 nvg.save();
517 scope(exit) nvg.restore();
519 auto edinfo = currEdit.calcHeight(nvg, wdt-3*2);
520 //if (edinfo.height < edinfo.lineh) edinfo.height = edinfo.lineh;
522 int edy = cy+hgt-cast(int)edinfo.height-3*2;
524 nvg.newPath();
525 nvg.roundedRect(cx+0.5f, edy+0.5f, wdt, edinfo.height+3*2, 6);
526 nvg.fillColor(NVGColor.black);
527 nvg.strokeColor(NVGColor("#f70"));
528 nvg.fill();
529 nvg.stroke();
531 nvg.intersectScissor(cx+2.5f, edy+2.5f, wdt-3*2+2, edinfo.height+2);
532 currEdit.draw(nvg, cx+3, edy+3, wdt-3*2, cast(int)edinfo.height);
534 hgt -= cast(int)edinfo.height+3*2;
537 nvg.newPath();
538 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
539 nvg.fillColor(NVGColor.black);
540 nvg.strokeColor(NVGColor("#f70"));
541 nvg.fill();
542 nvg.stroke();
544 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt-3*2, hgt-3*2);
545 version(all) {
546 immutable float scalex = (wdt-3*2-10*2)/BaphometDims;
547 immutable float scaley = (baphHgt-3*2-10*2)/BaphometDims;
548 immutable float scale = (scalex < scaley ? scalex : scaley)/1.5f;
549 immutable float sz = BaphometDims*scale;
550 nvg.strokeColor(NVGColor("#400"));
551 nvg.fillColor(NVGColor("#400"));
552 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);
555 immutable float sbx = cx+wdt-BND_SCROLLBAR_WIDTH-1.5f;
556 immutable float sby = cy+3.5f;
557 wdt -= BND_SCROLLBAR_WIDTH+1;
558 wdt -= 3*2;
559 hgt -= 3*2;
561 lay.relayout(wdt); // this is harmess if width wasn't changed
562 int ty = lay.textHeight-hgt+1-layOffset;
563 if (ty < 0) ty = 0;
564 layWinHeight = cast(int)hgt;
566 if (lay.textHeight > hgt) {
567 float h = lay.textHeight-hgt;
568 nvg.bndScrollBar(sbx, sby, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, ty/h, hgt/h);
569 } else {
570 nvg.bndScrollBar(sbx, sby, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);
573 nvg.intersectScissor(cx+2.5f, cy+2.5f, wdt+2, hgt+2);
574 nvg.drawLayouter(lay, ty, cx+3, cy+3, hgt);
576 if (msDoLogButton) {
577 uint btn = msDoLogButton;
578 msDoLogButton = 0;
579 msLastPressX -= cx+3;
580 msLastPressY -= cy+3;
581 if (msLastPressX >= 0 && msLastPressY >= 0 && msLastPressX < wdt && msLastPressY < hgt) {
582 auto widx = lay.wordAtXY(msLastPressX, ty+msLastPressY);
583 if (widx >= 0) {
584 auto w = lay.wordByIndex(widx);
585 // lmb
586 if (btn == 1) {
587 if (auto hr = cast(uint)widx in layUrlList) {
588 //conwriteln("URL CLICK: <", hr.url, ">");
589 openUrl(hr.url);
592 // rmb
593 if (btn == 3 && w.objectIdx >= 0) {
594 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(w.objectIdx)) {
595 assert(activeContact !is null);
596 activeContact.removeFromResendQueue(maw.msgid, maw.digest, maw.time);
597 maw.msgid = -1;
598 // yeah, repaint
599 glconPostScreenRepaint();
610 flushGui();
611 sdmain.eventLoop(15000,
612 // pulser: process resend queues here
613 delegate () {
614 if (sdmain.closed || clist is null) return;
615 clist.forEachAccount(delegate (Account acc) { acc.processResendQueue(); });
618 clist.forEachAccount(delegate (acc) { acc.forceOnline = false; });
619 clist.forEachAccount(delegate (Account acc) { acc.saveResendQueue(); acc.saveResendQueue(); });
620 toxCoreShutdownAll();
621 popupKillAll();
622 flushGui();
623 conProcessQueue(int.max/4);