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
;
23 import arsd
.simpledisplay
;
28 import iv
.nanovega
.blendish
;
43 // ////////////////////////////////////////////////////////////////////////// //
44 __gshared
int optCListWidth
= -1;
45 __gshared
bool optHRLastSeen
= true;
48 // ////////////////////////////////////////////////////////////////////////// //
49 private extern(C
) const(char)[] xfmt (const(char)* fmt
, ...) {
50 import core
.stdc
.stdio
: vsnprintf
;
51 import core
.stdc
.stdarg
;
52 static char[64] buf
= void;
55 auto len
= vsnprintf(buf
.ptr
, buf
.length
, fmt
, ap
);
61 // ////////////////////////////////////////////////////////////////////////// //
64 this (CList aowner
) { owner
= aowner
; }
70 @property Account
ownerAcc () => null;
72 // setup font face for this item
73 void setFontAlign () { fstash
.textAlign
= NVGTextAlign(NVGTextAlign
.H
.Left
, NVGTextAlign
.V
.Baseline
); }
74 void setFont () { fstash
.fontId
= "ui"; fstash
.size
= 19; }
75 void setSmallFont () { fstash
.fontId
= "ui"; fstash
.size
= 15; }
77 @property int height () => cast(int)fstash
.fontHeight
;
78 @property bool visible () => true;
80 bool onMouse (MouseEvent event
, bool meActive
) => false; // true: eaten; will modify fstash
81 bool onKey (KeyEvent event
, bool meActive
) => false; // true: eaten; will modify fstash
83 protected void drawPrepare (NVGContext nvg
) {
86 nvg
.setupCtxFrom(fstash
);
89 // no need to save context state here
90 void drawAt (NVGContext nvg
, int x0
, int y0
, int wdt
, bool selected
=false) {} // real rect is scissored
94 // ////////////////////////////////////////////////////////////////////////// //
95 class ListItemAccount
: ListItemBase
{
100 this (CList aowner
, Account aAcc
) { assert(aAcc
!is null); acc
= aAcc
; super(aowner
); }
102 override @property Account
ownerAcc () => acc
;
104 override void setFont () { fstash
.fontId
= "uib"; fstash
.size
= 20; }
106 override void drawAt (NVGContext nvg
, int x0
, int y0
, int wdt
, bool selected
=false) {
111 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
112 nvg
.fillColor(selected ?
NVGColor("#5ff0") : NVGColor("#5fff"));
115 final switch (acc
.status
) {
116 case ContactStatus
.Connecting
: nvg
.fillColor(NVGColor
.k8orange
); break;
117 case ContactStatus
.Offline
: nvg
.fillColor(NVGColor("#f00")); break;
118 case ContactStatus
.Online
: nvg
.fillColor(NVGColor("#fff")); break;
119 case ContactStatus
.Away
: nvg
.fillColor(NVGColor("#7557C7")); break;
120 case ContactStatus
.Busy
: nvg
.fillColor(NVGColor("#0057C7")); break;
122 if (acc
.isConnecting
) nvg
.fillColor(NVGColor
.k8orange
);
124 nvg
.textAlign
= NVGTextAlign
.V
.Baseline
;
125 nvg
.textAlign
= NVGTextAlign
.H
.Center
;
126 nvg
.text(x0
+wdt
/2, y0
+cast(int)nvg
.textFontAscender
, acc
.info
.nick
);
131 // ////////////////////////////////////////////////////////////////////////// //
132 class ListItemGroup
: ListItemBase
{
137 this (CList aowner
, Group aGroup
) { assert(aGroup
!is null); group
= aGroup
; super(aowner
); }
139 override @property Account
ownerAcc () => group
.acc
;
141 override @property bool visible () => group
.visible
;
144 override bool onMouse (MouseEvent event
, bool meActive
) {
145 if (event
== "LMB-Down") {
146 //conwriteln("!!! ", group.opened);
147 group
.opened
= !group
.opened
;
148 glconPostScreenRepaint();
154 override void drawAt (NVGContext nvg
, int x0
, int y0
, int wdt
, bool selected
=false) {
159 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
160 nvg
.fillColor(selected ?
NVGColor("#5880") : NVGColor("#5888"));
163 nvg
.fillColor(selected ? NVGColor
.white
: NVGColor
.yellow
);
164 nvg
.textAlign
= NVGTextAlign
.V
.Baseline
;
165 nvg
.textAlign
= NVGTextAlign
.H
.Center
;
166 nvg
.text(x0
+wdt
/2, y0
+cast(int)nvg
.textFontAscender
, group
.name
);
171 // ////////////////////////////////////////////////////////////////////////// //
172 class ListItemContact
: ListItemBase
{
177 this (CList aowner
, Contact aCt
) { assert(aCt
!is null); ct
= aCt
; super(aowner
); }
179 override @property Account
ownerAcc () => ct
.acc
;
181 override @property bool visible () => ct
.visible
;
183 //FIXME: 16 is icon height
184 override @property int height () {
185 import std
.algorithm
: max
;
187 auto hgt
= fstash
.fontHeight
;
189 if (ct
.info
.statusmsg
.length
) hgt
+= fstash
.fontHeight
; // status message
190 if (!ct
.online
) hgt
+= fstash
.fontHeight
; // last seen
191 return max(cast(int)hgt
, 16);
195 override bool onMouse (MouseEvent event
, bool meActive
) {
196 if (event
== "LMB-Down") {
197 owner
.activeItem
= this;
198 if (owner
.onActivateContactCB
!is null) owner
.onActivateContactCB(ct
);
199 glconPostScreenRepaint();
205 override void drawAt (NVGContext nvg
, int x0
, int y0
, int wdt
, bool selected
=false) {
206 import std
.algorithm
: max
;
210 int hgt
= max(cast(int)nvg
.textFontHeight
, 16);
213 int fullhgt
= height
;
215 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, fullhgt
);
216 nvg
.fillColor(NVGColor("#9600"));
225 if (ct
.requestPending
) {
227 } else if (ct
.acceptPending
) {
230 if (ct
.unreadCount
== 0) icon
= statusImgId
[ct
.status
]; else icon
= kittyMsg
;
233 nvg
.imageSize(icon
, iw
, ih
);
234 nvg
.rect(x0
, y0
+(hgt
-ih
)/2, iw
, ih
);
235 nvg
.fillPaint(nvg
.imagePattern(x0
, y0
+(hgt
-ih
)/2, iw
, ih
, 0, icon
));
240 nvg
.fillColor(NVGColor("#f70"));
241 //nvg.textAlign = NVGTextAlign.V.Top;
242 nvg
.textAlign
= NVGTextAlign
.V
.Middle
;
243 nvg
.textAlign
= NVGTextAlign
.H
.Left
;
244 nvg
.text(x0
+4+iw
+4, ty
, ct
.displayNick
);
246 // draw last status and lastseen
248 nvg
.setupCtxFrom(fstash
);
249 ty
= y0
+hgt
+cast(int)nvg
.textFontHeight
/2;
250 //ty += cast(int)nvg.textFontDescender;
253 if (ct
.info
.statusmsg
.length
) {
254 nvg
.fillColor
= NVGColor("#777");
255 nvg
.textAlign
= NVGTextAlign
.H
.Left
;
256 nvg
.text(x0
+4+iw
+4, ty
, ct
.info
.statusmsg
);
257 ty
+= cast(int)nvg
.textFontHeight
;
262 nvg
.fillColor
= NVGColor("#b30");
263 nvg
.textAlign
= NVGTextAlign
.H
.Right
;
264 if (ct
.info
.lastonlinetime
== 0) {
265 nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, "last seen: never");
268 enum OneDay
= 60*60*24;
269 auto nowut
= Clock
.currTime
.toUnixTime
/OneDay
;
270 auto ltt
= ct
.info
.lastonlinetime
/OneDay
;
271 if (ltt
>= nowut
) nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, "last seen: today");
272 else if (nowut
-ltt
== 1) nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, "last seen: yesterday");
273 else if (nowut
-ltt
<= 15) nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, xfmt("last seen: %u days ago", cast(int)nowut
-ltt
));
274 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));
275 else if (nowut
-ltt
<= 31) nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, "last seen: month ago");
276 else if (nowut
-ltt
< 365) {
277 if ((nowut
-ltt
)%31 <= 15) {
278 nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, xfmt("last seen: %u month ago", cast(int)(nowut
-ltt
)/31));
280 nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, xfmt("last seen: %u.5 month ago", cast(int)(nowut
-ltt
)/31));
283 if ((nowut
-ltt
)%365 < 365/10) {
284 nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, xfmt("last seen: %u years ago", cast(int)(nowut
-ltt
)/365));
286 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)));
290 SysTime
dt = SysTime
.fromUnixTime(ct
.info
.lastonlinetime
);
291 //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);
292 //nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, buf[0..len]);
293 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
));
301 // ////////////////////////////////////////////////////////////////////////// //
302 // visible contact list
304 private import core
.time
;
307 int mActiveItem
= -1; // active item (may be different from selected with cursor)
309 int mLastX
= -666, mLastY
= -666, mLastHeight
, mLastWidth
;
310 //MonoTime mLastClick = MonoTime.zero;
311 //int mLastClickItem = -1;
314 ListItemBase
[] items
;
319 // the first one is "main"; can return `null`
320 Account
mainAccount () {
321 foreach (ListItemBase li
; items
) if (auto lc
= cast(ListItemAccount
)li
) return lc
.acc
;
325 // the first one is "main"; can return `null`
326 Account
accountByPK (in ref PubKey pk
) {
327 foreach (ListItemBase li
; items
) {
328 if (auto lc
= cast(ListItemAccount
)li
) {
329 if (lc
.acc
.toxpk
[] == pk
[]) return lc
.acc
;
335 void forEachAccount (scope void delegate (Account acc
) dg
) {
336 if (dg
is null) return;
337 foreach (ListItemBase li
; items
) if (auto lc
= cast(ListItemAccount
)li
) dg(lc
.acc
);
340 void opOpAssign(string op
:"~") (ListItemBase li
) {
341 if (li
is null) return;
346 fstash
.fontId
= "ui";
349 fstash
.textAlign
= NVGTextAlign(NVGTextAlign
.H
.Left
, NVGTextAlign
.V
.Baseline
);
352 void removeAccount (Account acc
) {
353 if (acc
is null) return;
354 usize pos
= 0, dest
= 0;
355 while (pos
< items
.length
) {
356 if (items
[pos
].ownerAcc
!is acc
) {
357 if (pos
!= dest
) items
[dest
++] = items
[pos
];
364 void buildAccount (Account acc
) {
365 if (acc
is null) return;
370 items
~= new ListItemAccount(this, acc
);
372 scope(exit
) delete css
;
373 foreach (Group g
; acc
.groups
) {
374 items
~= new ListItemGroup(this, g
);
376 css
.assumeSafeAppend
;
377 foreach (Contact c
; acc
.contacts
.byValue
) if (c
.gid
== g
.gid
) css
~= c
;
378 import std
.algorithm
: sort
;
380 string s0
= a
.displayNick
;
381 string s1
= b
.displayNick
;
382 auto xlen
= (s0
.length
< s1
.length ? s0
.length
: s1
.length
);
383 foreach (immutable idx
, char c0
; s0
[0..xlen
]) {
384 if (c0
>= 'A' && c0
<= 'Z') c0
+= 32; // poor man's tolower()
386 if (c1
>= 'A' && c1
<= 'Z') c1
+= 32; // poor man's tolower()
387 if (auto d
= c0
-c1
) return (d
< 0);
389 return (s0
.length
< s1
.length
);
391 foreach (Contact c
; css
) items
~= new ListItemContact(this, c
);
396 // should be called after clist was drawn at least once
397 int itemAtY (int aty
) {
398 if (aty
< 0 || mLastHeight
< 1 || aty
>= mLastHeight
) return -1;
401 foreach (immutable iidx
, ListItemBase li
; items
) {
402 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
405 if (lh
< 1) continue;
406 if (aty
< lh
) return cast(int)iidx
;
412 final int calcTotalHeight () {
415 foreach (immutable iidx
, ListItemBase li
; items
) {
416 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
419 if (lh
< 1) continue;
425 static struct AIPos
{
427 @property bool valid () const pure nothrow @safe @nogc => (hgt
> 0);
430 AIPos
calcActiveItemPos () {
432 if (mActiveItem
< 0 || mActiveItem
>= items
.length
) return res
;
434 foreach (immutable iidx
, ListItemBase li
; items
) {
435 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
438 if (lh
< 1) continue;
439 if (iidx
== mActiveItem
) { res
.hgt
= lh
; return res
; }
446 final void resetActiveItem () nothrow @safe @nogc { mActiveItem
= -1; }
448 final @property int activeItemIndex () const pure nothrow @safe @nogc => mActiveItem
;
450 final @property void activeItemIndex (int idx
) nothrow @trusted {
451 if (idx
< 0 || idx
>= items
.length
) idx
= -1;
452 if (mActiveItem
== idx
) return;
454 try { glconPostScreenRepaint(); } catch (Exception e
) {} // sorry
457 final @property inout(ListItemBase
) activeItem () inout pure nothrow @trusted @nogc {
458 pragma(inline
, true);
459 return (mActiveItem
>= 0 && mActiveItem
< items
.length ?
cast(inout)items
[mActiveItem
] : null);
462 final @property void activeItem (ListItemBase it
) nothrow @trusted {
465 foreach (immutable ii
, const ListItemBase li
; items
) {
466 if (li
is it
) { idx
= cast(int)ii
; break; }
469 activeItemIndex
= idx
;
472 final bool isActiveContact (in ref PubKey pk
) const nothrow @trusted {
473 if (mActiveItem
< 0 || mActiveItem
>= items
.length
) return false;
474 if (auto ct
= cast(const(ListItemContact
))items
[mActiveItem
]) return (ct
.ct
.info
.pubkey
[] == pk
[]);
478 // if called with `null` ct, deactivate
479 void delegate (Contact ct
) onActivateContactCB
;
482 bool onMouse (MouseEvent event
) {
483 if (mLastWidth
< 1 || mLastHeight
< 1) return false;
484 int mx
= event
.x
-mLastX
;
485 int my
= event
.y
-mLastY
;
486 if (mx
< 0 || my
< 0 || mx
>= mLastWidth || my
>= mLastHeight
) return false;
488 int it
= itemAtY(my
);
490 if (items
[it
].onMouse(event
, it
== mActiveItem
)) return true;
493 enum ScrollHeight
= 32;
495 if (event
== "WheelUp") {
497 mTopY
-= ScrollHeight
;
498 if (mTopY
< 0) mTopY
= 0;
499 glconPostScreenRepaint();
504 if (event
== "WheelDown") {
505 auto th
= calcTotalHeight()-mLastHeight
;
506 if (th
> 0 && mTopY
< th
) {
507 mTopY
+= ScrollHeight
;
508 if (mTopY
> th
) mTopY
= th
;
509 glconPostScreenRepaint();
517 void makeActiveItemVisible () {
518 if (mActiveItem
< 0 || mActiveItem
>= items
.length || mLastHeight
< 1) return;
519 auto pos
= calcActiveItemPos();
520 if (!pos
.valid
) return;
524 } else if (mTopY
+mLastHeight
< pos
.y
+pos
.hgt
) {
525 mTopY
= pos
.y
-(mLastHeight
-pos
.hgt
);
526 if (mTopY
< 0) mTopY
= 0;
528 if (mTopY
!= oty
) glconPostScreenRepaint();
532 bool onKey (KeyEvent event
) {
533 // jump to next contact with unread messages
534 if (event
== "*-C-U") {
535 if (event
== "D-C-U") {
536 foreach (immutable iidx
, ListItemBase li
; items
) {
537 if (auto lc
= cast(ListItemContact
)li
) {
538 if (lc
.ct
.kfd
) continue;
539 if (lc
.ct
.unreadCount
> 0) {
540 if (mActiveItem
!= iidx
) {
542 makeActiveItemVisible();
543 glconPostScreenRepaint();
544 if (onActivateContactCB
!is null) onActivateContactCB(lc
.ct
);
555 private float sbpos
, sbsize
; // set in `drawAt()`
557 // the following two should be called after `drawAt()`; if `sbSize()` is <0, draw nothing
558 final float sbPosition () pure const nothrow @safe @nogc => sbpos
;
559 final float sbSize () pure const nothrow @safe @nogc => sbsize
;
561 // real rect is scissored
562 void drawAt (NVGContext nvg
, int x0
, int y0
, int wdt
, int hgt
) {
569 scope(exit
) nvg
.restore();
572 nvg
.setupCtxFrom(fstash
);
574 int totalHeight
= calcTotalHeight();
575 if (mTopY
> totalHeight
-hgt
) mTopY
= totalHeight
-hgt
;
576 if (mTopY
< 0) mTopY
= 0;
579 foreach (immutable iidx
, ListItemBase li
; items
) {
580 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
583 if (lh
< 1) continue;
584 if (y
+lh
> 0 && y
< hgt
) {
586 scope(exit
) nvg
.restore();
588 nvg
.intersectScissor(x0
, y0
+y
, wdt
, lh
);
589 li
.drawAt(nvg
, x0
, y0
+y
, wdt
, (iidx
== mActiveItem
));
591 nvg
.translate(x0
+0.5f, y0
+y
+0.5f);
592 nvg
.intersectScissor(0, 0, wdt
, lh
);
593 li
.drawAt(nvg
, 0, 0, wdt
);
600 if (totalHeight
> hgt
) {
601 //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);
602 sbpos
= mTopY
/cast(float)(totalHeight
-hgt
);
603 sbsize
= hgt
/cast(float)totalHeight
;
605 //nvg.bndScrollBar(x0+wdt-BND_SCROLLBAR_WIDTH+0.5f, y0+0.5f, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);