fixed (i REALLY hope so!) long-standing bug with urls becomes unclickable sometimes
[bioacid.git] / tklog.d
blobf48e8e8b507a815ce4bfd960872940690f986fd6
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 uint wordidx; // first word
60 //__gshared LayUrl[uint] layUrlList; // for each word
61 private __gshared LayUrl[] layUrlArray;
63 string findUrlByWordIndex (uint widx) {
64 foreach (const ref LayUrl u; layUrlArray) if (u.wordidx == widx) return u.url;
65 return null;
68 private void appendUrl (uint widx, string url) {
69 foreach (ref LayUrl u; layUrlArray) {
70 if (u.wordidx == widx) {
71 u.url = url;
72 return;
75 layUrlArray.length += 1;
76 layUrlArray.assumeSafeAppend;
77 layUrlArray[$-1].url = url;
78 layUrlArray[$-1].wordidx = widx;
81 private void urlWordsRemoved (uint widx, uint count) {
82 if (count == 0) return;
83 for (usize idx = 0; idx < layUrlArray.length; ++idx) {
84 if (layUrlArray[idx].wordidx < widx) continue;
85 // remove if removed ;-)
86 if (layUrlArray[idx].wordidx < widx+count) {
87 layUrlArray.unsafeArrayRemove(idx);
88 --idx;
89 continue;
91 layUrlArray[idx].wordidx -= count;
96 // ////////////////////////////////////////////////////////////////////////// //
97 class MessageStart : LayObject {
98 long msgid; // >0: outgoing, unacked yet
100 this (long aid=-1) nothrow @safe @nogc { msgid = aid; }
102 override int width () => 0;
103 override int spacewidth () => 0;
104 override int height () => 0;
105 override int ascent () => 0;
106 override int descent () => 0;
107 override bool canbreak () => true;
108 override bool spaced () => false;
109 // y is at baseline
110 override void draw (NVGContext ctx, float x, float y) {}
114 class MessageDividerStart : LayObject {
115 long msgid; // >0: outgoing, unacked yet
117 this (long aid=-1) nothrow @safe @nogc { msgid = aid; }
119 override int width () => 0;
120 override int spacewidth () => 0;
121 override int height () => 0;
122 override int ascent () => 0;
123 override int descent () => 0;
124 override bool canbreak () => true;
125 override bool spaced () => false;
126 // y is at baseline
127 override void draw (NVGContext ctx, float x, float y) {}
131 class MessageOutMark : LayObject {
132 long msgid; // >=0: outgoing, unacked yet (0: sent in offline mode)
133 TextDigest digest; // text digest
134 SysTime time;
136 this (long aid, SysTime atime, const(void)[] atext) nothrow @safe @nogc { msgid = aid; time = atime; digest = textDigest(atext); }
138 override int width () => kittyOut.width;
139 override int spacewidth () => 4;
140 override int height () => kittyOut.height;
141 override int ascent () => 0;
142 override int descent () => 0;
143 override bool canbreak () => true;
144 override bool spaced () => false;
145 // y is at baseline
146 override void draw (NVGContext ctx, float x, float y) {
147 if (msgid >= 0) {
148 nvg.save();
149 scope(exit) nvg.restore;
150 nvg.newPath();
151 // +3 is a hack
152 nvg.rect(x, y-height+3, width, height);
153 nvg.fillPaint(nvg.imagePattern(x, y-height+3, width, height, 0, kittyOut));
154 nvg.fill();
158 final bool isOurMark (const(void)[] atext, SysTime atime) nothrow @trusted {
159 pragma(inline, true);
160 return (atime == time && digest[] == textDigest(atext));
163 final bool isOurMark (long aid, const(void)[] atext, SysTime atime) nothrow @trusted {
164 pragma(inline, true);
165 return (aid > 0 ? (msgid == aid) : (atime == time && digest[] == textDigest(atext)));
170 // ////////////////////////////////////////////////////////////////////////// //
171 void logFixAckMessageId (long oldid, long newid, const(void)[] text, SysTime time) {
172 if (oldid == newid) return; // nothing to do
173 foreach (immutable uint widx; 0..lay.wordCount) {
174 auto w = lay.wordByIndex(widx);
175 int oidx = w.objectIdx;
176 if (oidx >= 0) {
177 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(oidx)) {
178 if (maw.isOurMark(oldid, text, time)) {
179 maw.msgid = newid;
180 glconPostScreenRepaint(); // redraw
187 void logResetAckMessageIds () {
188 bool doRefresh = false;
189 foreach (immutable uint widx; 0..lay.wordCount) {
190 auto w = lay.wordByIndex(widx);
191 int oidx = w.objectIdx;
192 if (oidx >= 0) {
193 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(oidx)) {
194 if (maw.msgid > 0) {
195 maw.msgid = 0;
196 doRefresh = true;
201 if (doRefresh) glconPostScreenRepaint(); // redraw
204 void ackLogMessage (long msgid) {
205 if (msgid <= 0) return;
206 foreach (immutable uint widx; 0..lay.wordCount) {
207 auto w = lay.wordByIndex(widx);
208 int oidx = w.objectIdx;
209 if (oidx >= 0) {
210 if (auto maw = cast(MessageOutMark)lay.objectAtIndex(oidx)) {
211 if (maw.msgid == msgid) {
212 maw.msgid = -1; // reset mark
213 glconPostScreenRepaint(); // redraw
221 // coordinates are adjusted so (0, 0) points to logical layouter top-left
223 bool logCheckCancelMark (int mx, int my) {
224 bool removeFromResendQueue (long msgid, in ref TextDigest digest, SysTime time) {
228 // ////////////////////////////////////////////////////////////////////////// //
229 void wipeLog () {
230 lay.wipeAll(true); // clear log, but delete objects
231 layUrlArray.unsafeArrayClear();
232 layOffset = 0;
236 // ////////////////////////////////////////////////////////////////////////// //
237 void addDividerLine (bool doflushgui=false) {
238 if (glconCtlWindow is null || glconCtlWindow.closed) return;
241 bool inFrame = nvg.inFrame;
242 if (!inFrame) {
243 glconCtlWindow.setAsCurrentOpenGlContext(); // make this window active
244 nvg.beginFrame(glconCtlWindow.width, glconCtlWindow.height);
246 scope(exit) {
247 if (!inFrame) {
248 nvg.endFrame();
249 if (doflushgui) flushGui();
250 glconCtlWindow.releaseCurrentOpenGlContext();
254 uint widx = 0;
255 while (widx < lay.wordCount) {
256 auto w = lay.wordByIndex(widx);
257 int oidx = w.objectIdx;
258 if (oidx >= 0) {
259 if (auto maw = cast(MessageDividerStart)lay.objectAtIndex(oidx)) {
260 // this always followed by expander; remove them both
261 lay.removeWordsAt(widx, 2);
262 urlWordsRemoved(widx, 2);
263 continue;
266 ++widx;
269 lay.fontStyle.fontsize = 2;
270 lay.fontStyle.color = NVGColor.k8orange.asUint;
271 lay.fontStyle.bgcolor = NVGColor("#aa0").asUint;
272 lay.fontStyle.monospace = true;
274 lay.putObject(new MessageDividerStart());
275 lay.putExpander();
276 lay.endPara();
277 lay.finalize();
280 // redraw
281 glconPostScreenRepaint();
285 // ////////////////////////////////////////////////////////////////////////// //
286 // `ct` can be `null` for "my message" or "system message"
287 void addTextToLog (Account acc, Contact ct, LogFile.Msg.Kind kind, bool action, const(char)[] msg, SysTime time, long msgid=-1, bool doflushgui=false) {
288 if (glconCtlWindow is null || glconCtlWindow.closed) return;
291 bool inFrame = nvg.inFrame;
292 if (!inFrame) {
293 glconCtlWindow.setAsCurrentOpenGlContext(); // make this window active
294 nvg.beginFrame(glconCtlWindow.width, glconCtlWindow.height);
296 scope(exit) {
297 if (!inFrame) {
298 nvg.endFrame();
299 if (doflushgui) flushGui();
300 glconCtlWindow.releaseCurrentOpenGlContext();
304 // add "message start" mark
305 lay.putObject(new MessageStart(msgid));
307 lay.fontStyle.fontsize = 16;
308 lay.fontStyle.color = NVGColor.k8orange.asUint;
309 //lay.fontStyle.bgcolor = NVGColor("#222").asUint;
310 lay.fontStyle.bgcolor = NVGColor("#5888").asUint;
311 lay.fontStyle.monospace = true;
313 NVGColor textColor;
315 final switch (kind) {
316 case LogFile.Msg.Kind.Outgoing: textColor = NVGColor.k8orange; lay.fontStyle.color = NVGColor("#c40").asUint; lay.put(acc.info.nick); break;
317 case LogFile.Msg.Kind.Incoming: textColor = NVGColor("#aaa"); lay.fontStyle.color = NVGColor("#777").asUint; lay.put(ct.info.nick); break;
318 case LogFile.Msg.Kind.Notification: textColor = NVGColor("#0c0"); lay.fontStyle.color = textColor.asUint; lay.put("*system*"); break;
321 lay.fontStyle.monospace = false;
322 //lay.putHardSpace(64);
323 lay.putExpander();
324 // add "message outgoing" mark
325 if (kind == LogFile.Msg.Kind.Outgoing) {
326 //conwriteln("OG: msgid=", msgid);
327 if (msgid <= 0 && ct !is null) {
328 msgid = ct.findInResendQueue(msg, time);
329 //conwriteln(" new msgid=", msgid);
331 lay.putObject(new MessageOutMark(msgid, time, msg));
332 lay.putExpander();
335 if (kind == LogFile.Msg.Kind.Incoming) lay.fontStyle.color = NVGColor("#888").asUint;
337 import std.datetime;
338 import std.format : format;
339 auto dt = cast(DateTime)time;
340 string tstr = "%04u/%02u/%02u".format(dt.year, dt.month, dt.day);
341 lay.put(tstr);
342 lay.putNBSP();
345 lay.fontStyle.color = (kind == LogFile.Msg.Kind.Incoming ? NVGColor("#888").asUint : NVGColor("#666").asUint);
346 lay.fontStyle.bgcolor = NVGColor("#006").asUint;
348 import std.datetime;
349 import std.format : format;
350 auto dt = cast(DateTime)time;
351 string tstr = "%02u:%02u:%02u".format(dt.hour, dt.minute, dt.second);
352 lay.put(tstr);
353 lay.putNBSP();
355 lay.endPara();
357 lay.fontStyle.bgcolor = NVGColor.transparent.asUint;
358 lay.fontStyle.fontsize = 20;
360 if (action) {
361 lay.fontStyle.color = NVGColor("#fff").asUint;
362 lay.put("/me ");
365 void xput (const(char)[] str) {
366 while (str.length) {
367 auto nl = str.indexOf('\n');
368 if (nl < 0) { lay.put(str); break; }
369 if (nl > 0) lay.put(str[0..nl]);
370 lay.endPara();
371 str = str[nl+1..$];
375 msg = msg.xstripright;
377 lay.fontStyle.color = textColor.asUint;
378 while (msg.length) {
379 auto nfo = urlDetect(msg);
380 if (!nfo.valid) {
381 xput(msg);
382 break;
384 // url found
385 xput(msg[0..nfo.pos]);
386 string url = msg[nfo.pos..nfo.end].idup;
387 msg = msg[nfo.end..$];
388 auto stword = lay.nextWordIndex;
389 lay.pushStyles();
390 auto c = lay.fontStyle.color;
391 scope(exit) { lay.popStyles; lay.fontStyle.color = c; }
392 lay.fontStyle.href = true;
393 lay.fontStyle.underline = true;
394 lay.fontStyle.color = NVGColor("#06f").asUint;
395 lay.put(url);
396 while (stword < lay.nextWordIndex) {
397 appendUrl(stword, url);
398 ++stword;
402 lay.endPara();
403 lay.finalize();
405 // redraw
406 glconPostScreenRepaint();
410 void addTextToLog (Account acc, Contact ct, in ref LogFile.Msg msg, long msgid=-1, bool doflushgui=false) {
411 import iv.utfutil : utf8Encode;
412 char[] text;
413 text.reserve(4096);
414 scope(exit) delete text;
415 // decode text
416 foreach (dchar dc; msg.byDChar) {
417 char[4] buf = void;
418 auto len = utf8Encode(buf[], dc);
419 text ~= buf[0..len];
421 addTextToLog(acc, ct, msg.kind, msg.isMe, text, msg.time, msgid, doflushgui);