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
;
22 import arsd
.simpledisplay
;
27 import iv
.nanovega
.blendish
;
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;
54 auto len
= vsnprintf(buf
.ptr
, buf
.length
, fmt
, ap
);
60 // ////////////////////////////////////////////////////////////////////////// //
63 this (CList aowner
) { owner
= aowner
; }
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
) {
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
{
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) {
110 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
111 nvg
.fillColor(selected ?
NVGColor("#5ff0") : NVGColor("#5fff"));
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
{
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
;
143 override bool onMouse (MouseEvent event
, bool meActive
) {
144 if (event
== "LMB-Down") {
145 //conwriteln("!!! ", group.opened);
146 group
.opened
= !group
.opened
;
147 glconPostScreenRepaint();
153 override void drawAt (NVGContext nvg
, int x0
, int y0
, int wdt
, bool selected
=false) {
158 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
159 nvg
.fillColor(selected ?
NVGColor("#5880") : NVGColor("#5888"));
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
{
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
;
194 auto hgt
= fstash
.fontHeight
;
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);
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();
212 override void drawAt (NVGContext nvg
, int x0
, int y0
, int wdt
, bool selected
=false) {
213 import std
.algorithm
: max
;
217 int hgt
= max(cast(int)nvg
.textFontHeight
, 16);
220 int fullhgt
= height
;
222 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, fullhgt
);
223 nvg
.fillColor(NVGColor("#9600"));
232 if (ct
.requestPending
) {
234 } else if (ct
.acceptPending
) {
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
));
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
255 nvg
.setupCtxFrom(fstash
);
256 ty
= y0
+hgt
+cast(int)nvg
.textFontHeight
/2;
257 //ty += cast(int)nvg.textFontDescender;
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
;
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");
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));
287 nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, xfmt("last seen: %u.5 month ago", cast(int)(nowut
-ltt
)/31));
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));
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)));
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
311 private import core
.time
;
314 int mActiveItem
= -1; // active item (may be different from selected with cursor)
316 int mLastX
= -666, mLastY
= -666, mLastHeight
, mLastWidth
;
317 //MonoTime mLastClick = MonoTime.zero;
318 //int mLastClickItem = -1;
321 ListItemBase
[] items
;
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
;
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
;
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;
353 fstash
.fontId
= "ui";
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
];
371 void buildAccount (Account acc
) {
372 if (acc
is null) return;
377 items
~= new ListItemAccount(this, acc
);
379 scope(exit
) delete css
;
380 foreach (Group g
; acc
.groups
) {
381 items
~= new ListItemGroup(this, g
);
383 css
.assumeSafeAppend
;
384 foreach (Contact c
; acc
.contacts
.byValue
) if (c
.gid
== g
.gid
) css
~= c
;
385 import std
.algorithm
: sort
;
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()
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
);
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;
408 foreach (immutable iidx
, ListItemBase li
; items
) {
409 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
412 if (lh
< 1) continue;
413 if (aty
< lh
) return cast(int)iidx
;
419 final int calcTotalHeight () {
422 foreach (immutable iidx
, ListItemBase li
; items
) {
423 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
426 if (lh
< 1) continue;
432 static struct AIPos
{
434 @property bool valid () const pure nothrow @safe @nogc => (hgt
> 0);
437 AIPos
calcActiveItemPos () {
439 if (mActiveItem
< 0 || mActiveItem
>= items
.length
) return res
;
441 foreach (immutable iidx
, ListItemBase li
; items
) {
442 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
445 if (lh
< 1) continue;
446 if (iidx
== mActiveItem
) { res
.hgt
= lh
; 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;
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 {
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
[]);
485 // if called with `null` ct, deactivate
486 void delegate (Contact ct
) onActivateContactCB
;
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
);
497 if (items
[it
].onMouse(event
, it
== mActiveItem
)) return true;
500 enum ScrollHeight
= 32;
502 if (event
== "WheelUp") {
504 mTopY
-= ScrollHeight
;
505 if (mTopY
< 0) mTopY
= 0;
506 glconPostScreenRepaint();
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();
524 void makeActiveItemVisible () {
525 if (mActiveItem
< 0 || mActiveItem
>= items
.length || mLastHeight
< 1) return;
526 auto pos
= calcActiveItemPos();
527 if (!pos
.valid
) return;
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();
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
) {
549 makeActiveItemVisible();
550 glconPostScreenRepaint();
551 if (onActivateContactCB
!is null) onActivateContactCB(lc
.ct
);
552 return true; // oops; stop right here ;-)
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
) {
577 scope(exit
) nvg
.restore();
580 nvg
.setupCtxFrom(fstash
);
582 int totalHeight
= calcTotalHeight();
583 if (mTopY
> totalHeight
-hgt
) mTopY
= totalHeight
-hgt
;
584 if (mTopY
< 0) mTopY
= 0;
587 foreach (immutable iidx
, ListItemBase li
; items
) {
588 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
591 if (lh
< 1) continue;
592 if (y
+lh
> 0 && y
< hgt
) {
594 scope(exit
) nvg
.restore();
596 nvg
.intersectScissor(x0
, y0
+y
, wdt
, lh
);
597 li
.drawAt(nvg
, x0
, y0
+y
, wdt
, (iidx
== mActiveItem
));
599 nvg
.translate(x0
+0.5f, y0
+y
+0.5f);
600 nvg
.intersectScissor(0, 0, wdt
, lh
);
601 li
.drawAt(nvg
, 0, 0, wdt
);
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
;
613 //nvg.bndScrollBar(x0+wdt-BND_SCROLLBAR_WIDTH+0.5f, y0+0.5f, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);