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;
47 // ////////////////////////////////////////////////////////////////////////// //
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
{
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) {
83 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
84 nvg
.fillColor(selected ?
NVGColor("#5ff0") : NVGColor("#5fff"));
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
{
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) {
118 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, hgt
);
119 nvg
.fillColor(selected ?
NVGColor("#5880") : NVGColor("#5888"));
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
{
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
;
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
;
157 hgt
+= nvg
.textFontHeight
;
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);
167 int fullhgt
= height
;
169 nvg
.rect(x0
+0.5f, y0
+0.5f, wdt
, fullhgt
);
170 nvg
.fillColor(NVGColor("#9600"));
179 if (ct
.requestPending
) {
181 } else if (ct
.acceptPending
) {
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
));
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
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");
209 import core
.stdc
.stdio
: snprintf
;
210 //conwriteln("ls0: ", ct.info.lastonlinetime);
211 SysTime
dt = SysTime
.fromUnixTime(ct
.info
.lastonlinetime
);
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
224 private import core
.time
;
227 int mActiveItem
= -1; // active item (may be different from selected with cursor)
229 int mLastX
= -666, mLastY
= -666, mLastHeight
, mLastWidth
;
230 //MonoTime mLastClick = MonoTime.zero;
231 //int mLastClickItem = -1;
234 ListItemBase
[] items
;
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
;
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
;
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;
270 //nvg.fontFace = "ui";
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
];
289 void buildAccount (Account acc
) {
290 if (acc
is null) return;
295 items
~= new ListItemAccount(acc
);
297 scope(exit
) delete css
;
298 foreach (Group g
; acc
.groups
) {
299 items
~= new ListItemGroup(g
);
301 css
.assumeSafeAppend
;
302 foreach (Contact c
; acc
.contacts
.byValue
) if (c
.gid
== g
.gid
) css
~= c
;
303 import std
.algorithm
: sort
;
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()
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
);
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;
326 foreach (immutable iidx
, ListItemBase li
; items
) {
327 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
330 if (lh
< 1) continue;
331 if (aty
< lh
) return cast(int)iidx
;
337 final int calcTotalHeight () {
340 foreach (immutable iidx
, ListItemBase li
; items
) {
341 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
344 if (lh
< 1) continue;
350 static struct AIPos
{
352 @property bool valid () const pure nothrow @safe @nogc => (hgt
> 0);
355 AIPos
calcActiveItemPos () {
357 if (mActiveItem
< 0 || mActiveItem
>= items
.length
) return res
;
359 foreach (immutable iidx
, ListItemBase li
; items
) {
360 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
363 if (lh
< 1) continue;
364 if (iidx
== mActiveItem
) { res
.hgt
= lh
; return res
; }
371 // if called with `null` ct, deactivate
372 void delegate (Contact ct
) onActivateContactCB
;
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
]) {
385 if (onActivateContactCB
!is null) onActivateContactCB(ci
.ct
);
386 glconPostScreenRepaint();
392 enum ScrollHeight
= 32;
394 if (event
== "WheelUp") {
396 mTopY
-= ScrollHeight
;
397 if (mTopY
< 0) mTopY
= 0;
398 glconPostScreenRepaint();
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();
416 void makeActiveItemVisible () {
417 if (mActiveItem
< 0 || mActiveItem
>= items
.length || mLastHeight
< 1) return;
418 auto pos
= calcActiveItemPos();
419 if (!pos
.valid
) return;
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();
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
) {
441 makeActiveItemVisible();
442 glconPostScreenRepaint();
443 if (onActivateContactCB
!is null) onActivateContactCB(lc
.ct
);
454 // real rect is scissored
455 void drawAt (int x0
, int y0
, int wdt
, int hgt
) {
462 scope(exit
) nvg
.restore();
465 int totalHeight
= calcTotalHeight();
466 if (mTopY
> totalHeight
-hgt
) mTopY
= totalHeight
-hgt
;
467 if (mTopY
< 0) mTopY
= 0;
470 foreach (immutable iidx
, ListItemBase li
; items
) {
471 if (iidx
!= mActiveItem
&& !li
.visible
) continue;
474 if (lh
< 1) continue;
476 if (y
+lh
> 0 && y
< hgt
) {
478 scope(exit
) nvg
.restore();
480 nvg
.intersectScissor(x0
, y0
+y
, wdt
, lh
);
481 li
.drawAt(x0
, y0
+y
, wdt
, (iidx
== mActiveItem
));
483 nvg
.translate(x0
+0.5f, y0
+y
+0.5f);
484 nvg
.intersectScissor(0, 0, wdt
, lh
);
485 li
.drawAt(0, 0, wdt
);
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
);
495 //nvg.bndScrollBar(x0+wdt-BND_SCROLLBAR_WIDTH+0.5f, y0+0.5f, BND_SCROLLBAR_WIDTH, hgt, BND_DEFAULT, 1, 1);