C-3 to set "busy" status
[bioacid.git] / tkclist.d
bloba7f2f87468e0fe36a7a715510ea9b6f8962f3ff8
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;
47 // ////////////////////////////////////////////////////////////////////////// //
48 class ListItemBase {
49 private:
50 this () {}
52 protected:
53 bool mVisible = true;
55 public:
56 void setupFont () => nvg.fontFace = "ui"; // setup font face for this item
57 @property int height () => cast(int)nvg.textFontHeight;
58 @property bool visible () => mVisible;
59 bool onMouse (MouseEvent event) => false; // true: eaten
60 bool onKey (KeyEvent event) => false; // true: eaten
61 void drawAt (int x0, int y0, int wdt, bool selected=false) {} // real rect is scissored
63 @property Account ownerAcc () => null;
67 // ////////////////////////////////////////////////////////////////////////// //
68 class ListItemAccount : ListItemBase {
69 public:
70 Account acc;
72 public:
73 this (Account aAcc) { assert(aAcc !is null); acc = aAcc; }
75 override @property Account ownerAcc () => acc;
77 override void setupFont () => nvg.fontFace = "uib"; // bold
79 override void drawAt (int x0, int y0, int wdt, bool selected=false) {
80 int hgt = height;
82 nvg.newPath();
83 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
84 nvg.fillColor(selected ? NVGColor("#5ff0") : NVGColor("#5fff"));
85 nvg.fill();
87 final switch (acc.status) {
88 case ContactStatus.Connecting: nvg.fillColor(NVGColor.k8orange); break;
89 case ContactStatus.Offline: nvg.fillColor(NVGColor("#f00")); break;
90 case ContactStatus.Online: nvg.fillColor(NVGColor("#fff")); break;
91 case ContactStatus.Away: nvg.fillColor(NVGColor("#7557C7")); break;
92 case ContactStatus.Busy: nvg.fillColor(NVGColor("#0057C7")); break;
94 if (acc.isConnecting) nvg.fillColor(NVGColor.k8orange);
95 nvg.textAlign = NVGTextAlign.V.Baseline;
96 nvg.textAlign = NVGTextAlign.H.Center;
97 nvg.text(x0+wdt/2, y0+cast(int)nvg.textFontAscender, acc.info.nick);
102 // ////////////////////////////////////////////////////////////////////////// //
103 class ListItemGroup : ListItemBase {
104 public:
105 Group group;
107 public:
108 this (Group aGroup) { assert(aGroup !is null); group = aGroup; }
110 override @property Account ownerAcc () => group.acc;
112 override @property bool visible () => group.visible;
114 override void drawAt (int x0, int y0, int wdt, bool selected=false) {
115 int hgt = height;
117 nvg.newPath();
118 nvg.rect(x0+0.5f, y0+0.5f, wdt, hgt);
119 nvg.fillColor(selected ? NVGColor("#5880") : NVGColor("#5888"));
120 nvg.fill();
122 nvg.fillColor(selected ? NVGColor.white : NVGColor.yellow);
123 nvg.textAlign = NVGTextAlign.V.Baseline;
124 nvg.textAlign = NVGTextAlign.H.Center;
125 nvg.text(x0+wdt/2, y0+cast(int)nvg.textFontAscender, group.name);
130 // ////////////////////////////////////////////////////////////////////////// //
131 class ListItemContact : ListItemBase {
132 public:
133 Contact ct;
135 public:
136 this (Contact aCt) { assert(aCt !is null); ct = aCt; }
138 override @property Account ownerAcc () => ct.acc;
140 override @property bool visible () => ct.visible;
142 final void setSmallFont () {
143 auto fsz = nvg.fontSize;
144 auto hgt = nvg.textFontHeight;
145 auto nsz = cast(int)(fsz-fsz/4);
146 if (nsz < 6) nsz = 6;
147 if (nsz > fsz) nsz = cast(int)fsz;
148 nvg.fontSize = nsz;
151 override @property int height () {
152 import std.algorithm : max;
153 //FIXME: 16 is image height
154 auto hgt = nvg.textFontHeight;
155 auto ofsz = nvg.fontSize;
156 setSmallFont();
157 hgt += nvg.textFontHeight;
158 nvg.fontSize = ofsz;
159 return max(cast(int)hgt, 16);
162 override void drawAt (int x0, int y0, int wdt, bool selected=false) {
163 import std.algorithm : max;
164 int hgt = max(cast(int)nvg.textFontHeight, 16);
166 if (selected) {
167 int fullhgt = height;
168 nvg.newPath();
169 nvg.rect(x0+0.5f, y0+0.5f, wdt, fullhgt);
170 nvg.fillColor(NVGColor("#9600"));
171 nvg.fill();
174 // draw icon
175 nvg.newPath();
176 int iw, ih;
177 NVGImage icon;
179 if (ct.requestPending) {
180 icon = kittyOut;
181 } else if (ct.acceptPending) {
182 icon = kittyFish;
183 } else {
184 if (ct.unreadCount == 0) icon = statusImgId[ct.status]; else icon = kittyMsg;
187 nvg.imageSize(icon, iw, ih);
188 nvg.rect(x0, y0+(hgt-ih)/2, iw, ih);
189 nvg.fillPaint(nvg.imagePattern(x0, y0+(hgt-ih)/2, iw, ih, 0, icon));
190 nvg.fill();
192 auto ty = y0+hgt/2;
194 nvg.fillColor(NVGColor("#f70"));
195 //nvg.textAlign = NVGTextAlign.V.Top;
196 nvg.textAlign = NVGTextAlign.V.Middle;
197 nvg.textAlign = NVGTextAlign.H.Left;
198 nvg.text(x0+4+iw+4, ty, ct.displayNick);
200 // draw last seen time
201 setSmallFont();
202 ty = y0+hgt+cast(int)nvg.textFontHeight/2;
203 //ty += cast(int)nvg.textFontDescender;
204 nvg.fillColor(NVGColor("#b30"));
205 nvg.textAlign = NVGTextAlign.H.Right;
206 if (ct.info.lastonlinetime == 0) {
207 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, "last seen: never");
208 } else {
209 import core.stdc.stdio : snprintf;
210 //conwriteln("ls0: ", ct.info.lastonlinetime);
211 SysTime dt = SysTime.fromUnixTime(ct.info.lastonlinetime);
212 char[64] buf = void;
213 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);
214 //conwriteln("ls1: ", buf[0..len]);
215 nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, buf[0..len]);
221 // ////////////////////////////////////////////////////////////////////////// //
222 // visible contact list
223 final class CList {
224 private import core.time;
226 private:
227 int mActiveItem = -1; // active item (may be different from selected with cursor)
228 int mTopY;
229 int mLastX = -666, mLastY = -666, mLastHeight, mLastWidth;
230 //MonoTime mLastClick = MonoTime.zero;
231 //int mLastClickItem = -1;
233 public:
234 ListItemBase[] items;
236 public:
237 this () {}
239 @property int activeItemIndex () const pure nothrow @safe @nogc => mActiveItem;
241 void resetActiveItem () nothrow @safe @nogc { mActiveItem = -1; }
243 // the first one is "main"; can return `null`
244 Account mainAccount () {
245 foreach (ListItemBase li; items) if (auto lc = cast(ListItemAccount)li) return lc.acc;
246 return null;
249 // the first one is "main"; can return `null`
250 Account accountByPK (in ref PubKey pk) {
251 foreach (ListItemBase li; items) {
252 if (auto lc = cast(ListItemAccount)li) {
253 if (lc.acc.toxpk[] == pk[]) return lc.acc;
256 return null;
259 void forEachAccount (scope void delegate (Account acc) dg) {
260 if (dg is null) return;
261 foreach (ListItemBase li; items) if (auto lc = cast(ListItemAccount)li) dg(lc.acc);
264 void opOpAssign(string op:"~") (ListItemBase li) {
265 if (li is null) return;
266 items ~= li;
269 void setFont () {
270 //nvg.fontFace = "ui";
271 nvg.fontSize = 20;
272 nvg.fontBlur = 0;
273 nvg.textAlign = NVGTextAlign.H.Left;
274 nvg.textAlign = NVGTextAlign.V.Baseline;
277 void removeAccount (Account acc) {
278 if (acc is null) return;
279 usize pos = 0, dest = 0;
280 while (pos < items.length) {
281 if (items[pos].ownerAcc !is acc) {
282 if (pos != dest) items[dest++] = items[pos];
284 ++pos;
286 items.length = dest;
289 void buildAccount (Account acc) {
290 if (acc is null) return;
291 //FIXME
292 mActiveItem = -1;
293 mTopY = 0;
294 removeAccount(acc);
295 items ~= new ListItemAccount(acc);
296 Contact[] css;
297 scope(exit) delete css;
298 foreach (Group g; acc.groups) {
299 items ~= new ListItemGroup(g);
300 css.length = 0;
301 css.assumeSafeAppend;
302 foreach (Contact c; acc.contacts.byValue) if (c.gid == g.gid) css ~= c;
303 import std.algorithm : sort;
304 css.sort!((a, b) {
305 string s0 = a.displayNick;
306 string s1 = b.displayNick;
307 auto xlen = (s0.length < s1.length ? s0.length : s1.length);
308 foreach (immutable idx, char c0; s0[0..xlen]) {
309 if (c0 >= 'A' && c0 <= 'Z') c0 += 32; // poor man's tolower()
310 char c1 = s1[idx];
311 if (c1 >= 'A' && c1 <= 'Z') c1 += 32; // poor man's tolower()
312 if (auto d = c0-c1) return (d < 0);
314 return (s0.length < s1.length);
316 foreach (Contact c; css) items ~= new ListItemContact(c);
320 // -1: oops
321 // should be called after clist was drawn at least once
322 int itemAtY (int aty) {
323 if (aty < 0 || mLastHeight < 1 || aty >= mLastHeight) return -1;
324 aty += mTopY;
325 setFont();
326 foreach (immutable iidx, ListItemBase li; items) {
327 if (iidx != mActiveItem && !li.visible) continue;
328 li.setupFont();
329 int lh = li.height;
330 if (lh < 1) continue;
331 if (aty < lh) return cast(int)iidx;
332 aty -= lh;
334 return -1;
337 final int calcTotalHeight () {
338 setFont();
339 int totalHeight = 0;
340 foreach (immutable iidx, ListItemBase li; items) {
341 if (iidx != mActiveItem && !li.visible) continue;
342 li.setupFont();
343 int lh = li.height;
344 if (lh < 1) continue;
345 totalHeight += lh;
347 return totalHeight;
350 static struct AIPos {
351 int y, hgt;
352 @property bool valid () const pure nothrow @safe @nogc => (hgt > 0);
355 AIPos calcActiveItemPos () {
356 AIPos res;
357 if (mActiveItem < 0 || mActiveItem >= items.length) return res;
358 setFont();
359 foreach (immutable iidx, ListItemBase li; items) {
360 if (iidx != mActiveItem && !li.visible) continue;
361 li.setupFont();
362 int lh = li.height;
363 if (lh < 1) continue;
364 if (iidx == mActiveItem) { res.hgt = lh; return res; }
365 res.y += lh;
367 res = res.init;
368 return res;
371 // if called with `null` ct, deactivate
372 void delegate (Contact ct) onActivateContactCB;
374 // true: eaten
375 bool onMouse (MouseEvent event) {
376 if (mLastWidth < 1 || mLastHeight < 1) return false;
377 int mx = event.x-mLastX;
378 int my = event.y-mLastY;
379 if (mx < 0 || my < 0 || mx >= mLastWidth || my >= mLastHeight) return false;
380 if (event == "LMB-Down") {
381 int it = itemAtY(my);
382 if (it >= 0 && it != mActiveItem) {
383 if (auto ci = cast(ListItemContact)items[it]) {
384 mActiveItem = it;
385 if (onActivateContactCB !is null) onActivateContactCB(ci.ct);
386 glconPostScreenRepaint();
389 return true;
392 enum ScrollHeight = 32;
394 if (event == "WheelUp") {
395 if (mTopY > 0) {
396 mTopY -= ScrollHeight;
397 if (mTopY < 0) mTopY = 0;
398 glconPostScreenRepaint();
400 return true;
403 if (event == "WheelDown") {
404 auto th = calcTotalHeight()-mLastHeight;
405 if (th > 0 && mTopY < th) {
406 mTopY += ScrollHeight;
407 if (mTopY > th) mTopY = th;
408 glconPostScreenRepaint();
410 return true;
413 return true;
416 void makeActiveItemVisible () {
417 if (mActiveItem < 0 || mActiveItem >= items.length || mLastHeight < 1) return;
418 auto pos = calcActiveItemPos();
419 if (!pos.valid) return;
420 auto oty = mTopY;
421 if (mTopY > pos.y) {
422 mTopY = pos.y;
423 } else if (mTopY+mLastHeight < pos.y+pos.hgt) {
424 mTopY = pos.y-(mLastHeight-pos.hgt);
425 if (mTopY < 0) mTopY = 0;
427 if (mTopY != oty) glconPostScreenRepaint();
430 // true: eaten
431 bool onKey (KeyEvent event) {
432 // jump to next contact with unread messages
433 if (event == "*-C-U") {
434 if (event == "D-C-U") {
435 foreach (immutable iidx, ListItemBase li; items) {
436 if (auto lc = cast(ListItemContact)li) {
437 if (lc.ct.kfd) continue;
438 if (lc.ct.unreadCount > 0) {
439 if (mActiveItem != iidx) {
440 mActiveItem = iidx;
441 makeActiveItemVisible();
442 glconPostScreenRepaint();
443 if (onActivateContactCB !is null) onActivateContactCB(lc.ct);
449 return true;
451 return false;
454 // real rect is scissored
455 void drawAt (int x0, int y0, int wdt, int hgt) {
456 mLastX = x0;
457 mLastY = y0;
458 mLastHeight = hgt;
459 mLastWidth = wdt;
461 nvg.save();
462 scope(exit) nvg.restore();
463 setFont();
465 int totalHeight = calcTotalHeight();
466 if (mTopY > totalHeight-hgt) mTopY = totalHeight-hgt;
467 if (mTopY < 0) mTopY = 0;
469 int y = -mTopY;
470 foreach (immutable iidx, ListItemBase li; items) {
471 if (iidx != mActiveItem && !li.visible) continue;
472 li.setupFont();
473 int lh = li.height;
474 if (lh < 1) continue;
475 totalHeight += lh;
476 if (y+lh > 0 && y < hgt) {
477 nvg.save();
478 scope(exit) nvg.restore();
479 version(all) {
480 nvg.intersectScissor(x0, y0+y, wdt, lh);
481 li.drawAt(x0, y0+y, wdt, (iidx == mActiveItem));
482 } else {
483 nvg.translate(x0+0.5f, y0+y+0.5f);
484 nvg.intersectScissor(0, 0, wdt, lh);
485 li.drawAt(0, 0, wdt);
488 y += lh;
489 if (y >= hgt) break;
492 if (totalHeight > hgt) {
493 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);
494 } else {
495 //nvg.bndScrollBar(x0+wdt-BND_SCROLLBAR_WIDTH+0.5f, y0+0.5f, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);