3 import std
.concurrency
;
7 import arsd
.simpledisplay
;
24 // ////////////////////////////////////////////////////////////////////////// //
25 public GxRect
getWorkArea () {
26 auto dpy
= XDisplayConnection
.get
;
27 auto root
= RootWindow(dpy
, DefaultScreen(dpy
));
28 auto atomWTF
= GetAtom
!("_NET_WORKAREA", true)(dpy
);
34 auto status
= XGetWindowProperty(dpy
, root
, atomWTF
, 0, 4, /*False*/0, AnyPropertyType
, &aType
, &format
, &itemCount
, &bytesAfter
, cast(void**)&propRes
);
35 if (status
>= Success
) {
36 if (propRes
!is null) {
37 auto rc
= GxRect(propRes
[0], propRes
[1], propRes
[2], propRes
[3]);
42 return GxRect(0, 0, 800, 600);
46 // ////////////////////////////////////////////////////////////////////////// //
47 __gshared SimpleWindow sdhint
;
48 __gshared string hinttext
= "not playing";
49 __gshared Image hintbackbuf
;
50 __gshared
uint* hintvbuf
;
51 __gshared Timer hintHideTimer
;
52 __gshared
int hintX
, hintY
;
55 // ////////////////////////////////////////////////////////////////////////// //
56 void setHint(T
:const(char)[]) (T
str) {
57 static if (is(T
== typeof(null))) {
60 if (hinttext
== str) return;
61 static if (is(T
== string
)) hinttext
= str; else hinttext
= str.idup
;
62 repaintHintWindow(false);
67 // ////////////////////////////////////////////////////////////////////////// //
68 void createHintWindow (int x
, int y
) {
69 if (sdhint
!is null) {
72 sdpyWindowClass
= "AMPER_HINT_WINDOW";
73 sdhint
= new SimpleWindow(120, gxTextHeightUtf
+2, "AmperHint", OpenGlOptions
.no
, Resizability
.fixedSize
, WindowTypes
.undecorated
);
74 sdhint
.setNetWMWindowType(GetAtom
!"_NET_WM_WINDOW_TYPE_DOCK"(sdhint
.display
)); // sorry for this hack
79 int textWidth
= gxTextWidthUtf(hinttext
)+4;
80 sdhint
.resize(textWidth
, sdhint
.height
);
83 repaintHintWindow(true);
87 void copyVBufToImage () {
88 import core
.stdc
.string
: memcpy
;
89 memcpy(hintbackbuf
.getDataPointer
, hintvbuf
, hintbackbuf
.width
*hintbackbuf
.height
*4);
93 void repaintHintWindow (bool forced
) {
94 if (sdhint
is null || sdhint
.closed
) return;
95 if (!forced
&& sdhint
.hidden
) return;
98 int textWidth
= gxTextWidthUtf(hinttext
)+4;
99 if (textWidth
< 8) textWidth
= 8;
100 auto wrc
= getWorkArea();
103 if (nx
+textWidth
> wrc
.x1
) nx
= wrc
.x1
-textWidth
+1;
104 if (nx
< wrc
.x0
) nx
= wrc
.x0
;
105 if (ny
+sdhint
.height
> wrc
.y1
) ny
= wrc
.y1
-sdhint
.height
+1;
106 if (ny
< wrc
.y0
) ny
= wrc
.y0
;
109 sdhint
.moveResize(hintX
, hintY
, textWidth
, sdhint
.height
);
112 if (hintbackbuf
is null || hintbackbuf
.width
!= sdhint
.width || hintbackbuf
.height
!= sdhint
.height
) {
113 import core
.stdc
.stdlib
: realloc
;
114 hintbackbuf
= new Image(sdhint
.width
, sdhint
.height
);
115 hintvbuf
= cast(uint*)realloc(hintvbuf
, hintbackbuf
.width
*hintbackbuf
.height
*hintvbuf
[0].sizeof
);
116 if (hintvbuf
is null) assert(0, "out of memory");
120 auto VBufWidthSave
= VBufWidth
;
121 auto VBufHeightSave
= VBufHeight
;
122 auto vglTexBufSave
= vglTexBuf
;
123 VBufWidth
= hintbackbuf
.width
;
124 VBufHeight
= hintbackbuf
.height
;
125 vglTexBuf
= hintvbuf
;
127 VBufWidth
= VBufWidthSave
;
128 VBufHeight
= VBufHeightSave
;
129 vglTexBuf
= vglTexBufSave
;
132 gxClearScreen(gxRGB
!(255, 255, 0));
133 gxDrawRect(0, 0, VBufWidth
, VBufHeight
, gxRGB
!(0, 0, 0));
134 gxClipRect
.shrinkBy(2, 1);
135 gxDrawTextUtf(gxClipRect
.x0
, gxClipRect
.y0
, hinttext
, gxRGB
!(0, 0, 0));
139 auto painter
= sdhint
.draw();
140 painter
.drawImage(Point(0, 0), hintbackbuf
);
146 // ////////////////////////////////////////////////////////////////////////// //
147 __gshared NotificationAreaIcon trayicon
;
148 __gshared Image trayimage
;
149 __gshared MemoryImage icon
; // 0: normal
152 void hideShowWindows () {
153 if (sdampwin
is null || sdampwin
.closed
) return;
154 if (sdampwin
.hidden
) {
155 vglShowWindow(sdampwin
);
157 if (sdplwin
is null || sdplwin
.closed
) {
158 if (dgGreatePListWindow
!is null) dgGreatePListWindow();
160 vglShowWindow(sdplwin
);
162 switchToWindow(sdampwin
);
166 vglHideWindow(sdplwin
);
167 vglHideWindow(sdampwin
);
172 void prepareTrayIcon () {
173 icon
= readPng("skins/notifyicon.png");
174 trayimage
= Image
.fromMemoryImage(icon
);
175 trayicon
= new NotificationAreaIcon("Amper", trayimage
, (int x
, int y
, MouseButton button
, ModifierState mods
) {
176 //conwritefln!"x=%d; y=%d; button=%u; mods=0x%04x"(x, y, button, mods);
177 if (button
== MouseButton
.middle
) {
183 if (button
== MouseButton
.left
) {
184 concmd("win_toggle");
188 trayicon
.onEnter
= delegate (int x
, int y
, ModifierState mods
) {
189 //conwritefln!"icon enter: x=%d; y=%d; mods=0x%04x"(x, y, mods);
191 conwritefln!"icon enter: x=%d; y=%d; mods=0x%04x"(x, y, mods);
192 int wx, wy, wdt, hgt;
193 trayicon.getWindowRect(wx, wy, wdt, hgt);
194 conwriteln("window rect: wx=", wx, "; wy=", wy, "; wdt=", wdt, "; hgt=", hgt);
196 if (sdhint
is null || sdhint
.hidden
) {
197 createHintWindow(x
, y
+2);
198 if (hintHideTimer
!is null) hintHideTimer
.destroy();
199 hintHideTimer
= new Timer(3000, delegate () {
200 if (hintHideTimer
!is null) {
201 hintHideTimer
.destroy();
202 hintHideTimer
= null;
203 if (sdhint
!is null && !sdhint
.closed
) sdhint
.hide();
208 trayicon
.onLeave
= delegate () {
209 //conwriteln("icon leave");
210 //if (sdhint !is null && !sdhint.closed) sdhint.hide();
215 // ////////////////////////////////////////////////////////////////////////// //
216 __gshared
ubyte vbNewScale
= 1;
219 // ////////////////////////////////////////////////////////////////////////// //
220 class ScrollTitleEvent
{}
221 __gshared ScrollTitleEvent evScrollTitle
;
223 shared static this () {
224 evScrollTitle
= new ScrollTitleEvent();
228 // ////////////////////////////////////////////////////////////////////////// //
229 __gshared
bool mainDrag
= false;
230 __gshared
int mainDrawPrevX
, mainDrawPrevY
;
233 // ////////////////////////////////////////////////////////////////////////// //
234 void closeAllIfMainIsClosed () {
236 (glconCtlWindow
is null || glconCtlWindow
.closed
) ||
237 (sdampwin
is null || sdampwin
.closed
);
239 if (sdhint
!is null && !sdhint
.closed
) sdhint
.close();
240 if (sdplwin
!is null && !sdplwin
.closed
) sdplwin
.close();
241 if (sdampwin
!is null && !sdampwin
.closed
) sdampwin
.close();
242 if (glconCtlWindow
!is null && !glconCtlWindow
.closed
) glconCtlWindow
.close();
247 // ////////////////////////////////////////////////////////////////////////// //
248 // create hidden control window
249 void createCtlWindow () {
250 sdpyWindowClass
= "AMPER_PLAYER_CTL";
251 glconCtlWindow
= new SimpleWindow(1, 1, "AmperCtl", OpenGlOptions
.no
, Resizability
.fixedSize
, WindowTypes
.hidden
);
253 glconCtlWindow
.onClosing
= delegate () {
254 //conwriteln("closing ctl window...");
255 if (sdplwin
!is null && !sdplwin
.closed
) sdplwin
.close();
256 if (sdampwin
!is null && !sdampwin
.closed
) sdampwin
.close();
259 glconCtlWindow
.onDestroyed
= delegate () {
260 //conwriteln("ctl window destroyed");
261 closeAllIfMainIsClosed();
264 glconCtlWindow
.addEventListener((QuitEvent evt
) {
265 scope(exit
) if (!conQueueEmpty()) glconPostDoConCommands();
266 scope(exit
) closeAllIfMainIsClosed();
267 if (glconCtlWindow
.closed
) return;
268 if (isQuitRequested
) { glconCtlWindow
.close(); return; }
272 void rebuildRepaint () {
273 scope(exit
) if (!conQueueEmpty()) glconPostDoConCommands();
274 scope(exit
) closeAllIfMainIsClosed();
275 if (glconCtlWindow
.closed
) return;
276 if (isQuitRequested
) { glconCtlWindow
.close(); return; }
277 //conwriteln("rebuilding screen");
279 //if (aplayIsPlaying) ampMain.curtime = aplayCurTime;
281 if (sdampwin
!is null && !sdampwin
.closed
&& !sdampwin
.hidden
) sdampwin
.redrawOpenGlSceneNow();
282 if (sdplwin
!is null && !sdplwin
.closed
&& !sdplwin
.hidden
) sdplwin
.redrawOpenGlSceneNow();
288 glconCtlWindow
.addEventListener((GLConScreenRebuildEvent evt
) { rebuildRepaint(); });
289 glconCtlWindow
.addEventListener((GLConScreenRepaintEvent evt
) { rebuildRepaint(); });
291 glconCtlWindow
.addEventListener((GLConDoConsoleCommandsEvent evt
) {
292 scope(exit
) if (!conQueueEmpty()) glconPostDoConCommands();
293 scope(exit
) closeAllIfMainIsClosed();
294 bool sendAnother
= false;
295 bool prevVisible
= isConsoleVisible
;
298 scope(exit
) consoleUnlock();
300 sendAnother
= !conQueueEmpty();
302 if (glconCtlWindow
.closed
) return;
303 if (isQuitRequested
) { glconCtlWindow
.close(); return; }
304 if (sendAnother
) glconPostDoConCommands();
306 if (prevVisible || isConsoleVisible) glconPostScreenRepaintDelayed();
307 if (vbNewScale != vbufEffScale) glconPostScreenRepaint();
309 if (vbNewScale
!= vglWindowScale(glconCtlWindow
)) {
310 vbNewScale
= vglScaleWindow(glconCtlWindow
, vbNewScale
);
311 vglScaleWindow(sdplwin
, vbNewScale
);
313 glconPostScreenRebuild();
317 glconCtlWindow
.addEventListener((ScrollTitleEvent evt
) {
318 scope(exit
) if (!conQueueEmpty()) glconPostDoConCommands();
319 scope(exit
) closeAllIfMainIsClosed();
320 if (glconCtlWindow
.closed
) return;
321 if (isQuitRequested
) { glconCtlWindow
.close(); return; }
322 //conwriteln("scrolling title");
323 ampMain
.scrollTitle();
324 if (!glconCtlWindow
.eventQueued
!ScrollTitleEvent
) glconCtlWindow
.postTimeout(evScrollTitle
, 100);
325 glconPostScreenRebuild();
328 glconCtlWindow
.addEventListener((EventFileLoaded evt
) {
330 conwriteln("ERROR loading '", evt
.filename
, "'");
331 setHint("not playing");
332 //glconCtlWindow.close();
336 setHint(evt
.artist
~" \u2014 "~evt
.title
);
337 //conwriteln("playing '", evt.filename, "': ", evt.artist, " -- ", evt.title);
340 glconCtlWindow
.addEventListener((EventFileComplete evt
) {
341 //glconCtlWindow.close();
342 setHint("not playing");
343 concmd("song_next tan");
348 // ////////////////////////////////////////////////////////////////////////// //
349 void createAmpWindow () {
350 sdpyWindowClass
= "AMPER_PLAYER";
352 sdampwin
= new SimpleWindow(skinImgMain
.width
*vbufEffScale
, skinImgMain
.height
*vbufEffScale
, "Amper", OpenGlOptions
.yes
, Resizability
.fixedSize
, WindowTypes
.undecorated
);
353 sdampwin
.setMinSize(skinImgMain
.width
*vbufEffScale
, skinImgMain
.height
*vbufEffScale
);
354 sdampwin
.setMaxSize(skinImgMain
.width
*vbufEffScale
, skinImgMain
.height
*vbufEffScale
);
355 //sdwin.hideCursor();
357 static if (is(typeof(&sdampwin
.closeQuery
))) {
358 sdampwin
.closeQuery
= delegate () { concmd("quit"); glconPostDoConCommands(); };
361 sdampwin
.windowResized
= delegate (int wdt
, int hgt
) {
362 sdampwin
.setMinSize(wdt
, hgt
);
363 sdampwin
.setMaxSize(wdt
, hgt
);
366 sdampwin
.redrawOpenGlScene
= delegate () {
368 //conwriteln("PAINT: sdampwin");
371 sdampwin
.visibleForTheFirstTime
= delegate () {
372 vglRegisterWindow
!"main"(sdampwin
);
377 sdampwin.handlePulse = delegate () {
381 sdampwin
.handleKeyEvent
= delegate (KeyEvent event
) {
382 if (event
.pressed
&& event
== "Escape") { concmd("quit"); return; }
383 //if (event.pressed && event == "Plus") { concmd("v_scale 2"); return; }
384 //if (event.pressed && event == "Minus") { concmd("v_scale 1"); return; }
385 //conwriteln(event.toStr);
386 ampMain
.onKey(event
);
387 glconPostScreenRebuild();
388 //sdwin.redrawOpenGlSceneNow();
391 sdampwin
.handleMouseEvent
= delegate (MouseEvent event
) {
393 if (event
.type
== MouseEventType
.motion
/*&& event.modifierState&ModifierState.leftButtonDown*/) {
397 int nx
= event
.x
*vbufEffScale
;
398 int ny
= event
.y
*vbufEffScale
;
399 XTranslateCoordinates(sdampwin
.display
, sdampwin
.window
, RootWindow(sdampwin
.display
, DefaultScreen(sdampwin
.display
)), nx
, ny
, &nx
, &ny
, &dummyw
);
400 int dx
= nx
-mainDrawPrevX
;
401 int dy
= ny
-mainDrawPrevY
;
405 XWindowAttributes xwa
;
406 XGetWindowAttributes(sdampwin
.display
, sdampwin
.window
, &xwa
);
407 XTranslateCoordinates(sdampwin
.display
, sdampwin
.window
, RootWindow(sdampwin
.display
, DefaultScreen(sdampwin
.display
)), xwa
.x
, xwa
.y
, &xwa
.x
, &xwa
.y
, &dummyw
);
408 sdampwin
.move(xwa
.x
+dx
, xwa
.y
+dy
);
410 if (sdplwin
!is null && !sdplwin
.closed
) {
411 XGetWindowAttributes(sdplwin
.display
, sdplwin
.window
, &xwa
);
412 XTranslateCoordinates(sdplwin
.display
, sdplwin
.window
, RootWindow(sdplwin
.display
, DefaultScreen(sdplwin
.display
)), xwa
.x
, xwa
.y
, &xwa
.x
, &xwa
.y
, &dummyw
);
413 sdplwin
.move(xwa
.x
+dx
, xwa
.y
+dy
);
418 if (event
.type
== MouseEventType
.buttonReleased
&& event
.button
== MouseButton
.left
) mainDrag
= false;
421 if (!ampMain
.onMouse(event
)) {
423 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
424 if (ampMain.widgetAt(event.x, event.y) is null) {
428 mainDrawPrevX = event.x*vbufEffScale;
429 mainDrawPrevY = event.y*vbufEffScale;
430 XTranslateCoordinates(sdampwin.display, sdampwin.window, RootWindow(sdampwin.display, DefaultScreen(sdampwin.display)), mainDrawPrevX, mainDrawPrevY, &mainDrawPrevX, &mainDrawPrevY, &dummyw);
435 glconPostScreenRebuild();
436 //sdwin.redrawOpenGlSceneNow();
440 sdampwin.handleCharEvent = delegate (dchar ch) {
444 sdampwin
.onFocusChange
= delegate (bool focused
) { ampMain
.focusChanged(focused
); };
446 if (!sdampwin
.eventQueued
!ScrollTitleEvent
) glconCtlWindow
.postEvent(evScrollTitle
);
450 // ////////////////////////////////////////////////////////////////////////// //
451 void createPListWindow () {
452 sdpyWindowClass
= "AMPER_PLAYLIST";
454 sdplwin
= new SimpleWindow(skinImgMain
.width
*vbufEffScale
, skinImgMain
.height
*vbufEffScale
, "Amper Playlist", OpenGlOptions
.yes
, Resizability
.allowResizing
, WindowTypes
.undecorated
);
455 sdplwin
.setMinSize(skinImgMain
.width
*vbufEffScale
, skinImgMain
.height
*vbufEffScale
);
456 sdplwin
.setMaxSize(4096, 4096);
457 //sdplwin.setResizeGranularity(5, 4);
458 sdplwin
.setResizeGranularity(25, 29);
460 static if (is(typeof(&sdplwin
.closeQuery
))) {
461 sdplwin
.closeQuery
= delegate () { /*concmd("quit"); glconPostDoConCommands();*/ };
464 sdplwin
.redrawOpenGlScene
= delegate () {
466 //conwriteln("PAINT: sdplwin");
467 //conwriteln(ampPList.curitem);
470 sdplwin
.visibleForTheFirstTime
= delegate () {
471 vglRegisterWindow
!"normal"(sdplwin
);
472 if (sdampwin
!is null && !sdampwin
.closed
) {
473 switchToWindow(sdampwin
);
477 XWindowAttributes xwa;
478 XGetWindowAttributes(sdampwin.display, sdampwin.window, &xwa);
479 XTranslateCoordinates(sdampwin.display, sdampwin.window, RootWindow(sdampwin.display, DefaultScreen(sdampwin.display)), xwa.x, xwa.y, &xwa.x, &xwa.y, &dummyw);
480 sdplwin.move(xwa.x, xwa.y+sdampwin.height);
486 sdplwin
.handleKeyEvent
= delegate (KeyEvent event
) {
487 ampPList
.onKey(event
);
488 glconPostScreenRebuild();
489 //sdplwin.redrawOpenGlSceneNow();
492 sdplwin
.handleMouseEvent
= delegate (MouseEvent event
) {
493 //conwriteln("PLMOUSE BEFORE: ", ampPList.curitem, " : ", event.type);
494 ampPList
.onMouse(event
);
495 //conwriteln("PLMOUSE AFTER: ", ampPList.curitem, " : ", event.type);
496 glconPostScreenRebuild();
497 //sdplwin.redrawOpenGlSceneNow();
500 sdplwin
.windowResized
= delegate (int wdt
, int hgt
) {
501 ampPList
.imgrc
= GxRect(0, 0, wdt
/vbufEffScale
, hgt
/vbufEffScale
);
502 //vglResizeBuffer(wdt/vbufEffScale, hgt/vbufEffScale, vbufEffScale);
503 //sdplwin.redrawOpenGlSceneNow();
506 sdplwin
.onFocusChange
= delegate (bool focused
) { ampPList
.focusChanged(focused
); };
510 // ////////////////////////////////////////////////////////////////////////// //
512 void fakeScanDir () {
513 ampPList.appendListItem(PListItem("Sonata Arctica \u2014 Replica", "/mnt/muzax/wtf", 142));
514 ampPList.appendListItem(PListItem("Zonata \u2014 Geronimo", "/mnt/muzax/wtf", 242));
516 foreach (immutable int idx; 3..42) {
517 import std.format : format;
518 ampPList.appendListItem(PListItem("song #%d".format(idx), "/mnt/muzax/wtf", 100+idx));
524 // ////////////////////////////////////////////////////////////////////////// //
525 void scanDir (ConString path
, bool append
) {
526 void appendFile (const(char)[] fname
) {
528 auto sio
= AudioStream
.open(VFile(fname
));
530 ampPList
.appendListItem(PListItem(sio
.artist
~" \u2014 "~sio
.title
, fname
.idup
, cast(int)(sio
.timetotal
/1000)));
531 //conwriteln("+++ ", de.name, ": ", sio.artist, " -- ", sio.title, ": ", cast(int)(sio.timetotal/1000));
534 //conwriteln("+++ ", de.name, ": FUUUUUUU");
536 } catch (Exception e
) {}
542 if (!append
) ampPList
.clear();
543 if (path
.exists
&& path
.isFile
) { appendFile(path
); return; }
544 foreach (DirEntry
de; dirEntries(path
.idup
, SpanMode
.shallow
)) {
545 if (!de.isFile
) continue;
548 } catch (Exception e
) {
549 conwriteln("ERROR scanning: ", e
.msg
);
551 //if (modeShuffle) ampPList.state.curitem = ampPList.findShuffleFirst();
552 //conwriteln(ampPList.findShuffleFirst(), " : ", ampPList.state.shuffleidx, " : ", ampPList.state.curplayingitem);
556 // ////////////////////////////////////////////////////////////////////////// //
557 void main (string
[] args
) {
560 glconShowKey
= "M-Grave";
562 conRegVar
!vbNewScale(1, 8, "v_scale", "window scale");
565 scope(exit
) aplayShutdown();
567 conProcessQueue(); // load config
568 conProcessArgs
!true(args
);
575 dgGreatePListWindow
= delegate () { createPListWindow(); };
576 if (!plVisible
) sdplwin
.hide();
580 GlobalHotkeyManager
.register("M-H-A", delegate () { hideShowWindows(); });
581 } catch (Exception e
) {
582 conwriteln("ERROR registering hotkey!");
585 conRegFunc
!((ConString path
, bool append
=false) {
587 scanDir(path
, append
);
588 } catch (Exception e
) {
589 conwriteln("scanning error: ", e
.msg
);
591 glconPostScreenRebuild();
592 })("scan_dir", "scan the given directory; 2nd ard is \"append\" bool flag");
596 glconPostScreenRebuild();
597 })("pl_clear", "clear playlist");
599 conRegFunc
!(() { hideShowWindows(); })("win_toggle", "show/hide Amper windows");
601 conRegFunc
!((int plidx
, bool forcestart
=false) {
602 if (!sdampwin
.closed
) {
603 ampPList
.playSongByIndex(plidx
, forcestart
);
604 glconPostScreenRebuild();
606 })("song_play_by_index", "play song from playlist by index");
608 conRegFunc
!((){ if (!sdampwin
.closed
) ampPList
.playPrevSong(); })("song_prev", "play previous song");
609 conRegFunc
!((bool forceplay
=false){ if (!sdampwin
.closed
) ampPList
.playNextSong(forceplay
); })("song_next", "play next song");
610 conRegFunc
!((){ if (!sdampwin
.closed
) ampPList
.playCurrentSong(); })("song_play", "play current song");
611 conRegFunc
!((){ if (!sdampwin
.closed
) ampPList
.stopSong(); })("song_stop", "stop current song");
612 conRegFunc
!((){ aplayTogglePause(); })("song_pause_toggle", "pause/unpause current song");
613 conRegFunc
!((bool pause
){ aplayPause(pause
); })("song_pause", "pause/unpause current song, with bool arg");
615 conRegFunc
!((uint msecs
){ aplaySeekMS(msecs
*1000); })("song_seek_abs", "absolute seek, in seconds");
616 conRegFunc
!((int msecs
){ aplaySeekMS(aplayCurTimeMS
+msecs
*1000); })("song_seek_rel", "relative seek, in seconds");
618 foreach (string path
; args
[1..$]) concmdf
!"scan_dir \"%s\" tan"(path
);
621 scope(exit
) stopRPCServer();
623 glconCtlWindow
.eventLoop(0);
626 conProcessQueue(int.max
/4);