command console is visible when activated
[bioacid.git] / bioacid.d
blob677507d074d97d66769e503c678646522cde38d2
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 fonts;
44 import popups;
45 import icondata;
46 import notifyicon;
47 import toxproto;
48 import miniedit;
51 // ////////////////////////////////////////////////////////////////////////// //
52 __gshared bool mainWindowActive = false;
53 __gshared bool mainWindowVisible = false;
56 shared static this () {
57 toxCoreSendEvent = delegate (Object msg) {
58 if (msg is null) return; // just in case
59 try {
60 if (glconCtlWindow is null || glconCtlWindow.closed) return;
61 glconCtlWindow.postEvent(msg);
62 } catch (Exception e) {}
67 // ////////////////////////////////////////////////////////////////////////// //
68 string getBrowserCommand (bool forceOpera=false) {
69 __gshared string browser;
70 if (forceOpera) return "opera";
71 if (browser.length == 0) {
72 import core.stdc.stdlib : getenv;
73 const(char)* evar = getenv("BROWSER");
74 if (evar !is null && evar[0]) {
75 import std.string : fromStringz;
76 browser = evar.fromStringz.idup;
77 } else {
78 browser = "opera";
81 return browser;
85 void openUrl (ConString url, bool forceOpera=false) {
86 if (url.length) {
87 import std.stdio : File;
88 import std.process;
89 try {
90 auto frd = File("/dev/null");
91 auto fwr = File("/dev/null", "w");
92 spawnProcess([getBrowserCommand(forceOpera), url.idup], frd, fwr, fwr, null, Config.detached);
93 } catch (Exception e) {
94 conwriteln("ERROR executing URL viewer (", e.msg, ")");
100 // ////////////////////////////////////////////////////////////////////////// //
101 struct UrlInfo {
102 int pos = -1, len = 0;
104 @property bool valid () const pure nothrow @safe @nogc => (pos >= 0 && len > 0);
105 @property int end () const pure nothrow @safe @nogc => (pos >= 0 && len > 0 ? pos+len : 0);
107 static UrlInfo Invalid () pure nothrow @safe @nogc => UrlInfo.init;
111 UrlInfo urlDetect (const(char)[] text) nothrow @trusted @nogc {
112 import iv.strex;
113 UrlInfo res;
114 auto dlpos = text.indexOf("://");
115 if (dlpos < 3) return res;
117 //{ import core.stdc.stdio; printf("det: <%.*s>\n", cast(uint)text.length, text.ptr); }
119 bool isProto (const(char)[] prt) nothrow @trusted @nogc {
120 if (dlpos < prt.length) return false;
121 if (!strEquCI(prt, text[dlpos-prt.length..dlpos])) return false;
122 // check word boundary
123 if (dlpos == prt.length) return true;
124 return !isalpha(text[dlpos-prt.length-1]);
127 if (isProto("ftp")) res.pos = cast(int)(dlpos-3);
128 else if (isProto("http")) res.pos = cast(int)(dlpos-4);
129 else if (isProto("https")) res.pos = cast(int)(dlpos-5);
130 else return res;
132 dlpos += 3; // skip "://"
134 // skip host name
135 for (; dlpos < text.length; ++dlpos) {
136 char ch = text[dlpos];
137 if (ch == '/') break;
138 if (!(isalnum(ch) || ch == '.' || ch == '-' || ch == ':' || ch == '@')) break;
141 // skip path
142 char[64] brcStack;
143 int brcSP = 0;
144 bool wasSharp = false;
146 for (; dlpos < text.length; ++dlpos) {
147 char ch = text[dlpos];
148 // hash
149 if (ch == '#') {
150 if (wasSharp) break;
151 wasSharp = true;
152 brcSP = 0;
153 continue;
155 // opening bracket
156 if (ch == '(' || ch == '[' || ch == '{') {
157 final switch (ch) {
158 case '(': ch = ')'; break;
159 case '[': ch = ']'; break;
160 case '{': ch = '}'; break;
162 if (brcSP < brcStack.length) brcStack[brcSP++] = ch;
163 continue;
165 // closing bracket
166 if (ch == ')' || ch == ']' || ch == '}') {
167 if (brcSP == 0 || brcStack[brcSP-1] != ch) break;
168 --brcSP;
169 continue;
171 if (ch == '.') {
172 if (brcSP == 0) {
173 if (dlpos == text.length || (!isalnum(text[dlpos+1]) && text[dlpos+1] != '_')) break;
175 continue;
177 if (ch <= ' ' || ch >= 127) break;
180 res.len = cast(int)(dlpos-res.pos);
182 return res;
186 // ////////////////////////////////////////////////////////////////////////// //
187 alias LayTextClass = LayTextD;
190 // ////////////////////////////////////////////////////////////////////////// //
191 __gshared string accBaseDir = ".";
193 __gshared NVGContext nvg = null;
194 __gshared NVGImage nvgSkullsImg;
195 __gshared NVGImage kittyOut, kittyMsg;
197 __gshared LayTextClass lay;
198 __gshared int layWinHeight = 0;
199 __gshared int layOffset = 0; // from bottom
201 __gshared int optCListWidth = -1;
202 __gshared int lastWindowWidth = -1;
204 __gshared NVGImage[5] statusImgId;
207 shared static ~this () {
208 //{ import core.stdc.stdio; printf("******************************\n"); }
209 nvgSkullsImg.clear();
210 //{ import core.stdc.stdio; printf("---\n"); }
211 foreach (ref img; statusImgId[]) img.clear();
212 kittyOut.clear();
213 kittyMsg.clear();
217 void buildStatusImages () {
218 version(none) {
219 statusImgId[ContactStatus.Offline] = nvg.createImageRGBA(16, 16, ctiOffline[], NVGImageFlags.NoFiltering);
220 statusImgId[ContactStatus.Online] = nvg.createImageRGBA(16, 16, ctiOnline[], NVGImageFlags.NoFiltering);
221 statusImgId[ContactStatus.Away] = nvg.createImageRGBA(16, 16, ctiAway[], NVGImageFlags.NoFiltering);
222 statusImgId[ContactStatus.Connecting] = nvg.createImageRGBA(16, 16, ctiOffline[], NVGImageFlags.NoFiltering);
223 } else {
224 //{ import core.stdc.stdio; printf("creating status image: Offline\n"); }
225 statusImgId[ContactStatus.Offline] = nvg.createImageRGBA(16, 16, baph16Gray[], NVGImageFlags.NoFiltering);
226 //{ import core.stdc.stdio; printf("creating status image: Online\n"); }
227 statusImgId[ContactStatus.Online] = nvg.createImageRGBA(16, 16, baph16Online[], NVGImageFlags.NoFiltering);
228 //{ import core.stdc.stdio; printf("creating status image: Away\n"); }
229 statusImgId[ContactStatus.Away] = nvg.createImageRGBA(16, 16, baph16Away[], NVGImageFlags.NoFiltering);
230 //{ import core.stdc.stdio; printf("creating status image: Busy\n"); }
231 statusImgId[ContactStatus.Busy] = nvg.createImageRGBA(16, 16, baph16Busy[], NVGImageFlags.NoFiltering);
232 //{ import core.stdc.stdio; printf("creating status image: Connecting\n"); }
233 statusImgId[ContactStatus.Connecting] = nvg.createImageRGBA(16, 16, baph16Orange[], NVGImageFlags.NoFiltering);
234 //{ import core.stdc.stdio; printf("+++ creatied status images...\n"); }
235 kittyOut = nvg.createImageRGBA(16, 16, kittyOutgoing[], NVGImageFlags.NoFiltering);
236 kittyMsg = nvg.createImageRGBA(16, 16, kittyMessage[], NVGImageFlags.NoFiltering);
241 // ////////////////////////////////////////////////////////////////////////// //
242 void loadFonts (NVGContext vg) {
243 vg.fonsContext.fonsAddStashFonts(fstash);
244 bndSetFont(vg.findFont("ui"));
248 // ////////////////////////////////////////////////////////////////////////// //
249 class MessageStart : LayObject {
250 long msgid; // >0: outgoing, unacked yet
252 this (long aid=-1) nothrow @safe @nogc { msgid = aid; }
254 override int width () => 0;
255 override int spacewidth () => 0;
256 override int height () => 0;
257 override int ascent () => 0;
258 override int descent () => 0;
259 override bool canbreak () => true;
260 override bool spaced () => false;
261 // y is at baseline
262 override void draw (NVGContext ctx, float x, float y) {}
266 class MessageOutMark : LayObject {
267 long msgid; // >0: outgoing, unacked yet
269 this (long aid=-1) nothrow @safe @nogc { msgid = aid; }
271 override int width () => kittyOut.width;
272 override int spacewidth () => 4;
273 override int height () => kittyOut.height;
274 override int ascent () => height;
275 override int descent () => 0;
276 override bool canbreak () => true;
277 override bool spaced () => false;
278 // y is at baseline
279 override void draw (NVGContext ctx, float x, float y) {
280 if (msgid > 0) {
281 nvg.save();
282 scope(exit) nvg.restore;
283 nvg.newPath();
284 nvg.rect(x+0.5, y+0.5, width, height);
285 nvg.fillPaint(nvg.imagePattern(0, 0, width, height, 0, kittyOut));
286 nvg.fill();
292 // ////////////////////////////////////////////////////////////////////////// //
293 static struct LayUrl {
294 string url;
295 uint wordidx; // first word
299 __gshared LayUrl[uint] layUrlList; // for each word
302 void wipeLog () {
303 lay.wipeAll(true); // clear log, but delete objects
304 layUrlList.clear();
305 layOffset = 0;
309 void addDividerLine (bool doflushgui=false) {
310 if (glconCtlWindow is null || glconCtlWindow.closed) return;
313 bool inFrame = nvg.inFrame;
314 if (!inFrame) {
315 glconCtlWindow.setAsCurrentOpenGlContext(); // make this window active
316 nvg.beginFrame(glconCtlWindow.width, glconCtlWindow.height);
318 scope(exit) {
319 if (!inFrame) {
320 nvg.endFrame();
321 if (doflushgui) flushGui();
322 glconCtlWindow.releaseCurrentOpenGlContext();
326 lay.fontStyle.fontsize = 2;
327 lay.fontStyle.color = NVGColor.k8orange.asUint;
328 lay.fontStyle.bgcolor = NVGColor("#aa0").asUint;
329 lay.fontStyle.monospace = true;
331 lay.putExpander();
332 lay.endPara();
333 lay.finalize();
335 // redraw
336 glconPostScreenRepaint();
340 // `ct` can be `null` for "my message" or "system message"
341 void addTextToLog (Account acc, Contact ct, LogFile.Msg.Kind kind, bool action, const(char)[] msg, SysTime time, long msgid=-1, bool doflushgui=false) {
342 if (glconCtlWindow is null || glconCtlWindow.closed) return;
345 bool inFrame = nvg.inFrame;
346 if (!inFrame) {
347 glconCtlWindow.setAsCurrentOpenGlContext(); // make this window active
348 nvg.beginFrame(glconCtlWindow.width, glconCtlWindow.height);
350 scope(exit) {
351 if (!inFrame) {
352 nvg.endFrame();
353 if (doflushgui) flushGui();
354 glconCtlWindow.releaseCurrentOpenGlContext();
358 // add "message start" mark
359 lay.putObject(new MessageStart(msgid));
361 lay.fontStyle.fontsize = 16;
362 lay.fontStyle.color = NVGColor.k8orange.asUint;
363 lay.fontStyle.bgcolor = NVGColor("#222").asUint;
364 lay.fontStyle.monospace = true;
366 NVGColor textColor;
368 final switch (kind) {
369 case LogFile.Msg.Kind.Outgoing: textColor = NVGColor.k8orange; lay.fontStyle.color = NVGColor("#c40").asUint; lay.put(acc.info.nick); break;
370 case LogFile.Msg.Kind.Incoming: textColor = NVGColor("#ccc"); lay.fontStyle.color = NVGColor("#666").asUint; lay.put(ct.info.nick); break;
371 case LogFile.Msg.Kind.Notification: textColor = NVGColor("#0c0"); lay.fontStyle.color = textColor.asUint; lay.put("*system*"); break;
374 lay.fontStyle.monospace = false;
375 //lay.putHardSpace(64);
376 lay.putExpander();
377 // add "message outgoing" mark
378 if (kind == LogFile.Msg.Kind.Outgoing && msgid > 0) {
379 //lay.put("!");
380 //conwriteln("msgoutmark");
381 lay.putObject(new MessageOutMark(msgid));
382 lay.putExpander();
386 import std.datetime;
387 import std.format : format;
388 auto dt = cast(DateTime)time;
389 string tstr = "%04u/%02u/%02u".format(dt.year, dt.month, dt.day);
390 lay.put(tstr);
391 lay.putNBSP();
394 lay.fontStyle.color = NVGColor("#aaa").asUint;
395 lay.fontStyle.bgcolor = NVGColor("#006").asUint;
397 import std.datetime;
398 import std.format : format;
399 auto dt = cast(DateTime)time;
400 string tstr = "%02u:%02u:%02u".format(dt.hour, dt.minute, dt.second);
401 lay.put(tstr);
402 lay.putNBSP();
404 lay.endPara();
406 lay.fontStyle.bgcolor = NVGColor.transparent.asUint;
407 lay.fontStyle.fontsize = 20;
409 if (action) {
410 lay.fontStyle.color = NVGColor("#fff").asUint;
411 lay.put("/me ");
414 void xput (const(char)[] str) {
415 while (str.length) {
416 auto nl = str.indexOf('\n');
417 if (nl < 0) { lay.put(str); break; }
418 if (nl > 0) lay.put(str[0..nl]);
419 lay.endPara();
420 str = str[nl+1..$];
424 msg = msg.xstripright;
426 lay.fontStyle.color = textColor.asUint;
427 while (msg.length) {
428 auto nfo = urlDetect(msg);
429 if (!nfo.valid) {
430 xput(msg);
431 break;
433 // url found
434 xput(msg[0..nfo.pos]);
435 string url = msg[nfo.pos..nfo.end].idup;
436 msg = msg[nfo.end..$];
437 auto stword = lay.nextWordIndex;
438 lay.pushStyles();
439 auto c = lay.fontStyle.color;
440 scope(exit) { lay.popStyles; lay.fontStyle.color = c; }
441 lay.fontStyle.href = true;
442 lay.fontStyle.underline = true;
443 lay.fontStyle.color = NVGColor("#06f").asUint;
444 lay.put(url);
445 while (stword < lay.nextWordIndex) {
446 layUrlList[stword] = LayUrl(url, stword);
447 ++stword;
451 lay.endPara();
452 lay.finalize();
454 // redraw
455 glconPostScreenRepaint();
459 void addTextToLog (Account acc, Contact ct, in ref LogFile.Msg msg, long msgid=-1, bool doflushgui=false) {
460 import iv.utfutil : utf8Encode;
461 char[] text;
462 text.reserve(4096);
463 scope(exit) delete text;
464 // decode text
465 foreach (dchar dc; msg.byDChar) {
466 char[4] buf = void;
467 auto len = utf8Encode(buf[], dc);
468 text ~= buf[0..len];
470 addTextToLog(acc, ct, msg.kind, msg.isMe, text, msg.time, msgid, doflushgui);
474 void ackLogMessage (long msgid) {
475 if (msgid <= 0) return;
476 foreach (immutable uint widx; 0..lay.wordCount) {
477 auto w = lay.wordByIndex(widx);
478 int oidx = w.objectIdx;
479 if (oidx >= 0) {
480 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(oidx)) {
481 if (maw.msgid == msgid) {
482 maw.msgid = -1; // reset mark
483 glconPostScreenRepaint(); // redraw
491 // ////////////////////////////////////////////////////////////////////////// //
492 final class Group {
493 private:
494 this (Account aOwner) { acc = aOwner; }
496 private:
497 bool mDirty; // true: write contact's config
498 string diskFileName;
500 public:
501 Account acc;
502 GroupOptions info;
504 public:
505 void markDirty () pure nothrow @safe @nogc => mDirty = true;
507 void save () {
508 import std.file : mkdirRecurse;
509 import std.path : dirName;
510 // save this contact
511 assert(acc !is null);
512 // create disk name
513 if (diskFileName.length == 0) {
514 diskFileName = acc.basePath~"/contacts/groups.rc";
516 mkdirRecurse(diskFileName.dirName);
517 if (serialize(info, diskFileName)) mDirty = false;
520 @property bool visible () const nothrow @trusted @nogc {
521 if (!hideIfNoVisibleMembers) return true; // always visible
522 // check if we have any visible members
523 foreach (const(Contact) c; acc.contacts.byValue) {
524 if (c.gid != info.gid) continue;
525 if (c.visibleNoGroupCheck) return true;
527 return false; // nobody's here
530 private bool getTriOpt(string fld, string fld2=null) () const nothrow @trusted @nogc {
531 enum lo = "info."~fld;
532 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
533 static if (fld2.length) return mixin("acc.info."~fld2); else return mixin("acc.info."~fld);
536 private int getIntOpt(string fld) () const nothrow @trusted @nogc {
537 enum lo = "info."~fld;
538 if (mixin(lo) >= 0) return mixin(lo);
539 return mixin("acc.info."~fld);
542 @property nothrow @safe {
543 uint gid () const pure @nogc => info.gid;
545 bool opened () const @nogc => info.opened;
546 void opened (bool v) @nogc { pragma(inline, true); if (info.opened != v) { info.opened = v; markDirty(); } }
548 string name () const @nogc => info.name;
549 void name (string v) @nogc { pragma(inline, true); if (v.length == 0) v = "<unnamed>"; if (info.name != v) { info.name = v; markDirty(); } }
551 string note () const @nogc => info.note;
552 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
554 @nogc {
555 bool showOffline () const => getTriOpt!"showOffline";
556 bool showPopup () const => getTriOpt!"showPopup";
557 bool blinkActivity () const => getTriOpt!"blinkActivity";
558 bool skipUnread () const => getTriOpt!"skipUnread";
559 bool hideIfNoVisibleMembers () const => getTriOpt!("hideIfNoVisibleMembers", "hideEmptyGroups");
560 bool ftranAllowed () const => getTriOpt!"ftranAllowed";
561 int resendRotDays () const => getIntOpt!"resendRotDays";
562 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
568 // ////////////////////////////////////////////////////////////////////////// //
569 final class Contact {
570 public:
571 // `Connecting` for non-account means "awaiting authorization"
573 static struct XMsg {
574 bool isMe; // "/me" message?
575 SysTime time;
576 string text;
577 long msgid; // ==0: unknown yet
578 int resendCount = 0;
580 MonoTime nextSendTime;
583 private:
584 this (Account aOwner) { acc = aOwner; edit = new MiniEdit(); }
586 private:
587 bool mDirty; // true: write contact's config
589 public:
590 Account acc;
591 string diskFileName;
592 ContactInfo info;
593 ContactStatus status = ContactStatus.Offline; // not saved, so if it safe to change it
594 MiniEdit edit;
595 XMsg[] resendQueue;
596 int unreadCount;
598 public:
599 void markDirty () pure nothrow @safe @nogc => mDirty = true;
601 void loadUnreadCount () nothrow {
602 assert(diskFileName.length);
603 assert(acc !is null);
604 try {
605 import std.path : dirName;
606 auto fi = VFile(diskFileName.dirName~"/logs/unread.dat");
607 unreadCount = fi.readNum!int;
608 } catch (Exception e) {
609 unreadCount = 0;
613 void saveUnreadCount () nothrow {
614 assert(diskFileName.length);
615 assert(acc !is null);
616 try {
617 import std.path : dirName;
618 auto fo = VFile(diskFileName.dirName~"/logs/unread.dat", "w");
619 fo.writeNum(unreadCount);
620 } catch (Exception e) {
624 void loadResendQueue () {
625 import std.path : dirName;
626 string fname = diskFileName.dirName~"/logs/resend.log";
627 LogFile lf;
628 lf.load(fname);
629 auto ctt = MonoTime.currTime;
630 foreach (const ref lmsg; lf.messages) {
631 XMsg xmsg;
632 xmsg.isMe = lmsg.isMe;
633 xmsg.time = lmsg.time;
634 xmsg.text = lmsg.text;
635 xmsg.msgid = 0;
636 xmsg.nextSendTime = ctt;
637 resendQueue ~= xmsg;
641 void saveResendQueue () {
642 import std.file : mkdirRecurse, remove;
643 import std.path : dirName;
644 assert(diskFileName.length);
645 assert(acc !is null);
646 mkdirRecurse(diskFileName.dirName~"/logs");
647 string fname = diskFileName.dirName~"/logs/resend.log";
648 try { remove(fname); } catch (Exception e) {}
649 if (resendQueue.length) {
650 foreach (const ref msg; resendQueue) {
651 LogFile.appendLine(fname, LogFile.Msg.Kind.Outgoing, msg.text, msg.isMe, msg.time);
656 void save () {
657 import std.file : mkdirRecurse;
658 import std.path : dirName;
659 // save this contact
660 assert(acc !is null);
661 // create disk name
662 if (diskFileName.length == 0) {
663 diskFileName = acc.basePath~"/contacts/"~tox_hex(info.pubkey[])~"/config.rc";
664 acc.contacts[info.pubkey] = this;
666 mkdirRecurse(diskFileName.dirName);
667 mkdirRecurse(diskFileName.dirName~"/avatars");
668 mkdirRecurse(diskFileName.dirName~"/files");
669 mkdirRecurse(diskFileName.dirName~"/fileparts");
670 mkdirRecurse(diskFileName.dirName~"/logs");
671 saveUnreadCount();
672 if (serialize(info, diskFileName)) mDirty = false;
675 public:
676 @property bool visibleNoGroupCheck () const nothrow @trusted @nogc => (showOffline || status != ContactStatus.Offline || unreadCount > 0);
678 @property bool visible () const nothrow @trusted @nogc {
679 if (unreadCount > 0) return true;
680 if (!showOffline && status == ContactStatus.Offline) return false;
681 auto grp = acc.groupById(gid);
682 return grp.visible;
685 @property nothrow @safe {
686 ContactInfo.Kind kind () const @nogc => info.kind;
687 void kind (ContactInfo.Kind v) @nogc { pragma(inline, true); if (info.kind != v) { info.kind = v; markDirty(); } }
689 uint gid () const @nogc => info.gid;
690 void gid (uint v) @nogc { pragma(inline, true); if (info.gid != v) { info.gid = v; markDirty(); } }
692 string nick () const @nogc => info.nick;
693 void nick (string v) @nogc { pragma(inline, true); if (v.length == 0) v = "<unknown>"; if (info.nick != v) { info.nick = v; markDirty(); } }
695 string visnick () const @nogc => info.visnick;
696 void visnick (string v) @nogc { pragma(inline, true); if (info.visnick != v) { info.visnick = v; markDirty(); } }
698 string displayNick () const @nogc => (info.visnick.length ? info.visnick : info.nick);
700 string statusmsg () const @nogc => info.statusmsg;
701 void statusmsg (string v) @nogc { pragma(inline, true); if (info.statusmsg != v) { info.statusmsg = v; markDirty(); } }
703 void setLastOnlineNow () {
704 try {
705 auto ut = Clock.currTime.toUTC().toUnixTime();
706 if (info.lastonlinetime != cast(uint)ut) { info.lastonlinetime = cast(uint)ut; markDirty(); }
707 } catch (Exception e) {}
710 string note () const @nogc => info.note;
711 void note (string v) @nogc { pragma(inline, true); if (info.note != v) { info.note = v; markDirty(); } }
714 private bool getTriOpt(string fld) () const nothrow @trusted @nogc {
715 enum lo = "info.opts."~fld;
716 if (mixin(lo) != TriOption.Default) return (mixin(lo) == TriOption.Yes);
717 auto grp = acc.groupById(info.gid);
718 enum go = "grp.info."~fld;
719 if (mixin(go) != TriOption.Default) return (mixin(go) == TriOption.Yes);
720 return mixin("acc.info."~fld);
723 private int getIntOpt(string fld) () const nothrow @trusted @nogc {
724 enum lo = "info.opts."~fld;
725 if (mixin(lo) >= 0) return mixin(lo);
726 auto grp = acc.groupById(info.gid);
727 enum go = "grp.info."~fld;
728 if (mixin(go) >= 0) return mixin(go);
729 return mixin("acc.info."~fld);
732 @property nothrow @safe @nogc {
733 bool showOffline () const => getTriOpt!"showOffline";
734 bool showPopup () const => getTriOpt!"showPopup";
735 bool blinkActivity () const => getTriOpt!"blinkActivity";
736 bool skipUnread () const => getTriOpt!"skipUnread";
737 bool ftranAllowed () const => getTriOpt!"ftranAllowed";
738 int resendRotDays () const => getIntOpt!"resendRotDays";
739 int hmcOnOpen () const => getIntOpt!"hmcOnOpen";
742 void loadLogInto (ref LogFile lf) {
743 import std.file : exists;
744 import std.path : dirName;
745 string lname = diskFileName.dirName~"/logs/hugelog.log";
746 if (lname.exists) lf.load(lname); else lf.clear();
749 void appendToLog (LogFile.Msg.Kind kind, const(char)[] text, bool isMe, SysTime time) {
750 import std.path : dirName;
751 string lname = diskFileName.dirName~"/logs/hugelog.log";
752 LogFile.appendLine(lname, kind, text, isMe, time);
755 void ackReceived (long msgid) {
756 if (msgid <= 0) return; // wtf?!
757 bool changed = false;
758 usize idx = 0;
759 while (idx < resendQueue.length) {
760 if (resendQueue[idx].msgid == msgid) {
761 foreach (immutable c; idx+1..resendQueue.length) resendQueue[c-1] = resendQueue[c];
762 resendQueue[$-1] = XMsg.init;
763 resendQueue.length -= 1;
764 resendQueue.assumeSafeAppend;
765 changed = true;
766 } else {
767 ++idx;
770 if (changed) saveResendQueue();
773 void processResendQueue () {
774 if (status == ContactStatus.Offline || status == ContactStatus.Connecting) return;
775 bool doSave = false;
776 auto ctt = MonoTime.currTime+30.seconds;
777 foreach (ref XMsg msg; resendQueue) {
778 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, msg.text, msg.isMe);
779 if (msgid < 0) break;
780 msg.msgid = msgid;
781 msg.nextSendTime = ctt;
782 if (msg.resendCount++ != 0) doSave = true;
784 if (doSave) saveResendQueue();
787 void send (const(char)[] text) {
788 void sendOne (const(char)[] text, bool action) {
789 if (text.length == 0) return; // just in case
791 SysTime now = Clock.currTime;
792 long msgid = toxCoreSendMessage(acc.toxpk, info.pubkey, text, action);
793 if (msgid < 0) { conwriteln("ERROR sending message to '", info.nick, "'"); return; }
795 // add this to resend queue
796 XMsg xmsg;
797 xmsg.isMe = action;
798 xmsg.time = now;
799 xmsg.msgid = msgid;
800 xmsg.nextSendTime = MonoTime.currTime+30.seconds;
801 xmsg.resendCount = 0;
804 import std.datetime;
805 import std.format : format;
806 auto dt = cast(DateTime)now;
807 xmsg.text = "[%04u/%02u/%02u %02u:%02u:%02u] %s".format(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, text);
810 resendQueue ~= xmsg;
812 if (activeContact is this) addTextToLog(acc, this, LogFile.Msg.Kind.Outgoing, action, text, now, msgid);
813 appendToLog(LogFile.Msg.Kind.Outgoing, text, action, now);
816 bool action = text.startsWith("/me ");
817 if (action) text = text[4..$].xstripleft;
819 while (text.length) {
820 auto ep = text.indexOf('\n');
821 if (ep < 0) ep = text.length; else ++ep; // include '\n'
822 // remove line if it contains only spaces
823 bool hasNonSpace = false;
824 foreach (immutable char ch; text[0..ep]) if (ch > ' ') { hasNonSpace = true; break; }
825 if (hasNonSpace) break;
826 text = text[ep..$];
828 while (text.length && text[$-1] <= ' ') text = text[0..$-1];
829 if (text.length == 0) return; // nothing to do
831 // now split it
832 //TODO: split at word boundaries
833 enum ReservedSpace = 23+3+3;
835 // k8: toxcore developers are idiots, so we have to do dynalloc here
836 auto tmpbuf = new char[](tox_max_message_length()+64);
837 scope(exit) delete tmpbuf;
839 bool first = true;
840 while (text.length) {
841 int epos = tox_max_message_length()-ReservedSpace;
842 if (epos < text.length) {
843 // find utf start
844 while (epos > 0) {
845 if (text[epos-1] < 128) break;
846 if ((text[epos-1]&0xc0) == 0xc0) break;
847 --epos;
849 } else {
850 epos = cast(int)text.length;
852 assert(epos > 0);
853 if (first && epos >= text.length) {
854 sendOne(text[0..epos], action);
855 } else {
856 int ofs = 0;
857 if (!first) { tmpbuf[0..3] = "..."; ofs = 3; }
858 tmpbuf[ofs..ofs+epos] = text[0..epos];
859 tmpbuf[ofs+epos..ofs+epos+3] = "...";
860 sendOne(tmpbuf[0..ofs+epos+3], action);
862 first = false;
863 text = text[epos..$];
866 //saveResendQueue();
871 // ////////////////////////////////////////////////////////////////////////// //
872 final class Account {
873 public:
874 void saveGroups () {
875 import std.algorithm : sort;
876 GroupOptions[] glist;
877 scope(exit) delete glist;
878 foreach (Group g; groups) glist ~= g.info;
879 glist.sort!((in ref a, in ref b) => a.gid < b.gid);
880 glist.serialize(basePath~"contacts/groups.rc");
883 private:
884 ContactStatus mStatus = ContactStatus.Offline;
886 public:
887 PubKey toxpk = toxCoreEmptyKey;
888 string toxDataDiskName;
889 string basePath; // with trailing "/"
890 ProtoOptions protoOpts;
891 AccountConfig info;
892 Group[] groups;
893 Contact[PubKey] contacts;
895 public:
896 bool mIAmConnecting = false;
897 bool mIAmOnline = false;
898 bool forceOnline = true; // set to `false` to stop autoreconnecting
899 //bool restoreOnline = false; // will be set to `true` if we need to reconnect
900 //bool doRefreshNicks = false;
902 public:
903 @property bool isConnecting () const nothrow @safe @nogc => mIAmConnecting;
905 @property ContactStatus status () const nothrow @safe @nogc {
906 if (!toxpk.isValidKey) return ContactStatus.Offline;
907 if (mIAmConnecting) return ContactStatus.Connecting;
908 if (!mIAmOnline) return ContactStatus.Offline;
909 return mStatus;
912 @property bool isOnline () const nothrow @safe @nogc {
913 if (!toxpk.isValidKey) return false;
914 if (mIAmConnecting) return false;
915 if (!mIAmOnline) return false;
916 return true;
919 @property void status (ContactStatus v) {
920 if (!toxpk.isValidKey) return;
921 conwriteln("changing status to ", v, " (old: ", mStatus, ")");
922 if (v == ContactStatus.Connecting) v = ContactStatus.Online;
923 if (v == ContactStatus.Offline) {
924 forceOnline = false;
925 } else {
926 forceOnline = true;
928 if (mStatus == ContactStatus.Offline) {
929 if (v != ContactStatus.Offline) mIAmConnecting = true;
931 toxCoreSetStatus(toxpk, v);
932 mStatus = v;
933 glconPostScreenRepaint();
935 fixTrayIcon();
938 void processResendQueue () {
939 if (!isOnline) return;
940 foreach (Contact ct; contacts.byValue) ct.processResendQueue();
943 void saveResendQueue () {
944 foreach (Contact ct; contacts.byValue) ct.saveResendQueue();
947 private:
948 void toxCreate () {
949 toxpk = toxCoreOpenAccount(toxDataDiskName);
950 if (!toxpk.isValidKey) {
951 conwriteln("creating new Tox account...");
952 string nick = info.nick;
953 if (nick.length > 0) {
954 //FIXME: utf
955 if (nick.length > tox_max_name_length()) nick = nick[0..tox_max_name_length()];
956 } else {
957 nick = "anonymous";
959 toxpk = toxCoreCreateAccount(toxDataDiskName, nick);
960 if (!toxpk.isValidKey) throw new Exception("cannot create Tox account");
964 // load contacts from ToxCore data and add 'em to contact database
965 void toxLoadKnownContacts () {
966 if (!toxpk.isValidKey) return;
967 toxCoreForEachFriend(toxpk, delegate (in ref PubKey self, in ref PubKey frpub, scope const(char)[] nick) {
968 if (nick.length == 0) nick = "<unknown>";
969 auto c = (frpub in contacts ? contacts[frpub] : null);
970 if (c is null) {
971 conwriteln("NEW friend with pk [", tox_hex(frpub), "]; name is: ", nick);
972 c = new Contact(this);
973 c.info.gid = 0;
974 c.info.nick = nick.idup;
975 c.info.pubkey[] = frpub[];
976 c.info.opts.showOffline = TriOption.Yes;
977 contacts[c.info.pubkey] = c;
978 c.save();
979 //HACK!
980 if (clist !is null) clist.buildAccount(this);
981 } else if (c.info.nick != nick) {
982 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; new name is: ", nick);
983 if (nick != "<unknown>" && (c.info.nick == "<unknown>" || c.info.nick.length == 0)) {
984 c.info.nick = nick.idup;
985 c.save();
987 } else {
988 conwriteln("OLD friend with pk [", tox_hex(frpub), "]; old name is: ", nick);
990 return false; // don't stop
994 private:
995 // connection established
996 void toxConnectionDropped () {
997 alias timp = this;
998 conprintfln("TOX[%s] CONNECTION DROPPED", timp.srvalias);
999 mIAmConnecting = false;
1000 mIAmOnline = false;
1001 if (forceOnline) {
1002 status = mStatus;
1003 mIAmConnecting = true;
1005 foreach (Contact ct; contacts.byValue) ct.status = ContactStatus.Offline;
1006 glconPostScreenRepaint();
1009 // connection established
1010 void toxConnectionEstablished () {
1011 alias timp = this;
1012 conprintfln("TOX[%s] CONNECTION ESTABLISHED", timp.srvalias);
1013 mIAmConnecting = false;
1014 mIAmOnline = true;
1015 toxCoreSetStatusMessage(toxpk, "Come taste the gasoline! [BioAcid]");
1016 toxLoadKnownContacts();
1017 glconPostScreenRepaint();
1020 void toxFriendOffline (in ref PubKey fpk) {
1021 if (auto ct = fpk in contacts) {
1022 if (ct.status != ContactStatus.Offline) {
1023 conwriteln("friend <", ct.info.nick, "> gone offline");
1024 ct.status = ContactStatus.Offline;
1025 glconPostScreenRepaint();
1030 void toxSelfStatus (ContactStatus cst) {
1031 if (mStatus != cst) {
1032 mStatus = cst;
1033 glconPostScreenRepaint();
1037 void toxFriendStatus (in ref PubKey fpk, ContactStatus cst) {
1038 if (auto ct = fpk in contacts) {
1039 if (ct.status != cst) {
1040 conwriteln("status for friend <", ct.info.nick, "> changed to ", cst);
1041 ct.status = cst;
1042 //if (ct.status != ContactStatus.Offline && ct.status != ContactStatus.Connecting) ct.processResendQueue();
1043 glconPostScreenRepaint();
1048 void toxFriendStatusMessage (in ref PubKey fpk, string msg) {
1049 if (auto ct = fpk in contacts) {
1050 if (ct.info.statusmsg != msg) {
1051 conwriteln("status message for friend <", ct.info.nick, "> changed to <", msg, ">");
1052 ct.info.statusmsg = msg;
1053 ct.save();
1054 glconPostScreenRepaint();
1059 void toxFriendReqest (in ref PubKey fpk, const(char)[] msg) {
1060 alias timp = this;
1061 conprintfln("TOX[%s] FRIEND REQUEST FROM [%s]: <%s>", timp.srvalias, tox_hex(fpk[]), msg);
1064 void toxFriendMessage (in ref PubKey fpk, bool action, string msg, SysTime time) {
1065 if (auto ct = fpk in contacts) {
1066 LogFile.Msg.Kind kind = LogFile.Msg.Kind.Incoming;
1067 ct.appendToLog(kind, msg, action, time);
1068 if (*ct is activeContact) {
1069 // if inactive or invisible, add divider line and increase unread count
1070 if (!mainWindowVisible || !mainWindowActive) {
1071 if (ct.unreadCount == 0) addDividerLine();
1072 ct.unreadCount += 1;
1073 ct.saveUnreadCount();
1075 addTextToLog(this, *ct, kind, action, msg, time);
1076 } else {
1077 ct.unreadCount += 1;
1078 ct.saveUnreadCount();
1083 // ack for sent message
1084 void toxMessageAck (in ref PubKey fpk, long msgid) {
1085 alias timp = this;
1086 conprintfln("TOX[%s] ACK MESSAGE FROM [%s]: %u", timp.srvalias, tox_hex(fpk[]), msgid);
1087 if (auto ct = fpk in contacts) {
1088 if (*ct is activeContact) ackLogMessage(msgid);
1089 ct.ackReceived(msgid);
1093 private:
1094 @property string srvalias () const pure nothrow @safe @nogc => info.nick;
1096 public:
1097 this (string aBaseDir) {
1098 import std.algorithm : sort;
1099 import std.file : DirEntry, SpanMode, dirEntries;
1100 import std.path : absolutePath, baseName;
1102 if (aBaseDir.length == 0 || aBaseDir == "." || aBaseDir == "./") {
1103 aBaseDir = "./";
1104 } else if (aBaseDir == "/") {
1105 assert(0, "wtf?!");
1106 } else {
1107 if (aBaseDir[$-1] != '/') aBaseDir ~= '/';
1110 basePath = aBaseDir.absolutePath;
1111 toxDataDiskName = basePath~"toxdata.tox";
1112 protoOpts.txtunser(VFile(basePath~"proto.rc"));
1113 info.txtunser(VFile(basePath~"config.rc"));
1115 // load groups
1116 GroupOptions[] glist;
1117 glist.txtunser(VFile(basePath~"contacts/groups.rc"));
1118 bool hasDefaultGroup = false;
1119 bool hasMoronsGroup = false;
1120 foreach (ref GroupOptions gi; glist[]) {
1121 auto g = new Group(this);
1122 g.info = gi;
1123 bool found = false;
1124 foreach (ref gg; groups[]) if (gg.gid == g.gid) { delete gg; gg = g; found = true; }
1125 if (!found) groups ~= g;
1126 if (g.gid == 0) hasDefaultGroup = true;
1127 if (g.gid == g.gid.max) hasMoronsGroup = true;
1130 // create default group if necessary
1131 if (!hasDefaultGroup) {
1132 GroupOptions gi;
1133 gi.gid = 0;
1134 gi.name = "default";
1135 gi.note = "default group for new contacts";
1136 gi.opened = true;
1137 auto g = new Group(this);
1138 g.info = gi;
1139 groups ~= g;
1142 // create morons group if necessary
1143 if (!hasMoronsGroup) {
1144 GroupOptions gi;
1145 gi.gid = gi.gid.max;
1146 gi.name = "<morons>";
1147 gi.note = "group for completely ignored dumbfucks";
1148 gi.opened = false;
1149 gi.showOffline = TriOption.No;
1150 gi.showPopup = TriOption.No;
1151 gi.blinkActivity = TriOption.No;
1152 gi.skipUnread = TriOption.Yes;
1153 gi.hideIfNoVisibleMembers = TriOption.Yes;
1154 gi.ftranAllowed = TriOption.No;
1155 gi.resendRotDays = 0;
1156 gi.hmcOnOpen = 0;
1157 auto g = new Group(this);
1158 g.info = gi;
1159 groups ~= g;
1160 saveGroups();
1163 groups.sort!((in ref a, in ref b) => a.gid < b.gid);
1165 if (!hasDefaultGroup || !hasMoronsGroup) saveGroups();
1167 // load contacts
1168 foreach (DirEntry de; dirEntries(basePath~"contacts", SpanMode.shallow)) {
1169 if (de.name.baseName == "." || de.name.baseName == "..") continue;
1170 try {
1171 import std.file : exists;
1172 if (!de.isDir) continue;
1173 string cfgfn = de.name~"/config.rc";
1174 if (!cfgfn.exists) continue;
1175 ContactInfo ci;
1176 ci.txtunser(VFile(cfgfn));
1177 auto c = new Contact(this);
1178 c.diskFileName = cfgfn;
1179 c.info = ci;
1180 contacts[c.info.pubkey] = c;
1181 c.loadResendQueue();
1182 c.loadUnreadCount();
1183 // fix contact group
1184 if (groupById!false(c.gid) is null) {
1185 c.info.gid = 0; // move to default group
1186 c.save();
1188 //conwriteln("loaded contact [", c.info.nick, "] from '", c.diskFileName, "'");
1189 } catch (Exception e) {
1190 conwriteln("ERROR loading contact from '", de.name, "/config.rc'");
1194 toxCreate();
1195 assert(toxpk.isValidKey, "something is VERY wrong here");
1196 conwriteln("created ToxCore for [", tox_hex(toxpk[]), "]");
1199 ~this () {
1200 if (toxpk.isValidKey) {
1201 toxCoreCloseAccount(toxpk);
1202 toxpk[] = toxCoreEmptyKey[];
1206 // will not write contact to disk
1207 Contact createEmptyContact () {
1208 auto c = new Contact(this);
1209 c.info.gid = 0;
1210 c.info.nick = "test contact";
1211 c.info.pubkey[] = 0;
1212 return c;
1215 // returns `null` if there is no such group, and `dofail` is `true`
1216 inout(Group) groupById(bool dofail=true) (uint agid) inout nothrow @nogc {
1217 foreach (const Group g; groups) if (g.gid == agid) return cast(typeof(return))g;
1218 static if (dofail) assert(0, "group not found"); else return null;
1221 int opApply () (scope int delegate (ref Contact ct) dg) {
1222 foreach (Contact ct; contacts.byValue) if (auto res = dg(ct)) return res;
1223 return 0;
1226 public:
1227 static Account CreateNew (string aBaseDir, string aAccName) {
1228 import std.file : mkdirRecurse;
1229 if (aBaseDir.length == 0 || aBaseDir == "." || aBaseDir == "./") {
1230 aBaseDir = "./";
1231 } else if (aBaseDir == "/") {
1232 assert(0, "wtf?!");
1233 } else {
1234 mkdirRecurse(aBaseDir);
1235 if (aBaseDir[$-1] != '/') aBaseDir ~= '/';
1237 mkdirRecurse(aBaseDir~"/contacts");
1238 // write protocol options
1240 ProtoOptions popt;
1241 popt.txtser(VFile(aBaseDir~"proto.rc", "w"), skipstname:true);
1243 // account options
1245 AccountConfig acc;
1246 acc.nick = aAccName;
1247 acc.showPopup = true;
1248 acc.blinkActivity = true;
1249 acc.hideEmptyGroups = false;
1250 acc.ftranAllowed = true;
1251 acc.resendRotDays = 4;
1252 acc.hmcOnOpen = 10;
1253 acc.txtser(VFile(aBaseDir~"config.rc", "w"), skipstname:true);
1255 // create default group
1257 GroupOptions[1] grp;
1258 grp[0].gid = 0;
1259 grp[0].name = "default";
1260 grp[0].opened = true;
1261 //grp[0].hideIfNoVisible = TriOption.Yes;
1262 grp[].txtser(VFile(aBaseDir~"contacts/groups.rc", "w"), skipstname:true);
1264 // now load it
1265 return new Account(aBaseDir);
1270 // ////////////////////////////////////////////////////////////////////////// //
1271 class ListItemBase {
1272 private:
1273 this () {}
1275 protected:
1276 bool mVisible = true;
1278 public:
1279 void setupFont () => nvg.fontFace = "ui"; // setup font face for this item
1280 @property int height () => cast(int)nvg.textFontHeight;
1281 @property bool visible () => mVisible;
1282 bool onMouse (MouseEvent event) => false; // true: eaten
1283 bool onKey (KeyEvent event) => false; // true: eaten
1284 void drawAt (int x0, int y0, int wdt, bool selected=false) {} // real rect is scissored
1286 @property Account ownerAcc () => null;
1290 class ListItemAccount : ListItemBase {
1291 public:
1292 Account acc;
1294 public:
1295 this (Account aAcc) { assert(aAcc !is null); acc = aAcc; }
1297 override @property Account ownerAcc () => acc;
1299 override void setupFont () => nvg.fontFace = "uib"; // bold
1301 override void drawAt (int x0, int y0, int wdt, bool selected=false) {
1302 int hgt = height;
1304 nvg.newPath();
1305 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
1306 nvg.fillColor(selected ? NVGColor("#5ff0") : NVGColor("#5fff"));
1307 nvg.fill();
1309 final switch (acc.status) {
1310 case ContactStatus.Connecting: nvg.fillColor(NVGColor.k8orange); break;
1311 case ContactStatus.Offline: nvg.fillColor(NVGColor("#f00")); break;
1312 case ContactStatus.Online: nvg.fillColor(NVGColor("#fff")); break;
1313 case ContactStatus.Away: nvg.fillColor(NVGColor("#7557C7")); break;
1314 case ContactStatus.Busy: nvg.fillColor(NVGColor("#0057C7")); break;
1316 if (acc.isConnecting) nvg.fillColor(NVGColor.k8orange);
1317 nvg.textAlign = NVGTextAlign.V.Baseline;
1318 nvg.textAlign = NVGTextAlign.H.Center;
1319 nvg.text(x0+wdt/2, y0+cast(int)nvg.textFontAscender, acc.info.nick);
1324 class ListItemGroup : ListItemBase {
1325 public:
1326 Group group;
1328 public:
1329 this (Group aGroup) { assert(aGroup !is null); group = aGroup; }
1331 override @property Account ownerAcc () => group.acc;
1333 override @property bool visible () => group.visible;
1335 override void drawAt (int x0, int y0, int wdt, bool selected=false) {
1336 int hgt = height;
1338 nvg.newPath();
1339 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
1340 nvg.fillColor(selected ? NVGColor("#5880") : NVGColor("#5888"));
1341 nvg.fill();
1343 nvg.fillColor(selected ? NVGColor.white : NVGColor.yellow);
1344 nvg.textAlign = NVGTextAlign.V.Baseline;
1345 nvg.textAlign = NVGTextAlign.H.Center;
1346 nvg.text(x0+wdt/2, y0+cast(int)nvg.textFontAscender, group.name);
1351 class ListItemContact : ListItemBase {
1352 public:
1353 Contact ct;
1355 public:
1356 this (Contact aCt) { assert(aCt !is null); ct = aCt; }
1358 override @property Account ownerAcc () => ct.acc;
1360 override @property bool visible () => ct.visible;
1362 override @property int height () { import std.algorithm : max; return max(cast(int)nvg.textFontHeight, 16); } //FIXME: 16 is image height
1364 override void drawAt (int x0, int y0, int wdt, bool selected=false) {
1365 int hgt = height;
1367 if (selected) {
1368 nvg.newPath();
1369 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
1370 nvg.fillColor(NVGColor("#9600"));
1371 nvg.fill();
1374 // draw icon
1375 nvg.newPath();
1376 int iw, ih;
1377 NVGImage icon;
1378 if (ct.unreadCount == 0) icon = statusImgId[ct.status]; else icon = kittyMsg;
1379 nvg.imageSize(icon, iw, ih);
1380 //conwriteln("image: (", iw, "x", ih, ")");
1381 nvg.rect(x0, y0+(hgt-ih)/2, iw, ih);
1382 nvg.fillPaint(nvg.imagePattern(x0, y0+(hgt-ih)/2, iw, ih, 0, icon));
1383 nvg.fill();
1385 nvg.fillColor(NVGColor("#f70"));
1386 nvg.textAlign = NVGTextAlign.V.Baseline;
1387 nvg.textAlign = NVGTextAlign.H.Left;
1388 //conwriteln(nvg.textFontDescender);
1389 nvg.text(x0+4+iw+4, y0+hgt+cast(int)nvg.textFontDescender, ct.displayNick);
1394 // ////////////////////////////////////////////////////////////////////////// //
1395 // visible contact list
1396 final class CList {
1397 private import core.time;
1399 private:
1400 int mActiveItem = -1; // active item (may be different from selected with cursor)
1401 int mTopY;
1402 int mLastX = -666, mLastY = -666, mLastHeight, mLastWidth;
1403 //MonoTime mLastClick = MonoTime.zero;
1404 //int mLastClickItem = -1;
1406 public:
1407 ListItemBase[] items;
1409 public:
1410 this () {}
1412 // the first one is "main"; can return `null`
1413 Account mainAccount () {
1414 foreach (ListItemBase li; items) if (auto lc = cast(ListItemAccount)li) return lc.acc;
1415 return null;
1418 // the first one is "main"; can return `null`
1419 Account accountByPK (in ref PubKey pk) {
1420 foreach (ListItemBase li; items) {
1421 if (auto lc = cast(ListItemAccount)li) {
1422 if (lc.acc.toxpk[] == pk[]) return lc.acc;
1425 return null;
1428 void forEachAccount (scope void delegate (Account acc) dg) {
1429 if (dg is null) return;
1430 foreach (ListItemBase li; items) if (auto lc = cast(ListItemAccount)li) dg(lc.acc);
1433 void opOpAssign(string op:"~") (ListItemBase li) {
1434 if (li is null) return;
1435 items ~= li;
1438 void setFont () {
1439 //nvg.fontFace = "ui";
1440 nvg.fontSize = 20;
1441 nvg.fontBlur = 0;
1442 nvg.textAlign = NVGTextAlign.H.Left;
1443 nvg.textAlign = NVGTextAlign.V.Baseline;
1446 void removeAccount (Account acc) {
1447 if (acc is null) return;
1448 usize pos = 0, dest = 0;
1449 while (pos < items.length) {
1450 if (items[pos].ownerAcc !is acc) {
1451 if (pos != dest) items[dest++] = items[pos];
1453 ++pos;
1455 items.length = dest;
1458 void buildAccount (Account acc) {
1459 if (acc is null) return;
1460 removeAccount(acc);
1461 items ~= new ListItemAccount(acc);
1462 Contact[] css;
1463 scope(exit) delete css;
1464 foreach (Group g; acc.groups) {
1465 items ~= new ListItemGroup(g);
1466 css.length = 0;
1467 css.assumeSafeAppend;
1468 foreach (Contact c; acc.contacts.byValue) if (c.gid == g.gid) css ~= c;
1469 import std.algorithm : sort;
1470 css.sort!((a, b) {
1471 string s0 = a.displayNick;
1472 string s1 = b.displayNick;
1473 auto xlen = (s0.length < s1.length ? s0.length : s1.length);
1474 foreach (immutable idx, char c0; s0[0..xlen]) {
1475 if (c0 >= 'A' && c0 <= 'Z') c0 += 32; // poor man's tolower()
1476 char c1 = s1[idx];
1477 if (c1 >= 'A' && c1 <= 'Z') c1 += 32; // poor man's tolower()
1478 if (auto d = c0-c1) return (d < 0);
1480 return (s0.length < s1.length);
1482 foreach (Contact c; css) items ~= new ListItemContact(c);
1486 // -1: oops
1487 // should be called after clist was drawn at least once
1488 int itemAtY (int aty) {
1489 if (aty < 0 || mLastHeight < 1 || aty >= mLastHeight) return -1;
1490 nvg.save();
1491 scope(exit) nvg.restore();
1492 setFont();
1493 foreach (immutable iidx, ListItemBase li; items) {
1494 if (iidx != mActiveItem && !li.visible) continue;
1495 li.setupFont();
1496 int lh = li.height;
1497 if (lh < 1) continue;
1498 if (aty < lh) return cast(int)iidx;
1499 aty -= lh;
1501 return -1;
1504 // if called with `null` ct, deactivate
1505 void delegate (Contact ct) onActivateContactCB;
1507 // true: eaten
1508 bool onMouse (MouseEvent event) {
1509 if (mLastWidth < 1 || mLastHeight < 1) return false;
1510 int mx = event.x-mLastX;
1511 int my = event.y-mLastY;
1512 if (mx < 0 || my < 0 || mx >= mLastWidth || my >= mLastHeight) return false;
1513 if (event == "LMB-Down") {
1514 int it = itemAtY(my);
1515 if (it >= 0 && it != mActiveItem) {
1516 if (auto ci = cast(ListItemContact)items[it]) {
1517 mActiveItem = it;
1518 if (onActivateContactCB !is null) onActivateContactCB(ci.ct);
1519 glconPostScreenRepaint();
1523 return true;
1526 // true: eaten
1527 bool onKey (KeyEvent event) {
1528 return false;
1531 // real rect is scissored
1532 void drawAt (int x0, int y0, int wdt, int hgt) {
1533 mLastX = x0;
1534 mLastY = y0;
1535 mLastHeight = hgt;
1536 mLastWidth = wdt;
1537 nvg.save();
1538 scope(exit) nvg.restore();
1539 setFont();
1540 int y = 0;
1541 foreach (immutable iidx, ListItemBase li; items) {
1542 if (iidx != mActiveItem && !li.visible) continue;
1543 li.setupFont();
1544 int lh = li.height;
1545 if (lh < 1) continue;
1546 nvg.save();
1547 scope(exit) nvg.restore();
1548 version(all) {
1549 nvg.intersectScissor(x0, y0+y, wdt, lh);
1550 li.drawAt(x0, y0+y, wdt, (iidx == mActiveItem));
1551 } else {
1552 nvg.translate(x0+0.5f, y0+y+0.5f);
1553 nvg.intersectScissor(0, 0, wdt, lh);
1554 li.drawAt(0, 0, wdt);
1556 y += lh;
1557 if (y >= hgt) break;
1563 // ////////////////////////////////////////////////////////////////////////// //
1564 __gshared CList clist;
1565 __gshared Contact activeContact;
1568 void loadAccount (string nick) {
1569 Account acc;
1571 try {
1572 acc = new Account(nick);
1573 } catch (Exception e) {
1574 conwriteln("creating account...");
1575 assert(0, "not yet");
1576 acc = Account.CreateNew("_fakeacc", "ketmar");
1578 // create fake contact
1579 if (acc.contacts.length == 0) {
1580 conwriteln("creating fake contact...");
1581 auto c = acc.createEmptyContact();
1582 c.info.nick = "test contact";
1583 c.info.pubkey[] = 0x55;
1584 c.save();
1587 clist.buildAccount(acc);
1591 // ////////////////////////////////////////////////////////////////////////// //
1592 // null: deactivate
1593 void doActivateContact (Contact ct) {
1594 if (activeContact is ct) return;
1596 activeContact = ct;
1597 wipeLog();
1598 if (ct is null) {
1599 //conwriteln("clear log");
1600 if (clist !is null) clist.mActiveItem = -1;
1601 glconPostScreenRepaint();
1602 } else {
1603 //conwriteln("activated contact <", ct.info.nick, ">: [", tox_hex(ct.info.pubkey), "]");
1604 LogFile log;
1605 ct.loadLogInto(log);
1606 auto mcount = cast(int)log.messages.length;
1607 int left = ct.hmcOnOpen;
1608 if (left < ct.unreadCount) left = ct.unreadCount;
1609 if (left > mcount) left = mcount;
1610 if (mcount > left) mcount = left;
1611 if (mcount > 0) {
1612 foreach (const ref msg; log.messages[$-mcount..$]) {
1613 if (left == ct.unreadCount) addDividerLine();
1614 addTextToLog(ct.acc, ct, msg);
1615 --left;
1618 if (ct.unreadCount != 0) { ct.unreadCount = 0; ct.saveUnreadCount(); }
1621 fixTrayIcon();
1625 //FIXME: scan all accounts
1626 void fixTrayIcon () {
1627 if (clist is null) return;
1628 auto acc = clist.mainAccount;
1629 if (acc is null) return;
1630 int unc = 0;
1631 foreach (Contact ct; acc) unc += ct.unreadCount;
1632 if (unc) {
1633 import std.format : format;
1634 setTrayUnread();
1635 setHint("unread: %d".format(unc));
1636 } else {
1637 setTrayStatus(acc.status);
1638 final switch (acc.status) {
1639 case ContactStatus.Connecting: setHint("connecting..."); break;
1640 case ContactStatus.Offline: setHint("offline"); break;
1641 case ContactStatus.Online: setHint("online"); break;
1642 case ContactStatus.Away: setHint("away"); break;
1643 case ContactStatus.Busy: setHint("busy"); break;
1649 void fixUnreadIndicators () {
1650 if (!mainWindowVisible || !mainWindowActive) return; // nothing to do
1651 if (activeContact is null || activeContact.unreadCount == 0) return; // nothing to do
1652 activeContact.unreadCount = 0;
1653 activeContact.unreadCount = 0;
1654 activeContact.saveUnreadCount();
1655 fixTrayIcon();
1659 // ////////////////////////////////////////////////////////////////////////// //
1660 //__gshared string accountNameToLoad = "_fakeacc";
1661 __gshared string accountNameToLoad = "";
1662 __gshared string globalHotkey = "M-H-F";
1665 void main (string[] args) {
1666 if (!TOX_VERSION_IS_ABI_COMPATIBLE()) assert(0, "invalid ToxCore library version");
1668 conRegVar!accountNameToLoad("starting_account", "account to load");
1670 //conwriteln("account '", acc.info.nick, "' loaded, ", acc.contacts.length, " contacts, ", acc.groups.length, " groups");
1672 //glconShowKey = "M-Grave";
1673 glconSetAndSealFPS(0); // draw-on-demand
1675 conProcessQueue(256*1024); // load config
1676 conProcessArgs!true(args);
1677 conProcessQueue(256*1024);
1679 if (accountNameToLoad.length == 0) assert(0, "no account to load");
1681 //setOpenGLContextVersion(3, 2); // up to GLSL 150
1682 setOpenGLContextVersion(2, 0); // it's enough
1684 loadAllFonts();
1687 NVGPathSet svp = null;
1689 //glconRunGLWindowResizeable(800, 600, "BioAcid", "BIOACID");
1690 sdpyWindowClass = "BIOACID";
1691 auto sdmain = new SimpleWindow(800, 600, "BioAcid", OpenGlOptions.yes, Resizability.allowResizing);
1692 glconCtlWindow = sdmain;
1694 sdmain.visibilityChanged = delegate (bool vis) { mainWindowVisible = vis; fixUnreadIndicators(); };
1695 sdmain.onFocusChange = delegate (bool focused) { mainWindowActive = focused; /*conwriteln("focus=", focused);*/ fixUnreadIndicators(); };
1697 try {
1698 if (globalHotkey.length > 0) {
1699 GlobalHotkeyManager.register(globalHotkey, delegate () { concmd("win_toggle"); glconPostDoConCommands!true(); });
1701 } catch (Exception e) {
1702 conwriteln("ERROR registering hotkey!");
1705 conRegFunc!(() {
1706 if (sdmain !is null) sdmain.close();
1707 })("quit", "quit BioAcid");
1709 conRegFunc!(() {
1710 if (sdmain !is null && !sdmain.closed) {
1711 //conwriteln("act=", mainWindowActive, "; vis=", mainWindowVisible, "; realvis=", sdmain.visible);
1712 if (!mainWindowVisible) {
1713 // this strange code brings window to the current desktop it if was on a different one
1714 sdmain.hide();
1715 sdmain.show();
1716 } else if (sdmain.visible) {
1717 sdmain.hide();
1718 } else {
1719 sdmain.show();
1721 flushGui();
1723 })("win_toggle", "show/hide main window");
1725 sdmain.addEventListener((GLConScreenRepaintEvent evt) {
1726 if (sdmain.closed) return;
1727 if (isQuitRequested) { sdmain.close(); return; }
1728 sdmain.redrawOpenGlSceneNow();
1731 sdmain.addEventListener((GLConDoConsoleCommandsEvent evt) {
1732 glconProcessEventMessage();
1735 sdmain.addEventListener((PopupCheckerEvent evt) {
1736 popupCheckExpirations();
1740 // ////////////////////////////////////////////////////////////////////// //
1741 // tox core events
1742 sdmain.addEventListener((ToxEventBase evt) {
1743 auto acc = clist.accountByPK(evt.self);
1745 if (acc is null) return;
1747 bool fixTray = false;
1748 scope(exit) { if (fixTray) fixTrayIcon(); glconPostScreenRepaint(); }
1750 // connection?
1751 if (auto e = cast(ToxEventConnection)evt) {
1752 if (e.who[] == acc.toxpk[]) {
1753 if (e.connected) acc.toxConnectionEstablished(); else acc.toxConnectionDropped();
1754 fixTray = true;
1755 } else {
1756 if (!e.connected) acc.toxFriendOffline(e.who);
1758 return;
1760 // status?
1761 if (auto e = cast(ToxEventStatus)evt) {
1762 if (e.who[] == acc.toxpk[]) {
1763 acc.toxSelfStatus(e.status);
1764 fixTray = true;
1765 } else {
1766 acc.toxFriendStatus(e.who, e.status);
1768 return;
1770 // status message?
1771 if (auto e = cast(ToxEventStatusMsg)evt) {
1772 if (e.who[] != acc.toxpk[]) {
1773 acc.toxFriendStatusMessage(e.who, e.message);
1775 return;
1777 // incoming text message?
1778 if (auto e = cast(ToxEventMessage)evt) {
1779 if (e.who[] != acc.toxpk[]) {
1780 acc.toxFriendMessage(e.who, e.action, e.message, e.time);
1781 fixTray = true;
1783 return;
1785 // ack outgoing text message?
1786 if (auto e = cast(ToxEventMessageAck)evt) {
1787 if (e.who[] != acc.toxpk[]) {
1788 acc.toxMessageAck(e.who, e.msgid);
1790 return;
1793 //glconProcessEventMessage();
1797 // ////////////////////////////////////////////////////////////////////// //
1798 sdmain.onClosing = delegate () {
1799 clist.forEachAccount(delegate (acc) { acc.forceOnline = false; });
1800 popupKillAll();
1801 if (nvg !is null) {
1802 sdmain.setAsCurrentOpenGlContext();
1803 scope(exit) { flushGui(); sdmain.releaseCurrentOpenGlContext(); }
1804 svp.kill();
1805 nvg.kill();
1807 assert(nvg is null);
1808 if (sdhint !is null) sdhint.close();
1809 if (trayicon !is null) trayicon.close();
1812 sdmain.closeQuery = delegate () {
1813 concmd("quit");
1814 glconPostDoConCommands!true();
1817 // first time setup
1818 sdmain.visibleForTheFirstTime = delegate () {
1819 if (sdmain.width > 1 && optCListWidth < 0) optCListWidth = sdmain.width/5;
1820 sdmain.setAsCurrentOpenGlContext(); // make this window active
1821 scope(exit) sdmain.releaseCurrentOpenGlContext();
1822 sdmain.vsync = false;
1824 glconInit(sdmain.width, sdmain.height);
1826 nvg = nvgCreateContext(NVGContextFlag.Antialias, NVGContextFlag.StencilStrokes, NVGContextFlag.FontNoAA);
1827 if (nvg is null) assert(0, "cannot initialize NanoVG");
1828 loadFonts(nvg);
1830 try {
1831 static immutable skullsPng = /*cast(immutable(ubyte)[])*/import("data/skulls.png");
1832 //nvgSkullsImg = nvg.createImage("skulls.png", NVGImageFlags.NoFiltering|NVGImageFlags.RepeatX|NVGImageFlags.RepeatY);
1833 auto xi = loadImageFromMemory(skullsPng[]);
1834 scope(exit) delete xi;
1835 //{ import core.stdc.stdio; printf("creating background image...\n"); }
1836 nvgSkullsImg = nvg.createImageFromMemoryImage(xi, NVGImageFlags.NoFiltering, NVGImageFlags.RepeatX, NVGImageFlags.RepeatY);
1837 //{ import core.stdc.stdio; printf("background image created\n"); }
1838 if (!nvgSkullsImg.valid) assert(0, "cannot load background image");
1839 } catch (Exception e) {
1840 assert(0, "cannot load background image");
1842 buildStatusImages();
1844 prepareTrayIcon();
1846 lay = new LayTextClass(laf, sdmain.width/2);
1847 lay.fontStyle.fontsize = 16;
1848 lay.fontStyle.color = NVGColor.darkorange.asUint;
1849 lay.fontStyle.monospace = true;
1850 lay.fontStyle.bgcolor = NVGColor("#222").asUint;
1851 lay.put("ketmar (бля)");
1852 lay.fontStyle.monospace = false;
1853 lay.putHardSpace(64);
1854 lay.putExpander();
1855 lay.put("!");
1856 lay.putExpander();
1857 lay.put("2018/02/12");
1858 lay.putNBSP();
1859 lay.fontStyle.color = NVGColor("#fff").asUint;
1860 lay.fontStyle.bgcolor = NVGColor("#006").asUint;
1861 lay.put("11:09:03");
1862 lay.endPara();
1863 lay.fontStyle.bgcolor = NVGColor.transparent.asUint;
1864 lay.fontStyle.color = NVGColor.darkorange.asUint;
1865 lay.fontStyle.fontsize = 20;
1866 lay.put("this is my text, lol (еб\u00adёна КОЧЕРГА!)");
1867 lay.endPara();
1868 lay.finalize();
1869 //conwriteln("wdt=", lay.width, "; text width=", lay.textWidth);
1870 lastWindowWidth = sdmain.width;
1872 clist = new CList();
1873 loadAccount(accountNameToLoad);
1874 //clist.buildAccount(acc);
1875 clist.onActivateContactCB = delegate (Contact ct) { doActivateContact(ct); };
1877 sdmain.setMinSize(640, 480);
1879 fixTrayIcon();
1880 //sdmain.redrawOpenGlSceneNow();
1883 sdmain.windowResized = delegate (int wdt, int hgt) {
1884 if (sdmain.closed) return;
1885 glconResize(wdt, hgt);
1886 glconPostScreenRepaint/*Delayed*/();
1887 //conwriteln("old=", lastWindowWidth, "; new=", wdt);
1888 if (wdt > 1 && optCListWidth > 0 && lastWindowWidth > 0 && lastWindowWidth != wdt) {
1889 immutable double frc = lastWindowWidth/optCListWidth;
1890 optCListWidth = cast(int)(wdt/frc);
1891 if (optCListWidth < 64) optCListWidth = 64;
1892 lastWindowWidth = wdt;
1894 lay.relayout(wdt);
1898 int mouseX = -666, mouseY = -666;
1901 sdmain.handleKeyEvent = delegate (KeyEvent event) {
1902 if (sdmain.closed) return;
1903 scope(exit) glconPostDoConCommands!true();
1904 if (glconKeyEvent(event)) return;
1906 auto acc = clist.mainAccount;
1908 if (event == "D-Escape") {
1909 if (sdmain !is null && !sdmain.closed && sdmain.visible) {
1910 sdmain.hide();
1911 flushGui();
1913 return;
1916 if (event == "D-C-Q") { concmd("quit"); return; }
1917 if (event == "D-C-1") { acc.status = ContactStatus.Online; return; }
1918 if (event == "D-C-2") { acc.status = ContactStatus.Away; return; }
1919 if (event == "D-C-0") { acc.status = ContactStatus.Offline; return; }
1921 if (event == "D-C-W") { doActivateContact(null); return; }
1923 if (clist !is null && clist.onKey(event)) return;
1925 if (event == "D-C-S-Enter") {
1926 static PopupWindow.Kind kind;
1927 //new PopupWindow(10, 10, "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
1928 showPopup(kind, "TEST popup", "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
1929 if (kind == PopupWindow.Kind.max) kind = PopupWindow.Kind.min; else ++kind;
1930 return;
1933 if (activeContact !is null) {
1934 if (event == "D-Enter") {
1935 auto text = activeContact.edit.text;
1936 activeContact.edit.clear();
1937 activeContact.send(text);
1938 glconPostScreenRepaint();
1939 return;
1941 if (activeContact.edit.onKey(event)) return;
1945 if (event == "D-Space") {
1946 // add random text
1947 lay.fontStyle.fontsize = 16;
1948 lay.fontStyle.color = NVGColor.k8orange.asUint;
1949 lay.fontStyle.monospace = true;
1950 lay.fontStyle.bgcolor = NVGColor("#222").asUint;
1951 lay.put("ketmar");
1952 lay.fontStyle.monospace = false;
1953 lay.putHardSpace(64);
1954 lay.putExpander();
1955 lay.put("!");
1956 lay.putExpander();
1957 lay.put("2018/02/12");
1958 lay.putNBSP();
1959 lay.fontStyle.color = NVGColor("#fff").asUint;
1960 lay.fontStyle.bgcolor = NVGColor("#006").asUint;
1961 lay.put("11:09:03");
1962 lay.endPara();
1963 lay.fontStyle.bgcolor = NVGColor.transparent.asUint;
1964 lay.fontStyle.color = NVGColor.k8orange.asUint;
1965 lay.fontStyle.fontsize = 20;
1966 import std.random;
1967 // words
1968 version(none) {
1969 foreach (immutable widx; 0..uniform!"[]"(2, 28)) {
1970 // one word
1971 if (widx != 0) lay.put(" ");
1972 foreach (; 0..uniform!"[]"(1, 9)) {
1973 char ch = cast(char)('a'+uniform!"[]"(0, 25));
1974 lay.put(ch);
1975 //lay.putSoftHypen();
1978 } else {
1979 // long word, for hyphenation test
1980 foreach (immutable idx; 0..228) {
1981 char ch = cast(char)('a'+uniform!"[]"(0, 25));
1982 lay.put(ch);
1983 if (idx%24 == 23) lay.putSoftHypen();
1986 lay.endPara();
1987 lay.finalize();
1989 setHint("boo!");
1990 // redraw
1991 glconPostScreenRepaint();
1992 return;
1997 int msLastPressX = -666, msLastPressY = -666;
1998 bool msDoLogButton = false;
2000 sdmain.handleMouseEvent = delegate (MouseEvent event) {
2001 if (sdmain.closed) return;
2002 scope(exit) glconPostDoConCommands!true();
2003 if (isConsoleVisible) return;
2004 mouseX = event.x;
2005 mouseY = event.y;
2007 // check for href
2008 //FIXME: process it here, not in renderer
2009 if (event == "LMB-Down") {
2010 msLastPressX = mouseX;
2011 msLastPressY = mouseY;
2012 msDoLogButton = true;
2013 glconPostScreenRepaint();
2016 if (clist !is null) {
2017 sdmain.setAsCurrentOpenGlContext(); // make this window active
2018 scope(exit) sdmain.releaseCurrentOpenGlContext();
2020 bool inFrame = nvg.inFrame;
2021 if (!inFrame) nvg.beginFrame(sdmain.width, sdmain.height);
2022 scope(exit) if (!inFrame) nvg.endFrame();
2024 if (clist.onMouse(event)) { /*glconPostScreenRepaint();*/ return; }
2027 // log scroll
2028 if (layWinHeight > 0) {
2029 enum ScrollHeight = 32;
2030 if (event == "WheelUp") {
2031 layOffset += ScrollHeight;
2032 if (layOffset > lay.textHeight-layWinHeight) layOffset = lay.textHeight-layWinHeight;
2033 } else if (event == "WheelDown") {
2034 layOffset -= ScrollHeight;
2036 if (layOffset < 0) layOffset = 0;
2039 // don't spam with repaint events
2040 if (event.type != MouseEventType.motion) glconPostScreenRepaint();
2043 sdmain.handleCharEvent = delegate (dchar ch) {
2044 if (sdmain.closed) return;
2045 scope(exit) glconPostDoConCommands!true();
2046 if (glconCharEvent(ch)) return;
2048 if (activeContact !is null) {
2049 if (activeContact.edit.onChar(ch)) return;
2053 // draw main screen
2054 sdmain.redrawOpenGlScene = delegate () {
2055 glconPostDoConCommands!true();
2056 if (sdmain.closed) return;
2057 sdmain.setAsCurrentOpenGlContext(); // make this window active
2058 scope(exit) sdmain.releaseCurrentOpenGlContext();
2060 // draw main screen
2061 scope(exit) glconDraw();
2063 if (nvg is null) return;
2065 //oglSetup2D(glconCtlWindow.width, glconCtlWindow.height);
2066 glViewport(0, 0, sdmain.width, sdmain.height);
2067 glMatrixMode(GL_MODELVIEW);
2068 //conwriteln(glconCtlWindow.width, "x", glconCtlWindow.height);
2070 glClearColor(0, 0, 0, 0);
2071 glClear(glNVGClearFlags/*|GL_COLOR_BUFFER_BIT*/);
2074 nvg.beginFrame(sdmain.width, sdmain.height);
2075 scope(exit) nvg.endFrame();
2077 if (clist !is null && optCListWidth > 0) {
2078 // draw contact list
2079 int cx = 1;
2080 int cy = 1;
2081 int wdt = optCListWidth;
2082 int hgt = nvg.height-cy*2;
2084 nvg.shapeAntiAlias = true;
2085 nvg.nonZeroFill;
2086 nvg.strokeWidth = 1;
2089 nvg.save();
2090 scope(exit) nvg.restore();
2092 nvg.newPath();
2093 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
2095 int w, h;
2096 nvg.imageSize(nvgSkullsImg, w, h);
2097 nvg.fillPaint(nvg.imagePattern(0, 0, w, h, 0, nvgSkullsImg));
2099 nvg.strokeColor(NVGColor("#f70"));
2100 nvg.fill();
2101 nvg.stroke();
2103 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt-3*2, hgt-3*2);
2105 clist.drawAt(cx+3, cy+3, wdt-3*2, hgt-3*2);
2109 nvg.save();
2110 scope(exit) nvg.restore();
2112 //nvg.transform(NVGMatrix.init);
2114 auto xf = nvg.currTransform;
2115 nvg.currTransform = xf;
2118 // draw chat log
2119 cx += wdt+2;
2120 wdt = nvg.width-cx-1;
2121 auto baphHgt = hgt;
2123 // calculate editor dimensions and draw editor
2124 if (activeContact !is null) {
2125 nvg.save();
2126 scope(exit) nvg.restore();
2128 auto edinfo = activeContact.edit.calcHeight(nvg, wdt-3*2);
2129 //if (edinfo.height < edinfo.lineh) edinfo.height = edinfo.lineh;
2131 int edy = cy+hgt-cast(int)edinfo.height-3*2;
2133 nvg.newPath();
2134 nvg.roundedRect(cx+0.5f, edy+0.5f, wdt, edinfo.height+3*2, 6);
2135 nvg.fillColor(NVGColor.black);
2136 nvg.strokeColor(NVGColor("#f70"));
2137 nvg.fill();
2138 nvg.stroke();
2140 nvg.intersectScissor(cx+2.5f, edy+2.5f, wdt-3*2+2, edinfo.height+2);
2141 activeContact.edit.draw(nvg, cx+3, edy+3, wdt-3*2, cast(int)edinfo.height);
2143 hgt -= cast(int)edinfo.height+3*2;
2146 nvg.newPath();
2147 nvg.roundedRect(cx+0.5f, cy+0.5f, wdt, hgt, 6);
2148 nvg.fillColor(NVGColor.black);
2149 nvg.strokeColor(NVGColor("#f70"));
2150 nvg.fill();
2151 nvg.stroke();
2153 nvg.intersectScissor(cx+3.5f, cy+3.5f, wdt-3*2, hgt-3*2);
2154 version(all) {
2155 immutable float scalex = (wdt-3*2-10*2)/BaphometDims;
2156 immutable float scaley = (baphHgt-3*2-10*2)/BaphometDims;
2157 immutable float scale = (scalex < scaley ? scalex : scaley)/1.5f;
2158 immutable float sz = BaphometDims*scale;
2159 nvg.strokeColor(NVGColor("#400"));
2160 nvg.fillColor(NVGColor("#400"));
2161 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);
2164 immutable float sbx = cx+wdt-BND_SCROLLBAR_WIDTH-1.5f;
2165 immutable float sby = cy+3.5f;
2166 wdt -= BND_SCROLLBAR_WIDTH+1;
2167 wdt -= 3*2;
2168 hgt -= 3*2;
2170 lay.relayout(wdt); // this is harmess if width wasn't changed
2171 int ty = lay.textHeight-hgt+1-layOffset;
2172 if (ty < 0) ty = 0;
2173 layWinHeight = cast(int)hgt;
2175 if (lay.textHeight > hgt) {
2176 float h = lay.textHeight-hgt;
2177 nvg.bndScrollBar(sbx, sby, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, ty/h, hgt/h);
2178 } else {
2179 nvg.bndScrollBar(sbx, sby, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);
2182 nvg.intersectScissor(cx+2.5f, cy+2.5f, wdt+2, hgt+2);
2183 nvg.drawLayouter(lay, ty, cx+3, cy+3, hgt);
2185 if (msDoLogButton) {
2186 msDoLogButton = false;
2187 auto widx = lay.wordAtXY(msLastPressX-(cx+3), ty+(msLastPressY-(cy+3)));
2188 if (widx >= 0) {
2189 if (auto hr = cast(uint)widx in layUrlList) {
2190 conwriteln("URL CLICK: <", hr.url);
2191 openUrl(hr.url);
2200 flushGui();
2201 sdmain.eventLoop(15000,
2202 // pulser: process resend queues here
2203 delegate () {
2204 if (sdmain.closed || clist is null) return;
2205 clist.forEachAccount(delegate (Account acc) { acc.processResendQueue(); });
2208 clist.forEachAccount(delegate (acc) { acc.forceOnline = false; });
2209 clist.forEachAccount(delegate (Account acc) { acc.saveResendQueue(); acc.saveResendQueue(); });
2210 toxCoreShutdownAll();
2211 popupKillAll();
2212 flushGui();
2213 conProcessQueue(int.max/4);