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