some cosmetix (force opengl viewport)
[bioacid.git] / tklog.d
blob0aba2afa7c5ae08a1fe313b73aebbc19b4f9d1ab
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 tklog is aliced;
18 import std.datetime;
20 import arsd.color;
21 import arsd.image;
22 import arsd.simpledisplay;
24 import iv.cmdcon;
25 import iv.cmdcon.gl;
26 import iv.nanovega;
27 import iv.nanovega.blendish;
28 import iv.nanovega.textlayouter;
29 import iv.strex;
30 import iv.tox;
31 import iv.unarray;
32 import iv.utfutil;
33 import iv.vfs.io;
35 import accdb;
36 import accobj;
37 import fonts;
38 import popups;
39 import icondata;
40 import notifyicon;
41 import toxproto;
43 import tkmain;
46 // ////////////////////////////////////////////////////////////////////////// //
47 alias LayTextClass = LayTextD;
50 // ////////////////////////////////////////////////////////////////////////// //
51 __gshared LayTextClass lay;
52 __gshared int layWinHeight = 0;
53 __gshared int layOffset = 0; // from bottom
55 static struct LayUrl {
56 string url;
57 union {
58 usize refcount;
59 usize nextFree; // index+1
62 @property bool isFree () const nothrow @safe @nogc => (url.length == 0);
65 private __gshared usize[string] layUrlIndex;
66 private __gshared usize layUrlFreeHead = 0;
67 private __gshared LayUrl[] layUrlArray;
70 private usize allocUrl (string url) {
71 usize res = layUrlFreeHead;
72 if (res) {
73 --res; // normalize
74 layUrlFreeHead = layUrlArray[res].nextFree;
75 } else {
76 res = layUrlArray.length;
77 layUrlArray.length += 1;
78 layUrlArray.assumeSafeAppend;
80 layUrlArray[res].url = url;
81 layUrlArray[res].refcount = 1;
82 layUrlIndex[url] = res;
83 return res;
87 private void freeUrl (usize idx) {
88 assert(idx < layUrlArray.length);
89 assert(!layUrlArray[idx].isFree);
90 if (--layUrlArray[idx].refcount) return;
91 //conwriteln("FREE URL idx=", idx, "; url=[", layUrlArray[idx].url, "]");
92 layUrlIndex.remove(layUrlArray[idx].url);
93 layUrlArray[idx].url = null;
94 layUrlArray[idx].nextFree = layUrlFreeHead;
95 layUrlFreeHead = idx+1;
99 string findUrlByWordIndex (uint widx) {
100 auto w = lay.wordByIndex(widx);
101 if (w is null) return null;
102 usize pos = w.udata;
103 if (pos < 1 || pos > layUrlArray.length) return null;
104 //conwriteln(" pos=", pos, "; url=[", layUrlArray[pos-1].url, "]");
105 --pos;
106 return layUrlArray[pos].url;
110 private void appendUrl (uint widx, string url) {
111 auto w = lay.wordByIndex(widx);
112 if (w is null) return;
113 assert(w.udata == 0);
114 if (url.length == 0) return;
115 auto ip = url in layUrlIndex;
116 if (ip) {
117 assert(!layUrlArray[*ip].isFree);
118 ++layUrlArray[*ip].refcount;
119 w.udata = (*ip)+1;
120 //conwriteln("OLD URL widx=", widx, "; udata=", w.udata, "; rc=", layUrlArray[*ip].refcount, "; url=[", layUrlArray[*ip].url, "]");
121 } else {
122 usize pos = allocUrl(url);
123 w.udata = pos+1;
124 //conwriteln("NEW URL widx=", widx, "; udata=", w.udata, "; rc=", layUrlArray[pos].refcount, "; url=[", layUrlArray[pos].url, "]");
129 private void urlWordsRemoved (uint widx, int count) {
130 if (count < 1) return;
131 foreach (uint wpos; widx..widx+count) {
132 auto w = lay.wordByIndex(wpos);
133 if (w is null) continue;
134 usize pos = w.udata;
135 if (!pos) continue;
136 --pos;
137 assert(pos < layUrlArray.length);
138 assert(!layUrlArray[pos].isFree);
139 assert(layUrlArray[pos].refcount);
140 //conwriteln("FREE WORD URL widx=", wpos, "; udata=", w.udata, "; rc=", layUrlArray[pos].refcount, "; url=[", layUrlArray[pos].url, "]");
141 freeUrl(pos);
146 private void urlWipeAll () {
147 layUrlArray.unsafeArrayClear();
148 layUrlIndex.clear();
149 layUrlFreeHead = 0;
153 // ////////////////////////////////////////////////////////////////////////// //
154 class MessageStart : LayObject {
155 long msgid; // >0: outgoing, unacked yet
157 this (long aid=-1) nothrow @safe @nogc { msgid = aid; }
159 override int width () => 0;
160 override int spacewidth () => 0;
161 override int height () => 0;
162 override int ascent () => 0;
163 override int descent () => 0;
164 override bool canbreak () => true;
165 override bool spaced () => false;
166 // y is at baseline
167 override void draw (NVGContext ctx, float x, float y) {}
171 class MessageDividerStart : LayObject {
172 long msgid; // >0: outgoing, unacked yet
174 this (long aid=-1) nothrow @safe @nogc { msgid = aid; }
176 override int width () => 0;
177 override int spacewidth () => 0;
178 override int height () => 0;
179 override int ascent () => 0;
180 override int descent () => 0;
181 override bool canbreak () => true;
182 override bool spaced () => false;
183 // y is at baseline
184 override void draw (NVGContext ctx, float x, float y) {}
188 class MessageOutMark : LayObject {
189 long msgid; // >=0: outgoing, unacked yet (0: sent in offline mode)
190 TextDigest digest; // text digest
191 SysTime time;
193 this (long aid, SysTime atime, const(void)[] atext) nothrow @safe @nogc { msgid = aid; time = atime; digest = textDigest(atext); }
195 override int width () => kittyOut.width;
196 override int spacewidth () => 4;
197 override int height () => kittyOut.height;
198 override int ascent () => 0;
199 override int descent () => 0;
200 override bool canbreak () => true;
201 override bool spaced () => false;
202 // y is at baseline
203 override void draw (NVGContext ctx, float x, float y) {
204 if (msgid >= 0) {
205 nvg.save();
206 scope(exit) nvg.restore;
207 nvg.newPath();
208 // +3 is a hack
209 nvg.rect(x, y-height+3, width, height);
210 nvg.fillPaint(nvg.imagePattern(x, y-height+3, width, height, 0, kittyOut));
211 nvg.fill();
215 final bool isOurMark (const(void)[] atext, SysTime atime) nothrow @trusted {
216 pragma(inline, true);
217 return (atime == time && digest[] == textDigest(atext));
220 final bool isOurMark (long aid, const(void)[] atext, SysTime atime) nothrow @trusted {
221 pragma(inline, true);
222 return (aid > 0 ? (msgid == aid) : (atime == time && digest[] == textDigest(atext)));
227 // ////////////////////////////////////////////////////////////////////////// //
228 void logFixAckMessageId (long oldid, long newid, const(void)[] text, SysTime time) {
229 if (oldid == newid) return; // nothing to do
230 foreach (immutable uint widx; 0..lay.wordCount) {
231 auto w = lay.wordByIndex(widx);
232 int oidx = w.objectIdx;
233 if (oidx >= 0) {
234 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(oidx)) {
235 if (maw.isOurMark(oldid, text, time)) {
236 maw.msgid = newid;
237 glconPostScreenRepaint(); // redraw
244 void logResetAckMessageIds () {
245 bool doRefresh = false;
246 foreach (immutable uint widx; 0..lay.wordCount) {
247 auto w = lay.wordByIndex(widx);
248 int oidx = w.objectIdx;
249 if (oidx >= 0) {
250 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(oidx)) {
251 if (maw.msgid > 0) {
252 maw.msgid = 0;
253 doRefresh = true;
258 if (doRefresh) glconPostScreenRepaint(); // redraw
261 void ackLogMessage (long msgid) {
262 if (msgid <= 0) return;
263 foreach (immutable uint widx; 0..lay.wordCount) {
264 auto w = lay.wordByIndex(widx);
265 int oidx = w.objectIdx;
266 if (oidx >= 0) {
267 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(oidx)) {
268 if (maw.msgid == msgid) {
269 maw.msgid = -1; // reset mark
270 glconPostScreenRepaint(); // redraw
278 // coordinates are adjusted so (0, 0) points to logical layouter top-left
280 bool logCheckCancelMark (int mx, int my) {
281 bool removeFromResendQueue (long msgid, in ref TextDigest digest, SysTime time) {
285 // ////////////////////////////////////////////////////////////////////////// //
286 void wipeLog () {
287 lay.wipeAll(true); // clear log, but delete objects
288 urlWipeAll();
289 layOffset = 0;
293 // ////////////////////////////////////////////////////////////////////////// //
294 void addDividerLine (bool doflushgui=false) {
295 if (glconCtlWindow is null || glconCtlWindow.closed) return;
298 bool inFrame = nvg.inFrame;
299 if (!inFrame) {
300 glconCtlWindow.setAsCurrentOpenGlContext(); // make this window active
301 glViewport(0, 0, glconCtlWindow.width, glconCtlWindow.height);
302 nvg.beginFrame(glconCtlWindow.width, glconCtlWindow.height);
304 scope(exit) {
305 if (!inFrame) {
306 nvg.endFrame();
307 if (doflushgui) flushGui();
308 glconCtlWindow.releaseCurrentOpenGlContext();
312 uint widx = 0;
313 while (widx < lay.wordCount) {
314 auto w = lay.wordByIndex(widx);
315 int oidx = w.objectIdx;
316 if (oidx >= 0) {
317 if (auto maw = cast(MessageDividerStart)lay.objectAtIndex(oidx)) {
318 // this always followed by expander; remove them both
319 lay.removeWordsAt(widx, 2);
320 urlWordsRemoved(widx, 2);
321 continue;
324 ++widx;
327 lay.fontStyle.fontsize = 2;
328 lay.fontStyle.color = NVGColor.k8orange.asUint;
329 lay.fontStyle.bgcolor = NVGColor("#aa0").asUint;
330 lay.fontStyle.monospace = true;
332 lay.putObject(new MessageDividerStart());
333 lay.putExpander();
334 lay.endPara();
335 lay.finalize();
338 // redraw
339 glconPostScreenRepaint();
343 // ////////////////////////////////////////////////////////////////////////// //
344 // `ct` can be `null` for "my message" or "system message"
345 void addTextToLog (Account acc, Contact ct, LogFile.Msg.Kind kind, bool action, const(char)[] msg, SysTime time, long msgid=-1, bool doflushgui=false) {
346 if (glconCtlWindow is null || glconCtlWindow.closed) return;
349 bool inFrame = nvg.inFrame;
350 if (!inFrame) {
351 glconCtlWindow.setAsCurrentOpenGlContext(); // make this window active
352 nvg.beginFrame(glconCtlWindow.width, glconCtlWindow.height);
354 scope(exit) {
355 if (!inFrame) {
356 nvg.endFrame();
357 if (doflushgui) flushGui();
358 glconCtlWindow.releaseCurrentOpenGlContext();
362 // add "message start" mark
363 lay.putObject(new MessageStart(msgid));
365 lay.fontStyle.fontsize = 16;
366 lay.fontStyle.color = NVGColor.k8orange.asUint;
367 //lay.fontStyle.bgcolor = NVGColor("#222").asUint;
368 lay.fontStyle.bgcolor = NVGColor("#5888").asUint;
369 lay.fontStyle.monospace = true;
371 NVGColor textColor;
373 final switch (kind) {
374 case LogFile.Msg.Kind.Outgoing: textColor = NVGColor.k8orange; lay.fontStyle.color = NVGColor("#c40").asUint; lay.put(acc.info.nick); break;
375 case LogFile.Msg.Kind.Incoming: textColor = NVGColor("#aaa"); lay.fontStyle.color = NVGColor("#777").asUint; lay.put(ct.info.nick); break;
376 case LogFile.Msg.Kind.Notification: textColor = NVGColor("#0c0"); lay.fontStyle.color = textColor.asUint; lay.put("*system*"); break;
379 lay.fontStyle.monospace = false;
380 //lay.putHardSpace(64);
381 lay.putExpander();
382 // add "message outgoing" mark
383 if (kind == LogFile.Msg.Kind.Outgoing) {
384 //conwriteln("OG: msgid=", msgid);
385 if (msgid <= 0 && ct !is null) {
386 msgid = ct.findInResendQueue(msg, time);
387 //conwriteln(" new msgid=", msgid);
389 lay.putObject(new MessageOutMark(msgid, time, msg));
390 lay.putExpander();
393 if (kind == LogFile.Msg.Kind.Incoming) lay.fontStyle.color = NVGColor("#888").asUint;
395 import std.datetime;
396 import std.format : format;
397 auto dt = cast(DateTime)time;
398 string tstr = "%04u/%02u/%02u".format(dt.year, dt.month, dt.day);
399 lay.put(tstr);
400 lay.putNBSP();
403 lay.fontStyle.color = (kind == LogFile.Msg.Kind.Incoming ? NVGColor("#888").asUint : NVGColor("#666").asUint);
404 lay.fontStyle.bgcolor = NVGColor("#006").asUint;
406 import std.datetime;
407 import std.format : format;
408 auto dt = cast(DateTime)time;
409 string tstr = "%02u:%02u:%02u".format(dt.hour, dt.minute, dt.second);
410 lay.put(tstr);
411 lay.putNBSP();
413 lay.endPara();
415 lay.fontStyle.bgcolor = NVGColor.transparent.asUint;
416 lay.fontStyle.fontsize = 20;
418 if (action) {
419 lay.fontStyle.color = NVGColor("#fff").asUint;
420 lay.put("/me ");
423 void xput (const(char)[] str) {
424 while (str.length) {
425 auto nl = str.indexOf('\n');
426 if (nl < 0) { lay.put(str); break; }
427 if (nl > 0) lay.put(str[0..nl]);
428 lay.endPara();
429 str = str[nl+1..$];
433 msg = msg.xstripright;
435 lay.fontStyle.color = textColor.asUint;
436 while (msg.length) {
437 auto nfo = urlDetect(msg);
438 if (!nfo.valid) {
439 xput(msg);
440 break;
442 // url found
443 xput(msg[0..nfo.pos]);
444 string url = msg[nfo.pos..nfo.end].idup;
445 msg = msg[nfo.end..$];
446 auto stword = lay.nextWordIndex;
447 lay.pushStyles();
448 auto c = lay.fontStyle.color;
449 scope(exit) { lay.popStyles; lay.fontStyle.color = c; }
450 lay.fontStyle.href = true;
451 lay.fontStyle.underline = true;
452 lay.fontStyle.color = NVGColor("#06f").asUint;
453 lay.put(url);
454 if (msg.length) {
455 lay.endWord(spaced:(msg[0] <= ' ' && msg[0] != '\n'));
456 } else {
457 lay.endWord(spaced:false);
459 while (stword < lay.nextWordIndex) {
460 appendUrl(stword, url);
461 ++stword;
465 lay.endPara();
466 lay.finalize();
468 // redraw
469 glconPostScreenRepaint();
473 void addTextToLog (Account acc, Contact ct, in ref LogFile.Msg msg, long msgid=-1, bool doflushgui=false) {
474 import iv.utfutil : utf8Encode;
475 char[] text;
476 text.reserve(4096);
477 scope(exit) delete text;
478 // decode text
479 foreach (dchar dc; msg.byDChar) {
480 char[4] buf = void;
481 auto len = utf8Encode(buf[], dc);
482 text ~= buf[0..len];
484 addTextToLog(acc, ct, msg.kind, msg.isMe, text, msg.time, msgid, doflushgui);