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
);
170 // ////////////////////////////////////////////////////////////////////////// //
171 class ListItemContact
: ListItemBase
{
176 this (CList aowner
, Contact aCt
) { assert(aCt
!is null); ct
= aCt
; super(aowner
); }
178 override @property Account
ownerAcc () => ct
.acc
;
180 override @property bool visible () => ct
.visible
;
182 //FIXME: 16 is icon height
183 override @property int height () {
184 import std
.algorithm
: max
;
186 auto hgt
= fstash
.fontHeight
;
188 if (ct
.info
.statusmsg
.length
) hgt
+= fstash
.fontHeight
; // status message
189 if (!ct
.online
) hgt
+= fstash
.fontHeight
; // last seen
190 return max(cast(int)hgt
, 16);
194 override bool onMouse (MouseEvent event
, bool meActive
) {
195 if (event
== "LMB-Down") {
196 owner
.activeItem
= this;
197 if (owner
.onActivateContactCB
!is null) owner
.onActivateContactCB(ct
);
198 glconPostScreenRepaint();
204 override void drawAt (NVGContext nvg
, int x0
, int y0
, int wdt
, bool selected
=false) {
205 import std
.algorithm
: max
;
209 int hgt
= max(cast(int)nvg
.textFontHeight
, 16);
212 int fullhgt
= height
;
214 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, fullhgt
);
215 nvg
.fillColor(NVGColor("#9600"));
224 if (ct
.requestPending
) {
226 } else if (ct
.acceptPending
) {
229 if (ct
.unreadCount
== 0) icon
= statusImgId
[ct
.status
]; else icon
= kittyMsg
;
232 nvg
.imageSize(icon
, iw
, ih
);
233 nvg
.rect(x0
, y0
+(hgt
-ih
)/2, iw
, ih
);
234 nvg
.fillPaint(nvg
.imagePattern(x0
, y0
+(hgt
-ih
)/2, iw
, ih
, 0, icon
));
239 nvg
.fillColor(NVGColor("#f70"));
240 //nvg.textAlign = NVGTextAlign.V.Top;
241 nvg
.textAlign
= NVGTextAlign
.V
.Middle
;
242 nvg
.textAlign
= NVGTextAlign
.H
.Left
;
243 nvg
.text(x0
+4+iw
+4, ty
, ct
.displayNick
);
245 // draw last status and lastseen
247 nvg
.setupCtxFrom(fstash
);
248 ty
= y0
+hgt
+cast(int)nvg
.textFontHeight
/2;
249 //ty += cast(int)nvg.textFontDescender;
252 if (ct
.info
.statusmsg
.length
) {
253 nvg
.fillColor
= NVGColor("#777");
254 nvg
.textAlign
= NVGTextAlign
.H
.Left
;
255 nvg
.text(x0
+4+iw
+4, ty
, ct
.info
.statusmsg
);
256 ty
+= cast(int)nvg
.textFontHeight
;
261 nvg
.fillColor
= NVGColor("#b30");
262 nvg
.textAlign
= NVGTextAlign
.H
.Right
;
263 if (ct
.info
.lastonlinetime
== 0) {
264 nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, "last seen: never");
267 enum OneDay
= 60*60*24;
268 auto nowut
= Clock
.currTime
.toUnixTime
/OneDay
;
269 auto ltt
= ct
.info
.lastonlinetime
/OneDay
;
270 if (ltt
>= nowut
) nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, "last seen: today");
271 else if (nowut
-ltt
== 1) nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, "last seen: yesterday");
272 else if (nowut
-ltt
<= 15) nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, xfmt("last seen: %u days ago", cast(int)(nowut
-ltt
)));
273 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));
274 else if (nowut
-ltt
<= 31) nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, "last seen: month ago");
275 else if (nowut
-ltt
< 365) {
276 if ((nowut
-ltt
)%31 <= 15) {
277 nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, xfmt("last seen: %u month ago", cast(int)(nowut
-ltt
)/31));
279 nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, xfmt("last seen: %u.5 month ago", cast(int)(nowut
-ltt
)/31));
282 if ((nowut
-ltt
)%365 < 365/10) {
283 nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, xfmt("last seen: %u years ago", cast(int)(nowut
-ltt
)/365));
285 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)));
289 SysTime
dt = SysTime
.fromUnixTime(ct
.info
.lastonlinetime
);
290 //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);
291 //nvg.text(x0+wdt-BND_SCROLLBAR_WIDTH-1, ty, buf[0..len]);
292 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
));
300 // ////////////////////////////////////////////////////////////////////////// //
301 // visible contact list
303 private import core
.time
;
306 int mActiveItem
= -1; // active item (may be different from selected with cursor)
308 int mLastX
= -666, mLastY
= -666, mLastHeight
, mLastWidth
;
309 //MonoTime mLastClick = MonoTime.zero;
310 //int mLastClickItem = -1;
313 ListItemBase
[] items
;
318 // the first one is "main"; can return `null`
319 Account
mainAccount () {
320 foreach (ListItemBase li
; items
) if (auto lc
= cast(ListItemAccount
)li
) return lc
.acc
;
324 // the first one is "main"; can return `null`
325 Account
accountByPK (in ref PubKey pk
) {
326 foreach (ListItemBase li
; items
) {
327 if (auto lc
= cast(ListItemAccount
)li
) {
328 if (lc
.acc
.toxpk
[] == pk
[]) return lc
.acc
;
334 void forEachAccount (scope void delegate (Account acc
) dg
) {
335 if (dg
is null) return;
336 foreach (ListItemBase li
; items
) if (auto lc
= cast(ListItemAccount
)li
) dg(lc
.acc
);
339 void opOpAssign(string op
:"~") (ListItemBase li
) {
340 if (li
is null) return;
345 fstash
.fontId
= "ui";
348 fstash
.textAlign
= NVGTextAlign(NVGTextAlign
.H
.Left
, NVGTextAlign
.V
.Baseline
);
351 void removeAccount (Account acc
) {
352 if (acc
is null) return;
353 usize pos
= 0, dest
= 0;
354 while (pos
< items
.length
) {
355 if (items
[pos
].ownerAcc
!is acc
) {
356 if (pos
!= dest
) items
[dest
++] = items
[pos
];
363 void buildAccount (Account acc
) {
364 if (acc
is null) return;
369 items
~= new ListItemAccount(this, acc
);
371 scope(exit
) delete css
;
372 foreach (Group g
; acc
.groups
) {
373 items
~= new ListItemGroup(this, g
);
375 css
.assumeSafeAppend
;
376 foreach (Contact c
; acc
.contacts
.byValue
) if (c
.gid
== g
.gid
) css
~= c
;
377 import std
.algorithm
: sort
;
379 string s0
= a
.displayNick
;
380 string s1
= b
.displayNick
;
381 auto xlen
= (s0
.length
< s1
.length ? s0
.length
: s1
.length
);
382 foreach (immutable idx
, char c0
; s0
[0..xlen
]) {
383 if (c0
>= 'A' && c0
<= 'Z') c0
+= 32; // poor man's tolower()
385 if (c1
>= 'A' && c1
<= 'Z') c1
+= 32; // poor man's tolower()
386 if (auto d
= c0
-c1
) return (d
< 0);
388 return (s0
.length
< s1
.length
);
390 foreach (Contact c
; css
) items
~= new ListItemContact(this, c
);
395 // should be called after clist was drawn at least once
396 int itemAtY (int aty
) {
397 if (aty
< 0 || mLastHeight
< 1 || aty
>= mLastHeight
) return -1;
400 foreach (immutable iidx
, ListItemBase li
; items
) {
401 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
404 if (lh
< 1) continue;
405 if (aty
< lh
) return cast(int)iidx
;
411 final int calcTotalHeight () {
414 foreach (immutable iidx
, ListItemBase li
; items
) {
415 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
418 if (lh
< 1) continue;
424 static struct AIPos
{
426 @property bool valid () const pure nothrow @safe @nogc => (hgt
> 0);
429 AIPos
calcActiveItemPos () {
431 if (mActiveItem
< 0 || mActiveItem
>= items
.length
) return res
;
433 foreach (immutable iidx
, ListItemBase li
; items
) {
434 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
437 if (lh
< 1) continue;
438 if (iidx
== mActiveItem
) { res
.hgt
= lh
; return res
; }
445 final void resetActiveItem () nothrow @safe @nogc { mActiveItem
= -1; }
447 final @property int activeItemIndex () const pure nothrow @safe @nogc => mActiveItem
;
449 final @property void activeItemIndex (int idx
) nothrow @trusted {
450 if (idx
< 0 || idx
>= items
.length
) idx
= -1;
451 if (mActiveItem
== idx
) return;
453 try { glconPostScreenRepaint(); } catch (Exception e
) {} // sorry
456 final @property inout(ListItemBase
) activeItem () inout pure nothrow @trusted @nogc {
457 pragma(inline
, true);
458 return (mActiveItem
>= 0 && mActiveItem
< items
.length ?
cast(inout)items
[mActiveItem
] : null);
461 final @property void activeItem (ListItemBase it
) nothrow @trusted {
464 foreach (immutable ii
, const ListItemBase li
; items
) {
465 if (li
is it
) { idx
= cast(int)ii
; break; }
468 activeItemIndex
= idx
;
471 final bool isActiveContact (in ref PubKey pk
) const nothrow @trusted {
472 if (mActiveItem
< 0 || mActiveItem
>= items
.length
) return false;
473 if (auto ct
= cast(const(ListItemContact
))items
[mActiveItem
]) return (ct
.ct
.info
.pubkey
[] == pk
[]);
477 // if called with `null` ct, deactivate
478 void delegate (Contact ct
) onActivateContactCB
;
481 bool onMouse (MouseEvent event
) {
482 if (mLastWidth
< 1 || mLastHeight
< 1) return false;
483 int mx
= event
.x
-mLastX
;
484 int my
= event
.y
-mLastY
;
485 if (mx
< 0 || my
< 0 || mx
>= mLastWidth || my
>= mLastHeight
) return false;
487 int it
= itemAtY(my
);
489 if (items
[it
].onMouse(event
, it
== mActiveItem
)) return true;
492 enum ScrollHeight
= 32;
494 if (event
== "WheelUp") {
496 mTopY
-= ScrollHeight
;
497 if (mTopY
< 0) mTopY
= 0;
498 glconPostScreenRepaint();
503 if (event
== "WheelDown") {
504 auto th
= calcTotalHeight()-mLastHeight
;
505 if (th
> 0 && mTopY
< th
) {
506 mTopY
+= ScrollHeight
;
507 if (mTopY
> th
) mTopY
= th
;
508 glconPostScreenRepaint();
516 void makeActiveItemVisible () {
517 if (mActiveItem
< 0 || mActiveItem
>= items
.length || mLastHeight
< 1) return;
518 auto pos
= calcActiveItemPos();
519 if (!pos
.valid
) return;
523 } else if (mTopY
+mLastHeight
< pos
.y
+pos
.hgt
) {
524 mTopY
= pos
.y
-(mLastHeight
-pos
.hgt
);
525 if (mTopY
< 0) mTopY
= 0;
527 if (mTopY
!= oty
) glconPostScreenRepaint();
531 bool onKey (KeyEvent event
) {
532 // jump to next contact with unread messages
533 if (event
== "*-C-U") {
534 if (event
== "D-C-U") {
535 foreach (immutable iidx
, ListItemBase li
; items
) {
536 if (auto lc
= cast(ListItemContact
)li
) {
537 if (lc
.ct
.kfd
) continue;
538 if (lc
.ct
.unreadCount
> 0) {
539 if (mActiveItem
!= iidx
) {
541 makeActiveItemVisible();
542 glconPostScreenRepaint();
543 if (onActivateContactCB
!is null) onActivateContactCB(lc
.ct
);
554 private float sbpos
, sbsize
; // set in `drawAt()`
556 // the following two should be called after `drawAt()`; if `sbSize()` is <0, draw nothing
557 final float sbPosition () pure const nothrow @safe @nogc => sbpos
;
558 final float sbSize () pure const nothrow @safe @nogc => sbsize
;
560 // real rect is scissored
561 void drawAt (NVGContext nvg
, int x0
, int y0
, int wdt
, int hgt
) {
568 scope(exit
) nvg
.restore();
571 nvg
.setupCtxFrom(fstash
);
573 int totalHeight
= calcTotalHeight();
574 if (mTopY
> totalHeight
-hgt
) mTopY
= totalHeight
-hgt
;
575 if (mTopY
< 0) mTopY
= 0;
578 foreach (immutable iidx
, ListItemBase li
; items
) {
579 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
582 if (lh
< 1) continue;
583 if (y
+lh
> 0 && y
< hgt
) {
585 scope(exit
) nvg
.restore();
587 nvg
.intersectScissor(x0
, y0
+y
, wdt
, lh
);
588 li
.drawAt(nvg
, x0
, y0
+y
, wdt
, (iidx
== mActiveItem
));
590 nvg
.translate(x0
+0.5f, y0
+y
+0.5f);
591 nvg
.intersectScissor(0, 0, wdt
, lh
);
592 li
.drawAt(nvg
, 0, 0, wdt
);
599 if (totalHeight
> hgt
) {
600 //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);
601 sbpos
= mTopY
/cast(float)(totalHeight
-hgt
);
602 sbsize
= hgt
/cast(float)totalHeight
;
604 //nvg.bndScrollBar(x0+wdt-BND_SCROLLBAR_WIDTH+0.5f, y0+0.5f, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);