two new contact commands: "/always" and "/normal" (and cosmetic fixes)
[bioacid.git] / tkclist.d
blob443e092f23b31773f371852a47db2fef61d7d210
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 tkclist 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.nanovega;
28 import iv.nanovega.blendish;
29 import iv.strex;
30 import iv.vfs.io;
32 import accdb;
33 import accobj;
34 import fonts;
35 import popups;
36 import icondata;
37 import notifyicon;
38 import toxproto;
40 import tkmain;
43 // ////////////////////////////////////////////////////////////////////////// //
44 __gshared int optCListWidth = -1;
45 __gshared bool optHRLastSeen = true;
48 // ////////////////////////////////////////////////////////////////////////// //
49 private extern(C) const(char)[] xfmt (const(char)* fmt, ...) {
50 import core.stdc.stdio : vsnprintf;
51 import core.stdc.stdarg;
52 static char[64] buf = void;
53 va_list ap;
54 va_start(ap, fmt);
55 auto len = vsnprintf(buf.ptr, buf.length, fmt, ap);
56 va_end(ap);
57 return buf[0..len];
61 // ////////////////////////////////////////////////////////////////////////// //
62 class ListItemBase {
63 private:
64 this (CList aowner) { owner = aowner; }
66 protected:
67 CList owner;
69 public:
70 @property Account ownerAcc () => null;
72 // setup font face for this item
73 void setFontAlign () { fstash.textAlign = NVGTextAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline); }
74 void setFont () { fstash.fontId = "ui"; fstash.size = 19; }
75 void setSmallFont () { fstash.fontId = "ui"; fstash.size = 15; }
77 @property int height () => cast(int)fstash.fontHeight;
78 @property bool visible () => true;
80 bool onMouse (MouseEvent event, bool meActive) => false; // true: eaten; will modify fstash
81 bool onKey (KeyEvent event, bool meActive) => false; // true: eaten; will modify fstash
83 protected void drawPrepare (NVGContext nvg) {
84 setFont();
85 setFontAlign();
86 nvg.setupCtxFrom(fstash);
89 // no need to save context state here
90 void drawAt (NVGContext nvg, int x0, int y0, int wdt, bool selected=false) {} // real rect is scissored
94 // ////////////////////////////////////////////////////////////////////////// //
95 class ListItemAccount : ListItemBase {
96 public:
97 Account acc;
99 public:
100 this (CList aowner, Account aAcc) { assert(aAcc !is null); acc = aAcc; super(aowner); }
102 override @property Account ownerAcc () => acc;
104 override void setFont () { fstash.fontId = "uib"; fstash.size = 20; }
106 override void drawAt (NVGContext nvg, int x0, int y0, int wdt, bool selected=false) {
107 drawPrepare(nvg);
109 int hgt = height;
110 nvg.newPath();
111 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
112 nvg.fillColor(selected ? NVGColor("#5ff0") : NVGColor("#5fff"));
113 nvg.fill();
115 final switch (acc.status) {
116 case ContactStatus.Connecting: nvg.fillColor(NVGColor.k8orange); break;
117 case ContactStatus.Offline: nvg.fillColor(NVGColor("#f00")); break;
118 case ContactStatus.Online: nvg.fillColor(NVGColor("#fff")); break;
119 case ContactStatus.Away: nvg.fillColor(NVGColor("#7557C7")); break;
120 case ContactStatus.Busy: nvg.fillColor(NVGColor("#0057C7")); break;
122 if (acc.isConnecting) nvg.fillColor(NVGColor.k8orange);
124 nvg.textAlign = NVGTextAlign.V.Baseline;
125 nvg.textAlign = NVGTextAlign.H.Center;
126 nvg.text(x0+wdt/2, y0+cast(int)nvg.textFontAscender, acc.info.nick);
131 // ////////////////////////////////////////////////////////////////////////// //
132 class ListItemGroup : ListItemBase {
133 public:
134 Group group;
136 public:
137 this (CList aowner, Group aGroup) { assert(aGroup !is null); group = aGroup; super(aowner); }
139 override @property Account ownerAcc () => group.acc;
141 override @property bool visible () => group.visible;
143 // true: eaten
144 override bool onMouse (MouseEvent event, bool meActive) {
145 if (event == "LMB-Down") {
146 //conwriteln("!!! ", group.opened);
147 group.opened = !group.opened;
148 glconPostScreenRepaint();
149 return true;
151 return false;
154 override void drawAt (NVGContext nvg, int x0, int y0, int wdt, bool selected=false) {
155 drawPrepare(nvg);
157 int hgt = height;
158 nvg.newPath();
159 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
160 nvg.fillColor(selected ? NVGColor("#5880") : NVGColor("#5888"));
161 nvg.fill();
163 nvg.fillColor(selected ? NVGColor.white : NVGColor.yellow);
164 nvg.textAlign = NVGTextAlign.V.Baseline;
165 nvg.textAlign = NVGTextAlign.H.Center;
166 nvg.text(x0+wdt/2, y0+cast(int)nvg.textFontAscender, group.name);
171 // ////////////////////////////////////////////////////////////////////////// //
172 class ListItemContact : ListItemBase {
173 public:
174 Contact ct;
176 public:
177 this (CList aowner, Contact aCt) { assert(aCt !is null); ct = aCt; super(aowner); }
179 override @property Account ownerAcc () => ct.acc;
181 override @property bool visible () => ct.visible;
183 //FIXME: 16 is icon height
184 override @property int height () {
185 import std.algorithm : max;
186 setFont();
187 auto hgt = fstash.fontHeight;
188 setSmallFont();
189 if (ct.info.statusmsg.length) hgt += fstash.fontHeight; // status message
190 if (!ct.online) hgt += fstash.fontHeight; // last seen
191 return max(cast(int)hgt, 16);
194 // true: eaten
195 override bool onMouse (MouseEvent event, bool meActive) {
196 if (event == "LMB-Down") {
197 owner.activeItem = this;
198 if (owner.onActivateContactCB !is null) owner.onActivateContactCB(ct);
199 glconPostScreenRepaint();
200 return true;
202 return false;
205 override void drawAt (NVGContext nvg, int x0, int y0, int wdt, bool selected=false) {
206 import std.algorithm : max;
208 drawPrepare(nvg);
210 int hgt = max(cast(int)nvg.textFontHeight, 16);
212 if (selected) {
213 int fullhgt = height;
214 nvg.newPath();
215 nvg.rect(x0+0.5f, y0+0.5f, wdt, fullhgt);
216 nvg.fillColor(NVGColor("#9600"));
217 nvg.fill();
220 // draw icon
221 nvg.newPath();
222 int iw, ih;
223 NVGImage icon;
225 if (ct.requestPending) {
226 icon = kittyOut;
227 } else if (ct.acceptPending) {
228 icon = kittyFish;
229 } else {
230 if (ct.unreadCount == 0) icon = statusImgId[ct.status]; else icon = kittyMsg;
233 nvg.imageSize(icon, iw, ih);
234 nvg.rect(x0, y0+(hgt-ih)/2, iw, ih);
235 nvg.fillPaint(nvg.imagePattern(x0, y0+(hgt-ih)/2, iw, ih, 0, icon));
236 nvg.fill();
238 auto ty = y0+hgt/2;
240 nvg.fillColor(NVGColor("#f70"));
241 //nvg.textAlign = NVGTextAlign.V.Top;
242 nvg.textAlign = NVGTextAlign.V.Middle;
243 nvg.textAlign = NVGTextAlign.H.Left;
244 nvg.text(x0+4+iw+4, ty, ct.displayNick);
246 // draw last status and lastseen
247 setSmallFont();
248 nvg.setupCtxFrom(fstash);
249 ty = y0+hgt+cast(int)nvg.textFontHeight/2;
250 //ty += cast(int)nvg.textFontDescender;
252 // draw status text
253 if (ct.info.statusmsg.length) {
254 nvg.fillColor = NVGColor("#777");
255 nvg.textAlign = NVGTextAlign.H.Left;
256 nvg.text(x0+4+iw+4, ty, ct.info.statusmsg);
257 ty += cast(int)nvg.textFontHeight;
260 // draw lastseen
261 if (!ct.online) {
262 nvg.fillColor = NVGColor("#b30");
263 nvg.textAlign = NVGTextAlign.H.Right;
264 if (ct.info.lastonlinetime == 0) {
265 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: never");
266 } else {
267 if (optHRLastSeen) {
268 enum OneDay = 60*60*24;
269 auto nowut = Clock.currTime.toUnixTime/OneDay;
270 auto ltt = ct.info.lastonlinetime/OneDay;
271 if (ltt >= nowut) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: today");
272 else if (nowut-ltt == 1) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: yesterday");
273 else if (nowut-ltt <= 15) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u days ago", cast(int)nowut-ltt));
274 else if (nowut-ltt <= 28) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u weeks ago", cast(int)(nowut-ltt)/7));
275 else if (nowut-ltt <= 31) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: month ago");
276 else if (nowut-ltt < 365) {
277 if ((nowut-ltt)%31 <= 15) {
278 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u month ago", cast(int)(nowut-ltt)/31));
279 } else {
280 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u.5 month ago", cast(int)(nowut-ltt)/31));
282 } else {
283 if ((nowut-ltt)%365 < 365/10) {
284 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u years ago", cast(int)(nowut-ltt)/365));
285 } else {
286 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u.%u month ago", cast(int)(nowut-ltt)/365, cast(int)(nowut-ltt)%365/(365/10)));
289 } else {
290 SysTime dt = SysTime.fromUnixTime(ct.info.lastonlinetime);
291 //auto len = snprintf(buf.ptr, buf.length, "ls: %04u/%02u/%02u %02u:%02u:%02u", cast(uint)dt.year, cast(uint)dt.month, cast(uint)dt.day, cast(uint)dt.hour, cast(uint)dt.minute, cast(uint)dt.second);
292 //nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, buf[0..len]);
293 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("ls: %04u/%02u/%02u %02u:%02u:%02u", cast(uint)dt.year, cast(uint)dt.month, cast(uint)dt.day, cast(uint)dt.hour, cast(uint)dt.minute, cast(uint)dt.second));
301 // ////////////////////////////////////////////////////////////////////////// //
302 // visible contact list
303 final class CList {
304 private import core.time;
306 private:
307 int mActiveItem = -1; // active item (may be different from selected with cursor)
308 int mTopY;
309 int mLastX = -666, mLastY = -666, mLastHeight, mLastWidth;
310 //MonoTime mLastClick = MonoTime.zero;
311 //int mLastClickItem = -1;
313 public:
314 ListItemBase[] items;
316 public:
317 this () {}
319 // the first one is "main"; can return `null`
320 Account mainAccount () {
321 foreach (ListItemBase li; items) if (auto lc = cast(ListItemAccount)li) return lc.acc;
322 return null;
325 // the first one is "main"; can return `null`
326 Account accountByPK (in ref PubKey pk) {
327 foreach (ListItemBase li; items) {
328 if (auto lc = cast(ListItemAccount)li) {
329 if (lc.acc.toxpk[] == pk[]) return lc.acc;
332 return null;
335 void forEachAccount (scope void delegate (Account acc) dg) {
336 if (dg is null) return;
337 foreach (ListItemBase li; items) if (auto lc = cast(ListItemAccount)li) dg(lc.acc);
340 void opOpAssign(string op:"~") (ListItemBase li) {
341 if (li is null) return;
342 items ~= li;
345 void setFont () {
346 fstash.fontId = "ui";
347 fstash.size = 20;
348 //fstash.blur = 0;
349 fstash.textAlign = NVGTextAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
352 void removeAccount (Account acc) {
353 if (acc is null) return;
354 usize pos = 0, dest = 0;
355 while (pos < items.length) {
356 if (items[pos].ownerAcc !is acc) {
357 if (pos != dest) items[dest++] = items[pos];
359 ++pos;
361 items.length = dest;
364 void buildAccount (Account acc) {
365 if (acc is null) return;
366 //FIXME
367 mActiveItem = -1;
368 mTopY = 0;
369 removeAccount(acc);
370 items ~= new ListItemAccount(this, acc);
371 Contact[] css;
372 scope(exit) delete css;
373 foreach (Group g; acc.groups) {
374 items ~= new ListItemGroup(this, g);
375 css.length = 0;
376 css.assumeSafeAppend;
377 foreach (Contact c; acc.contacts.byValue) if (c.gid == g.gid) css ~= c;
378 import std.algorithm : sort;
379 css.sort!((a, b) {
380 string s0 = a.displayNick;
381 string s1 = b.displayNick;
382 auto xlen = (s0.length < s1.length ? s0.length : s1.length);
383 foreach (immutable idx, char c0; s0[0..xlen]) {
384 if (c0 >= 'A' && c0 <= 'Z') c0 += 32; // poor man's tolower()
385 char c1 = s1[idx];
386 if (c1 >= 'A' && c1 <= 'Z') c1 += 32; // poor man's tolower()
387 if (auto d = c0-c1) return (d < 0);
389 return (s0.length < s1.length);
391 foreach (Contact c; css) items ~= new ListItemContact(this, c);
395 // -1: oops
396 // should be called after clist was drawn at least once
397 int itemAtY (int aty) {
398 if (aty < 0 || mLastHeight < 1 || aty >= mLastHeight) return -1;
399 aty += mTopY;
400 setFont();
401 foreach (immutable iidx, ListItemBase li; items) {
402 if (iidx != mActiveItem && !li.visible) continue;
403 li.setFont();
404 int lh = li.height;
405 if (lh < 1) continue;
406 if (aty < lh) return cast(int)iidx;
407 aty -= lh;
409 return -1;
412 final int calcTotalHeight () {
413 setFont();
414 int totalHeight = 0;
415 foreach (immutable iidx, ListItemBase li; items) {
416 if (iidx != mActiveItem && !li.visible) continue;
417 li.setFont();
418 int lh = li.height;
419 if (lh < 1) continue;
420 totalHeight += lh;
422 return totalHeight;
425 static struct AIPos {
426 int y, hgt;
427 @property bool valid () const pure nothrow @safe @nogc => (hgt > 0);
430 AIPos calcActiveItemPos () {
431 AIPos res;
432 if (mActiveItem < 0 || mActiveItem >= items.length) return res;
433 setFont();
434 foreach (immutable iidx, ListItemBase li; items) {
435 if (iidx != mActiveItem && !li.visible) continue;
436 li.setFont();
437 int lh = li.height;
438 if (lh < 1) continue;
439 if (iidx == mActiveItem) { res.hgt = lh; return res; }
440 res.y += lh;
442 res = res.init;
443 return res;
446 final void resetActiveItem () nothrow @safe @nogc { mActiveItem = -1; }
448 final @property int activeItemIndex () const pure nothrow @safe @nogc => mActiveItem;
450 final @property void activeItemIndex (int idx) nothrow @trusted {
451 if (idx < 0 || idx >= items.length) idx = -1;
452 if (mActiveItem == idx) return;
453 mActiveItem = idx;
454 try { glconPostScreenRepaint(); } catch (Exception e) {} // sorry
457 final @property inout(ListItemBase) activeItem () inout pure nothrow @trusted @nogc {
458 pragma(inline, true);
459 return (mActiveItem >= 0 && mActiveItem < items.length ? cast(inout)items[mActiveItem] : null);
462 final @property void activeItem (ListItemBase it) nothrow @trusted {
463 int idx = -1;
464 if (it !is null) {
465 foreach (immutable ii, const ListItemBase li; items) {
466 if (li is it) { idx = cast(int)ii; break; }
469 activeItemIndex = idx;
472 final bool isActiveContact (in ref PubKey pk) const nothrow @trusted {
473 if (mActiveItem < 0 || mActiveItem >= items.length) return false;
474 if (auto ct = cast(const(ListItemContact))items[mActiveItem]) return (ct.ct.info.pubkey[] == pk[]);
475 return false;
478 // if called with `null` ct, deactivate
479 void delegate (Contact ct) onActivateContactCB;
481 // true: eaten
482 bool onMouse (MouseEvent event) {
483 if (mLastWidth < 1 || mLastHeight < 1) return false;
484 int mx = event.x-mLastX;
485 int my = event.y-mLastY;
486 if (mx < 0 || my < 0 || mx >= mLastWidth || my >= mLastHeight) return false;
488 int it = itemAtY(my);
489 if (it >= 0) {
490 if (items[it].onMouse(event, it == mActiveItem)) return true;
493 enum ScrollHeight = 32;
495 if (event == "WheelUp") {
496 if (mTopY > 0) {
497 mTopY -= ScrollHeight;
498 if (mTopY < 0) mTopY = 0;
499 glconPostScreenRepaint();
501 return true;
504 if (event == "WheelDown") {
505 auto th = calcTotalHeight()-mLastHeight;
506 if (th > 0 && mTopY < th) {
507 mTopY += ScrollHeight;
508 if (mTopY > th) mTopY = th;
509 glconPostScreenRepaint();
511 return true;
514 return true;
517 void makeActiveItemVisible () {
518 if (mActiveItem < 0 || mActiveItem >= items.length || mLastHeight < 1) return;
519 auto pos = calcActiveItemPos();
520 if (!pos.valid) return;
521 auto oty = mTopY;
522 if (mTopY > pos.y) {
523 mTopY = pos.y;
524 } else if (mTopY+mLastHeight < pos.y+pos.hgt) {
525 mTopY = pos.y-(mLastHeight-pos.hgt);
526 if (mTopY < 0) mTopY = 0;
528 if (mTopY != oty) glconPostScreenRepaint();
531 // true: eaten
532 bool onKey (KeyEvent event) {
533 // jump to next contact with unread messages
534 if (event == "*-C-U") {
535 if (event == "D-C-U") {
536 foreach (immutable iidx, ListItemBase li; items) {
537 if (auto lc = cast(ListItemContact)li) {
538 if (lc.ct.kfd) continue;
539 if (lc.ct.unreadCount > 0) {
540 if (mActiveItem != iidx) {
541 mActiveItem = iidx;
542 makeActiveItemVisible();
543 glconPostScreenRepaint();
544 if (onActivateContactCB !is null) onActivateContactCB(lc.ct);
550 return true;
552 return false;
555 private float sbpos, sbsize; // set in `drawAt()`
557 // the following two should be called after `drawAt()`; if `sbSize()` is <0, draw nothing
558 final float sbPosition () pure const nothrow @safe @nogc => sbpos;
559 final float sbSize () pure const nothrow @safe @nogc => sbsize;
561 // real rect is scissored
562 void drawAt (NVGContext nvg, int x0, int y0, int wdt, int hgt) {
563 mLastX = x0;
564 mLastY = y0;
565 mLastHeight = hgt;
566 mLastWidth = wdt;
568 nvg.save();
569 scope(exit) nvg.restore();
571 setFont();
572 nvg.setupCtxFrom(fstash);
574 int totalHeight = calcTotalHeight();
575 if (mTopY > totalHeight-hgt) mTopY = totalHeight-hgt;
576 if (mTopY < 0) mTopY = 0;
578 int y = -mTopY;
579 foreach (immutable iidx, ListItemBase li; items) {
580 if (iidx != mActiveItem && !li.visible) continue;
581 li.setFont();
582 int lh = li.height;
583 if (lh < 1) continue;
584 if (y+lh > 0 && y < hgt) {
585 nvg.save();
586 scope(exit) nvg.restore();
587 version(all) {
588 nvg.intersectScissor(x0, y0+y, wdt, lh);
589 li.drawAt(nvg, x0, y0+y, wdt, (iidx == mActiveItem));
590 } else {
591 nvg.translate(x0+0.5f, y0+y+0.5f);
592 nvg.intersectScissor(0, 0, wdt, lh);
593 li.drawAt(nvg, 0, 0, wdt);
596 y += lh;
597 if (y >= hgt) break;
600 if (totalHeight > hgt) {
601 //nvg.bndScrollBar(x0+wdt-BND_SCROLLBAR_WIDTH+0.5f, y0+0.5f, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, mTopY/cast(float)(totalHeight-hgt), hgt/cast(float)totalHeight);
602 sbpos = mTopY/cast(float)(totalHeight-hgt);
603 sbsize = hgt/cast(float)totalHeight;
604 } else {
605 //nvg.bndScrollBar(x0+wdt-BND_SCROLLBAR_WIDTH+0.5f, y0+0.5f, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);
606 sbpos = 0;
607 sbsize = -1;