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/>.
22 import arsd
.simpledisplay
;
40 // ////////////////////////////////////////////////////////////////////////// //
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");
54 static immutable ubyte[] BuiltinSkinData
= cast(immutable(ubyte)[])import("skins/pb2000.zip");
58 // ////////////////////////////////////////////////////////////////////////// //
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
;
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) {
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);
117 foreach (XPoint
[] poly
; region
) {
118 auto r1
= XPolygonRegion(poly
.ptr
, cast(int)poly
.length
, WindingRule
);
122 XRegion dr
= XCreateRegion();
123 XUnionRegion(reg
, r1
, dr
);
129 assert(reg
!is null);
130 XShapeCombineRegion(w
.impl
.display
, w
.impl
.window
, ShapeBounding
, 0, 0, reg
, ShapeSet
);
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
{
172 nothrow @trusted @nogc:
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
] == '=') {
180 //while (pos < s.length && s.ptr[pos] <= ' ') ++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
;
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
;
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
;
206 return gxrgb(rgb
[0], rgb
[1], rgb
[2]);
210 void parsePlaylistConfig (VFile fl
) {
211 bool inTextSection
= false;
212 foreach (/*auto*/ line
; fl
.byLine
) {
214 if (line
.length
== 0 || line
.ptr
[0] == ';') continue;
215 //conwriteln("<", line, ">");
216 if (line
[0] == '[') {
217 inTextSection
= line
.strEquCI("[Text]");
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");
246 while (s
.length
&& s
.ptr
[0].isdigit
) {
247 res
= res
*10+s
.ptr
[0]-'0';
250 if (s
.length
> 0 && s
.ptr
[0] != ',' && s
.ptr
[0] > ' ') throw new Exception("invalid integer");
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");
262 foreach (immutable n
; 0..count
) {
264 p
.x
= cast(short)getToken(pts
);
265 p
.y
= cast(short)getToken(pts
);
274 assert(res
!is null);
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
) {
285 if (line
.length
== 0 || line
.ptr
[0] == ';') continue;
286 if (line
[0] == '[') {
287 if (line
.length
< 3) { cursection
= null; continue; }
289 foreach (ref char ch
; line
) if (ch
>= 'A' && ch
<= 'Z') ch
+= 32;
290 cursection
= line
.idup
;
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
;
300 aa
[kv
.key
.idup
] = kv
.value
.idup
;
301 sections
[cursection
] = aa
;
306 void parseReg (ref XPoint
[][] rg
, string name
) {
308 if (auto ssp
= name
in sections
) {
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
) {
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") {
335 if (ximg
.width
>= 5) {
336 foreach (immutable int dy
, ref uint c
; skin
.tickerColClear
[]) {
337 Color cc
= ximg
.getPixel(4, dy
%ximg
.height
);
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
);
351 } catch (Exception e
) {
352 conwriteln("ERROR loading skin image: '", fname
, "'...");
353 conwriteln(e
.toString
);
358 bool pleditLoaded
, regionLoaded
;
362 if (skinfile
!= "!BUILTIN!") {
363 vid
= vfsAddPak(skinfile
, "skin:");
365 vid
= vfsAddPak(wrapMemoryRO(BuiltinSkinData
), "skin:");
367 } catch (Exception e
) {
368 conwriteln("ERROR loading archive '"~skinfile
.idup
~"'");
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")) {
384 parsePlaylistConfig(VFile(de.name
));
386 } catch (Exception e
) {
387 conwriteln("ERROR loading 'pledit.txt'");
388 conwriteln(e
.toString
);
394 if (!regionLoaded
&& fname
.strEquCI("region.txt")) {
396 parseRegionConfig(VFile(de.name
));
398 } catch (Exception e
) {
399 conwriteln("ERROR loading 'pledit.txt'");
400 conwriteln(e
.toString
);
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
);
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");
438 // ////////////////////////////////////////////////////////////////////////// //
439 class AmpWindow
: EWindow
{
442 this (EPixmap
* aimg
) {
443 if (aimg
is null) throw new Exception("no image for window");
445 super(img
.width
, img
.height
);
448 protected override void paintBackground () {
449 img
.blitAt(swin
, 0, 0);
452 override bool onKeyPost (KeyEvent event
) {
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; }
465 // ////////////////////////////////////////////////////////////////////////// //
466 class AmpWidgetButton
: EWidget
{
471 void delegate () onAction
;
473 protected this (GxRect arc
) {
477 this (EPixmap
* aimg
) {
478 if (aimg
is null) throw new Exception("no image for window");
480 imgrc
= GxRect(0, 0, aimg
.width
, aimg
.height
);
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");
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 () {
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
); }
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
);
523 if (super.onMouse(event
)) return true;
524 if (active
) return true;
530 // ////////////////////////////////////////////////////////////////////////// //
531 class AmpWidgetTitleButton
: AmpWidgetButton
{
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");
538 actimgrc
= aactimgrc
;
540 super(GxRect(x
, y
, imgrc
.width
, imgrc
.height
));
543 override void onPaint () {
545 if (active
) irc
= actimgrc
;
546 img
.blitRect(swin
, rc
.x0
, rc
.y0
, irc
);
551 // ////////////////////////////////////////////////////////////////////////// //
552 class AmpWidgetToggle(string type
) : AmpWidgetButton
{
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
;
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);
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);
579 onAction
= delegate () { checkedvar
= !checkedvar
; };
584 override void onPaint () {
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
{
596 int titleofsmovedir
= 1;
597 int titlemovepause
= 8;
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
;
608 void setTitle(T
:const(char)[]) (T atitle
) {
609 static if (is(T
== typeof(null))) {
612 if (title
== atitle
) return;
613 static if (is(T
== string
)) title
= atitle
; else title
= atitle
.idup
;
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
);
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
);
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;
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
) {
656 if (title
.length
!= 0) {
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
{
681 this (EPixmap
* aimg
, GxRect arc
) {
687 override void onPaint () {
688 int val
= softVolume
;
689 if (val
< 0) val
= 0;
690 if (val
> maxvalue
) val
= maxvalue
;
692 auto bgrc
= GxRect(0, 0, img
.width
, 13);
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
;
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
) {
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);
718 if (event
.button
== MouseButton
.wheelUp
) {
719 if (softVolume
< maxvalue
) concmdf
!"soft_volume %d"(softVolume
+1);
728 // ////////////////////////////////////////////////////////////////////////// //
729 class AmpWidgetPosBar
: AmpWidgetButton
{
736 int frozenPos
= -666; // fixed position
737 MonoTime unfreezeTime
;
739 this (EPixmap
* aimg
, GxRect arc
) {
745 final void freezeAt (int frx
) {
747 unfreezeTime
= MonoTime
.currTime
+100.msecs
;
750 final void unfreeze () {
754 final int calcKnobX () {
755 if (frozenPos
!= -666) {
756 auto ctt
= MonoTime
.currTime
;
757 if (ctt
< unfreezeTime
) return frozenPos
;
761 int cur
= aplayCurTimeMS
;
762 int tot
= aplayTotalTimeMS
;
764 if (cur
< 0) cur
= 0; else if (cur
> tot
) cur
= tot
;
765 knobx
= (248-28)*cur
/tot
;
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
;
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
;
795 aplaySeekMS(tot
*newknobx
/(248-28));
800 override void onGrabbedMotion (MouseEvent event
) {
801 int nx
= event
.x
-rc
.x0
-dragofs
;
803 if (nx
>= rc
.width
-28) nx
= rc
.width
-28;
807 override bool onMouse (MouseEvent event
) {
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);
815 if (event
.button
== MouseButton
.wheelUp
) {
816 concmdf
!"song_seek_rel %d"(10);
825 // ////////////////////////////////////////////////////////////////////////// //
826 class AmpMainWindow
: AmpWindow
{
827 // option buttons (vertical): O A I D U
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
;
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 () {
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);
881 if (tm
>= BlinkPeriod
) return;
885 int curtime
= aplayCurTime
;
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);
899 // here, it is complicated
900 if (skin
.imgNums
.width
> 99) {
902 skin
.imgNums
.blitRect(swin
, 36, 26, GxRect(9*11, 0, 9, 13));
905 skin
.imgNums
.blitRect(swin
, 36, 26+6, GxRect(9*2, 6, 9, 1));
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 () {
926 int actidx
= -1; // 0: stereo; 1: mono
927 if (aplayIsPlaying
) actidx
= (aplayIsStereo ?
0 : 1);
930 auto irc
= GxRect(29, 0, 29, 12);
931 irc
.y0
= (actidx
== 1 ?
0 : 12);
932 skin
.imgMonoStereo
.blitRect(swin
, x
, y
, irc
);
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
954 override bool onKeyPost (KeyEvent event
) {
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; }
981 void newSong(T
:const(char)[]) (T tit
) {
982 wtitle
.setTitle
!T(tit
);
987 // ////////////////////////////////////////////////////////////////////////// //
994 int duration
; // in seconds
999 class AmpPListWindow
: AmpWindow
{
1000 static struct State
{
1006 int curplayingitem
= -1;
1010 int lastItemClickIndex
= -1;
1011 MonoTime lastItemClickTime
;
1012 //private XlibTCImage* ttc;
1016 super(&skin
.imgMain
);
1017 img
= &skin
.imgPlaylist
;
1020 auto saveData () { return state
; }
1022 void restoreData (ref 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
;
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
1041 prev
= state
.shuffleidx
[cidx
]-1;
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; }
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;
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
1070 next
= state
.shuffleidx
[cidx
]+1;
1071 if (next
>= state
.items
.length
) {
1072 if (!modeRepeat
) { concmd("song_stop"); return; }
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);
1079 if (next
>= state
.items
.length
) {
1080 if (!modeRepeat
) { concmd("song_stop"); return; }
1083 if (state
.curitem
== cidx
) state
.curitem
= next
;
1084 state
.curplayingitem
= next
;
1086 if (state
.curitem
== cidx
) state
.curitem
= next
;
1087 //state.curplayingitem = next;
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
1098 playSongByIndex(cidx
, true);
1101 void playSelectedSong () {
1102 if (state
.items
.length
== 0) { concmd("song_stop"); return; }
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
);
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
;
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
;
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
);
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
);
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);
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
) {
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
;
1186 bool updated
= false;
1187 foreach (immutable idx
, ref PListItem pi
; state
.items
) {
1188 if (!pi
.scanned
&& pi
.fname
== filename
) {
1193 pi
.duration
= durationms
/1000;
1195 if (pi
.duration
> 0) state
.totaldur
+= pi
.duration
;
1199 if (updated
) glconPostScreenRepaint();
1202 private bool removeItemByIndex (usize idx
) {
1203 if (idx
>= state
.items
.length
) return false;
1204 auto pi
= &state
.items
[idx
];
1206 if (pi
.duration
> 0) state
.totaldur
-= pi
.duration
;
1208 aplayCancelScan(pi
.fname
);
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
;
1232 final void scanResultFailed (string filename
) {
1233 bool updated
= false;
1235 while (idx
< state
.items
.length
) {
1236 if (state
.items
[idx
].fname
== filename
) {
1237 removeItemByIndex(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;
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;
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
;
1270 if (tl
> ci
) tl
= ci
;
1271 if (tl
+vfi
<= ci
) tl
= ci
-vfi
+1;
1273 if (el
>= state
.items
.length
) tl
= cast(int)state
.items
.length
-vfi
;
1278 final @property int knobYOfs () nothrow @trusted {
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
));
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();
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
;
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
);
1325 auto dur
= state
.items
.ptr
[idx
].duration
;
1329 len
= snprintf(buf
.ptr
, buf
.length
, "%d:%02d:%02d", dur
/(60*60), (dur
/60)%60, dur
%60);
1331 len
= snprintf(buf
.ptr
, buf
.length
, "%d:%02d", dur
/60, dur
%60);
1333 auto tw
= gxTextWidthUtf(buf
[0..len
]);
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
;
1342 if (plEllipsisAtStart
) {
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;
1349 while (start
< tit
.length
) if (dc
.decode(cast(ubyte)tit
.ptr
[start
++])) break;
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;
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
;
1372 protected override void paintBackground () {
1375 auto irc
= GxRect(0, 0, 25, 20);
1376 if (!active
) irc
.moveBy(0, 21);
1381 while (tx
< width
-irc
.width
) {
1382 img
.blitRect(swin
, tx
, 0, irc
);
1388 irtitle
.width
= 100;
1389 img
.blitRect(swin
, (width
-irtitle
.width
)/2, 0, irtitle
);
1392 img
.blitRect(swin
, 0, 0, irc
);
1395 img
.blitRect(swin
, width
-irc
.width
, 0, irc
);
1396 // left and right bars
1402 while (ty
< height
-irc
.height
) {
1405 img
.blitRect(swin
, 0, ty
, irc
);
1409 img
.blitRect(swin
, width
-irc
.width
, ty
, irc
);
1413 // left part: 0,72 125x38
1414 // right part: 126,72 150x38
1415 // tile: 179,0 25x38
1417 ty
= height
-irc
.height
;
1423 while (tx
< width
-150) {
1424 img
.blitRect(swin
, tx
, ty
, irc
);
1427 // bottom left corner
1431 img
.blitRect(swin
, 0, ty
, irc
);
1432 // bottom right corner
1435 img
.blitRect(swin
, width
-irc
.width
, ty
, irc
);
1440 protected override void paintFinished () {
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
) {
1463 if (listClipRect
.inside(event
.x
, event
.y
)) {
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
;
1471 case MouseButton
.wheelDown
:
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
;
1483 if ((ctt
-lastItemClickTime
).total
!"msecs" < 350) {
1484 concmdf
!"song_play_by_index %d tan"(lastItemClickIndex
);
1486 lastItemClickIndex
= -1;
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
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
1514 int bandidx
; // -1: preamp
1516 this (int aidx
, int x
, int y
) {
1519 rc
= GxRect(x
, y
, 14, 63);
1521 //value = aplayGetEqBand(aidx);
1524 override void onPaint () {
1526 if (val
< 0) val
= 0;
1527 if (val
> maxvalue
) val
= maxvalue
;
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 () {
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; }
1561 int kyofs
= knobYOfs
;
1562 if (event
.y
>= kyofs
&& event
.y
< kyofs
+11) {
1566 setValue(locy2value(event
.y
-5));
1568 dragYOfs
= event
.y
-kyofs
;
1571 override void onReleased (MouseEvent event
) {
1574 //if (cmd.length) concmd(cmd);
1575 //if (onAction !is null) onAction();
1579 override void onGrabbedMotion (MouseEvent event
) {
1582 event
.y
-= dragYOfs
;
1583 setValue(locy2value(event
.y
));
1587 override bool onMouse (MouseEvent event
) {
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; }
1599 // ////////////////////////////////////////////////////////////////////////// //
1600 class AmpEqWindow
: AmpWindow
{
1601 AmpWidgetEqSlider preamp
;
1602 AmpWidgetEqSlider
[10] bands
;
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"));
1611 preamp
= cast(AmpWidgetEqSlider
)addWidget(new AmpWidgetEqSlider(-1, 21, 38));
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
);