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 bioacid
is aliced
;
23 import arsd
.simpledisplay
;
30 import iv
.nanovega
.blendish
;
31 import iv
.nanovega
.textlayouter
;
40 version(sfnt_test
) import iv
.nanovega
.simplefont
;
56 // ////////////////////////////////////////////////////////////////////////// //
57 //__gshared string accountNameToLoad = "_fakeacc";
58 __gshared string accountNameToLoad
= "";
59 __gshared string globalHotkey
= "M-H-F";
62 void main (string
[] args
) {
63 if (!TOX_VERSION_IS_ABI_COMPATIBLE()) assert(0, "invalid ToxCore library version");
65 conRegVar
!accountNameToLoad("starting_account", "account to load");
66 conRegVar
!optShowOffline("show_offline", "always show offline contacts?");
68 //conwriteln("account '", acc.info.nick, "' loaded, ", acc.contacts.length, " contacts, ", acc.groups.length, " groups");
70 //glconShowKey = "M-Grave";
71 glconSetAndSealFPS(0); // draw-on-demand
73 conProcessQueue(256*1024); // load config
74 conProcessArgs
!true(args
);
75 conProcessQueue(256*1024);
77 if (accountNameToLoad
.length
== 0) assert(0, "no account to load");
79 //setOpenGLContextVersion(3, 2); // up to GLSL 150
80 setOpenGLContextVersion(2, 0); // it's enough
85 NVGPathSet svp
= null;
87 //glconRunGLWindowResizeable(800, 600, "BioAcid", "BIOACID");
88 sdpyWindowClass
= "BIOACID";
89 sdmain
= new SimpleWindow(800, 600, "BioAcid", OpenGlOptions
.yes
, Resizability
.allowResizing
);
90 glconCtlWindow
= sdmain
;
93 setupToxEventListener(sdmain
);
95 sdmain
.visibilityChanged
= delegate (bool vis
) { mainWindowVisible
= vis
; fixUnreadIndicators(); };
96 sdmain
.onFocusChange
= delegate (bool focused
) { mainWindowActive
= focused
; /*conwriteln("focus=", focused);*/ fixUnreadIndicators(); };
98 MiniEdit sysedit
= new MiniEdit();
100 MiniEdit
currEdit () { return (activeContact
!is null ? activeContact
.edit
: sysedit
); }
103 if (globalHotkey
.length
> 0) {
104 GlobalHotkeyManager
.register(globalHotkey
, delegate () { concmd("win_toggle"); glconPostDoConCommands
!true(); });
106 } catch (Exception e
) {
107 conwriteln("ERROR registering hotkey!");
111 if (sdmain
!is null) sdmain
.close();
112 })("quit", "quit BioAcid");
115 if (sdmain
!is null && !sdmain
.closed
) {
116 //conwriteln("act=", mainWindowActive, "; vis=", mainWindowVisible, "; realvis=", sdmain.visible);
117 if (!mainWindowVisible
) {
118 // this strange code brings window to the current desktop it if was on a different one
121 } else if (sdmain
.visible
) {
128 })("win_toggle", "show/hide main window");
130 sdmain
.addEventListener((GLConScreenRepaintEvent evt
) {
131 if (sdmain
.closed
) return;
132 if (isQuitRequested
) { sdmain
.close(); return; }
133 sdmain
.redrawOpenGlSceneNow();
136 sdmain
.addEventListener((GLConDoConsoleCommandsEvent evt
) {
137 glconProcessEventMessage();
140 sdmain
.addEventListener((PopupCheckerEvent evt
) {
141 popupCheckExpirations();
145 // ////////////////////////////////////////////////////////////////////// //
146 sdmain
.onClosing
= delegate () {
147 clist
.forEachAccount(delegate (acc
) { acc
.forceOnline
= false; });
150 sdmain
.setAsCurrentOpenGlContext();
151 scope(exit
) { flushGui(); sdmain
.releaseCurrentOpenGlContext(); }
156 if (sdhint
!is null) sdhint
.close();
157 if (trayicon
!is null) trayicon
.close();
160 sdmain
.closeQuery
= delegate () {
162 glconPostDoConCommands
!true();
166 sdmain
.visibleForTheFirstTime
= delegate () {
167 if (sdmain
.width
> 1 && optCListWidth
< 0) optCListWidth
= sdmain
.width
/5;
168 sdmain
.setAsCurrentOpenGlContext(); // make this window active
169 scope(exit
) sdmain
.releaseCurrentOpenGlContext();
170 sdmain
.vsync
= false;
172 glconInit(sdmain
.width
, sdmain
.height
);
174 nvg
= nvgCreateContext(NVGContextFlag
.Antialias
, NVGContextFlag
.StencilStrokes
, NVGContextFlag
.FontNoAA
);
175 if (nvg
is null) assert(0, "cannot initialize NanoVG");
179 static immutable skullsPng
= /*cast(immutable(ubyte)[])*/import("data/skulls.png");
180 //nvgSkullsImg = nvg.createImage("skulls.png", NVGImageFlags.NoFiltering|NVGImageFlags.RepeatX|NVGImageFlags.RepeatY);
181 auto xi
= loadImageFromMemory(skullsPng
[]);
182 scope(exit
) delete xi
;
183 //{ import core.stdc.stdio; printf("creating background image...\n"); }
184 nvgSkullsImg
= nvg
.createImageFromMemoryImage(xi
, NVGImageFlags
.NoFiltering
, NVGImageFlags
.RepeatX
, NVGImageFlags
.RepeatY
);
185 //{ import core.stdc.stdio; printf("background image created\n"); }
186 if (!nvgSkullsImg
.valid
) assert(0, "cannot load background image");
187 } catch (Exception e
) {
188 assert(0, "cannot load background image");
194 lay
= new LayTextClass(laf
, sdmain
.width
/2);
195 //conwriteln("wdt=", lay.width, "; text width=", lay.textWidth);
196 lastWindowWidth
= sdmain
.width
;
199 loadAccount(accountNameToLoad
);
200 //clist.buildAccount(acc);
201 clist
.onActivateContactCB
= delegate (Contact ct
) { doActivateContact(ct
); };
202 addContactCommands();
204 sdmain
.setMinSize(640, 480);
207 //sdmain.redrawOpenGlSceneNow();
210 sdmain
.windowResized
= delegate (int wdt
, int hgt
) {
211 if (sdmain
.closed
) return;
212 glconResize(wdt
, hgt
);
213 glconPostScreenRepaint
/*Delayed*/();
214 //conwriteln("old=", lastWindowWidth, "; new=", wdt);
215 if (wdt
> 1 && optCListWidth
> 0 && lastWindowWidth
> 0 && lastWindowWidth
!= wdt
) {
216 immutable double frc
= lastWindowWidth
/optCListWidth
;
217 optCListWidth
= cast(int)(wdt
/frc
);
218 if (optCListWidth
< 64) optCListWidth
= 64;
219 lastWindowWidth
= wdt
;
225 int mouseX
= -666, mouseY
= -666;
228 sdmain
.handleKeyEvent
= delegate (KeyEvent event
) {
229 if (sdmain
.closed
) return;
230 scope(exit
) glconPostDoConCommands
!true();
231 if (glconKeyEvent(event
)) return;
233 auto acc
= clist
.mainAccount
;
235 if (event
== "D-Escape") {
236 if (sdmain
!is null && !sdmain
.closed
&& sdmain
.visible
) {
243 if (event
== "D-C-Q") { concmd("quit"); return; }
244 if (event
== "D-C-1") { acc
.status
= ContactStatus
.Online
; return; }
245 if (event
== "D-C-2") { acc
.status
= ContactStatus
.Away
; return; }
246 if (event
== "D-C-3") { acc
.status
= ContactStatus
.Busy
; return; }
247 if (event
== "D-C-0") { acc
.status
= ContactStatus
.Offline
; return; }
249 if (event
== "D-C-W") { doActivateContact(null); return; }
251 if (clist
!is null) {
252 sdmain
.setAsCurrentOpenGlContext(); // make this window active
253 scope(exit
) sdmain
.releaseCurrentOpenGlContext();
255 bool inFrame
= nvg
.inFrame
;
256 if (!inFrame
) nvg
.beginFrame(sdmain
.width
, sdmain
.height
);
257 scope(exit
) if (!inFrame
) nvg
.endFrame();
261 scope(exit
) nvg
.restore();
262 if (clist
.onKey(event
)) return;
266 if (event
== "D-C-S-Enter") {
267 static PopupWindow
.Kind kind
;
268 //new PopupWindow(10, 10, "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
269 showPopup(kind
, "TEST popup", "this is my text, lol (еб\u00adёна КОЧЕРГА!)");
270 if (kind
== PopupWindow
.Kind
.max
) kind
= PopupWindow
.Kind
.min
; else ++kind
;
274 if (event
== "D-Enter") {
275 auto text
= currEdit
.text
.xstrip
;
277 if (text
.length
== 0) { glconPostScreenRepaint(); return; }
281 while (text
.length
&& text
[0] <= ' ') text
= text
[1..$];
282 if (text
.length
== 0) return null;
283 if (text
[0] == '"' || text
[0] == '\'') {
287 while (text
.length
&& text
[0] != ech
) {
288 if (text
[0] == '\\') {
290 if (text
.length
== 0) break;
293 case '\r': res
~= '\r'; break;
294 case '\n': res
~= '\n'; break;
295 case '\t': res
~= '\t'; break;
296 default: res
~= ch
; break;
303 if (text
.length
) { assert(text
[0] == ech
); text
= text
[1..$]; }
307 while (ep
< text
.length
&& text
[ep
] > ' ') ++ep
;
308 auto res
= text
[0..ep
];
314 if (activeContact
!is null) {
316 if (text
[0] == '/' && !text
.startsWith("/me ") && !text
.startsWith("/me\t")) {
318 auto cmd
= getWord();
319 if (cmd
== "accept") {
320 if (toxCoreAddFriend(activeContact
.acc
.toxpk
, activeContact
.info
.pubkey
)) {
321 addTextToLog(activeContact
.acc
, activeContact
, LogFile
.Msg
.Kind
.Notification
, false, "accepted!", systimeNow
);
322 activeContact
.kind
= ContactInfo
.Kind
.Friend
;
323 activeContact
.save();
325 addTextToLog(activeContact
.acc
, activeContact
, LogFile
.Msg
.Kind
.Notification
, false, "ERROR accepting!", systimeNow
);
327 } else if (cmd
== "kfd") {
328 toxCoreRemoveFriend(activeContact
.acc
.toxpk
, activeContact
.info
.pubkey
);
329 addTextToLog(activeContact
.acc
, activeContact
, LogFile
.Msg
.Kind
.Notification
, false, "KILL! FUCK! DIE!", systimeNow
);
330 activeContact
.kind
= ContactInfo
.Kind
.KillFuckDie
;
331 activeContact
.save();
332 } else if (cmd
== "friend") {
333 if (!isValidAddr(activeContact
.info
.fraddr
)) { conwriteln("invalid address"); return; }
334 string msg
= getWord();
335 if (msg
.length
== 0) { conwriteln("address: '", tox_hex(activeContact
.info
.fraddr
), "'; please, specify message!"); return; }
336 if (msg
.length
> tox_max_friend_request_length()) { conwriteln("address: '", tox_hex(activeContact
.info
.fraddr
), "'; message too long"); return; }
337 if (!acc
.sendFriendRequest(activeContact
.info
.fraddr
, msg
)) { conwriteln("address: '", tox_hex(activeContact
.info
.fraddr
), "'; error sending friend request"); return; }
338 } else if (cmd
== "status") {
339 string msg
= getWord();
340 if (msg
.length
== 0) { conwriteln("message?"); return; }
341 if (!toxCoreSetStatusMessage(activeContact
.acc
.toxpk
, msg
)) { conwriteln("ERROR: cannot set status message"); return; }
342 activeContact
.acc
.info
.statusmsg
= msg
;
343 try { activeContact
.acc
.save(); } catch (Exception e
) { conwriteln("ERROR saving account: ", e
.msg
); }
345 addTextToLog(activeContact
.acc
, activeContact
, LogFile
.Msg
.Kind
.Notification
, false, "wut?!", systimeNow
);
348 activeContact
.send(text
);
352 if (acc
!is null && text
.length
&& text
[0] == '/') {
355 auto cmd
= getWord();
357 case "friend": // add new friend
358 string addr
= getWord();
359 ToxAddr fraddr
= decodeAddrStr(addr
);
360 if (!isValidAddr(fraddr
)) { conwriteln("invalid address: '", addr
, "'"); return; }
361 string msg
= getWord();
362 if (msg
.length
== 0) { conwriteln("address: '", addr
, "'; please, specify message!"); return; }
363 if (msg
.length
> tox_max_friend_request_length()) { conwriteln("address: '", addr
, "'; message too long"); return; }
364 //if (!toxCoreSendFriendRequest(acc.toxpk, fraddr, msg)) { conwriteln("address: '", addr, "'; error sending friend request");
365 if (!acc
.sendFriendRequest(fraddr
, msg
)) { conwriteln("address: '", addr
, "'; error sending friend request"); return; }
368 string msg
= getWord();
369 if (msg
.length
== 0) { conwriteln("message?"); return; }
370 if (!toxCoreSetStatusMessage(acc
.toxpk
, msg
)) { conwriteln("ERROR: cannot set status message"); return; }
371 acc
.info
.statusmsg
= msg
;
372 try { acc
.save(); } catch (Exception e
) { conwriteln("ERROR saving account: ", e
.msg
); }
375 conwriteln("unknown command: '", cmd
, "'");
379 if (text
.length
) conwriteln("NOT A COMMAND: ", text
);
382 glconPostScreenRepaint();
386 if (currEdit
.onKey(event
)) return;
389 int msLastPressX
= -666, msLastPressY
= -666;
390 uint msDoLogButton
= 0; // =0: none; 1: left; 2: middle; 3: right
392 sdmain
.handleMouseEvent
= delegate (MouseEvent event
) {
393 if (sdmain
.closed
) return;
394 scope(exit
) glconPostDoConCommands
!true();
395 if (isConsoleVisible
) return;
399 // check for click in log
400 //FIXME: process it here, not in renderer
401 if (event
== "LMB-Down") { msLastPressX
= mouseX
; msLastPressY
= mouseY
; msDoLogButton
= 1; glconPostScreenRepaint(); }
402 if (event
== "MMB-Down") { msLastPressX
= mouseX
; msLastPressY
= mouseY
; msDoLogButton
= 2; glconPostScreenRepaint(); }
403 if (event
== "RMB-Down") { msLastPressX
= mouseX
; msLastPressY
= mouseY
; msDoLogButton
= 3; glconPostScreenRepaint(); }
405 if (clist
!is null) {
406 sdmain
.setAsCurrentOpenGlContext(); // make this window active
407 scope(exit
) sdmain
.releaseCurrentOpenGlContext();
409 bool inFrame
= nvg
.inFrame
;
410 if (!inFrame
) nvg
.beginFrame(sdmain
.width
, sdmain
.height
);
411 scope(exit
) if (!inFrame
) nvg
.endFrame();
415 scope(exit
) nvg
.restore();
416 if (clist
.onMouse(event
)) return;
421 if (layWinHeight
> 0) {
422 enum ScrollHeight
= 32;
423 if (event
== "WheelUp") {
424 layOffset
+= ScrollHeight
;
425 if (layOffset
> lay
.textHeight
-layWinHeight
) layOffset
= lay
.textHeight
-layWinHeight
;
426 } else if (event
== "WheelDown") {
427 layOffset
-= ScrollHeight
;
429 if (layOffset
< 0) layOffset
= 0;
432 // don't spam with repaint events
433 if (event
.type
!= MouseEventType
.motion
) glconPostScreenRepaint();
436 sdmain
.handleCharEvent
= delegate (dchar ch
) {
437 if (sdmain
.closed
) return;
438 scope(exit
) glconPostDoConCommands
!true();
439 if (glconCharEvent(ch
)) return;
441 if (currEdit
.onChar(ch
)) return;
445 sdmain
.redrawOpenGlScene
= delegate () {
446 glconPostDoConCommands
!true();
447 if (sdmain
.closed
) return;
448 sdmain
.setAsCurrentOpenGlContext(); // make this window active
449 scope(exit
) sdmain
.releaseCurrentOpenGlContext();
452 scope(exit
) glconDraw();
454 if (nvg
is null) return;
456 //oglSetup2D(glconCtlWindow.width, glconCtlWindow.height);
457 glViewport(0, 0, sdmain
.width
, sdmain
.height
);
458 glMatrixMode(GL_MODELVIEW
);
459 //conwriteln(glconCtlWindow.width, "x", glconCtlWindow.height);
461 glClearColor(0, 0, 0, 0);
462 glClear(glNVGClearFlags
/*|GL_COLOR_BUFFER_BIT*/);
465 nvg
.beginFrame(sdmain
.width
, sdmain
.height
);
466 scope(exit
) nvg
.endFrame();
468 if (clist
!is null && optCListWidth
> 0) {
472 int wdt
= optCListWidth
;
473 int hgt
= nvg
.height
-cy
*2;
475 nvg
.shapeAntiAlias
= true;
481 scope(exit
) nvg
.restore();
484 nvg
.roundedRect(cx
+0.5f, cy
+0.5f, wdt
, hgt
, 6);
487 nvg
.imageSize(nvgSkullsImg
, w
, h
);
488 nvg
.fillPaint(nvg
.imagePattern(0, 0, w
, h
, 0, nvgSkullsImg
));
490 nvg
.strokeColor(NVGColor("#f70"));
494 nvg
.intersectScissor(cx
+3.5f, cy
+3.5f, wdt
-3*2, hgt
-3*2);
496 clist
.drawAt(cx
+3, cy
+3, wdt
-3*2, hgt
-3*2);
501 scope(exit
) nvg
.restore();
503 //nvg.transform(NVGMatrix.init);
505 auto xf = nvg.currTransform;
506 nvg.currTransform = xf;
511 wdt
= nvg
.width
-cx
-1;
514 // calculate editor dimensions and draw editor
517 scope(exit
) nvg
.restore();
519 auto edinfo
= currEdit
.calcHeight(nvg
, wdt
-3*2);
520 //if (edinfo.height < edinfo.lineh) edinfo.height = edinfo.lineh;
522 int edy
= cy
+hgt
-cast(int)edinfo
.height
-3*2;
525 nvg
.roundedRect(cx
+0.5f, edy
+0.5f, wdt
, edinfo
.height
+3*2, 6);
526 nvg
.fillColor(NVGColor
.black
);
527 nvg
.strokeColor(NVGColor("#f70"));
531 nvg
.intersectScissor(cx
+2.5f, edy
+2.5f, wdt
-3*2+2, edinfo
.height
+2);
532 currEdit
.draw(nvg
, cx
+3, edy
+3, wdt
-3*2, cast(int)edinfo
.height
);
534 hgt
-= cast(int)edinfo
.height
+3*2;
538 nvg
.roundedRect(cx
+0.5f, cy
+0.5f, wdt
, hgt
, 6);
539 nvg
.fillColor(NVGColor
.black
);
540 nvg
.strokeColor(NVGColor("#f70"));
544 nvg
.intersectScissor(cx
+3.5f, cy
+3.5f, wdt
-3*2, hgt
-3*2);
546 immutable float scalex
= (wdt
-3*2-10*2)/BaphometDims
;
547 immutable float scaley
= (baphHgt
-3*2-10*2)/BaphometDims
;
548 immutable float scale
= (scalex
< scaley ? scalex
: scaley
)/1.5f;
549 immutable float sz
= BaphometDims
*scale
;
550 nvg
.strokeColor(NVGColor("#400"));
551 nvg
.fillColor(NVGColor("#400"));
552 nvg
.renderBaphomet(cx
+10.5f+(wdt
-3*2-10*2)/2-sz
/2, cy
+10.5f+(baphHgt
-3*2-10*2)/2-sz
/2, scale
, scale
);
555 immutable float sbx
= cx
+wdt
-BND_SCROLLBAR_WIDTH
-1.5f;
556 immutable float sby
= cy
+3.5f;
557 wdt
-= BND_SCROLLBAR_WIDTH
+1;
561 lay
.relayout(wdt
); // this is harmess if width wasn't changed
562 int ty
= lay
.textHeight
-hgt
+1-layOffset
;
564 layWinHeight
= cast(int)hgt
;
566 if (lay
.textHeight
> hgt
) {
567 float h
= lay
.textHeight
-hgt
;
568 nvg
.bndScrollBar(sbx
, sby
, BND_SCROLLBAR_WIDTH
, hgt
, BND_DEFAULT
, ty
/h
, hgt
/h
);
570 nvg
.bndScrollBar(sbx
, sby
, BND_SCROLLBAR_WIDTH
, hgt
, BND_DEFAULT
, 1, 1);
573 nvg
.intersectScissor(cx
+2.5f, cy
+2.5f, wdt
+2, hgt
+2);
574 nvg
.drawLayouter(lay
, ty
, cx
+3, cy
+3, hgt
);
577 uint btn
= msDoLogButton
;
579 msLastPressX
-= cx
+3;
580 msLastPressY
-= cy
+3;
581 if (msLastPressX
>= 0 && msLastPressY
>= 0 && msLastPressX
< wdt
&& msLastPressY
< hgt
) {
582 auto widx
= lay
.wordAtXY(msLastPressX
, ty
+msLastPressY
);
584 auto w
= lay
.wordByIndex(widx
);
587 if (auto hr
= cast(uint)widx
in layUrlList
) {
588 //conwriteln("URL CLICK: <", hr.url, ">");
593 if (btn
== 3 && w
.objectIdx
>= 0) {
594 if (auto maw
= cast(MessageOutMark
)lay
.objectAtIndex(w
.objectIdx
)) {
595 assert(activeContact
!is null);
596 activeContact
.removeFromResendQueue(maw
.msgid
, maw
.digest
, maw
.time
);
599 glconPostScreenRepaint();
611 sdmain
.eventLoop(15000,
612 // pulser: process resend queues here
614 if (sdmain
.closed || clist
is null) return;
615 clist
.forEachAccount(delegate (Account acc
) { acc
.processResendQueue(); });
618 clist
.forEachAccount(delegate (acc
) { acc
.forceOnline
= false; });
619 clist
.forEachAccount(delegate (Account acc
) { acc
.saveResendQueue(); acc
.saveResendQueue(); });
620 toxCoreShutdownAll();
623 conProcessQueue(int.max
/4);