slightly nicer (so-called "human friendly") lastseen display
[bioacid.git] / tkclist.d
blob95c8835f388618073c67753b0017065f9c6d10be
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 class ListItemBase {
50 private:
51 this () {}
53 protected:
54 bool mVisible = true;
56 public:
57 void setupFont () => nvg.fontFace = "ui"; // setup font face for this item
58 @property int height () => cast(int)nvg.textFontHeight;
59 @property bool visible () => mVisible;
60 bool onMouse (MouseEvent event) => false; // true: eaten
61 bool onKey (KeyEvent event) => false; // true: eaten
62 void drawAt (int x0, int y0, int wdt, bool selected=false) {} // real rect is scissored
64 @property Account ownerAcc () => null;
68 // ////////////////////////////////////////////////////////////////////////// //
69 class ListItemAccount : ListItemBase {
70 public:
71 Account acc;
73 public:
74 this (Account aAcc) { assert(aAcc !is null); acc = aAcc; }
76 override @property Account ownerAcc () => acc;
78 override void setupFont () => nvg.fontFace = "uib"; // bold
80 override void drawAt (int x0, int y0, int wdt, bool selected=false) {
81 int hgt = height;
83 nvg.newPath();
84 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
85 nvg.fillColor(selected ? NVGColor("#5ff0") : NVGColor("#5fff"));
86 nvg.fill();
88 final switch (acc.status) {
89 case ContactStatus.Connecting: nvg.fillColor(NVGColor.k8orange); break;
90 case ContactStatus.Offline: nvg.fillColor(NVGColor("#f00")); break;
91 case ContactStatus.Online: nvg.fillColor(NVGColor("#fff")); break;
92 case ContactStatus.Away: nvg.fillColor(NVGColor("#7557C7")); break;
93 case ContactStatus.Busy: nvg.fillColor(NVGColor("#0057C7")); break;
95 if (acc.isConnecting) nvg.fillColor(NVGColor.k8orange);
96 nvg.textAlign = NVGTextAlign.V.Baseline;
97 nvg.textAlign = NVGTextAlign.H.Center;
98 nvg.text(x0+wdt/2, y0+cast(int)nvg.textFontAscender, acc.info.nick);
103 // ////////////////////////////////////////////////////////////////////////// //
104 class ListItemGroup : ListItemBase {
105 public:
106 Group group;
108 public:
109 this (Group aGroup) { assert(aGroup !is null); group = aGroup; }
111 override @property Account ownerAcc () => group.acc;
113 override @property bool visible () => group.visible;
115 override void drawAt (int x0, int y0, int wdt, bool selected=false) {
116 int hgt = height;
118 nvg.newPath();
119 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
120 nvg.fillColor(selected ? NVGColor("#5880") : NVGColor("#5888"));
121 nvg.fill();
123 nvg.fillColor(selected ? NVGColor.white : NVGColor.yellow);
124 nvg.textAlign = NVGTextAlign.V.Baseline;
125 nvg.textAlign = NVGTextAlign.H.Center;
126 nvg.text(x0+wdt/2, y0+cast(int)nvg.textFontAscender, group.name);
131 // ////////////////////////////////////////////////////////////////////////// //
132 private extern(C) const(char)[] xfmt (const(char)* fmt, ...) {
133 import core.stdc.stdio : vsnprintf;
134 import core.stdc.stdarg;
135 static char[64] buf = void;
136 va_list ap;
137 va_start(ap, fmt);
138 auto len = vsnprintf(buf.ptr, buf.length, fmt, ap);
139 va_end(ap);
140 return buf[0..len];
144 // ////////////////////////////////////////////////////////////////////////// //
145 class ListItemContact : ListItemBase {
146 public:
147 Contact ct;
149 public:
150 this (Contact aCt) { assert(aCt !is null); ct = aCt; }
152 override @property Account ownerAcc () => ct.acc;
154 override @property bool visible () => ct.visible;
156 final void setSmallFont () {
157 auto fsz = nvg.fontSize;
158 auto hgt = nvg.textFontHeight;
159 auto nsz = cast(int)(fsz-fsz/4);
160 if (nsz < 6) nsz = 6;
161 if (nsz > fsz) nsz = cast(int)fsz;
162 nvg.fontSize = nsz;
165 override @property int height () {
166 import std.algorithm : max;
167 //FIXME: 16 is image height
168 auto hgt = nvg.textFontHeight;
169 auto ofsz = nvg.fontSize;
170 setSmallFont();
171 hgt += nvg.textFontHeight;
172 nvg.fontSize = ofsz;
173 return max(cast(int)hgt, 16);
176 override void drawAt (int x0, int y0, int wdt, bool selected=false) {
177 import std.algorithm : max;
178 int hgt = max(cast(int)nvg.textFontHeight, 16);
180 if (selected) {
181 int fullhgt = height;
182 nvg.newPath();
183 nvg.rect(x0+0.5f, y0+0.5f, wdt, fullhgt);
184 nvg.fillColor(NVGColor("#9600"));
185 nvg.fill();
188 // draw icon
189 nvg.newPath();
190 int iw, ih;
191 NVGImage icon;
193 if (ct.requestPending) {
194 icon = kittyOut;
195 } else if (ct.acceptPending) {
196 icon = kittyFish;
197 } else {
198 if (ct.unreadCount == 0) icon = statusImgId[ct.status]; else icon = kittyMsg;
201 nvg.imageSize(icon, iw, ih);
202 nvg.rect(x0, y0+(hgt-ih)/2, iw, ih);
203 nvg.fillPaint(nvg.imagePattern(x0, y0+(hgt-ih)/2, iw, ih, 0, icon));
204 nvg.fill();
206 auto ty = y0+hgt/2;
208 nvg.fillColor(NVGColor("#f70"));
209 //nvg.textAlign = NVGTextAlign.V.Top;
210 nvg.textAlign = NVGTextAlign.V.Middle;
211 nvg.textAlign = NVGTextAlign.H.Left;
212 nvg.text(x0+4+iw+4, ty, ct.displayNick);
214 // draw last seen time
215 setSmallFont();
216 ty = y0+hgt+cast(int)nvg.textFontHeight/2;
217 //ty += cast(int)nvg.textFontDescender;
218 nvg.fillColor(NVGColor("#b30"));
219 nvg.textAlign = NVGTextAlign.H.Right;
220 if (ct.info.lastonlinetime == 0) {
221 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: never");
222 } else {
223 if (optHRLastSeen) {
224 enum OneDay = 60*60*24;
225 auto nowut = Clock.currTime.toUnixTime/OneDay;
226 auto ltt = ct.info.lastonlinetime/OneDay;
227 if (ltt >= nowut) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: today");
228 else if (nowut-ltt == 1) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: yesterday");
229 else if (nowut-ltt <= 15) nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u days ago", cast(int)nowut-ltt));
230 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));
231 else if (nowut-ltt < 365) {
232 if ((nowut-ltt)%31 <= 15) {
233 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u month ago", cast(int)(nowut-ltt)/31));
234 } else {
235 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u.5 month ago", cast(int)(nowut-ltt)/31));
237 } else {
238 if ((nowut-ltt)%365 < 365/10) {
239 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, xfmt("last seen: %u years ago", cast(int)(nowut-ltt)/365));
240 } else {
241 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)));
244 } else {
245 SysTime dt = SysTime.fromUnixTime(ct.info.lastonlinetime);
246 //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);
247 //nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, buf[0..len]);
248 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));
255 // ////////////////////////////////////////////////////////////////////////// //
256 // visible contact list
257 final class CList {
258 private import core.time;
260 private:
261 int mActiveItem = -1; // active item (may be different from selected with cursor)
262 int mTopY;
263 int mLastX = -666, mLastY = -666, mLastHeight, mLastWidth;
264 //MonoTime mLastClick = MonoTime.zero;
265 //int mLastClickItem = -1;
267 public:
268 ListItemBase[] items;
270 public:
271 this () {}
273 @property int activeItemIndex () const pure nothrow @safe @nogc => mActiveItem;
275 void resetActiveItem () nothrow @safe @nogc { mActiveItem = -1; }
277 // the first one is "main"; can return `null`
278 Account mainAccount () {
279 foreach (ListItemBase li; items) if (auto lc = cast(ListItemAccount)li) return lc.acc;
280 return null;
283 // the first one is "main"; can return `null`
284 Account accountByPK (in ref PubKey pk) {
285 foreach (ListItemBase li; items) {
286 if (auto lc = cast(ListItemAccount)li) {
287 if (lc.acc.toxpk[] == pk[]) return lc.acc;
290 return null;
293 void forEachAccount (scope void delegate (Account acc) dg) {
294 if (dg is null) return;
295 foreach (ListItemBase li; items) if (auto lc = cast(ListItemAccount)li) dg(lc.acc);
298 void opOpAssign(string op:"~") (ListItemBase li) {
299 if (li is null) return;
300 items ~= li;
303 void setFont () {
304 //nvg.fontFace = "ui";
305 nvg.fontSize = 20;
306 nvg.fontBlur = 0;
307 nvg.textAlign = NVGTextAlign.H.Left;
308 nvg.textAlign = NVGTextAlign.V.Baseline;
311 void removeAccount (Account acc) {
312 if (acc is null) return;
313 usize pos = 0, dest = 0;
314 while (pos < items.length) {
315 if (items[pos].ownerAcc !is acc) {
316 if (pos != dest) items[dest++] = items[pos];
318 ++pos;
320 items.length = dest;
323 void buildAccount (Account acc) {
324 if (acc is null) return;
325 //FIXME
326 mActiveItem = -1;
327 mTopY = 0;
328 removeAccount(acc);
329 items ~= new ListItemAccount(acc);
330 Contact[] css;
331 scope(exit) delete css;
332 foreach (Group g; acc.groups) {
333 items ~= new ListItemGroup(g);
334 css.length = 0;
335 css.assumeSafeAppend;
336 foreach (Contact c; acc.contacts.byValue) if (c.gid == g.gid) css ~= c;
337 import std.algorithm : sort;
338 css.sort!((a, b) {
339 string s0 = a.displayNick;
340 string s1 = b.displayNick;
341 auto xlen = (s0.length < s1.length ? s0.length : s1.length);
342 foreach (immutable idx, char c0; s0[0..xlen]) {
343 if (c0 >= 'A' && c0 <= 'Z') c0 += 32; // poor man's tolower()
344 char c1 = s1[idx];
345 if (c1 >= 'A' && c1 <= 'Z') c1 += 32; // poor man's tolower()
346 if (auto d = c0-c1) return (d < 0);
348 return (s0.length < s1.length);
350 foreach (Contact c; css) items ~= new ListItemContact(c);
354 // -1: oops
355 // should be called after clist was drawn at least once
356 int itemAtY (int aty) {
357 if (aty < 0 || mLastHeight < 1 || aty >= mLastHeight) return -1;
358 aty += mTopY;
359 setFont();
360 foreach (immutable iidx, ListItemBase li; items) {
361 if (iidx != mActiveItem && !li.visible) continue;
362 li.setupFont();
363 int lh = li.height;
364 if (lh < 1) continue;
365 if (aty < lh) return cast(int)iidx;
366 aty -= lh;
368 return -1;
371 final int calcTotalHeight () {
372 setFont();
373 int totalHeight = 0;
374 foreach (immutable iidx, ListItemBase li; items) {
375 if (iidx != mActiveItem && !li.visible) continue;
376 li.setupFont();
377 int lh = li.height;
378 if (lh < 1) continue;
379 totalHeight += lh;
381 return totalHeight;
384 static struct AIPos {
385 int y, hgt;
386 @property bool valid () const pure nothrow @safe @nogc => (hgt > 0);
389 AIPos calcActiveItemPos () {
390 AIPos res;
391 if (mActiveItem < 0 || mActiveItem >= items.length) return res;
392 setFont();
393 foreach (immutable iidx, ListItemBase li; items) {
394 if (iidx != mActiveItem && !li.visible) continue;
395 li.setupFont();
396 int lh = li.height;
397 if (lh < 1) continue;
398 if (iidx == mActiveItem) { res.hgt = lh; return res; }
399 res.y += lh;
401 res = res.init;
402 return res;
405 // if called with `null` ct, deactivate
406 void delegate (Contact ct) onActivateContactCB;
408 // true: eaten
409 bool onMouse (MouseEvent event) {
410 if (mLastWidth < 1 || mLastHeight < 1) return false;
411 int mx = event.x-mLastX;
412 int my = event.y-mLastY;
413 if (mx < 0 || my < 0 || mx >= mLastWidth || my >= mLastHeight) return false;
414 if (event == "LMB-Down") {
415 int it = itemAtY(my);
416 if (it >= 0 && it != mActiveItem) {
417 if (auto ci = cast(ListItemContact)items[it]) {
418 mActiveItem = it;
419 if (onActivateContactCB !is null) onActivateContactCB(ci.ct);
420 glconPostScreenRepaint();
423 return true;
426 enum ScrollHeight = 32;
428 if (event == "WheelUp") {
429 if (mTopY > 0) {
430 mTopY -= ScrollHeight;
431 if (mTopY < 0) mTopY = 0;
432 glconPostScreenRepaint();
434 return true;
437 if (event == "WheelDown") {
438 auto th = calcTotalHeight()-mLastHeight;
439 if (th > 0 && mTopY < th) {
440 mTopY += ScrollHeight;
441 if (mTopY > th) mTopY = th;
442 glconPostScreenRepaint();
444 return true;
447 return true;
450 void makeActiveItemVisible () {
451 if (mActiveItem < 0 || mActiveItem >= items.length || mLastHeight < 1) return;
452 auto pos = calcActiveItemPos();
453 if (!pos.valid) return;
454 auto oty = mTopY;
455 if (mTopY > pos.y) {
456 mTopY = pos.y;
457 } else if (mTopY+mLastHeight < pos.y+pos.hgt) {
458 mTopY = pos.y-(mLastHeight-pos.hgt);
459 if (mTopY < 0) mTopY = 0;
461 if (mTopY != oty) glconPostScreenRepaint();
464 // true: eaten
465 bool onKey (KeyEvent event) {
466 // jump to next contact with unread messages
467 if (event == "*-C-U") {
468 if (event == "D-C-U") {
469 foreach (immutable iidx, ListItemBase li; items) {
470 if (auto lc = cast(ListItemContact)li) {
471 if (lc.ct.kfd) continue;
472 if (lc.ct.unreadCount > 0) {
473 if (mActiveItem != iidx) {
474 mActiveItem = iidx;
475 makeActiveItemVisible();
476 glconPostScreenRepaint();
477 if (onActivateContactCB !is null) onActivateContactCB(lc.ct);
483 return true;
485 return false;
488 // real rect is scissored
489 void drawAt (int x0, int y0, int wdt, int hgt) {
490 mLastX = x0;
491 mLastY = y0;
492 mLastHeight = hgt;
493 mLastWidth = wdt;
495 nvg.save();
496 scope(exit) nvg.restore();
497 setFont();
499 int totalHeight = calcTotalHeight();
500 if (mTopY > totalHeight-hgt) mTopY = totalHeight-hgt;
501 if (mTopY < 0) mTopY = 0;
503 int y = -mTopY;
504 foreach (immutable iidx, ListItemBase li; items) {
505 if (iidx != mActiveItem && !li.visible) continue;
506 li.setupFont();
507 int lh = li.height;
508 if (lh < 1) continue;
509 totalHeight += lh;
510 if (y+lh > 0 && y < hgt) {
511 nvg.save();
512 scope(exit) nvg.restore();
513 version(all) {
514 nvg.intersectScissor(x0, y0+y, wdt, lh);
515 li.drawAt(x0, y0+y, wdt, (iidx == mActiveItem));
516 } else {
517 nvg.translate(x0+0.5f, y0+y+0.5f);
518 nvg.intersectScissor(0, 0, wdt, lh);
519 li.drawAt(0, 0, wdt);
522 y += lh;
523 if (y >= hgt) break;
526 if (totalHeight > hgt) {
527 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);
528 } else {
529 //nvg.bndScrollBar(x0+wdt-BND_SCROLLBAR_WIDTH+0.5f, y0+0.5f, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);