prevent popup flooding
[bioacid.git] / notifyicon.d
blob3845e56cebbe748835063275439747b36d217032
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 notifyicon 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.gxx;
27 import iv.meta;
28 import iv.nanovega;
29 import iv.nanovega.textlayouter;
30 import iv.strex;
31 import iv.tox;
32 import iv.sdpyutil;
33 import iv.unarray;
34 import iv.utfutil;
35 import iv.vfs.io;
37 import accdb;
38 import fonts;
39 import icondata;
42 // ////////////////////////////////////////////////////////////////////////// //
43 class TooltipWindow : SimpleWindow {
44 public: // utilities
45 static string charArrayToString(T) (T[] str) if (isAnyCharType!(T, true)) {
46 if (str.length == 0) return null;
47 static if (isWideCharType!(T, true)) {
48 int len = 0;
49 char[4] ut;
50 foreach (auto ch; str) len += utf8Encode(ut[], ch);
51 string res;
52 res.reserve(len);
53 foreach (auto ch; str) {
54 auto l = utf8Encode(ut[], ch);
55 assert(l > 0);
56 res ~= ut[0..l];
58 return res;
59 } else {
60 static if (is(T == immutable(char))) return str; else return str.idup;
64 private:
65 static GxRect getWorkArea () {
66 GxRect rc;
67 getWorkAreaRect(rc.x0, rc.y0, rc.width, rc.height);
68 return rc;
71 static GxSize hintWindowSize(T) (T[] hinttext) nothrow if (isAnyCharType!(T, true)) {
72 laf.font = "ui";
73 laf.size = 18;
74 int textWidth = laf.textWidth(hinttext)+6;
75 if (textWidth < 8) textWidth = 8;
76 return GxSize(textWidth, laf.textHeight+4);
79 private:
80 NVGContext hintvg;
81 string hinttext;
82 int initX, initY;
83 bool active;
85 private:
86 void cbOnClosing () {
87 active = false;
88 if (hintvg !is null) {
89 this.setAsCurrentOpenGlContext();
90 scope(exit) { flushGui(); this.releaseCurrentOpenGlContext(); }
91 hintvg.kill();
92 assert(hintvg is null);
93 //conwriteln("ONCLOSING!");
97 void cbWindowResized (int wdt, int hgt) {
98 if (this.closed) return;
99 if (!this.visible) return;
100 this.redrawOpenGlSceneNow();
103 void cbInit () {
104 this.setAsCurrentOpenGlContext(); // make this window active
105 scope(exit) this.releaseCurrentOpenGlContext();
106 this.vsync = false;
107 hintvg = nvgCreateContext(NVGContextFlag.FontNoAA);
108 if (hintvg is null) assert(0, "cannot initialize NanoVG");
109 //hintvg.createFont("ui", uiFontNames[0]);
110 //if (nvg !is null) hintvg.addFontsFrom(nvg); else hintvg.createFont("ui", uiFontNames[0]);
111 hintvg.fonsContext.addFontsFrom(fstash);
112 //conwriteln("FIRST TIME");
113 //this.redrawOpenGlSceneNow();
116 void cbOnPaint () {
117 if (this.closed) return;
118 if (hintvg !is null) {
119 active = true;
120 this.setAsCurrentOpenGlContext(); // make this window active
121 scope(exit) this.releaseCurrentOpenGlContext();
123 glViewport(0, 0, this.width, this.height);
124 glClearColor(0, 0, 0, 0);
125 glClear(glNVGClearFlags/*|GL_COLOR_BUFFER_BIT*/);
127 hintvg.beginFrame(this.width, this.height);
128 scope(exit) hintvg.endFrame();
129 //hintvg.shapeAntiAlias = false;
131 //auto wsz = hintWindowSize(hinttext);
132 //conwritefln!"sz=(%d,%d); wsz=(%d,%d); nsz=(%d,%d)"(this.width, this.height, wsz.width, wsz.height, hintvg.width, hintvg.height);
134 hintvg.newPath();
135 hintvg.rect(0.5, 0.5, hintvg.width-1, hintvg.height-1);
136 hintvg.strokeWidth = 1;
137 hintvg.strokeColor(NVGColor.black);
138 hintvg.fillColor(NVGColor.yellow);
139 hintvg.fill();
140 hintvg.stroke();
142 hintvg.fontFace = "ui";
143 hintvg.fontSize = 18;
144 hintvg.textAlign = NVGTextAlign.H.Left;
145 hintvg.textAlign = NVGTextAlign.V.Baseline;
146 hintvg.fillColor(NVGColor.black);
147 hintvg.text(3, 2+hintvg.textFontAscender, hinttext);
149 //flushGui();
152 void calcSizePos (out GxRect rc) {
153 auto wsz = hintWindowSize(hinttext);
154 auto wrc = getWorkArea();
155 int nx = initX;
156 int ny = initY;
157 if (nx+wsz.width > wrc.x1) nx = wrc.x1-wsz.width+1;
158 if (nx < wrc.x0) nx = wrc.x0;
159 if (ny+wsz.height > wrc.y1) ny = wrc.y1-wsz.height+1;
160 if (ny < wrc.y0) ny = wrc.y0;
161 // set return value
162 rc.x0 = nx;
163 rc.y0 = ny;
164 rc.width = wsz.width;
165 rc.height = wsz.height;
168 public:
169 override void close () { active = false; super.close(); }
171 this(T) (int ax, int ay, T[] atext) if (isAnyCharType!(T, true)) {
172 assert(laf !is null);
174 hinttext = charArrayToString(atext);
175 initX = ax;
176 initY = ay;
178 GxRect wpos;
179 calcSizePos(out wpos);
181 this.onClosing = &cbOnClosing;
182 this.windowResized = &cbWindowResized;
183 this.visibleForTheFirstTime = &cbInit;
184 this.redrawOpenGlScene = &cbOnPaint;
185 //this.handleMouseEvent = &cbOnMouse;
188 auto oldWClass = sdpyWindowClass;
189 scope(exit) sdpyWindowClass = oldWClass;
190 sdpyWindowClass = "BIOACID_HINT_WINDOW";
191 super(wpos.width, wpos.height, "BioAcidHint", OpenGlOptions.yes, Resizability.fixedSize, WindowTypes.undecorated, WindowFlags.skipTaskbar|WindowFlags.alwaysOnTop|WindowFlags.cannotBeActivated|WindowFlags.dontAutoShow);
193 XSetWindowBackground(this.impl.display, this.impl.window, gxRGB!(255, 255, 0));
195 // sorry for this hack
196 this.setNetWMWindowType(GetAtom!("_NET_WM_WINDOW_TYPE_DOCK", true)(this.display));
197 //this.setNetWMWindowType(GetAtom!("_NET_WM_WINDOW_TYPE_TOOLTIP", true)(this.display));
199 Atom[4] atoms;
200 atoms[0] = GetAtom!("_NET_WM_STATE_STICKY", true)(this.impl.display);
201 atoms[1] = GetAtom!("_NET_WM_STATE_SKIP_TASKBAR", true)(this.impl.display);
202 atoms[2] = GetAtom!("_NET_WM_STATE_SKIP_PAGER", true)(this.impl.display);
203 atoms[3] = GetAtom!("_NET_WM_STATE_ABOVE", true)(this.impl.display);
204 XChangeProperty(
205 this.impl.display,
206 this.impl.window,
207 GetAtom!("_NET_WM_STATE", true)(this.impl.display),
208 XA_ATOM,
209 32 /* bits */,
210 0 /*PropModeReplace*/,
211 atoms.ptr,
212 cast(int)atoms.length);
214 //if (this.hidden) this.show();
215 XMoveWindow(this.impl.display, this.impl.window, 6000, 6000);
216 this.show();
217 //this.moveResize(wpos.x0, wpos.y0, wpos.width, wpos.height);
218 XMoveResizeWindow(this.impl.display, this.impl.window, 6000, 6000, wpos.width, wpos.height);
219 flushGui();
220 //conwritefln!"CREATED! (%d,%d) [%s]"(wsz.width, wsz.height, hinttext);
221 XMoveResizeWindow(this.impl.display, this.impl.window, wpos.x0, wpos.y0, wpos.width, wpos.height);
222 flushGui();
225 string hint () const nothrow @safe @nogc { pragma(inline, true); return hinttext; }
227 void hint(T) (T[] atext) if (isAnyCharType!(T, true)) {
228 string ht = charArrayToString(atext);
229 if (ht != hinttext) {
230 //TODO: check if resize works (it doesn't)
231 hinttext = ht;
232 GxRect wpos;
233 calcSizePos(out wpos);
234 //conwritefln!"NEW: sz=(%d,%d); wsz=(%d,%d)"(this.width, this.height, wpos.width, wpos.height);
235 if (!this.closed && active) {
236 //this.setAsCurrentOpenGlContext(); // make this window active
237 //scope(exit) this.releaseCurrentOpenGlContext();
238 //this.moveResize(wpos.x0, wpos.y0, wpos.width, wpos.height);
239 XMoveResizeWindow(this.impl.display, this.impl.window, wpos.x0, wpos.y0, wpos.width, wpos.height);
241 //flushGui();
245 @property GxPoint initPos () const nothrow @safe @nogc { pragma(inline, true); return GxPoint(initX, initY); }
249 // ////////////////////////////////////////////////////////////////////////// //
250 __gshared TooltipWindow sdhint;
251 __gshared Timer hintHideTimer;
252 __gshared string lastHintText;
255 // ////////////////////////////////////////////////////////////////////////// //
256 void setHint(T:const(char)[]) (T str) {
257 static if (is(T == typeof(null))) {
258 setHint("");
259 } else {
260 lastHintText = TooltipWindow.charArrayToString(str);
261 if (sdhint is null || sdhint.closed || sdhint.hidden) return;
262 //sdhint.hint = lastHintText;
263 auto pos = sdhint.initPos();
264 sdhint.close();
265 sdhint = new TooltipWindow(pos.x, pos.y, lastHintText);
270 // ////////////////////////////////////////////////////////////////////////// //
271 __gshared NotificationAreaIcon trayicon;
272 __gshared Image[6] trayimage; // offline, online, away; same as Status
273 __gshared ContactStatus traystatus = ContactStatus.Offline;
274 __gshared bool trayIsUnread = false;
275 __gshared bool trayBlinkStatus = true; // true: show kitty
276 __gshared Timer blinkTimer;
279 void setTrayUnread () {
280 if (!trayIsUnread) {
281 trayIsUnread = true;
282 if (blinkTimer is null) {
283 blinkTimer = new Timer(800, delegate () {
284 trayBlinkStatus = !trayBlinkStatus;
285 if (trayIsUnread) {
286 trayicon.icon = (trayBlinkStatus ? trayimage[$-1] : trayimage[traystatus]);
287 flushGui(); // or it may not redraw itself
291 if (trayBlinkStatus) {
292 trayicon.icon = trayimage[$-1];
293 flushGui(); // or it may not redraw itself
299 void setTrayStatus (ContactStatus status) {
300 if (status != traystatus || trayIsUnread) {
301 trayIsUnread = false;
302 traystatus = status;
303 trayicon.icon = trayimage[traystatus];
304 flushGui(); // or it may not redraw itself
309 void prepareTrayIcon () {
310 static Image buildTrayIcon (const(uint)[] img) {
311 auto icon = new TrueColorImage(16, 16);
312 scope(exit) { delete icon.imageData.bytes; delete icon; }
313 foreach (immutable y; 0..16) {
314 foreach (immutable x; 0..16) {
315 Color c;
316 c.asUint = img[y*16+x];
317 if (c.a < 0x20) c = Color.black;
318 else if (c.a < 0xff) {
319 c = c.toBW;
320 c = Color(c.r*c.a/255, c.g*c.a/255, c.b*c.a/255);
322 icon.setPixel(x, y, c);
325 return Image.fromMemoryImage(icon);
328 trayimage[ContactStatus.Offline] = buildTrayIcon(baph16Gray[]);
329 trayimage[ContactStatus.Online] = buildTrayIcon(baph16Online[]);
330 trayimage[ContactStatus.Away] = buildTrayIcon(baph16Away[]);
331 trayimage[ContactStatus.Busy] = buildTrayIcon(baph16Busy[]);
332 trayimage[ContactStatus.Connecting] = buildTrayIcon(baph16Orange[]);
333 trayimage[$-1] = buildTrayIcon(kittyMessage[]);
334 //traystatus = Contact.Status.Online;
335 trayicon = new NotificationAreaIcon("BioAcid", trayimage[traystatus], (int x, int y, MouseButton button, ModifierState mods) {
336 //conwritefln!"x=%d; y=%d; button=%u; mods=0x%04x"(x, y, button, mods);
337 if (button == MouseButton.middle) {
338 concmd("quit");
339 glconPostDoConCommands!true();
340 return;
342 if (button == MouseButton.left) {
343 concmd("win_toggle");
344 glconPostDoConCommands!true();
345 return;
348 trayicon.onEnter = delegate (int x, int y, ModifierState mods) {
349 if (sdhint is null || sdhint.hidden) {
350 if (sdhint && !sdhint.closed) sdhint.close();
351 sdhint = null;
352 sdhint = new TooltipWindow(x+18, y+2, lastHintText);
353 if (hintHideTimer !is null) hintHideTimer.destroy();
354 if (sdhint !is null) {
355 hintHideTimer = new Timer(2000, delegate () {
356 if (hintHideTimer !is null) {
357 hintHideTimer.destroy();
358 hintHideTimer = null;
359 if (sdhint !is null && !sdhint.closed) {
360 sdhint.close();
361 sdhint = null;
362 //sdhint.hide();
363 flushGui();
365 sdhint = null; // just in case
371 trayicon.onLeave = delegate () {