switched to GPLv3 ONLY, because i don't trust FSF anymore
[amper.git] / amperskin.d
blob7a6a4b9580b8524e3543b951438ddf76576bc53c
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 amperskin;
18 import core.time;
20 import arsd.color;
21 import arsd.image;
22 import arsd.simpledisplay;
24 import iv.alice;
25 import iv.cmdcon;
26 import iv.cmdcongl;
27 import iv.simplealsa;
28 import iv.strex;
29 import iv.utfutil;
30 import iv.vfs.io;
32 import aplayer;
34 import egfx;
35 import egfx.xshape;
37 import amperopts;
40 // ////////////////////////////////////////////////////////////////////////// //
41 class QuitEvent {}
44 void postQuitEvent () {
45 if (glconCtlWindow is null) return;
46 if (!glconCtlWindow.eventQueued!QuitEvent) glconCtlWindow.postEvent(new QuitEvent());
50 // ////////////////////////////////////////////////////////////////////////// //
51 version(default_skin_is_winamp) {
52 static immutable ubyte[] BuiltinSkinData = cast(immutable(ubyte)[])import("skins/winamp2.zip");
53 } else {
54 static immutable ubyte[] BuiltinSkinData = cast(immutable(ubyte)[])import("skins/pb2000.zip");
58 // ////////////////////////////////////////////////////////////////////////// //
59 struct AmperSkin {
60 static struct ImgFile { string name; }
61 @ImgFile("main") EPixmap imgMain;
62 @ImgFile("cbuttons") EPixmap imgCButtons;
63 @ImgFile("shufrep") EPixmap imgShufRep;
64 @ImgFile("numbers") EPixmap imgNums;
65 @ImgFile("nums_ex") EPixmap imgNumsEx;
66 @ImgFile("pledit") EPixmap imgPlaylist;
67 @ImgFile("titlebar") EPixmap imgTitle;
68 @ImgFile("volume") EPixmap imgVolume;
69 @ImgFile("posbar") EPixmap imgPosBar;
70 @ImgFile("text") EPixmap imgText;
71 @ImgFile("monoster") EPixmap imgMonoStereo;
72 @ImgFile("eqmain") EPixmap imgEq;
73 uint plistColNormal = gxTransparent;
74 uint plistColCurrent = gxTransparent;
75 uint plistColNormalBG = gxTransparent;
76 uint plistColCurrentBG = gxTransparent;
77 XPoint[][] mainRegion;
78 XPoint[][] eqRegion;
79 uint[6] tickerColClear = gxTransparent;
80 bool tickerColClearEasy;
81 uint tickerColText = gxTransparent;
83 // i can generate this with mixin, but meh...
84 void updateWith (ref AmperSkin sk) {
85 if (sk.imgMain.valid) { imgMain = sk.imgMain; mainRegion = sk.mainRegion; }
86 if (sk.imgCButtons.valid) imgCButtons = sk.imgCButtons;
87 if (sk.imgShufRep.valid) imgShufRep = sk.imgShufRep;
88 if (sk.imgNums.valid) imgNums = sk.imgNums;
89 if (sk.imgPlaylist.valid) imgPlaylist = sk.imgPlaylist;
90 if (sk.imgTitle.valid) imgTitle = sk.imgTitle;
91 if (sk.imgVolume.valid) imgVolume = sk.imgVolume;
92 if (sk.imgPosBar.valid) imgPosBar = sk.imgPosBar;
93 if (sk.imgText.valid) imgText = sk.imgText;
94 if (sk.imgMonoStereo.valid) imgMonoStereo = sk.imgMonoStereo;
95 if (sk.imgEq.valid) { imgEq = sk.imgEq; eqRegion = sk.eqRegion; }
96 if (!sk.plistColNormal.gxIsTransparent) plistColNormal = sk.plistColNormal;
97 if (!sk.plistColCurrent.gxIsTransparent) plistColCurrent = sk.plistColCurrent;
98 if (!sk.plistColNormalBG.gxIsTransparent) plistColNormalBG = sk.plistColNormalBG;
99 if (!sk.plistColCurrentBG.gxIsTransparent) plistColCurrentBG = sk.plistColCurrentBG;
100 foreach (immutable idx, uint c; sk.tickerColClear[]) {
101 if (!c.gxIsTransparent) tickerColClear[idx] = c;
103 if (!sk.tickerColText.gxIsTransparent) tickerColText = sk.tickerColText;
104 tickerColClearEasy = true;
105 foreach (uint c; tickerColClear[1..$]) if (c != tickerColClear[0]) { tickerColClearEasy = false; break; }
108 static void setWindowRegion (SimpleWindow w, XPoint[][] region) {
109 if (w is null || w.closed) return;
110 if (region.length == 0) {
111 XRectangle[1] rects;
112 rects[0] = XRectangle(0, 0, cast(short)ampMain.width, cast(short)ampMain.height);
113 XShapeCombineRectangles(w.impl.display, w.impl.window, ShapeBounding, 0, 0, rects.ptr, cast(int)rects.length, ShapeSet, 0);
114 } else {
115 // create regions
116 XRegion reg;
117 foreach (XPoint[] poly; region) {
118 auto r1 = XPolygonRegion(poly.ptr, cast(int)poly.length, WindingRule);
119 if (reg is null) {
120 reg = r1;
121 } else {
122 XRegion dr = XCreateRegion();
123 XUnionRegion(reg, r1, dr);
124 XDestroyRegion(reg);
125 XDestroyRegion(r1);
126 reg = dr;
129 assert(reg !is null);
130 XShapeCombineRegion(w.impl.display, w.impl.window, ShapeBounding, 0, 0, reg, ShapeSet);
131 XDestroyRegion(reg);
135 void setupRegions () {
136 setWindowRegion(sdampwin, mainRegion);
137 setWindowRegion(sdeqwin, eqRegion);
142 __gshared AmperSkin skin;
145 public void setupSkinRegions () {
146 .skin.setupRegions();
150 // ////////////////////////////////////////////////////////////////////////// //
151 __gshared AmpMainWindow ampMain;
152 __gshared AmpPListWindow ampPList;
153 __gshared AmpEqWindow ampEq;
156 // ////////////////////////////////////////////////////////////////////////// //
157 void loadSkin(bool initial=false) (ConString skinfile) {
158 static if (!initial) AmperSkin skin;
160 static if (initial) {
161 skin.plistColNormal = gxRGB!(0, 255, 0);
162 skin.plistColCurrent = gxRGB!(255, 255, 255);
163 skin.plistColNormalBG = gxRGB!(0, 0, 0);
164 skin.plistColCurrentBG = gxRGB!(0, 0, 255);
165 skin.tickerColClear[] = gxRGB!(0, 0, 0);
166 skin.tickerColText = skin.plistColNormal;
169 static struct KeyValue {
170 ConString key;
171 ConString value;
172 nothrow @trusted @nogc:
173 this (ConString s) {
174 s = s.xstrip;
175 usize pos = 0;
176 while (pos < s.length && s.ptr[pos] != '=' && s.ptr[pos] != ';') ++pos;
177 key = s[0..pos].xstrip;
178 if (pos < s.length && s.ptr[pos] == '=') {
179 ++pos;
180 //while (pos < s.length && s.ptr[pos] <= ' ') ++pos;
181 auto stpos = pos;
182 while (pos < s.length && s.ptr[pos] != ';') ++pos;
183 value = s[stpos..pos].xstrip;
187 uint asColor () const {
188 auto val = value.xstrip;
189 if (val.length != 4 && val.length != 7) return gxTransparent;
190 int[3] rgb;
191 if (val.length == 4) {
192 foreach (immutable idx, char ch; val[1..$]) {
193 int dg = digitInBase(ch, 16);
194 if (dg < 0) return gxTransparent;
195 dg = 255*dg/15;
196 rgb[idx] = dg;
198 } else {
199 foreach (immutable idx; 0..3) {
200 int d0 = digitInBase(val[1+idx*2], 16);
201 int d1 = digitInBase(val[2+idx*2], 16);
202 if (d0 < 0 || d1 < 0) return gxTransparent;
203 rgb[idx] = d0*16+d1;
206 return gxrgb(rgb[0], rgb[1], rgb[2]);
210 void parsePlaylistConfig (VFile fl) {
211 bool inTextSection = false;
212 foreach (/*auto*/ line; fl.byLine) {
213 line = line.xstrip;
214 if (line.length == 0 || line.ptr[0] == ';') continue;
215 //conwriteln("<", line, ">");
216 if (line[0] == '[') {
217 inTextSection = line.strEquCI("[Text]");
218 continue;
220 if (inTextSection) {
221 auto kv = KeyValue(line);
222 //conwritefln!"[%s]=<%s>"(kv.key, kv.value);
223 auto clr = kv.asColor;
224 if (clr.gxIsTransparent) continue;
225 //conwritefln!"[%s]=#%06X"(kv.key, clr);
226 if (kv.key.strEquCI("Normal")) skin.plistColNormal = clr;
227 else if (kv.key.strEquCI("Current")) skin.plistColCurrent = clr;
228 else if (kv.key.strEquCI("NormalBG")) skin.plistColNormalBG = clr;
229 //else if (kv.key.strEquCI("CurrentBG")) skinPListColCurrentBG = clr;
230 else if (kv.key.strEquCI("SelectedBG")) skin.plistColCurrentBG = clr;
235 static XPoint[][] parseRegionData (string ptcount, string pts) {
236 static string removeDelimiters (string s) {
237 while (s.length > 0 && (s.ptr[0] <= ' ' || s.ptr[0] == ',')) s = s[1..$];
238 return (s.length ? s : null);
241 static int getToken (ref string s) {
242 s = removeDelimiters(s);
243 if (s.length == 0) throw new Exception("invalid integer");
244 if (!s.ptr[0].isdigit) throw new Exception("invalid integer");
245 int res = 0;
246 while (s.length && s.ptr[0].isdigit) {
247 res = res*10+s.ptr[0]-'0';
248 s = s[1..$];
250 if (s.length > 0 && s.ptr[0] != ',' && s.ptr[0] > ' ') throw new Exception("invalid integer");
251 return res;
254 XPoint[][] res;
255 for (;;) {
256 ptcount = removeDelimiters(ptcount);
257 if (ptcount.length == 0) break;
258 int count = getToken(ptcount);
259 if (count < 3) throw new Exception("ooops. degenerate poly in region");
260 XPoint[] pta;
261 pta.reserve(count);
262 foreach (immutable n; 0..count) {
263 XPoint p;
264 p.x = cast(short)getToken(pts);
265 p.y = cast(short)getToken(pts);
266 pta ~= p;
268 res ~= pta;
270 if (res is null) {
271 res.reserve(1);
272 res ~= [];
273 res.length = 0;
274 assert(res !is null);
276 return res;
279 void parseRegionConfig (VFile fl) {
280 if (skin.mainRegion !is null && skin.eqRegion !is null) return;
281 string cursection = null;
282 string[string][string] sections;
283 foreach (/*auto*/ line; fl.byLine) {
284 line = line.xstrip;
285 if (line.length == 0 || line.ptr[0] == ';') continue;
286 if (line[0] == '[') {
287 if (line.length < 3) { cursection = null; continue; }
288 line = line[1..$-1];
289 foreach (ref char ch; line) if (ch >= 'A' && ch <= 'Z') ch += 32;
290 cursection = line.idup;
291 continue;
293 if (cursection.length) {
294 auto kv = KeyValue(line);
295 if (kv.key.length == 0) continue;
296 if (auto ssp = cursection in sections) {
297 if (kv.key !in *ssp) (*ssp)[kv.key.idup] = kv.value.idup;
298 } else {
299 string[string] aa;
300 aa[kv.key.idup] = kv.value.idup;
301 sections[cursection] = aa;
306 void parseReg (ref XPoint[][] rg, string name) {
307 if (rg is null) {
308 if (auto ssp = name in sections) {
309 string np, pts;
310 foreach (const ref kv; (*ssp).byKeyValue) {
311 if (kv.key.strEquCI("NumPoints")) np = kv.value;
312 else if (kv.key.strEquCI("PointList")) pts = kv.value;
314 if (np.length && pts.length) rg = parseRegionData(np, pts);
319 parseReg(skin.mainRegion, "normal");
320 parseReg(skin.eqRegion, "equalizer");
323 static bool isGoodImageExtension (ConString fname) {
324 return (fname.guessImageFormatFromExtension != ImageFileFormat.Unknown);
327 EPixmap loadImage(string skinname) (string fname) {
328 try {
329 auto fl = VFile(fname);
330 MemoryImage ximg = loadImageFromFile(fl);
331 scope(exit) delete ximg;
332 if (ximg.width < 1 || ximg.height < 1 || ximg.width > 1024 || ximg.height > 1024) throw new Exception("invalid image");
333 static if (skinname == "text") {
334 // text fill color
335 if (ximg.width >= 5) {
336 foreach (immutable int dy, ref uint c; skin.tickerColClear[]) {
337 Color cc = ximg.getPixel(4, dy%ximg.height);
338 c = c2img(cc);
339 // try to find out text color
340 if (skin.tickerColText.gxIsTransparent) {
341 foreach (immutable int dx; 0..4) {
342 uint cx = c2img(ximg.getPixel(dx, dy%ximg.height));
343 if (cx != c) { skin.tickerColText = cx; break; }
349 auto xtc = XlibTCImage(ximg);
350 return EPixmap(xtc);
351 } catch (Exception e) {
352 conwriteln("ERROR loading skin image: '", fname, "'...");
353 conwriteln(e.toString);
354 throw e;
358 bool pleditLoaded, regionLoaded;
360 VFSDriverId vid;
361 try {
362 if (skinfile != "!BUILTIN!") {
363 vid = vfsAddPak(skinfile, "skin:");
364 } else {
365 vid = vfsAddPak(wrapMemoryRO(BuiltinSkinData), "skin:");
367 } catch (Exception e) {
368 conwriteln("ERROR loading archive '"~skinfile.idup~"'");
369 throw e;
371 scope(exit) vfsRemovePak(vid);
372 foreach (/*auto*/ de; vfsFileList) {
373 auto fname = de.name;
374 auto xpos = fname.lastIndexOf('/');
375 if (xpos >= 0) fname = fname[xpos+1..$];
376 else if (fname.startsWith("skin:")) fname = fname[5..$];
377 xpos = fname.lastIndexOf('.');
378 if (xpos <= 0) continue;
379 auto iname = fname[0..xpos];
380 //conwriteln("[", de.name, "]:[", fname, "]:[", iname, "]");
382 if (!pleditLoaded && fname.strEquCI("pledit.txt")) {
383 try {
384 parsePlaylistConfig(VFile(de.name));
385 pleditLoaded = true;
386 } catch (Exception e) {
387 conwriteln("ERROR loading 'pledit.txt'");
388 conwriteln(e.toString);
389 throw e;
391 continue;
394 if (!regionLoaded && fname.strEquCI("region.txt")) {
395 try {
396 parseRegionConfig(VFile(de.name));
397 regionLoaded = true;
398 } catch (Exception e) {
399 conwriteln("ERROR loading 'pledit.txt'");
400 conwriteln(e.toString);
401 throw e;
403 continue;
406 if (!isGoodImageExtension(de.name)) continue;
408 foreach (string memb; __traits(allMembers, AmperSkin)) {
409 import std.traits : hasUDA, getUDAs;
410 static if (hasUDA!(__traits(getMember, AmperSkin, memb), AmperSkin.ImgFile)) {
411 enum SFN = getUDAs!(__traits(getMember, AmperSkin, memb), AmperSkin.ImgFile)[0].name;
412 if (iname.strEquCI(SFN)) {
413 if (!__traits(getMember, skin, memb).valid) __traits(getMember, skin, memb) = loadImage!SFN(de.name);
419 if (skin.imgNumsEx.valid) skin.imgNums = skin.imgNumsEx;
421 static if (!initial) {
422 .skin.updateWith(skin);
423 } else {
424 foreach (string memb; __traits(allMembers, AmperSkin)) {
425 import std.traits : hasUDA, getUDAs;
426 static if (hasUDA!(__traits(getMember, AmperSkin, memb), AmperSkin.ImgFile)) {
427 enum SFN = getUDAs!(__traits(getMember, AmperSkin, memb), AmperSkin.ImgFile)[0].name;
428 static if (SFN != "nums_ex") {
429 if (!__traits(getMember, skin, memb).valid) throw new Exception("skin image '"~SFN~"' not found");
434 setupSkinRegions();
438 // ////////////////////////////////////////////////////////////////////////// //
439 class AmpWindow : EWindow {
440 EPixmap* img;
442 this (EPixmap* aimg) {
443 if (aimg is null) throw new Exception("no image for window");
444 img = aimg;
445 super(img.width, img.height);
448 protected override void paintBackground () {
449 img.blitAt(swin, 0, 0);
452 override bool onKeyPost (KeyEvent event) {
453 if (event.pressed) {
454 if (event == "C-Q") { postQuitEvent(); return true; }
455 if (event == "C-P") { concmd("pl_visible toggle"); return true; }
456 if (event == "C-E") { concmd("eq_visible toggle"); return true; }
457 if (event == "C-S") { concmd("mode_shuffle toggle"); return true; }
458 if (event == "C-R") { concmd("mode_repeat toggle"); return true; }
460 return false;
465 // ////////////////////////////////////////////////////////////////////////// //
466 class AmpWidgetButton : EWidget {
467 EPixmap* img;
468 GxRect imgrc;
469 string cmd;
471 void delegate () onAction;
473 protected this (GxRect arc) {
474 super(arc);
477 this (EPixmap* aimg) {
478 if (aimg is null) throw new Exception("no image for window");
479 img = aimg;
480 imgrc = GxRect(0, 0, aimg.width, aimg.height);
481 super(imgrc);
484 this (EPixmap* aimg, int x, int y, GxRect aimgrc, string acmd=null) {
485 if (aimg is null) throw new Exception("no image for window");
486 img = aimg;
487 imgrc = aimgrc;
488 cmd = acmd;
489 super(GxRect(x, y, aimgrc.width, aimgrc.height));
492 void onGrabbed (MouseEvent event) {}
494 void onReleased (MouseEvent event) {
495 if (rc.inside(event.x, event.y)) {
496 if (cmd.length) concmd(cmd);
497 if (onAction !is null) onAction();
501 void onGrabbedMotion (MouseEvent event) {}
503 override void onPaint () {
504 auto irc = imgrc;
505 if (active) irc.moveBy(0, imgrc.height);
506 img.blitRect(swin, rc.x0, rc.y0, irc);
509 override bool onMouse (MouseEvent event) {
510 if (event.button == MouseButton.left) {
511 if (event.type == MouseEventType.buttonPressed && rc.inside(event.x, event.y)) {
512 if (!active) { active = true; onGrabbed(event); }
513 return true;
516 if (event.type == MouseEventType.buttonReleased && event.button == MouseButton.left) {
517 if (active) { onReleased(event); active = false; return true; }
519 if (event.type == MouseEventType.motion && event.modifierState&ModifierState.leftButtonDown && active) {
520 onGrabbedMotion(event);
521 return true;
523 if (super.onMouse(event)) return true;
524 if (active) return true;
525 return false;
530 // ////////////////////////////////////////////////////////////////////////// //
531 class AmpWidgetTitleButton : AmpWidgetButton {
532 GxRect actimgrc;
534 this (EPixmap* aimg, int x, int y, GxRect aimgrc, GxRect aactimgrc, string acmd=null) {
535 if (aimg is null) throw new Exception("no image for window");
536 img = aimg;
537 imgrc = aimgrc;
538 actimgrc = aactimgrc;
539 cmd = acmd;
540 super(GxRect(x, y, imgrc.width, imgrc.height));
543 override void onPaint () {
544 auto irc = imgrc;
545 if (active) irc = actimgrc;
546 img.blitRect(swin, rc.x0, rc.y0, irc);
551 // ////////////////////////////////////////////////////////////////////////// //
552 class AmpWidgetToggle(string type) : AmpWidgetButton {
553 bool* checked;
554 private bool checkedvar;
555 GxRect[2][2] xrc; // [active][pressed]
557 this (bool* cvp, EPixmap* aimg, int x, int y, GxRect aimgrc, string acmd=null) {
558 super(aimg, x, y, aimgrc, acmd);
559 rc.width = aimgrc.width;
560 rc.height = aimgrc.height;
561 xrc[0][] = aimgrc;
562 xrc[1][] = aimgrc;
563 static if (type == "small") {
564 xrc[0][1].moveBy(aimgrc.width*2, 0);
565 xrc[1][0].moveBy(0, aimgrc.height);
566 xrc[1][1].moveBy(aimgrc.width*2, aimgrc.height);
567 } else if (type == "hor") {
568 xrc[0][0].moveBy(50*0, 0);
569 xrc[0][1].moveBy(59*2, 0);
570 xrc[1][0].moveBy(59*1, 0);
571 xrc[1][1].moveBy(59*3, 0);
572 } else {
573 xrc[0][1].moveBy(0, aimgrc.height*1);
574 xrc[1][0].moveBy(0, aimgrc.height*2);
575 xrc[1][1].moveBy(0, aimgrc.height*3);
577 if (cvp is null) {
578 cvp = &checkedvar;
579 onAction = delegate () { checkedvar = !checkedvar; };
581 checked = cvp;
584 override void onPaint () {
585 auto irc = imgrc;
586 int idx0 = (*checked ? 1 : 0);
587 int idx1 = (active ? 1 : 0);
588 img.blitRect(swin, rc.x0, rc.y0, xrc[idx0][idx1]);
593 // ////////////////////////////////////////////////////////////////////////// //
594 class AmpWidgetSongTitle : AmpWidgetButton {
595 int titleofs = 0;
596 int titleofsmovedir = 1;
597 int titlemovepause = 8;
598 string title;
599 bool dragging = false;
601 this(T:const(char)[]) (GxRect arc, T atitle) {
602 static if (is(T == typeof(null))) title = null;
603 else static if (is(T == string)) title = atitle;
604 else title = atitle.idup;
605 super(arc);
608 void setTitle(T:const(char)[]) (T atitle) {
609 static if (is(T == typeof(null))) {
610 setTitle("");
611 } else {
612 if (title == atitle) return;
613 static if (is(T == string)) title = atitle; else title = atitle.idup;
614 titleofs = 0;
615 titleofsmovedir = 1;
616 titlemovepause = 8;
620 override void onPaint () {
621 if (title.length && !rc.empty) {
622 if (skin.tickerColClearEasy) {
623 uint clr = skin.tickerColClear[0];
624 drawFillRect(rc.x0, rc.y0, rc.width, rc.height, clr);
625 } else {
626 foreach (immutable int dy; 0..rc.height) {
627 uint clr = skin.tickerColClear[dy%skin.tickerColClear.length];
628 drawFillRect(rc.x0, rc.y0+dy, rc.width, 1, clr);
631 setClipRect(rc);
632 scope(exit) resetClip();
633 gxDrawTextUtf(swin.impl.display, cast(Drawable)swin.impl.buffer, swin.impl.gc, rc.x0-titleofs, rc.y0-2, title, skin.tickerColText);
637 final void scrollTitle () {
638 if (parent.activeWidget !is this) dragging = false;
639 if (dragging) return;
640 if (titlemovepause-- >= 0) return;
641 titlemovepause = 0;
642 if (title.length == 0) { titleofs = 0; titleofsmovedir = 1; titlemovepause = 8; return; }
643 auto tw = gxTextWidthUtf(title);
644 if (tw <= rc.width) { titleofs = 0; titleofsmovedir = 1; titlemovepause = 8; return; }
645 titleofs += titleofsmovedir;
646 if (titleofs <= 0) { titleofs = 0; titleofsmovedir = 1; titlemovepause = 8; }
647 else if (titleofs >= tw-rc.width) { titleofs = tw-rc.width; titleofsmovedir = -1; titlemovepause = 8; }
650 override void onDeactivate () { dragging = false; }
652 override void onGrabbed (MouseEvent event) { dragging = true; }
654 override void onReleased (MouseEvent event) {
655 dragging = false;
656 if (title.length != 0) {
657 titlemovepause = 10;
658 auto tw = gxTextWidthUtf(title);
659 if (titleofs <= 0) { titleofs = 0; titleofsmovedir = 1; }
660 else if (titleofs >= tw-rc.width) { titleofs = tw-rc.width; titleofsmovedir = -1; }
664 override void onGrabbedMotion (MouseEvent event) {
665 if (title.length != 0 && event.dx != 0) {
666 titleofs -= event.dx;
667 //conwriteln("dx=", event.dx, "; titleofs=", titleofs);
668 titleofsmovedir = (event.dx < 0 ? 1 : -1);
669 auto tw = gxTextWidthUtf(title);
670 if (titleofs <= 0) titleofs = 0;
671 else if (titleofs >= tw-rc.width) titleofs = tw-rc.width;
677 // ////////////////////////////////////////////////////////////////////////// //
678 class AmpWidgetVolumeSlider : AmpWidgetButton {
679 enum maxvalue = 63;
681 this (EPixmap* aimg, GxRect arc) {
682 super(aimg);
683 img = aimg;
684 rc = arc;
687 override void onPaint () {
688 int val = softVolume;
689 if (val < 0) val = 0;
690 if (val > maxvalue) val = maxvalue;
691 int sliderx = 0;
692 auto bgrc = GxRect(0, 0, img.width, 13);
693 if (maxvalue > 0) {
694 int iidx = 27*val/maxvalue;
695 bgrc.moveBy(0, 15*iidx);
696 sliderx = (rc.width-14)*val/maxvalue;
698 img.blitRect(swin, rc.x0, rc.y0, bgrc);
699 img.blitRect(swin, rc.x0+sliderx, rc.y0, GxRect(active ? 0 : 15, img.height-12, 14, 12));
702 override void onGrabbedMotion (MouseEvent event) {
703 int nx = event.x-rc.x0;
704 if (nx < 0) nx = 0;
705 if (nx >= rc.width-14) nx = rc.width-14;
706 int nv = 63*nx/(rc.width-14);
707 if (nv != softVolume) concmdf!"soft_volume %d"(nv);
710 override bool onMouse (MouseEvent event) {
711 // mouse wheel
712 if (super.onMouse(event)) return true;
713 if (event.type == MouseEventType.buttonPressed && rc.inside(event.x, event.y) && !active) {
714 if (event.button == MouseButton.wheelDown) {
715 if (softVolume > 0) concmdf!"soft_volume %d"(softVolume-1);
716 return true;
718 if (event.button == MouseButton.wheelUp) {
719 if (softVolume < maxvalue) concmdf!"soft_volume %d"(softVolume+1);
720 return true;
723 return active;
728 // ////////////////////////////////////////////////////////////////////////// //
729 class AmpWidgetPosBar : AmpWidgetButton {
730 // at: 16,72
731 // bar size: 248,10
732 // 248,0; 29,10
733 // 278,0; 29,10
734 int newknobx = -666;
735 int dragofs;
736 int frozenPos = -666; // fixed position
737 MonoTime unfreezeTime;
739 this (EPixmap* aimg, GxRect arc) {
740 super(aimg);
741 img = aimg;
742 rc = arc;
745 final void freezeAt (int frx) {
746 frozenPos = frx;
747 unfreezeTime = MonoTime.currTime+100.msecs;
750 final void unfreeze () {
751 frozenPos = -666;
754 final int calcKnobX () {
755 if (frozenPos != -666) {
756 auto ctt = MonoTime.currTime;
757 if (ctt < unfreezeTime) return frozenPos;
758 frozenPos = -666;
760 int knobx = 0;
761 int cur = aplayCurTimeMS;
762 int tot = aplayTotalTimeMS;
763 if (tot > 0) {
764 if (cur < 0) cur = 0; else if (cur > tot) cur = tot;
765 knobx = (248-28)*cur/tot;
767 return knobx;
770 override void onPaint () {
771 int knobx = (newknobx == -666 ? calcKnobX() : newknobx);
772 img.blitRect(swin, rc.x0, rc.y0, GxRect(0, 0, 248, 10));
773 img.blitRect(swin, rc.x0+knobx, rc.y0, GxRect(active ? 278 : 248, 0, 29, 10));
776 override void onDeactivate () { newknobx = -666; }
778 override void onGrabbed (MouseEvent event) {
779 newknobx = (newknobx == -666 ? calcKnobX : newknobx);
780 int x = event.x-rc.x0;
781 if (x >= newknobx && x < newknobx+29) {
782 dragofs = x-newknobx;
783 } else {
784 dragofs = 29/2;
785 newknobx = x-dragofs;
786 if (newknobx < 0) newknobx = 0;
787 if (newknobx >= rc.width-28) newknobx = rc.width-28;
791 override void onReleased (MouseEvent event) {
792 auto tot = aplayTotalTimeMS;
793 if (tot > 0) {
794 freezeAt(newknobx);
795 aplaySeekMS(tot*newknobx/(248-28));
797 newknobx = -666;
800 override void onGrabbedMotion (MouseEvent event) {
801 int nx = event.x-rc.x0-dragofs;
802 if (nx < 0) nx = 0;
803 if (nx >= rc.width-28) nx = rc.width-28;
804 newknobx = nx;
807 override bool onMouse (MouseEvent event) {
808 // mouse wheel
809 if (super.onMouse(event)) return true;
810 if (event.type == MouseEventType.buttonPressed && rc.inside(event.x, event.y) && !active) {
811 if (event.button == MouseButton.wheelDown) {
812 concmdf!"song_seek_rel %d"(-10);
813 return true;
815 if (event.button == MouseButton.wheelUp) {
816 concmdf!"song_seek_rel %d"(10);
817 return true;
820 return active;
825 // ////////////////////////////////////////////////////////////////////////// //
826 class AmpMainWindow : AmpWindow {
827 // option buttons (vertical): O A I D U
828 // pos: 11, 24
829 // size: 7, 8
830 // menu button: 6, 3 size: 9, 9
831 // minimize button: 244, 3 size 9, 9
832 // maximize button: 254, 3 size 9, 9
833 // close button: 264, 3 size 9, 9
834 AmpWidgetSongTitle wtitle;
835 MonoTime stblinktime;
836 bool blinking;
838 this () {
839 super(&skin.imgMain);
840 addWidget(new AmpWidgetButton(&skin.imgCButtons, 16, 88, GxRect(0, 0, 23, 18), "song_prev"));
841 addWidget(new AmpWidgetButton(&skin.imgCButtons, 39, 88, GxRect(23, 0, 23, 18), "song_play"));
842 addWidget(new AmpWidgetButton(&skin.imgCButtons, 62, 88, GxRect(46, 0, 23, 18), "song_pause_toggle"));
843 addWidget(new AmpWidgetButton(&skin.imgCButtons, 85, 88, GxRect(69, 0, 23, 18), "song_stop"));
844 addWidget(new AmpWidgetButton(&skin.imgCButtons, 108, 88, GxRect(92, 0, 22, 18), "song_next"));
845 addWidget(new AmpWidgetButton(&skin.imgCButtons, 136, 88, GxRect(114, 0, 22, 16), "song_eject"));
846 addWidget(new AmpWidgetToggle!"vert"(&modeShuffle, &skin.imgShufRep, 164, 89, GxRect(28, 0, 46, 15), "mode_shuffle toggle"));
847 addWidget(new AmpWidgetToggle!"vert"(&modeRepeat, &skin.imgShufRep, 210, 89, GxRect(0, 0, 28, 15), "mode_repeat toggle"));
848 addWidget(new AmpWidgetToggle!"small"(&eqVisible, &skin.imgShufRep, 219, 58, GxRect(0, 61, 23, 12), "eq_visible toggle"));
849 addWidget(new AmpWidgetToggle!"small"(&plVisible, &skin.imgShufRep, 242, 58, GxRect(23, 61, 23, 12), "pl_visible toggle"));
850 wtitle = cast(AmpWidgetSongTitle)addWidget(new AmpWidgetSongTitle(GxRect(109+2, 24, 157-4, 12), "Amper audio player: the blast from the past!"));
851 addWidget(new AmpWidgetVolumeSlider(&skin.imgVolume, GxRect(107, 57, 68, 13)));
852 addWidget(new AmpWidgetPosBar(&skin.imgPosBar, GxRect(16, 72, 248, 10)));
854 addWidget(new AmpWidgetTitleButton(&skin.imgTitle, 6, 3, GxRect(0, 0, 9, 9), GxRect(0, 9, 9, 9), "")); // menu
855 addWidget(new AmpWidgetTitleButton(&skin.imgTitle, 244, 3, GxRect(9, 0, 9, 9), GxRect(9, 9, 9, 9), "")); // minimize
856 addWidget(new AmpWidgetTitleButton(&skin.imgTitle, 254, 3, GxRect(0, 18, 9, 9), GxRect(9, 18, 9, 9), "")); // maximize
857 addWidget(new AmpWidgetTitleButton(&skin.imgTitle, 264, 3, GxRect(18, 0, 9, 9), GxRect(18, 9, 9, 9), "win_toggle"));
860 final void scrollTitle () {
861 if (wtitle !is null) wtitle.scrollTitle();
864 final bool isInsideTimer (int x, int y) {
865 return (x >= 36 && y >= 26 && x < 99 && y < 26+13);
868 final void drawTime () {
869 // 9x13
870 void drawDigit (int x, int dig) {
871 if (dig < 0) dig = 0; else if (dig > 9) dig = 9;
872 skin.imgNums.blitRect(swin, x, 26, GxRect(9*dig, 0, 9, 13));
874 if (aplayIsPlaying && aplayIsPaused) {
875 enum BlinkPeriod = 800;
876 //glconPostScreenRepaintDelayed(100);
877 auto ctt = MonoTime.currTime;
878 if (!blinking) { stblinktime = ctt; blinking = true; }
879 auto tm = (ctt-stblinktime).total!"msecs"%(BlinkPeriod*2);
880 //conwriteln(tm);
881 if (tm >= BlinkPeriod) return;
882 } else {
883 blinking = false;
885 int curtime = aplayCurTime;
886 if (modeRemaining) {
887 int dur = aplayTotalTime;
888 if (dur > 0) curtime = dur-curtime;
890 if (curtime < 0) curtime = 0;
891 int mins = curtime/60;
892 int secs = curtime%60;
893 //if (mins > 99) mins = 99;
894 drawDigit(48, mins/10);
895 drawDigit(60, mins%10);
896 drawDigit(78, secs/10);
897 drawDigit(90, secs%10);
898 if (modeRemaining) {
899 // here, it is complicated
900 if (skin.imgNums.width > 99) {
901 // has minus
902 skin.imgNums.blitRect(swin, 36, 26, GxRect(9*11, 0, 9, 13));
903 } else {
904 // no minus, use "2"
905 skin.imgNums.blitRect(swin, 36, 26+6, GxRect(9*2, 6, 9, 1));
907 } else {
908 // blank space instead of minus
909 skin.imgNums.blitRect(swin, 36, 26, GxRect(9*10, 0, 9, 13));
913 final void drawSampleRate () {
914 void drawDigit (int x, int dig) {
915 if (dig < 0) dig = 0; else if (dig > 9) dig = 9;
916 skin.imgText.blitRect(swin, x, 43, GxRect(5*dig, 6, 5, 6));
918 int srt = aplaySampleRate()/1000;
919 drawDigit(156+5*0, (srt/10)%10);
920 drawDigit(156+5*1, srt%10);
923 final void drawMonoStereo () {
924 //212, 41 29, 12
925 //239, 41 29, 12
926 int actidx = -1; // 0: stereo; 1: mono
927 if (aplayIsPlaying) actidx = (aplayIsStereo ? 0 : 1);
928 enum x = 212;
929 enum y = 41;
930 auto irc = GxRect(29, 0, 29, 12);
931 irc.y0 = (actidx == 1 ? 0 : 12);
932 skin.imgMonoStereo.blitRect(swin, x, y, irc);
933 irc.x0 = 0;
934 irc.y0 = (actidx == 0 ? 0 : 12);
935 skin.imgMonoStereo.blitRect(swin, x+irc.width, y, irc);
938 protected override void paintBackground () {
939 img.blitAt(swin, 0, 0);
940 auto irc = GxRect(27, 0, 275, 14);
941 if (!active) irc.moveBy(0, 15);
942 skin.imgTitle.blitRect(swin, 0, 0, irc); // title
943 skin.imgTitle.blitRect(swin, 11, 24, GxRect(305, 2, 7, 39)); // option buttions
945 skinImgTitle.blitRect(264, 3, GxRect(18, 0, 9, 9)); // close
946 skinImgTitle.blitRect(244, 3, GxRect(9, 0, 9, 9)); // minimize
947 skinImgTitle.blitRect(254, 3, GxRect(0, 18, 9, 9)); // maximize
949 drawTime();
950 drawSampleRate();
951 drawMonoStereo();
954 override bool onKeyPost (KeyEvent event) {
955 if (event.pressed) {
956 if (event == "P") { concmd("pl_visible toggle"); return true; }
957 if (event == "E") { concmd("eq_visible toggle"); return true; }
958 if (event == "S") { concmd("mode_shuffle toggle"); return true; }
959 if (event == "R") { concmd("mode_repeat toggle"); return true; }
960 if (event == "Z") { concmd("song_prev"); return true; }
961 if (event == "X") { concmd("song_play"); return true; }
962 if (event == "C") { concmd("song_pause_toggle"); return true; }
963 if (event == "V") { concmd("song_stop"); return true; }
964 if (event == "B") { concmd("song_next"); return true; }
965 if (event == "Left") { concmd("song_seek_rel -10"); return true; }
966 if (event == "Right") { concmd("song_seek_rel +10"); return true; }
967 if (event == "Up") { concmd("soft_volume_rel +2"); return true; }
968 if (event == "Down") { concmd("soft_volume_rel -2"); return true; }
969 if (event == "Delete") { concmd("soft_volume 31"); return true; }
971 return super.onKeyPost(event);
974 override bool onMousePost (MouseEvent event) {
975 if (event.type == MouseEventType.buttonPressed && event.button == MouseButton.left) {
976 if (isInsideTimer(event.x, event.y)) { concmd("mode_remaining toggle"); return true; }
978 return false;
981 void newSong(T:const(char)[]) (T tit) {
982 wtitle.setTitle!T(tit);
987 // ////////////////////////////////////////////////////////////////////////// //
988 struct PListItem {
989 string fname;
990 string visline;
991 string title;
992 string album;
993 string artist;
994 int duration; // in seconds
995 bool scanned;
999 class AmpPListWindow : AmpWindow {
1000 static struct State {
1001 PListItem[] items;
1002 int[] shuffleidx;
1003 int totaldur = 0;
1004 int topitem = 0;
1005 int curitem = 0;
1006 int curplayingitem = -1;
1009 State state;
1010 int lastItemClickIndex = -1;
1011 MonoTime lastItemClickTime;
1012 //private XlibTCImage* ttc;
1014 public:
1015 this () {
1016 super(&skin.imgMain);
1017 img = &skin.imgPlaylist;
1020 auto saveData () { return state; }
1022 void restoreData (ref State sd) {
1023 state = sd;
1024 lastItemClickIndex = -1;
1027 int findShuffleFirst () {
1028 if (state.items.length == 0) return 0;
1029 foreach (immutable idx, int val; state.shuffleidx) if (val == 0) return cast(int)idx;
1030 return 0;
1033 void playPrevSong () {
1034 scope(exit) glconPostScreenRepaint();
1035 if (state.items.length == 0) { concmd("song_stop"); return; }
1036 int cidx = state.curplayingitem;
1037 if (cidx < 0) cidx = state.curitem;
1038 if (cidx < 0 || cidx >= state.items.length) { concmd("song_stop"); return; } // just in case
1039 int prev;
1040 if (modeShuffle) {
1041 prev = state.shuffleidx[cidx]-1;
1042 if (prev < 0) {
1043 if (!modeRepeat) { concmd("song_stop"); return; }
1044 prev = cast(int)state.items.length-1;
1046 foreach (immutable idx, int val; state.shuffleidx) if (val == prev) { prev = cast(int)idx; break; }
1047 //conwriteln("P: cur=", state.curitem, "; prev=", prev, "; shuf=", state.shuffleidx);
1048 if (prev < 0 || prev >= state.items.length) { concmd("song_stop"); return; }
1049 } else {
1050 prev = cidx-1;
1051 if (prev < 0) {
1052 if (!modeRepeat) { concmd("song_stop"); return; }
1053 prev = cast(int)state.items.length-1;
1056 if (state.curitem == cidx) state.curitem = prev;
1057 //state.curplayingitem = prev;
1058 normTop();
1059 concmdf!"song_play_by_index %d"(prev);
1062 void playNextSong (bool forceplay=false) {
1063 scope(exit) glconPostScreenRepaint();
1064 if (state.items.length == 0) { concmd("song_stop"); return; }
1065 int cidx = state.curplayingitem;
1066 if (cidx < 0) cidx = (modeShuffle ? findShuffleFirst : state.curitem);
1067 if (cidx < 0 || cidx >= state.items.length) { concmd("song_stop"); return; } // just in case
1068 int next;
1069 if (modeShuffle) {
1070 next = state.shuffleidx[cidx]+1;
1071 if (next >= state.items.length) {
1072 if (!modeRepeat) { concmd("song_stop"); return; }
1073 next = 0;
1075 foreach (immutable idx, int val; state.shuffleidx) if (val == next) { next = cast(int)idx; break; }
1076 //conwriteln("N: cur=", state.curitem, "; next=", next, "; shuf=", state.shuffleidx);
1077 } else {
1078 next = cidx+1;
1079 if (next >= state.items.length) {
1080 if (!modeRepeat) { concmd("song_stop"); return; }
1081 next = 0;
1083 if (state.curitem == cidx) state.curitem = next;
1084 state.curplayingitem = next;
1086 if (state.curitem == cidx) state.curitem = next;
1087 //state.curplayingitem = next;
1088 normTop();
1089 concmdf!"song_play_by_index %d %s"(next, (forceplay ? "tan" :""));
1092 void playCurrentSong () {
1093 if (state.items.length == 0) { concmd("song_stop"); return; }
1094 int cidx = state.curplayingitem;
1095 if (cidx < 0) cidx = (modeShuffle ? findShuffleFirst : 0);
1096 if (cidx < 0 || cidx >= state.items.length) { concmd("song_stop"); return; } // just in case
1097 normTop();
1098 playSongByIndex(cidx, true);
1101 void playSelectedSong () {
1102 if (state.items.length == 0) { concmd("song_stop"); return; }
1103 normTop();
1104 if (state.curitem < 0 || state.curitem >= state.items.length) { concmd("song_stop"); return; }
1105 playSongByIndex(state.curitem, true);
1108 void playSongByIndex (int plidx, bool forcestart=false) {
1109 if (plidx >= 0 && plidx < state.items.length) {
1110 ampPList.state.curplayingitem = plidx;
1111 ampMain.newSong(state.items[plidx].visline);
1112 aplayPlayFile(state.items[plidx].fname, forcestart);
1116 void stopSong () {
1117 aplayStopFile();
1120 void clear () {
1121 foreach (ref PListItem pi; state.items) if (!pi.scanned) aplayCancelScan(pi.fname);
1122 state.items[] = PListItem.init;
1123 state.items.length = 0;
1124 state.items.assumeSafeAppend;
1125 state.shuffleidx.length = 0;
1126 state.shuffleidx.assumeSafeAppend;
1127 state.totaldur = 0;
1128 state.topitem = 0;
1129 state.curitem = 0;
1130 state.curplayingitem = -1;
1131 lastItemClickIndex = -1;
1134 void appendListItem (string fname) {
1135 auto li = PListItem(fname);
1136 //if (li.duration > 0) state.totaldur += li.duration;
1138 auto optr = state.items.ptr;
1139 state.items ~= li;
1140 if (state.items.ptr !is optr) {
1141 import core.memory : GC;
1142 if (state.items.ptr is GC.addrOf(state.items.ptr)) GC.setAttr(state.items.ptr, GC.BlkAttr.NO_INTERIOR);
1145 // update shuffle
1147 auto optr = state.shuffleidx.ptr;
1148 state.shuffleidx ~= cast(int)state.shuffleidx.length;
1149 if (state.shuffleidx.ptr !is optr) {
1150 import core.memory : GC;
1151 if (state.shuffleidx.ptr is GC.addrOf(state.shuffleidx.ptr)) GC.setAttr(state.shuffleidx.ptr, GC.BlkAttr.NO_INTERIOR);
1153 import std.random;
1154 auto swapn = uniform!"[)"(0, state.shuffleidx.length);
1155 if (swapn != state.shuffleidx.length-1) {
1156 auto v = state.shuffleidx[swapn];
1157 state.shuffleidx[swapn] = state.shuffleidx[$-1];
1158 state.shuffleidx[$-1] = v;
1161 lastItemClickIndex = -1;
1163 foreach (immutable idx, ref PListItem pi; state.items) {
1164 if (pi.scanned && pi.fname == li.fname) {
1165 scanResult(pi.fname, pi.album, pi.artist, pi.title, pi.duration*1000);
1166 break;
1169 if (!state.items[$-1].scanned) {
1170 state.items[$-1].visline = state.items[$-1].fname;
1171 aplayQueueScan(state.items[$-1].fname);
1175 final void scanResult (string filename, string album, string artist, string title, int durationms) {
1176 string tit;
1177 if (artist.length != 0 && title.length != 0) {
1178 tit = artist~" \u2014 "~title;
1179 } else if (artist.length != 0) {
1180 tit = artist~" \u2014 Unkown Song";
1181 } else if (title.length != 0) {
1182 tit = "Unknown Artist \u2014 "~title;
1183 } else {
1184 tit = filename;
1186 bool updated = false;
1187 foreach (immutable idx, ref PListItem pi; state.items) {
1188 if (!pi.scanned && pi.fname == filename) {
1189 pi.visline = tit;
1190 pi.title = title;
1191 pi.album = album;
1192 pi.artist = artist;
1193 pi.duration = durationms/1000;
1194 pi.scanned = true;
1195 if (pi.duration > 0) state.totaldur += pi.duration;
1196 updated = true;
1199 if (updated) glconPostScreenRepaint();
1202 private bool removeItemByIndex (usize idx) {
1203 if (idx >= state.items.length) return false;
1204 auto pi = &state.items[idx];
1205 if (pi.scanned) {
1206 if (pi.duration > 0) state.totaldur -= pi.duration;
1207 } else {
1208 aplayCancelScan(pi.fname);
1210 pi.visline = null;
1211 pi.title = null;
1212 pi.album = null;
1213 pi.artist = null;
1214 pi.duration = -1;
1215 pi.scanned = true;
1216 auto ssidx = state.shuffleidx[idx];
1217 foreach (immutable cc; idx+1..state.items.length) {
1218 state.items.ptr[cc-1] = state.items.ptr[cc];
1219 state.shuffleidx.ptr[cc-1] = state.shuffleidx.ptr[cc];
1221 state.items[$-1] = PListItem.init;
1222 state.items.length -= 1;
1223 state.items.assumeSafeAppend;
1224 state.shuffleidx.length -= 1;
1225 state.shuffleidx.assumeSafeAppend;
1226 // fix shuffle indicies
1227 foreach (ref sv; state.shuffleidx) if (sv >= ssidx) --sv;
1228 normTop();
1229 return true;
1232 final void scanResultFailed (string filename) {
1233 bool updated = false;
1234 usize idx = 0;
1235 while (idx < state.items.length) {
1236 if (state.items[idx].fname == filename) {
1237 removeItemByIndex(idx);
1238 updated = true;
1239 } else {
1240 ++idx;
1243 if (updated) glconPostScreenRepaint();
1246 final @property int visibleFullItems () const nothrow @trusted {
1247 int hgt = height-20-38;
1248 int res = hgt/gxTextHeightUtf;
1249 if (res < 1) res = 1;
1250 return res;
1253 final @property int visibleItems () const nothrow @trusted {
1254 int hgt = height-20-38;
1255 int res = hgt/gxTextHeightUtf;
1256 if (hgt%gxTextHeightUtf) ++res;
1257 if (res < 1) res = 1;
1258 return res;
1261 void normTop () nothrow @trusted {
1262 if (state.curitem < 0) state.curitem = 0;
1263 if (state.items.length == 0) { state.topitem = 0; return; }
1264 if (state.curitem >= state.items.length) state.curitem = cast(int)state.items.length-1;
1265 immutable vfi = visibleFullItems;
1266 immutable vi = visibleItems;
1267 immutable ci = state.curitem;
1268 int tl = state.topitem;
1269 if (tl < 0) tl = 0;
1270 if (tl > ci) tl = ci;
1271 if (tl+vfi <= ci) tl = ci-vfi+1;
1272 int el = tl+vi-1;
1273 if (el >= state.items.length) tl = cast(int)state.items.length-vfi;
1274 if (tl < 0) tl = 0;
1275 state.topitem = tl;
1278 final @property int knobYOfs () nothrow @trusted {
1279 normTop();
1280 version (none) {
1281 if (state.items.length < visibleFullItems+1) return 0;
1282 int hgt = imgrc.height-37-19-18;
1283 if (hgt < 1) return 0;
1284 return cast(int)(hgt*state.topitem/(state.items.length-visibleFullItems));
1285 } else {
1286 if (state.items.length < 2) return 0;
1287 int hgt = height-37-19-18;
1288 if (hgt < 1) return 0;
1289 return cast(int)(hgt*state.curitem/(state.items.length-1));
1291 // normal knob: 52,53 8x18 at width-15,[19..height-37)
1294 final void paintScrollKnob () {
1295 int xofs = width-15;
1296 int yofs = knobYOfs+19;
1297 img.blitRect(swin, xofs, yofs, GxRect(52, 53, 8, 18));
1300 final GxRect listClipRect () {
1301 return GxRect(11, 20, width-11-20+1, height-20-38);
1304 final void paintList () {
1305 char[1024] buf = void;
1306 GxRect savedclip = listClipRect();
1308 normTop();
1310 setClipRect(savedclip);
1311 scope(exit) resetClip();
1312 drawFillRect(savedclip, skin.plistColNormalBG);
1313 if (state.items.length < 1) return;
1314 int ty = savedclip.y0;
1315 int idx = state.topitem;
1316 assert(idx >= 0);
1317 while (idx < state.items.length && ty <= savedclip.y1) {
1318 import core.stdc.stdio : snprintf;
1320 if (state.curitem == idx) drawFillRect(savedclip.x0, ty, savedclip.width, gxTextHeightUtf, skin.plistColCurrentBG);
1322 uint clr = (/*state.curitem == idx ||*/ state.curplayingitem == idx ? skin.plistColCurrent : skin.plistColNormal);
1324 int timeWidth = 1;
1325 auto dur = state.items.ptr[idx].duration;
1326 usize len;
1327 if (dur > 0) {
1328 if (dur >= 60*60) {
1329 len = snprintf(buf.ptr, buf.length, "%d:%02d:%02d", dur/(60*60), (dur/60)%60, dur%60);
1330 } else {
1331 len = snprintf(buf.ptr, buf.length, "%d:%02d", dur/60, dur%60);
1333 auto tw = gxTextWidthUtf(buf[0..len]);
1334 timeWidth = tw+6;
1335 gxDrawTextUtf(swin.impl.display, cast(Drawable)swin.impl.buffer, swin.impl.gc, savedclip.x1-tw, ty, buf[0..len], clr);
1337 int availWidth = savedclip.width-timeWidth-1;
1340 string tit = state.items.ptr[idx].visline;
1341 len = 0;
1342 if (plEllipsisAtStart) {
1343 uint start = 0;
1344 while (start < tit.length) {
1345 len = snprintf(buf.ptr, buf.length, "%d. %s%.*s", idx+1, (start ? "...".ptr : "".ptr), cast(uint)(tit.length-start), tit.ptr+start);
1346 auto twdt = gxTextWidthUtf(buf[0..len]);
1347 if (twdt <= availWidth) break;
1348 Utf8DecoderFast dc;
1349 while (start < tit.length) if (dc.decode(cast(ubyte)tit.ptr[start++])) break;
1350 len = 0;
1352 } else {
1353 bool ellipsis = false;
1354 while (tit.length) {
1355 len = snprintf(buf.ptr, buf.length, "%d. %.*s%s", idx+1, cast(uint)tit.length, tit.ptr, (ellipsis ? "...".ptr : "".ptr));
1356 auto twdt = gxTextWidthUtf(buf[0..len]);
1357 if (twdt <= availWidth) break;
1358 tit = tit.utfchop;
1359 ellipsis = true;
1360 len = 0;
1363 if (len == 0) len = snprintf(buf.ptr, buf.length, "%d. ...", idx+1);
1364 gxDrawTextUtf(swin.impl.display, cast(Drawable)swin.impl.buffer, swin.impl.gc, savedclip.x0+2, ty, buf[0..len], clr);
1367 ty += gxTextHeightUtf;
1368 ++idx;
1372 protected override void paintBackground () {
1373 // frame
1374 int tx, ty;
1375 auto irc = GxRect(0, 0, 25, 20);
1376 if (!active) irc.moveBy(0, 21);
1377 // top frame
1378 // tiles
1379 irc.x0 = 127;
1380 tx = irc.width;
1381 while (tx < width-irc.width) {
1382 img.blitRect(swin, tx, 0, irc);
1383 tx += irc.width;
1385 // top title
1386 auto irtitle = irc;
1387 irtitle.x0 = 26;
1388 irtitle.width = 100;
1389 img.blitRect(swin, (width-irtitle.width)/2, 0, irtitle);
1390 // top left corner
1391 irc.x0 = 0;
1392 img.blitRect(swin, 0, 0, irc);
1393 // top right corner
1394 irc.x0 = 153;
1395 img.blitRect(swin, width-irc.width, 0, irc);
1396 // left and right bars
1397 ty = irc.height;
1398 irc.width = 25;
1399 //irc.width = 12;
1400 irc.height = 29;
1401 irc.y0 = 42;
1402 while (ty < height-irc.height) {
1403 // left bar
1404 irc.x0 = 0;
1405 img.blitRect(swin, 0, ty, irc);
1406 // right bar
1407 irc.x0 = 26;
1408 //irc.x0 = 31;
1409 img.blitRect(swin, width-irc.width, ty, irc);
1410 ty += irc.height;
1412 // bottom frame
1413 // left part: 0,72 125x38
1414 // right part: 126,72 150x38
1415 // tile: 179,0 25x38
1416 irc.height = 38;
1417 ty = height-irc.height;
1418 // tiles
1419 irc.width = 25;
1420 irc.x0 = 179;
1421 irc.y0 = 0;
1422 tx = 125;
1423 while (tx < width-150) {
1424 img.blitRect(swin, tx, ty, irc);
1425 tx += irc.width;
1427 // bottom left corner
1428 irc.x0 = 0;
1429 irc.y0 = 72;
1430 irc.width = 125;
1431 img.blitRect(swin, 0, ty, irc);
1432 // bottom right corner
1433 irc.x0 = 126;
1434 irc.width = 150;
1435 img.blitRect(swin, width-irc.width, ty, irc);
1436 // paint list
1437 paintList();
1440 protected override void paintFinished () {
1441 paintScrollKnob();
1442 // close pressed: 52,42 9x9 at width-10
1443 // normal knob: 52,53 8x18 at width-15,[19..height-37)
1444 // pressed knob: 61,53 8x18 at width-15,[19..height-37)
1445 // resize knob at top-right, size is 20x20
1448 override bool onKeyPost (KeyEvent event) {
1449 if (event.pressed) {
1450 scope(exit) normTop();
1451 if (event == "Up") { --state.curitem; return true; }
1452 if (event == "Down") { ++state.curitem; return true; }
1453 if (event == "Home") { state.curitem = 0; return true; }
1454 if (event == "End") { state.curitem = cast(int)state.items.length-1; return true; }
1455 if (event == "Enter") { concmdf!"song_play_by_index %d tan"(state.curitem); return true; }
1456 if (event == "Delete") { removeItemByIndex(state.curitem); return true; }
1458 return super.onKeyPost(event);
1461 override bool onMousePost (MouseEvent event) {
1462 //setListClip();
1463 if (listClipRect.inside(event.x, event.y)) {
1464 normTop();
1465 scope(exit) { normTop(); if (lastItemClickIndex != state.curitem) lastItemClickIndex = -1; }
1466 if (event.type == MouseEventType.buttonPressed) {
1467 switch (event.button) {
1468 case MouseButton.wheelUp:
1469 if (state.curitem > 0) --state.curitem;
1470 return true;
1471 case MouseButton.wheelDown:
1472 ++state.curitem;
1473 return true;
1474 case MouseButton.left:
1475 int ci = state.topitem+(event.y-listClipRect.y0)/gxTextHeightUtf;
1476 if (ci != state.curitem) { lastItemClickIndex = -1; state.curitem = ci; normTop(); }
1477 auto ctt = MonoTime.currTime;
1478 //conwriteln("curitem=", curitem, "; lastItemClickIndex=", lastItemClickIndex, "; delta=", (ctt-lastItemClickTime).total!"msecs");
1479 if (lastItemClickIndex != state.curitem) {
1480 lastItemClickIndex = state.curitem;
1481 lastItemClickTime = ctt;
1482 } else {
1483 if ((ctt-lastItemClickTime).total!"msecs" < 350) {
1484 concmdf!"song_play_by_index %d tan"(lastItemClickIndex);
1486 lastItemClickIndex = -1;
1488 break;
1489 default:
1493 return false;
1498 // ////////////////////////////////////////////////////////////////////////// //
1499 class AmpWidgetEqSlider : AmpWidgetButton {
1500 // first row: 13, 164
1501 // second row: 13, 229
1502 // element dimensions: 14, 63
1503 // 2 rows by 14 items; item index is idx*15
1504 // preamp: 21, 38
1505 // 10 eq bars: 78, 38
1506 // knob: 0, 164 0, 176
1507 // knob size: 11, 11
1508 // x: +1; y: +52-pos
1509 enum zerovalue = 14;
1510 enum maxvalue = zerovalue*2-1; // 2 rows by 14 items
1511 //int value;
1512 bool dragging;
1513 int dragYOfs;
1514 int bandidx; // -1: preamp
1516 this (int aidx, int x, int y) {
1517 super(&skin.imgEq);
1518 img = &skin.imgEq;
1519 rc = GxRect(x, y, 14, 63);
1520 bandidx = aidx;
1521 //value = aplayGetEqBand(aidx);
1524 override void onPaint () {
1525 int val = value;
1526 if (val < 0) val = 0;
1527 if (val > maxvalue) val = maxvalue;
1528 int col = val%14;
1529 int row = val/14;
1530 img.blitRect(swin, rc.x0, rc.y0, GxRect(13+15*col, 164+65*row, 14, 63));
1531 img.blitRect(swin, rc.x0+1, rc.y0+knobYOfs, GxRect(0, (active ? 176 : 164), 11, 11));
1534 final int value () { return aplayGetEqBand(bandidx); }
1536 final void setValue (int newv) {
1537 if (newv < 0) newv = 0;
1538 if (newv > maxvalue) newv = maxvalue;
1539 //if (newv != value) value = newv;
1540 aplaySetEqBand(bandidx, newv);
1543 final int knobYOfs () {
1544 int val = value;
1545 if (val < 0) val = 0;
1546 if (val > maxvalue) val = maxvalue;
1547 return 52-(51*val/maxvalue);
1550 final int locy2value (int locy) {
1551 if (locy < 2) locy = 2;
1552 if (locy > 51) locy = 51;
1553 return maxvalue-maxvalue*locy/51;
1556 override void onGrabbed (MouseEvent event) {
1557 if (bandidx < 0) { parent.activeWidget = null; return; }
1558 if (!rc.inside(event.x, event.y)) { parent.activeWidget = null; return; }
1559 dragging = true;
1560 event.y -= rc.y0;
1561 int kyofs = knobYOfs;
1562 if (event.y >= kyofs && event.y < kyofs+11) {
1563 // inside the knob
1564 } else {
1565 // outside the knob
1566 setValue(locy2value(event.y-5));
1568 dragYOfs = event.y-kyofs;
1571 override void onReleased (MouseEvent event) {
1572 if (dragging) {
1573 dragging = false;
1574 //if (cmd.length) concmd(cmd);
1575 //if (onAction !is null) onAction();
1579 override void onGrabbedMotion (MouseEvent event) {
1580 if (dragging) {
1581 event.y -= rc.y0;
1582 event.y -= dragYOfs;
1583 setValue(locy2value(event.y));
1587 override bool onMouse (MouseEvent event) {
1588 // mouse wheel
1589 if (super.onMouse(event)) return true;
1590 if (event.type == MouseEventType.buttonPressed && rc.inside(event.x, event.y) && !active) {
1591 if (event.button == MouseButton.wheelDown) { if (bandidx >= 0) setValue(value-1); return true; }
1592 if (event.button == MouseButton.wheelUp) { if (bandidx >= 0) setValue(value+1); return true; }
1594 return active;
1599 // ////////////////////////////////////////////////////////////////////////// //
1600 class AmpEqWindow : AmpWindow {
1601 AmpWidgetEqSlider preamp;
1602 AmpWidgetEqSlider[10] bands;
1604 this () {
1605 super(&skin.imgEq);
1606 setSize(275, 116);
1607 addWidget(new AmpWidgetToggle!"hor"(&alsaEnableEqualizer, &skin.imgEq, 14, 18, GxRect(10, 119, 25, 12), "use_equalizer toggle")); // on/off
1608 addWidget(new AmpWidgetToggle!"hor"(null, &skin.imgEq, 39, 18, GxRect(35, 119, 33, 12), "")); // auto
1609 addWidget(new AmpWidgetButton(&skin.imgEq, 217, 18, GxRect(224, 164, 44, 12), "song_prev"));
1610 // preamp
1611 preamp = cast(AmpWidgetEqSlider)addWidget(new AmpWidgetEqSlider(-1, 21, 38));
1612 // band sliders
1613 foreach (immutable int idx; 0..10) bands[idx] = cast(AmpWidgetEqSlider)addWidget(new AmpWidgetEqSlider(idx, 72+18*idx, 38));
1616 override bool onKeyPost (KeyEvent event) {
1617 return super.onKeyPost(event);