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