fixed (i REALLY hope so!) long-standing bug with urls becomes unclickable sometimes
[bioacid.git] / tkclist.d
blob738de8fcfdd779c2f51665dee44db3a687ad459a
1 /* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 module tkclist is aliced;
18 import std.datetime;
20 import arsd.color;
21 import arsd.image;
22 import arsd.simpledisplay;
24 import iv.cmdcon;
25 import iv.cmdcon.gl;
26 import iv.nanovega;
27 import iv.nanovega.blendish;
28 import iv.strex;
29 import iv.vfs.io;
31 import accdb;
32 import accobj;
33 import fonts;
34 import popups;
35 import icondata;
36 import notifyicon;
37 import toxproto;
39 import tkmain;
42 // ////////////////////////////////////////////////////////////////////////// //
43 __gshared int optCListWidth = -1;
44 __gshared bool optHRLastSeen = true;
47 // ////////////////////////////////////////////////////////////////////////// //
48 private extern(C) const(char)[] xfmt (const(char)* fmt, ...) {
49 import core.stdc.stdio : vsnprintf;
50 import core.stdc.stdarg;
51 static char[64] buf = void;
52 va_list ap;
53 va_start(ap, fmt);
54 auto len = vsnprintf(buf.ptr, buf.length, fmt, ap);
55 va_end(ap);
56 return buf[0..len];
60 // ////////////////////////////////////////////////////////////////////////// //
61 class ListItemBase {
62 private:
63 this (CList aowner) { owner = aowner; }
65 protected:
66 CList owner;
68 public:
69 @property Account ownerAcc () => null;
71 // setup font face for this item
72 void setFontAlign () { fstash.textAlign = NVGTextAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline); }
73 void setFont () { fstash.fontId = "ui"; fstash.size = 19; }
74 void setSmallFont () { fstash.fontId = "ui"; fstash.size = 15; }
76 @property int height () => cast(int)fstash.fontHeight;
77 @property bool visible () => true;
79 bool onMouse (MouseEvent event, bool meActive) => false; // true: eaten; will modify fstash
80 bool onKey (KeyEvent event, bool meActive) => false; // true: eaten; will modify fstash
82 protected void drawPrepare (NVGContext nvg) {
83 setFont();
84 setFontAlign();
85 nvg.setupCtxFrom(fstash);
88 // no need to save context state here
89 void drawAt (NVGContext nvg, int x0, int y0, int wdt, bool selected=false) {} // real rect is scissored
93 // ////////////////////////////////////////////////////////////////////////// //
94 class ListItemAccount : ListItemBase {
95 public:
96 Account acc;
98 public:
99 this (CList aowner, Account aAcc) { assert(aAcc !is null); acc = aAcc; super(aowner); }
101 override @property Account ownerAcc () => acc;
103 override void setFont () { fstash.fontId = "uib"; fstash.size = 20; }
105 override void drawAt (NVGContext nvg, int x0, int y0, int wdt, bool selected=false) {
106 drawPrepare(nvg);
108 int hgt = height;
109 nvg.newPath();
110 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
111 nvg.fillColor(selected ? NVGColor("#5ff0") : NVGColor("#5fff"));
112 nvg.fill();
114 final switch (acc.status) {
115 case ContactStatus.Connecting: nvg.fillColor(NVGColor.k8orange); break;
116 case ContactStatus.Offline: nvg.fillColor(NVGColor("#f00")); break;
117 case ContactStatus.Online: nvg.fillColor(NVGColor("#fff")); break;
118 case ContactStatus.Away: nvg.fillColor(NVGColor("#7557C7")); break;
119 case ContactStatus.Busy: nvg.fillColor(NVGColor("#0057C7")); break;
121 if (acc.isConnecting) nvg.fillColor(NVGColor.k8orange);
123 nvg.textAlign = NVGTextAlign.V.Baseline;
124 nvg.textAlign = NVGTextAlign.H.Center;
125 nvg.text(x0+wdt/2, y0+cast(int)nvg.textFontAscender, acc.info.nick);
130 // ////////////////////////////////////////////////////////////////////////// //
131 class ListItemGroup : ListItemBase {
132 public:
133 Group group;
135 public:
136 this (CList aowner, Group aGroup) { assert(aGroup !is null); group = aGroup; super(aowner); }
138 override @property Account ownerAcc () => group.acc;
140 override @property bool visible () => group.visible;
142 // true: eaten
143 override bool onMouse (MouseEvent event, bool meActive) {
144 if (event == "LMB-Down") {
145 //conwriteln("!!! ", group.opened);
146 group.opened = !group.opened;
147 glconPostScreenRepaint();
148 return true;
150 return false;
153 override void drawAt (NVGContext nvg, int x0, int y0, int wdt, bool selected=false) {
154 drawPrepare(nvg);
156 int hgt = height;
157 nvg.newPath();
158 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
159 nvg.fillColor(selected ? NVGColor("#5880") : NVGColor("#5888"));
160 nvg.fill();
162 nvg.fillColor(selected ? NVGColor.white : NVGColor.yellow);
163 nvg.textAlign = NVGTextAlign.V.Baseline;
164 nvg.textAlign = NVGTextAlign.H.Center;
165 nvg.text(x0+wdt/2, y0+cast(int)nvg.textFontAscender, group.name);
170 // ////////////////////////////////////////////////////////////////////////// //
171 class ListItemContact : ListItemBase {
172 public:
173 Contact ct;
175 public:
176 this (CList aowner, Contact aCt) { assert(aCt !is null); ct = aCt; super(aowner); }
178 override @property Account ownerAcc () => ct.acc;
180 override @property bool visible () => ct.visible;
182 //FIXME: 16 is icon height
183 override @property int height () {
184 import std.algorithm : max;
185 setFont();
186 auto hgt = fstash.fontHeight;
187 setSmallFont();
188 if (ct.info.statusmsg.length) hgt += fstash.fontHeight; // status message
189 if (!ct.online) hgt += fstash.fontHeight; // last seen
190 return max(cast(int)hgt, 16);
193 // true: eaten
194 override bool onMouse (MouseEvent event, bool meActive) {
195 if (event == "LMB-Down") {
196 owner.activeItem = this;
197 if (owner.onActivateContactCB !is null) owner.onActivateContactCB(ct);
198 glconPostScreenRepaint();
199 return true;
201 return false;
204 override void drawAt (NVGContext nvg, int x0, int y0, int wdt, bool selected=false) {
205 import std.algorithm : max;
207 drawPrepare(nvg);
209 int hgt = max(cast(int)nvg.textFontHeight, 16);
211 if (selected) {
212 int fullhgt = height;
213 nvg.newPath();
214 nvg.rect(x0+0.5f, y0+0.5f, wdt, fullhgt);
215 nvg.fillColor(NVGColor("#9600"));
216 nvg.fill();
219 // draw icon
220 nvg.newPath();
221 int iw, ih;
222 NVGImage icon;
224 if (ct.requestPending) {
225 icon = kittyOut;
226 } else if (ct.acceptPending) {
227 icon = kittyFish;
228 } else {
229 if (ct.unreadCount == 0) icon = statusImgId[ct.status]; else icon = kittyMsg;
232 nvg.imageSize(icon, iw, ih);
233 nvg.rect(x0, y0+(hgt-ih)/2, iw, ih);
234 nvg.fillPaint(nvg.imagePattern(x0, y0+(hgt-ih)/2, iw, ih, 0, icon));
235 nvg.fill();
237 auto ty = y0+hgt/2;
239 nvg.fillColor(NVGColor("#f70"));
240 //nvg.textAlign = NVGTextAlign.V.Top;
241 nvg.textAlign = NVGTextAlign.V.Middle;
242 nvg.textAlign = NVGTextAlign.H.Left;
243 nvg.text(x0+4+iw+4, ty, ct.displayNick);
245 // draw last status and lastseen
246 setSmallFont();
247 nvg.setupCtxFrom(fstash);
248 ty = y0+hgt+cast(int)nvg.textFontHeight/2;
249 //ty += cast(int)nvg.textFontDescender;
251 // draw status text
252 if (ct.info.statusmsg.length) {
253 nvg.fillColor = NVGColor("#777");
254 nvg.textAlign = NVGTextAlign.H.Left;
255 nvg.text(x0+4+iw+4, ty, ct.info.statusmsg);
256 ty += cast(int)nvg.textFontHeight;
259 // draw lastseen
260 if (!ct.online) {
261 nvg.fillColor = NVGColor("#b30");
262 nvg.textAlign = NVGTextAlign.H.Right;
263 if (ct.info.lastonlinetime == 0) {
264 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: never");
265 } else {
266 if (optHRLastSeen) {
267 enum OneDay = 60*60*24;
268 auto nowut = Clock.currTime.toUnixTime/OneDay;
269 auto ltt = ct.info.lastonlinetime/OneDay;
270 if (ltt >= nowut) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: today");
271 else if (nowut-ltt == 1) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: yesterday");
272 else if (nowut-ltt <= 15) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u days ago", cast(int)(nowut-ltt)));
273 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));
274 else if (nowut-ltt <= 31) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: month ago");
275 else if (nowut-ltt < 365) {
276 if ((nowut-ltt)%31 <= 15) {
277 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u month ago", cast(int)(nowut-ltt)/31));
278 } else {
279 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u.5 month ago", cast(int)(nowut-ltt)/31));
281 } else {
282 if ((nowut-ltt)%365 < 365/10) {
283 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u years ago", cast(int)(nowut-ltt)/365));
284 } else {
285 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u.%u years ago", cast(int)(nowut-ltt)/365, cast(int)(nowut-ltt)%365/(365/10)));
288 } else {
289 SysTime dt = SysTime.fromUnixTime(ct.info.lastonlinetime);
290 //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);
291 //nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, buf[0..len]);
292 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));
300 // ////////////////////////////////////////////////////////////////////////// //
301 // visible contact list
302 final class CList {
303 private import core.time;
305 private:
306 int mActiveItem = -1; // active item (may be different from selected with cursor)
307 int mTopY;
308 int mLastX = -666, mLastY = -666, mLastHeight, mLastWidth;
309 //MonoTime mLastClick = MonoTime.zero;
310 //int mLastClickItem = -1;
312 public:
313 ListItemBase[] items;
315 public:
316 this () {}
318 // the first one is "main"; can return `null`
319 Account mainAccount () {
320 foreach (ListItemBase li; items) if (auto lc = cast(ListItemAccount)li) return lc.acc;
321 return null;
324 // the first one is "main"; can return `null`
325 Account accountByPK (in ref PubKey pk) {
326 foreach (ListItemBase li; items) {
327 if (auto lc = cast(ListItemAccount)li) {
328 if (lc.acc.toxpk[] == pk[]) return lc.acc;
331 return null;
334 void forEachAccount (scope void delegate (Account acc) dg) {
335 if (dg is null) return;
336 foreach (ListItemBase li; items) if (auto lc = cast(ListItemAccount)li) dg(lc.acc);
339 void opOpAssign(string op:"~") (ListItemBase li) {
340 if (li is null) return;
341 items ~= li;
344 void setFont () {
345 fstash.fontId = "ui";
346 fstash.size = 20;
347 //fstash.blur = 0;
348 fstash.textAlign = NVGTextAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
351 void removeAccount (Account acc) {
352 if (acc is null) return;
353 usize pos = 0, dest = 0;
354 while (pos < items.length) {
355 if (items[pos].ownerAcc !is acc) {
356 if (pos != dest) items[dest++] = items[pos];
358 ++pos;
360 items.length = dest;
363 void buildAccount (Account acc) {
364 if (acc is null) return;
365 //FIXME
366 mActiveItem = -1;
367 mTopY = 0;
368 removeAccount(acc);
369 items ~= new ListItemAccount(this, acc);
370 Contact[] css;
371 scope(exit) delete css;
372 foreach (Group g; acc.groups) {
373 items ~= new ListItemGroup(this, g);
374 css.length = 0;
375 css.assumeSafeAppend;
376 foreach (Contact c; acc.contacts.byValue) if (c.gid == g.gid) css ~= c;
377 import std.algorithm : sort;
378 css.sort!((a, b) {
379 string s0 = a.displayNick;
380 string s1 = b.displayNick;
381 auto xlen = (s0.length < s1.length ? s0.length : s1.length);
382 foreach (immutable idx, char c0; s0[0..xlen]) {
383 if (c0 >= 'A' && c0 <= 'Z') c0 += 32; // poor man's tolower()
384 char c1 = s1[idx];
385 if (c1 >= 'A' && c1 <= 'Z') c1 += 32; // poor man's tolower()
386 if (auto d = c0-c1) return (d < 0);
388 return (s0.length < s1.length);
390 foreach (Contact c; css) items ~= new ListItemContact(this, c);
394 // -1: oops
395 // should be called after clist was drawn at least once
396 int itemAtY (int aty) {
397 if (aty < 0 || mLastHeight < 1 || aty >= mLastHeight) return -1;
398 aty += mTopY;
399 setFont();
400 foreach (immutable iidx, ListItemBase li; items) {
401 if (iidx != mActiveItem && !li.visible) continue;
402 li.setFont();
403 int lh = li.height;
404 if (lh < 1) continue;
405 if (aty < lh) return cast(int)iidx;
406 aty -= lh;
408 return -1;
411 final int calcTotalHeight () {
412 setFont();
413 int totalHeight = 0;
414 foreach (immutable iidx, ListItemBase li; items) {
415 if (iidx != mActiveItem && !li.visible) continue;
416 li.setFont();
417 int lh = li.height;
418 if (lh < 1) continue;
419 totalHeight += lh;
421 return totalHeight;
424 static struct AIPos {
425 int y, hgt;
426 @property bool valid () const pure nothrow @safe @nogc => (hgt > 0);
429 AIPos calcActiveItemPos () {
430 AIPos res;
431 if (mActiveItem < 0 || mActiveItem >= items.length) return res;
432 setFont();
433 foreach (immutable iidx, ListItemBase li; items) {
434 if (iidx != mActiveItem && !li.visible) continue;
435 li.setFont();
436 int lh = li.height;
437 if (lh < 1) continue;
438 if (iidx == mActiveItem) { res.hgt = lh; return res; }
439 res.y += lh;
441 res = res.init;
442 return res;
445 final void resetActiveItem () nothrow @safe @nogc { mActiveItem = -1; }
447 final @property int activeItemIndex () const pure nothrow @safe @nogc => mActiveItem;
449 final @property void activeItemIndex (int idx) nothrow @trusted {
450 if (idx < 0 || idx >= items.length) idx = -1;
451 if (mActiveItem == idx) return;
452 mActiveItem = idx;
453 try { glconPostScreenRepaint(); } catch (Exception e) {} // sorry
456 final @property inout(ListItemBase) activeItem () inout pure nothrow @trusted @nogc {
457 pragma(inline, true);
458 return (mActiveItem >= 0 && mActiveItem < items.length ? cast(inout)items[mActiveItem] : null);
461 final @property void activeItem (ListItemBase it) nothrow @trusted {
462 int idx = -1;
463 if (it !is null) {
464 foreach (immutable ii, const ListItemBase li; items) {
465 if (li is it) { idx = cast(int)ii; break; }
468 activeItemIndex = idx;
471 final bool isActiveContact (in ref PubKey pk) const nothrow @trusted {
472 if (mActiveItem < 0 || mActiveItem >= items.length) return false;
473 if (auto ct = cast(const(ListItemContact))items[mActiveItem]) return (ct.ct.info.pubkey[] == pk[]);
474 return false;
477 // if called with `null` ct, deactivate
478 void delegate (Contact ct) onActivateContactCB;
480 // true: eaten
481 bool onMouse (MouseEvent event) {
482 if (mLastWidth < 1 || mLastHeight < 1) return false;
483 int mx = event.x-mLastX;
484 int my = event.y-mLastY;
485 if (mx < 0 || my < 0 || mx >= mLastWidth || my >= mLastHeight) return false;
487 int it = itemAtY(my);
488 if (it >= 0) {
489 if (items[it].onMouse(event, it == mActiveItem)) return true;
492 enum ScrollHeight = 32;
494 if (event == "WheelUp") {
495 if (mTopY > 0) {
496 mTopY -= ScrollHeight;
497 if (mTopY < 0) mTopY = 0;
498 glconPostScreenRepaint();
500 return true;
503 if (event == "WheelDown") {
504 auto th = calcTotalHeight()-mLastHeight;
505 if (th > 0 && mTopY < th) {
506 mTopY += ScrollHeight;
507 if (mTopY > th) mTopY = th;
508 glconPostScreenRepaint();
510 return true;
513 return true;
516 void makeActiveItemVisible () {
517 if (mActiveItem < 0 || mActiveItem >= items.length || mLastHeight < 1) return;
518 auto pos = calcActiveItemPos();
519 if (!pos.valid) return;
520 auto oty = mTopY;
521 if (mTopY > pos.y) {
522 mTopY = pos.y;
523 } else if (mTopY+mLastHeight < pos.y+pos.hgt) {
524 mTopY = pos.y-(mLastHeight-pos.hgt);
525 if (mTopY < 0) mTopY = 0;
527 if (mTopY != oty) glconPostScreenRepaint();
530 // true: eaten
531 bool onKey (KeyEvent event) {
532 // jump to next contact with unread messages
533 if (event == "*-C-U") {
534 if (event == "D-C-U") {
535 foreach (immutable iidx, ListItemBase li; items) {
536 if (auto lc = cast(ListItemContact)li) {
537 if (lc.ct.kfd) continue;
538 if (lc.ct.unreadCount > 0) {
539 if (mActiveItem != iidx) {
540 mActiveItem = iidx;
541 makeActiveItemVisible();
542 glconPostScreenRepaint();
543 if (onActivateContactCB !is null) onActivateContactCB(lc.ct);
549 return true;
551 return false;
554 private float sbpos, sbsize; // set in `drawAt()`
556 // the following two should be called after `drawAt()`; if `sbSize()` is <0, draw nothing
557 final float sbPosition () pure const nothrow @safe @nogc => sbpos;
558 final float sbSize () pure const nothrow @safe @nogc => sbsize;
560 // real rect is scissored
561 void drawAt (NVGContext nvg, int x0, int y0, int wdt, int hgt) {
562 mLastX = x0;
563 mLastY = y0;
564 mLastHeight = hgt;
565 mLastWidth = wdt;
567 nvg.save();
568 scope(exit) nvg.restore();
570 setFont();
571 nvg.setupCtxFrom(fstash);
573 int totalHeight = calcTotalHeight();
574 if (mTopY > totalHeight-hgt) mTopY = totalHeight-hgt;
575 if (mTopY < 0) mTopY = 0;
577 int y = -mTopY;
578 foreach (immutable iidx, ListItemBase li; items) {
579 if (iidx != mActiveItem && !li.visible) continue;
580 li.setFont();
581 int lh = li.height;
582 if (lh < 1) continue;
583 if (y+lh > 0 && y < hgt) {
584 nvg.save();
585 scope(exit) nvg.restore();
586 version(all) {
587 nvg.intersectScissor(x0, y0+y, wdt, lh);
588 li.drawAt(nvg, x0, y0+y, wdt, (iidx == mActiveItem));
589 } else {
590 nvg.translate(x0+0.5f, y0+y+0.5f);
591 nvg.intersectScissor(0, 0, wdt, lh);
592 li.drawAt(nvg, 0, 0, wdt);
595 y += lh;
596 if (y >= hgt) break;
599 if (totalHeight > hgt) {
600 //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);
601 sbpos = mTopY/cast(float)(totalHeight-hgt);
602 sbsize = hgt/cast(float)totalHeight;
603 } else {
604 //nvg.bndScrollBar(x0+wdt-BND_SCROLLBAR_WIDTH+0.5f, y0+0.5f, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);
605 sbpos = 0;
606 sbsize = -1;