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 // ////////////////////////////////////////////////////////////////////////// //
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
{
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) {
84 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
85 nvg
.fillColor(selected ?
NVGColor("#5ff0") : NVGColor("#5fff"));
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
{
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) {
119 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
120 nvg
.fillColor(selected ?
NVGColor("#5880") : NVGColor("#5888"));
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;
138 auto len
= vsnprintf(buf
.ptr
, buf
.length
, fmt
, ap
);
144 // ////////////////////////////////////////////////////////////////////////// //
145 class ListItemContact
: ListItemBase
{
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
;
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
;
171 hgt
+= nvg
.textFontHeight
;
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);
181 int fullhgt
= height
;
183 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, fullhgt
);
184 nvg
.fillColor(NVGColor("#9600"));
193 if (ct
.requestPending
) {
195 } else if (ct
.acceptPending
) {
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
));
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
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");
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));
235 nvg
.text(x0
+wdt
-BND_SCROLLBAR_WIDTH
-1, ty
, xfmt("last seen: %u.5 month ago", cast(int)(nowut
-ltt
)/31));
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));
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)));
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
258 private import core
.time
;
261 int mActiveItem
= -1; // active item (may be different from selected with cursor)
263 int mLastX
= -666, mLastY
= -666, mLastHeight
, mLastWidth
;
264 //MonoTime mLastClick = MonoTime.zero;
265 //int mLastClickItem = -1;
268 ListItemBase
[] items
;
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
;
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
;
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;
304 //nvg.fontFace = "ui";
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
];
323 void buildAccount (Account acc
) {
324 if (acc
is null) return;
329 items
~= new ListItemAccount(acc
);
331 scope(exit
) delete css
;
332 foreach (Group g
; acc
.groups
) {
333 items
~= new ListItemGroup(g
);
335 css
.assumeSafeAppend
;
336 foreach (Contact c
; acc
.contacts
.byValue
) if (c
.gid
== g
.gid
) css
~= c
;
337 import std
.algorithm
: sort
;
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()
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
);
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;
360 foreach (immutable iidx
, ListItemBase li
; items
) {
361 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
364 if (lh
< 1) continue;
365 if (aty
< lh
) return cast(int)iidx
;
371 final int calcTotalHeight () {
374 foreach (immutable iidx
, ListItemBase li
; items
) {
375 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
378 if (lh
< 1) continue;
384 static struct AIPos
{
386 @property bool valid () const pure nothrow @safe @nogc => (hgt
> 0);
389 AIPos
calcActiveItemPos () {
391 if (mActiveItem
< 0 || mActiveItem
>= items
.length
) return res
;
393 foreach (immutable iidx
, ListItemBase li
; items
) {
394 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
397 if (lh
< 1) continue;
398 if (iidx
== mActiveItem
) { res
.hgt
= lh
; return res
; }
405 // if called with `null` ct, deactivate
406 void delegate (Contact ct
) onActivateContactCB
;
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
]) {
419 if (onActivateContactCB
!is null) onActivateContactCB(ci
.ct
);
420 glconPostScreenRepaint();
426 enum ScrollHeight
= 32;
428 if (event
== "WheelUp") {
430 mTopY
-= ScrollHeight
;
431 if (mTopY
< 0) mTopY
= 0;
432 glconPostScreenRepaint();
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();
450 void makeActiveItemVisible () {
451 if (mActiveItem
< 0 || mActiveItem
>= items
.length || mLastHeight
< 1) return;
452 auto pos
= calcActiveItemPos();
453 if (!pos
.valid
) return;
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();
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
) {
475 makeActiveItemVisible();
476 glconPostScreenRepaint();
477 if (onActivateContactCB
!is null) onActivateContactCB(lc
.ct
);
488 // real rect is scissored
489 void drawAt (int x0
, int y0
, int wdt
, int hgt
) {
496 scope(exit
) nvg
.restore();
499 int totalHeight
= calcTotalHeight();
500 if (mTopY
> totalHeight
-hgt
) mTopY
= totalHeight
-hgt
;
501 if (mTopY
< 0) mTopY
= 0;
504 foreach (immutable iidx
, ListItemBase li
; items
) {
505 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
508 if (lh
< 1) continue;
510 if (y
+lh
> 0 && y
< hgt
) {
512 scope(exit
) nvg
.restore();
514 nvg
.intersectScissor(x0
, y0
+y
, wdt
, lh
);
515 li
.drawAt(x0
, y0
+y
, wdt
, (iidx
== mActiveItem
));
517 nvg
.translate(x0
+0.5f, y0
+y
+0.5f);
518 nvg
.intersectScissor(0, 0, wdt
, lh
);
519 li
.drawAt(0, 0, wdt
);
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
);
529 //nvg.bndScrollBar(x0+wdt-BND_SCROLLBAR_WIDTH+0.5f, y0+0.5f, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);