1 /*******************************************************************************
2 * Goggles Music Manager *
3 ********************************************************************************
4 * Copyright (C) 2006-2021 by Sander Jansen. All Rights Reserved *
5 * --- *
6 * This program is free software: you can redistribute it and/or modify *
7 * it under the terms of the GNU General Public License as published by *
8 * the Free Software Foundation, either version 3 of the License, or *
9 * (at your option) any later version. *
10 * *
11 * This program is distributed in the hope that it will be useful, *
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of *
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the *
14 * GNU General Public License for more details. *
15 * *
16 * You should have received a copy of the GNU General Public License *
17 * along with this program. If not, see http://www.gnu.org/licenses. *
18 ********************************************************************************/
19 #include "gmdefs.h"
20 #include "gmutils.h"
21 #include "GMTrack.h"
22 #include "GMApp.h"
23 #include "GMTrackList.h"
24 #include "GMSource.h"
25 #include "GMPlayerManager.h"
26 #include "GMTrackView.h"
27 #include "GMCoverCache.h"
28
29 #include "GMList.h"
30
31 #define ICON_SPACING 4 // Spacing between icon and label
32 #define SIDE_SPACING 6 // Left or right spacing between items
33 #define LINE_SPACING 4 // Line spacing between items
34
35
begins_with_keyword(const FXString & t)36 static inline FXbool begins_with_keyword(const FXString & t){
37 const FXStringList & keywords = GMPlayerManager::instance()->getPreferences().gui_sort_keywords;
38 for (FXint i=0;i<keywords.no();i++){
39 if (comparecase(t,keywords[i],keywords[i].length())==0) return true;
40 }
41 return false;
42 }
43
genre_list_sort(const FXListItem * pa,const FXListItem * pb)44 FXint genre_list_sort(const FXListItem* pa,const FXListItem* pb){
45 return comparecase(pa->getText(),pb->getText());
46 }
genre_list_sort_reverse(const FXListItem * pa,const FXListItem * pb)47 FXint genre_list_sort_reverse(const FXListItem* pa,const FXListItem* pb){
48 return -comparecase(pa->getText(),pb->getText());
49 }
50
51
generic_name_sort(const FXListItem * pa,const FXListItem * pb)52 FXint generic_name_sort(const FXListItem* pa,const FXListItem* pb){
53 FXint a=0,b=0;
54 if (begins_with_keyword(pa->getText())) a=FXMIN(pa->getText().length()-1,pa->getText().find(' ')+1);
55 if (begins_with_keyword(pb->getText())) b=FXMIN(pb->getText().length()-1,pb->getText().find(' ')+1);
56 return comparecase(&pa->getText()[a],&pb->getText()[b]);
57 }
58
generic_name_sort_reverse(const FXListItem * pa,const FXListItem * pb)59 FXint generic_name_sort_reverse(const FXListItem* pa,const FXListItem* pb){
60 FXint a=0,b=0;
61 if (begins_with_keyword(pa->getText())) a=FXMIN(pa->getText().length()-1,pa->getText().find(' ')+1);
62 if (begins_with_keyword(pb->getText())) b=FXMIN(pb->getText().length()-1,pb->getText().find(' ')+1);
63 return comparecase(&pb->getText()[b],&pa->getText()[a]);
64 }
65
source_list_sort(const FXTreeItem * pa,const FXTreeItem * pb)66 FXint source_list_sort(const FXTreeItem* pa,const FXTreeItem* pb){
67 const GMSource * const sa = static_cast<const GMSource*>(pa->getData());
68 const GMSource * const sb = static_cast<const GMSource*>(pb->getData());
69 if (sa->getType()>sb->getType()) return 1;
70 else if (sa->getType()<sb->getType()) return -1;
71 FXint a=0,b=0;
72 if (begins_with_keyword(pa->getText())) a=FXMIN(pa->getText().length()-1,pa->getText().find(' ')+1);
73 if (begins_with_keyword(pb->getText())) b=FXMIN(pb->getText().length()-1,pb->getText().find(' ')+1);
74 return compareversion(&pa->getText()[a],&pb->getText()[b]);
75 }
76
source_list_sort_reverse(const FXTreeItem * pa,const FXTreeItem * pb)77 FXint source_list_sort_reverse(const FXTreeItem* pa,const FXTreeItem* pb){
78 const GMSource * const sa = static_cast<const GMSource*>(pa->getData());
79 const GMSource * const sb = static_cast<const GMSource*>(pb->getData());
80 if (sa->getType()>sb->getType()) return 1;
81 else if (sa->getType()<sb->getType()) return -1;
82 FXint a=0,b=0;
83 if (begins_with_keyword(pa->getText())) a=FXMIN(pa->getText().length()-1,pa->getText().find(' ')+1);
84 if (begins_with_keyword(pb->getText())) b=FXMIN(pb->getText().length()-1,pb->getText().find(' ')+1);
85 return -compareversion(&pa->getText()[a],&pb->getText()[b]);
86 }
87
88
89
90 // Object implementation
91 FXIMPLEMENT(GMListItem,FXListItem,nullptr,0)
92
93
94
95 // Draw item
draw(const GMList * list,FXDC & dc,FXint xx,FXint yy,FXint ww,FXint hh) const96 void GMListItem::draw(const GMList* list,FXDC& dc,FXint xx,FXint yy,FXint ww,FXint hh) const {
97 FXFont *font=list->getFont();
98 FXint ih=0,th=0;
99 if(icon) ih=icon->getHeight();
100 if(!label.empty()) th=font->getFontHeight();
101 if(isSelected())
102 dc.setForeground(list->getSelBackColor());
103 // else
104 // dc.setForeground(list->getBackColor()); // FIXME maybe paint background in onPaint?
105
106 dc.fillRectangle(xx,yy,ww,hh);
107 if(hasFocus()){
108 dc.drawFocusRectangle(xx+1,yy+1,ww-2,hh-2);
109 }
110 xx+=SIDE_SPACING/2;
111 if(icon){
112 dc.drawIcon(icon,xx,yy+(hh-ih)/2);
113 xx+=ICON_SPACING+icon->getWidth();
114 }
115 if(!label.empty()){
116 if(!isEnabled())
117 dc.setForeground(makeShadowColor(list->getBackColor()));
118 else if(isSelected())
119 dc.setForeground(list->getSelTextColor());
120 else
121 dc.setForeground(list->getTextColor());
122 dc.drawText(xx,yy+(hh-th)/2+font->getFontAscent(),label);
123 }
124 }
125
126
127
128 // Map
129 FXDEFMAP(GMList) GMListMap[]={
130 FXMAPFUNC(SEL_PAINT,0,GMList::onPaint),
131 };
132
133 // Object implementation
FXIMPLEMENT(GMList,FXList,GMListMap,ARRAYNUMBER (GMListMap))134 FXIMPLEMENT(GMList,FXList,GMListMap,ARRAYNUMBER(GMListMap))
135
136
137 GMList::GMList(FXComposite *p,FXObject* tgt,FXSelector sel,FXuint opts,FXint x,FXint y,FXint w,FXint h) : FXList(p,tgt,sel,opts,x,y,w,h) {
138 rowcolor=GMPlayerManager::instance()->getPreferences().gui_row_color;
139 thickfont=font;
140
141 delete vertical;
142 delete horizontal;
143 delete corner;
144
145 vertical=new GMScrollBar(this,this,GMList::ID_VSCROLLED,SCROLLBAR_VERTICAL);
146 horizontal=new GMScrollBar(this,this,GMList::ID_HSCROLLED,SCROLLBAR_HORIZONTAL);
147 corner=new GMScrollCorner(this);
148
149 }
150
~GMList()151 GMList::~GMList(){
152 }
153
154 // Create custom item
createItem(const FXString & text,FXIcon * icon,void * ptr)155 FXListItem *GMList::createItem(const FXString& text,FXIcon* icon,void* ptr){
156 return new GMListItem(text,icon,ptr);
157 }
158
159 // Draw item list
onPaint(FXObject *,FXSelector,void * ptr)160 long GMList::onPaint(FXObject*,FXSelector,void* ptr){
161 FXEvent* event=(FXEvent*)ptr;
162 FXDCWindow dc(this,event);
163 FXint i,y,h;
164
165 // Set font
166 dc.setFont(font);
167
168 // Paint items
169 y=pos_y;
170 for(i=0; i<items.no(); i++){
171 h=items[i]->getHeight(this);
172 if(event->rect.y<=y+h && y<event->rect.y+event->rect.h){
173 if (i%2)
174 dc.setForeground(rowcolor);
175 else
176 dc.setForeground(backColor);
177
178 if (items[i]->getData()==(void*)(FXival)-1)
179 dc.setFont(thickfont);
180 else
181 dc.setFont(font);
182 dynamic_cast<GMListItem*>(items[i])->draw(this,dc,pos_x,y,FXMAX(listWidth,getVisibleWidth()),h);
183 }
184 y+=h;
185 }
186
187 // Paint blank area below items
188 if(y<event->rect.y+event->rect.h){
189 dc.setForeground(backColor);
190 dc.fillRectangle(event->rect.x,y,event->rect.w,event->rect.y+event->rect.h-y);
191 }
192 return 1;
193 }
194
195
196 // Object implementation
197 FXIMPLEMENT(GMTreeItem,FXTreeItem,nullptr,0)
198
GMTreeItem()199 GMTreeItem::GMTreeItem(){
200 }
201
202 // Get item height
getHeight(const FXTreeList * list) const203 FXint GMTreeItem::getHeight(const FXTreeList* list) const {
204 FXFont *font=list->getFont();
205 FXint th=0,oih=0,cih=0;
206 if(openIcon) oih=openIcon->getHeight();
207 if(closedIcon) cih=closedIcon->getHeight();
208 if(!label.empty()) th=font->getFontHeight();
209 // if (oih>16) oih+=4;
210 // if (cih>16) cih+=4;
211 return FXMAX3(th,oih,cih)+4;
212 }
213
214
215 // Draw item
draw(const FXTreeList * list,FXDC & dc,FXint xx,FXint yy,FXint,FXint hh) const216 void GMTreeItem::draw(const FXTreeList* list,FXDC& dc,FXint xx,FXint yy,FXint /* ww*/,FXint hh) const {
217 FXIcon *icon=(state&OPENED)?openIcon:closedIcon;
218 FXFont *font=list->getFont();
219 FXint th=0,tw=0,ih=0,iw=0;//ox=xx,oy=yy;
220 xx+=SIDE_SPACING/2;
221 if(icon){
222 iw=icon->getWidth();
223 ih=icon->getHeight();
224 dc.drawIcon(icon,xx,yy+(hh-ih)/2);
225 xx+=ICON_SPACING+iw;
226 }
227
228 if(!label.empty()){
229 tw=4+font->getTextWidth(label.text(),label.length());
230 th=4+font->getFontHeight();
231 yy+=(hh-th)/2;
232 // if(isSelected()){
233 // dc.setForeground(list->getSelBackColor());
234 // dc.fillRectangle(xx,yy,tw,th);
235 // dc.fillRectangle(ox,oy,ww,hh);
236 // }
237 if(hasFocus()){
238 dc.drawFocusRectangle(xx+1,yy+1,tw-2,th-2);
239 }
240 if(!isEnabled())
241 dc.setForeground(makeShadowColor(list->getBackColor()));
242 else if(isSelected())
243 dc.setForeground(list->getSelTextColor());
244 else
245 dc.setForeground(list->getTextColor());
246 dc.drawText(xx+2,yy+font->getFontAscent()+2,label);
247 }
248 }
249
250
251
252 #define DEFAULT_INDENT 8 // Indent between parent and child
253 #define HALFBOX_SIZE 4 // Half box size
254 #define BOX_FUDGE 3 // Fudge border around box
255
256
257 // Map
258 FXDEFMAP(GMTreeList) GMTreeListMap[]={
259 FXMAPFUNC(SEL_PAINT,0,GMTreeList::onPaint)
260 };
261
262 // Object implementation
FXIMPLEMENT(GMTreeList,FXTreeList,GMTreeListMap,ARRAYNUMBER (GMTreeListMap))263 FXIMPLEMENT(GMTreeList,FXTreeList,GMTreeListMap,ARRAYNUMBER(GMTreeListMap))
264
265
266 /// Construct a new, initially empty tree list
267 GMTreeList::GMTreeList(FXComposite *p,FXObject* tgt,FXSelector sel,FXuint opts,FXint x,FXint y,FXint w,FXint h) : FXTreeList(p,tgt,sel,opts,x,y,w,h) {
268 rowcolor=GMPlayerManager::instance()->getPreferences().gui_row_color;
269 GMScrollArea::replaceScrollbars(this);
270 }
271
createItem(const FXString & text,FXIcon * oi,FXIcon * ci,void * ptr)272 FXTreeItem* GMTreeList::createItem(const FXString& text,FXIcon* oi,FXIcon* ci,void* ptr) {
273 return (FXTreeItem*)new GMTreeItem(text,oi,ci,ptr);
274 }
275
276 // Draw item list
onPaint(FXObject *,FXSelector,void * ptr)277 long GMTreeList::onPaint(FXObject*,FXSelector,void* ptr){
278 FXEvent* event=(FXEvent*)ptr;
279 FXTreeItem* item=firstitem;
280 FXTreeItem* p;
281 FXint yh,xh,x,y,w,h,xp,hh,cc=0;
282 FXDCWindow dc(this,event);
283 dc.setFont(font);
284 x=pos_x;
285 y=pos_y;
286 if(options&TREELIST_ROOT_BOXES) x+=(4+indent);
287 while(item && y<event->rect.y+event->rect.h){
288 w=item->getWidth(this);
289 h=item->getHeight(this);
290 cc++;
291 if(event->rect.y<=y+h){
292
293 // Draw item
294 dc.setForeground(backColor);
295 dc.fillRectangle(0,y,width,h);
296
297 if (!item->isSelected()) {
298 if (cc%2) {
299 // dc.setForeground(backColor);
300 // dc.fillRectangle(0,y,x+2,h);
301
302 dc.setForeground(rowcolor);
303 dc.fillRectangle(x,y,width-x,h);
304 // dc.fillRectangle(x+2,y,width-x-2,h);
305 }
306 else {
307 dc.setForeground(backColor);
308 dc.fillRectangle(0,y,width,h);
309 }
310 }
311 else {
312 // dc.setForeground(backColor);
313 // dc.fillRectangle(0,y,x+2,h);
314
315 dc.setForeground(getSelBackColor());
316 // dc.fillRectangle(x+2,y,width-x-2,h);
317 dc.fillRectangle(x,y,width-x,h);
318 }
319
320 dynamic_cast<GMTreeItem*>(item)->draw(this,dc,x,y,w,h);
321
322
323 // Show other paraphernalia such as dotted lines and expand-boxes
324 if((options&(TREELIST_SHOWS_LINES|TREELIST_SHOWS_BOXES)) && (((GMTreeItem*)item)->parent || (options&TREELIST_ROOT_BOXES))){
325 hh=h/2;
326 yh=y+hh;
327 xh=x-indent+(SIDE_SPACING/2);
328 dc.setForeground(lineColor);
329 dc.setBackground(backColor);
330 dc.setStipple(STIPPLE_GRAY,pos_x&1,pos_y&1);
331 if(options&TREELIST_SHOWS_LINES){ // Connect items with lines
332 p=((GMTreeItem*)item)->parent;
333 xp=xh;
334 dc.setFillStyle(FILL_OPAQUESTIPPLED);
335 while(p){
336 xp-=(indent+p->getHeight(this)/2);
337 if(((GMTreeItem*)p)->next) dc.fillRectangle(xp,y,1,h);
338 p=((GMTreeItem*)p)->parent;
339 }
340 if((options&TREELIST_SHOWS_BOXES) && (item->hasItems() || item->getFirst())){
341 if(((GMTreeItem*)item)->prev || ((GMTreeItem*)item)->parent) dc.fillRectangle(xh,y,1,yh-y-HALFBOX_SIZE);
342 if(((GMTreeItem*)item)->next) dc.fillRectangle(xh,yh+HALFBOX_SIZE,1,y+h-yh-HALFBOX_SIZE);
343 }
344 else{
345 if(((GMTreeItem*)item)->prev || ((GMTreeItem*)item)->parent) dc.fillRectangle(xh,y,1,hh);
346 if(((GMTreeItem*)item)->next) dc.fillRectangle(xh,yh,1,h);
347 dc.fillRectangle(xh,yh,x+(SIDE_SPACING/2)-2-xh,1);
348 }
349 dc.setFillStyle(FILL_SOLID);
350 }
351
352 // Boxes before items for expand/collapse of item
353 if((options&TREELIST_SHOWS_BOXES) && (item->hasItems() || item->getFirst())){
354 dc.setFillStyle(FILL_OPAQUESTIPPLED);
355 dc.fillRectangle(xh+4,yh,(SIDE_SPACING/2)-2,1);
356 dc.setFillStyle(FILL_SOLID);
357 dc.drawRectangle(xh-HALFBOX_SIZE,yh-HALFBOX_SIZE,HALFBOX_SIZE+HALFBOX_SIZE,HALFBOX_SIZE+HALFBOX_SIZE);
358 dc.setForeground(textColor);
359 dc.fillRectangle(xh-HALFBOX_SIZE+2,yh,HALFBOX_SIZE+HALFBOX_SIZE-3,1);
360 if(!(options&TREELIST_AUTOSELECT) && !item->isExpanded()){
361 dc.fillRectangle(xh,yh-HALFBOX_SIZE+2,1,HALFBOX_SIZE+HALFBOX_SIZE-3);
362 }
363 }
364 }
365 }
366
367 y+=h;
368
369 // Move on to the next item
370 if(((GMTreeItem*)item)->first && ((options&TREELIST_AUTOSELECT) || ((GMTreeItem*)item)->isExpanded())){
371 x+=(indent+h/2);
372 item=((GMTreeItem*)item)->first;
373 continue;
374 }
375 while(!((GMTreeItem*)item)->next && ((GMTreeItem*)item)->parent){
376 item=((GMTreeItem*)item)->parent;
377 x-=(indent+item->getHeight(this)/2);
378 }
379 item=((GMTreeItem*)item)->next;
380 }
381 if(y<event->rect.y+event->rect.h){
382 dc.setForeground(backColor);
383 dc.fillRectangle(event->rect.x,y,event->rect.w,event->rect.y+event->rect.h-y);
384 }
385 return 1;
386 }
387