added text selection, quoting, and copying selected text to the clipboard
[bioacid.git] / tkclist.d
blob1d37b215213bdd76cb3a2072b2c9d6fc087a1249
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);
167 // opened/closed sign
168 //string xop = (group.opened ? "\u2bde" : "\u2bdf");
169 // alas: pt sans doesn't have alot of special symbols
170 string xop = (group.opened ? "\u221a" : "\u2248");
171 float xopwdt = nvg.textWidth(xop);
172 nvg.fillColor(group.opened ? NVGColor("#aaa") : NVGColor("#666"));
173 nvg.text(x0+wdt-2-xopwdt, y0+cast(int)nvg.textFontAscender, xop);
178 // ////////////////////////////////////////////////////////////////////////// //
179 class ListItemContact : ListItemBase {
180 public:
181 Contact ct;
183 public:
184 this (CList aowner, Contact aCt) { assert(aCt !is null); ct = aCt; super(aowner); }
186 override @property Account ownerAcc () => ct.acc;
188 override @property bool visible () => ct.visible;
190 //FIXME: 16 is icon height
191 override @property int height () {
192 import std.algorithm : max;
193 setFont();
194 auto hgt = fstash.fontHeight;
195 setSmallFont();
196 if (ct.info.statusmsg.length) hgt += fstash.fontHeight; // status message
197 if (!ct.online) hgt += fstash.fontHeight; // last seen
198 return max(cast(int)hgt, 16);
201 // true: eaten
202 override bool onMouse (MouseEvent event, bool meActive) {
203 if (event == "LMB-Down") {
204 owner.activeItem = this;
205 if (owner.onActivateContactCB !is null) owner.onActivateContactCB(ct);
206 glconPostScreenRepaint();
207 return true;
209 return false;
212 override void drawAt (NVGContext nvg, int x0, int y0, int wdt, bool selected=false) {
213 import std.algorithm : max;
215 drawPrepare(nvg);
217 int hgt = max(cast(int)nvg.textFontHeight, 16);
219 if (selected) {
220 int fullhgt = height;
221 nvg.newPath();
222 nvg.rect(x0+0.5f, y0+0.5f, wdt, fullhgt);
223 nvg.fillColor(NVGColor("#9600"));
224 nvg.fill();
227 // draw icon
228 nvg.newPath();
229 int iw, ih;
230 NVGImage icon;
232 if (ct.requestPending) {
233 icon = kittyOut;
234 } else if (ct.acceptPending) {
235 icon = kittyFish;
236 } else {
237 if (ct.unreadCount == 0) icon = statusImgId[ct.status]; else icon = kittyMsg;
240 nvg.imageSize(icon, iw, ih);
241 nvg.rect(x0, y0+(hgt-ih)/2, iw, ih);
242 nvg.fillPaint(nvg.imagePattern(x0, y0+(hgt-ih)/2, iw, ih, 0, icon));
243 nvg.fill();
245 auto ty = y0+hgt/2;
247 nvg.fillColor(NVGColor("#f70"));
248 //nvg.textAlign = NVGTextAlign.V.Top;
249 nvg.textAlign = NVGTextAlign.V.Middle;
250 nvg.textAlign = NVGTextAlign.H.Left;
251 nvg.text(x0+4+iw+4, ty, ct.displayNick);
253 // draw last status and lastseen
254 setSmallFont();
255 nvg.setupCtxFrom(fstash);
256 ty = y0+hgt+cast(int)nvg.textFontHeight/2;
257 //ty += cast(int)nvg.textFontDescender;
259 // draw status text
260 if (ct.info.statusmsg.length) {
261 nvg.fillColor = NVGColor("#777");
262 nvg.textAlign = NVGTextAlign.H.Left;
263 nvg.text(x0+4+iw+4, ty, ct.info.statusmsg);
264 ty += cast(int)nvg.textFontHeight;
267 // draw lastseen
268 if (!ct.online) {
269 nvg.fillColor = NVGColor("#b30");
270 nvg.textAlign = NVGTextAlign.H.Right;
271 if (ct.info.lastonlinetime == 0) {
272 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: never");
273 } else {
274 if (optHRLastSeen) {
275 enum OneDay = 60*60*24;
276 auto nowut = Clock.currTime.toUnixTime/OneDay;
277 auto ltt = ct.info.lastonlinetime/OneDay;
278 if (ltt >= nowut) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: today");
279 else if (nowut-ltt == 1) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: yesterday");
280 else if (nowut-ltt <= 15) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u days ago", cast(int)(nowut-ltt)));
281 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));
282 else if (nowut-ltt <= 31) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: month ago");
283 else if (nowut-ltt < 365) {
284 if ((nowut-ltt)%31 <= 15) {
285 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u month ago", cast(int)(nowut-ltt)/31));
286 } else {
287 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u.5 month ago", cast(int)(nowut-ltt)/31));
289 } else {
290 if ((nowut-ltt)%365 < 365/10) {
291 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u years ago", cast(int)(nowut-ltt)/365));
292 } else {
293 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)));
296 } else {
297 SysTime dt = SysTime.fromUnixTime(ct.info.lastonlinetime);
298 //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);
299 //nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, buf[0..len]);
300 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));
308 // ////////////////////////////////////////////////////////////////////////// //
309 // visible contact list
310 final class CList {
311 private import core.time;
313 private:
314 int mActiveItem = -1; // active item (may be different from selected with cursor)
315 int mTopY;
316 int mLastX = -666, mLastY = -666, mLastHeight, mLastWidth;
317 //MonoTime mLastClick = MonoTime.zero;
318 //int mLastClickItem = -1;
320 public:
321 ListItemBase[] items;
323 public:
324 this () {}
326 // the first one is "main"; can return `null`
327 Account mainAccount () {
328 foreach (ListItemBase li; items) if (auto lc = cast(ListItemAccount)li) return lc.acc;
329 return null;
332 // the first one is "main"; can return `null`
333 Account accountByPK (in ref PubKey pk) {
334 foreach (ListItemBase li; items) {
335 if (auto lc = cast(ListItemAccount)li) {
336 if (lc.acc.toxpk[] == pk[]) return lc.acc;
339 return null;
342 void forEachAccount (scope void delegate (Account acc) dg) {
343 if (dg is null) return;
344 foreach (ListItemBase li; items) if (auto lc = cast(ListItemAccount)li) dg(lc.acc);
347 void opOpAssign(string op:"~") (ListItemBase li) {
348 if (li is null) return;
349 items ~= li;
352 void setFont () {
353 fstash.fontId = "ui";
354 fstash.size = 20;
355 //fstash.blur = 0;
356 fstash.textAlign = NVGTextAlign(NVGTextAlign.H.Left, NVGTextAlign.V.Baseline);
359 void removeAccount (Account acc) {
360 if (acc is null) return;
361 usize pos = 0, dest = 0;
362 while (pos < items.length) {
363 if (items[pos].ownerAcc !is acc) {
364 if (pos != dest) items[dest++] = items[pos];
366 ++pos;
368 items.length = dest;
371 void buildAccount (Account acc) {
372 if (acc is null) return;
373 //FIXME
374 mActiveItem = -1;
375 mTopY = 0;
376 removeAccount(acc);
377 items ~= new ListItemAccount(this, acc);
378 Contact[] css;
379 scope(exit) delete css;
380 foreach (Group g; acc.groups) {
381 items ~= new ListItemGroup(this, g);
382 css.length = 0;
383 css.assumeSafeAppend;
384 foreach (Contact c; acc.contacts.byValue) if (c.gid == g.gid) css ~= c;
385 import std.algorithm : sort;
386 css.sort!((a, b) {
387 string s0 = a.displayNick;
388 string s1 = b.displayNick;
389 auto xlen = (s0.length < s1.length ? s0.length : s1.length);
390 foreach (immutable idx, char c0; s0[0..xlen]) {
391 if (c0 >= 'A' && c0 <= 'Z') c0 += 32; // poor man's tolower()
392 char c1 = s1[idx];
393 if (c1 >= 'A' && c1 <= 'Z') c1 += 32; // poor man's tolower()
394 if (auto d = c0-c1) return (d < 0);
396 return (s0.length < s1.length);
398 foreach (Contact c; css) items ~= new ListItemContact(this, c);
402 // -1: oops
403 // should be called after clist was drawn at least once
404 int itemAtY (int aty) {
405 if (aty < 0 || mLastHeight < 1 || aty >= mLastHeight) return -1;
406 aty += mTopY;
407 setFont();
408 foreach (immutable iidx, ListItemBase li; items) {
409 if (iidx != mActiveItem && !li.visible) continue;
410 li.setFont();
411 int lh = li.height;
412 if (lh < 1) continue;
413 if (aty < lh) return cast(int)iidx;
414 aty -= lh;
416 return -1;
419 final int calcTotalHeight () {
420 setFont();
421 int totalHeight = 0;
422 foreach (immutable iidx, ListItemBase li; items) {
423 if (iidx != mActiveItem && !li.visible) continue;
424 li.setFont();
425 int lh = li.height;
426 if (lh < 1) continue;
427 totalHeight += lh;
429 return totalHeight;
432 static struct AIPos {
433 int y, hgt;
434 @property bool valid () const pure nothrow @safe @nogc => (hgt > 0);
437 AIPos calcActiveItemPos () {
438 AIPos res;
439 if (mActiveItem < 0 || mActiveItem >= items.length) return res;
440 setFont();
441 foreach (immutable iidx, ListItemBase li; items) {
442 if (iidx != mActiveItem && !li.visible) continue;
443 li.setFont();
444 int lh = li.height;
445 if (lh < 1) continue;
446 if (iidx == mActiveItem) { res.hgt = lh; return res; }
447 res.y += lh;
449 res = res.init;
450 return res;
453 final void resetActiveItem () nothrow @safe @nogc { mActiveItem = -1; }
455 final @property int activeItemIndex () const pure nothrow @safe @nogc => mActiveItem;
457 final @property void activeItemIndex (int idx) nothrow @trusted {
458 if (idx < 0 || idx >= items.length) idx = -1;
459 if (mActiveItem == idx) return;
460 mActiveItem = idx;
461 try { glconPostScreenRepaint(); } catch (Exception e) {} // sorry
464 final @property inout(ListItemBase) activeItem () inout pure nothrow @trusted @nogc {
465 pragma(inline, true);
466 return (mActiveItem >= 0 && mActiveItem < items.length ? cast(inout)items[mActiveItem] : null);
469 final @property void activeItem (ListItemBase it) nothrow @trusted {
470 int idx = -1;
471 if (it !is null) {
472 foreach (immutable ii, const ListItemBase li; items) {
473 if (li is it) { idx = cast(int)ii; break; }
476 activeItemIndex = idx;
479 final bool isActiveContact (in ref PubKey pk) const nothrow @trusted {
480 if (mActiveItem < 0 || mActiveItem >= items.length) return false;
481 if (auto ct = cast(const(ListItemContact))items[mActiveItem]) return (ct.ct.info.pubkey[] == pk[]);
482 return false;
485 // if called with `null` ct, deactivate
486 void delegate (Contact ct) onActivateContactCB;
488 // true: eaten
489 bool onMouse (MouseEvent event) {
490 if (mLastWidth < 1 || mLastHeight < 1) return false;
491 int mx = event.x-mLastX;
492 int my = event.y-mLastY;
493 if (mx < 0 || my < 0 || mx >= mLastWidth || my >= mLastHeight) return false;
495 int it = itemAtY(my);
496 if (it >= 0) {
497 if (items[it].onMouse(event, it == mActiveItem)) return true;
500 enum ScrollHeight = 32;
502 if (event == "WheelUp") {
503 if (mTopY > 0) {
504 mTopY -= ScrollHeight;
505 if (mTopY < 0) mTopY = 0;
506 glconPostScreenRepaint();
508 return true;
511 if (event == "WheelDown") {
512 auto th = calcTotalHeight()-mLastHeight;
513 if (th > 0 && mTopY < th) {
514 mTopY += ScrollHeight;
515 if (mTopY > th) mTopY = th;
516 glconPostScreenRepaint();
518 return true;
521 return true;
524 void makeActiveItemVisible () {
525 if (mActiveItem < 0 || mActiveItem >= items.length || mLastHeight < 1) return;
526 auto pos = calcActiveItemPos();
527 if (!pos.valid) return;
528 auto oty = mTopY;
529 if (mTopY > pos.y) {
530 mTopY = pos.y;
531 } else if (mTopY+mLastHeight < pos.y+pos.hgt) {
532 mTopY = pos.y-(mLastHeight-pos.hgt);
533 if (mTopY < 0) mTopY = 0;
535 if (mTopY != oty) glconPostScreenRepaint();
538 // true: eaten
539 bool onKey (KeyEvent event) {
540 // jump to next contact with unread messages
541 if (event == "*-C-U") {
542 if (event == "D-C-U") {
543 foreach (immutable iidx, ListItemBase li; items) {
544 if (auto lc = cast(ListItemContact)li) {
545 if (lc.ct.kfd) continue;
546 if (lc.ct.unreadCount > 0) {
547 if (mActiveItem != iidx) {
548 mActiveItem = iidx;
549 makeActiveItemVisible();
550 glconPostScreenRepaint();
551 if (onActivateContactCB !is null) onActivateContactCB(lc.ct);
552 return true; // oops; stop right here ;-)
558 return true;
560 return false;
563 private float sbpos, sbsize; // set in `drawAt()`
565 // the following two should be called after `drawAt()`; if `sbSize()` is <0, draw nothing
566 final float sbPosition () pure const nothrow @safe @nogc => sbpos;
567 final float sbSize () pure const nothrow @safe @nogc => sbsize;
569 // real rect is scissored
570 void drawAt (NVGContext nvg, int x0, int y0, int wdt, int hgt) {
571 mLastX = x0;
572 mLastY = y0;
573 mLastHeight = hgt;
574 mLastWidth = wdt;
576 nvg.save();
577 scope(exit) nvg.restore();
579 setFont();
580 nvg.setupCtxFrom(fstash);
582 int totalHeight = calcTotalHeight();
583 if (mTopY > totalHeight-hgt) mTopY = totalHeight-hgt;
584 if (mTopY < 0) mTopY = 0;
586 int y = -mTopY;
587 foreach (immutable iidx, ListItemBase li; items) {
588 if (iidx != mActiveItem && !li.visible) continue;
589 li.setFont();
590 int lh = li.height;
591 if (lh < 1) continue;
592 if (y+lh > 0 && y < hgt) {
593 nvg.save();
594 scope(exit) nvg.restore();
595 version(all) {
596 nvg.intersectScissor(x0, y0+y, wdt, lh);
597 li.drawAt(nvg, x0, y0+y, wdt, (iidx == mActiveItem));
598 } else {
599 nvg.translate(x0+0.5f, y0+y+0.5f);
600 nvg.intersectScissor(0, 0, wdt, lh);
601 li.drawAt(nvg, 0, 0, wdt);
604 y += lh;
605 if (y >= hgt) break;
608 if (totalHeight > hgt) {
609 //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);
610 sbpos = mTopY/cast(float)(totalHeight-hgt);
611 sbsize = hgt/cast(float)totalHeight;
612 } else {
613 //nvg.bndScrollBar(x0+wdt-BND_SCROLLBAR_WIDTH+0.5f, y0+0.5f, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);
614 sbpos = 0;
615 sbsize = -1;