1 //===========================================
2 //  Lumina-desktop source code
3 //  Copyright (c) 2017, Ken Moore
4 //  Available under the 3-clause BSD license
5 //  See the LICENSE file for full details
6 //===========================================
7 #include "LIconCache.h"
8 
9 #include <LuminaOS.h>
10 #include <LUtils.h>
11 #include <LuminaXDG.h>
12 
13 #include <QDir>
14 #include <QtConcurrent>
15 
LIconCache(QObject * parent)16 LIconCache::LIconCache(QObject *parent) : QObject(parent){
17   connect(this, SIGNAL(InternalIconLoaded(QString, QDateTime, QByteArray*)), this, SLOT(IconLoaded(QString, QDateTime, QByteArray*)) );
18 }
19 
~LIconCache()20 LIconCache::~LIconCache(){
21 
22 
23 }
24 
instance()25 LIconCache* LIconCache::instance(){
26   static LIconCache cache;
27   return &cache;
28 }
29 
30 // === PUBLIC ===
31 //Icon Checks
exists(QString icon)32 bool LIconCache::exists(QString icon){
33   if(icon.isEmpty()){ return false; }
34   if(HASH.contains(icon)){ return true; } //already
35   else if(!icon.startsWith("/")){
36     //relative path to file (from icon theme?)
37     QString path = findFile(icon);
38     if(!path.isEmpty() && QFile::exists(path)){ return true; }
39   }else{
40     //absolute path to file
41     return QFile::exists(icon);
42   }
43   return false;
44 }
45 
isLoaded(QString icon)46 bool LIconCache::isLoaded(QString icon){
47   if(icon.isEmpty()){ return false; }
48   if(HASH.contains(icon)){
49     return !HASH[icon].icon.isNull();
50   }
51   return false;
52 }
53 
findFile(QString icon)54 QString LIconCache::findFile(QString icon){
55   if(icon.isEmpty()){ return ""; }
56 
57   //Get the currently-set theme
58   QString cTheme = QIcon::themeName();
59   if(cTheme.isEmpty()){
60     QIcon::setThemeName("material-design-light");
61     cTheme = "material-design-light";
62   }
63   //Make sure the current search paths correspond to this theme
64   if( QDir::searchPaths("icontheme").filter("/"+cTheme+"/").isEmpty() ){
65     //Need to reset search paths: setup the "icontheme" "material-design-light" and "fallback" sets
66     // - Get all the base icon directories
67     QStringList paths;
68       paths << QDir::homePath()+"/.icons/"; //ordered by priority - local user dirs first
69       QStringList xdd = QString(getenv("XDG_DATA_HOME")).split(":");
70         xdd << QString(getenv("XDG_DATA_DIRS")).split(":");
71         for(int i=0; i<xdd.length(); i++){
72           if(QFile::exists(xdd[i]+"/icons")){ paths << xdd[i]+"/icons/"; }
73         }
74     //Now load all the dirs into the search paths
75     QStringList theme, oxy, fall;
76     QStringList themedeps = LXDG::getIconThemeDepChain(cTheme, paths);
77     for(int i=0; i<paths.length(); i++){
78       theme << getChildIconDirs( paths[i]+cTheme);
79       for(int j=0; j<themedeps.length(); j++){ theme << getChildIconDirs(paths[i]+themedeps[j]); }
80       oxy << getChildIconDirs(paths[i]+"material-design-light"); //Lumina base icon set
81       fall << getChildIconDirs(paths[i]+"hicolor"); //XDG fallback (apps add to this)
82     }
83     //Now load all the icon theme dependencies in order (Theme1 -> Theme2 -> Theme3 -> Fallback)
84 
85     //fall << LOS::AppPrefix()+"share/pixmaps"; //always use this as well as a final fallback
86     QDir::setSearchPaths("icontheme", theme);
87     QDir::setSearchPaths("default", oxy);
88     QDir::setSearchPaths("fallback", fall);
89     //qDebug() << "Setting Icon Search Paths:" << "\nicontheme:" << theme << "\nmaterial-design-light:" << oxy << "\nfallback:" << fall;
90   }
91   //Find the icon in the search paths
92   QIcon ico;
93   QStringList srch; srch << "icontheme" << "default" << "fallback";
94   for(int i=0; i<srch.length() && ico.isNull(); i++){
95     if(QFile::exists(srch[i]+":"+icon+".svg") && !icon.contains("libreoffice") ){
96       return QFileInfo(srch[i]+":"+icon+".svg").absoluteFilePath();
97     }else if(QFile::exists(srch[i]+":"+icon+".png")){
98       return QFileInfo(srch[i]+":"+icon+".png").absoluteFilePath();
99     }
100   }
101   //If still no icon found, look for any image format in the "pixmaps" directory
102   if(QFile::exists(LOS::AppPrefix()+"share/pixmaps/"+icon)){
103     if(QFileInfo(LOS::AppPrefix()+"share/pixmaps/"+icon).isDir()){ return ""; }
104     return (LOS::AppPrefix()+"share/pixmaps/"+icon);
105   }else{
106     //Need to scan for any close match in the directory
107     QDir pix(LOS::AppPrefix()+"share/pixmaps");
108     QStringList formats = LUtils::imageExtensions();
109     QStringList found = pix.entryList(QStringList() << icon, QDir::Files, QDir::Unsorted);
110     if(found.isEmpty()){ found = pix.entryList(QStringList() << icon+"*", QDir::Files, QDir::Unsorted); }
111     //qDebug() << "Found pixmaps:" << found << formats;
112     //Use the first one found that is a valid format
113     for(int i=0; i<found.length(); i++){
114       if( formats.contains(found[i].section(".",-1).toLower()) ){
115         return pix.absoluteFilePath(found[i]);
116       }
117     }
118   }
119   return ""; //no file found
120 }
121 
122 
loadIcon(QAbstractButton * button,QString icon,bool noThumb)123 void LIconCache::loadIcon(QAbstractButton *button, QString icon, bool noThumb){
124   if(icon.isEmpty()){ return; }
125   if(icon=="appcafe"){ qDebug() << "appcafe icon:" << isThemeIcon(icon) << HASH.contains(icon); }
126   if(isThemeIcon(icon)){
127     button->setIcon( iconFromTheme(icon));
128     return ;
129   }
130   //See if the icon has already been loaded into the HASH
131   bool needload = !HASH.contains(icon);
132   if(!needload){
133     if(!noThumb && !HASH[icon].thumbnail.isNull()){ button->setIcon( HASH[icon].thumbnail ); return; }
134     else if(!HASH[icon].icon.isNull()){ button->setIcon( HASH[icon].icon ); return; }
135   }
136   //Need to load the icon
137   icon_data idata;
138   if(HASH.contains(icon)){ idata = HASH.value(icon); }
139   else { idata = createData(icon); }
140     idata.pendingButtons << QPointer<QAbstractButton>(button); //save this button for later
141   HASH.insert(icon, idata);
142   if(needload){ startReadFile(icon, idata.fullpath); }
143 }
144 
loadIcon(QAction * action,QString icon,bool noThumb)145 void LIconCache::loadIcon(QAction *action, QString icon, bool noThumb){
146   if(icon.isEmpty()){ return; }
147   if(isThemeIcon(icon)){
148     action->setIcon( iconFromTheme(icon));
149     return ;
150   }
151   //See if the icon has already been loaded into the HASH
152   bool needload = !HASH.contains(icon);
153   if(!needload){
154     if(!noThumb && !HASH[icon].thumbnail.isNull()){ action->setIcon( HASH[icon].thumbnail ); return; }
155     else if(!HASH[icon].icon.isNull()){ action->setIcon( HASH[icon].icon ); return; }
156   }
157   //Need to load the icon
158   icon_data idata;
159   if(HASH.contains(icon)){ idata = HASH.value(icon); }
160   else { idata = createData(icon); }
161     idata.pendingActions << QPointer<QAction>(action); //save this button for later
162   HASH.insert(icon, idata);
163   if(needload){ startReadFile(icon, idata.fullpath); }
164 }
165 
loadIcon(QLabel * label,QString icon,bool noThumb)166 void LIconCache::loadIcon(QLabel *label, QString icon, bool noThumb){
167   if(icon.isEmpty()){ return; }
168   if(isThemeIcon(icon)){
169     label->setPixmap( iconFromTheme(icon).pixmap(label->sizeHint()) );
170     return ;
171   }
172   //See if the icon has already been loaded into the HASH
173   bool needload = !HASH.contains(icon);
174   if(!needload){
175     if(!noThumb && !HASH[icon].thumbnail.isNull()){ label->setPixmap( HASH[icon].thumbnail.pixmap(label->sizeHint()) ); return; }
176     else if(!HASH[icon].icon.isNull()){ label->setPixmap( HASH[icon].icon.pixmap(label->sizeHint()) ); return; }
177   }
178   //Need to load the icon
179   icon_data idata;
180   if(HASH.contains(icon)){ idata = HASH.value(icon); }
181   else { idata = createData(icon);
182     if(idata.fullpath.isEmpty()){ return; } //nothing to do
183   }
184   idata.pendingLabels << QPointer<QLabel>(label); //save this QLabel for later
185   HASH.insert(icon, idata);
186   if(needload){ startReadFile(icon, idata.fullpath); }
187 }
188 
loadIcon(QMenu * action,QString icon,bool noThumb)189 void LIconCache::loadIcon(QMenu *action, QString icon, bool noThumb){
190   if(icon.isEmpty()){ return; }
191   if(isThemeIcon(icon)){
192     action->setIcon( iconFromTheme(icon));
193     return ;
194   }
195   //See if the icon has already been loaded into the HASH
196   bool needload = !HASH.contains(icon);
197   if(!needload){
198     if(!noThumb && !HASH[icon].thumbnail.isNull()){ action->setIcon( HASH[icon].thumbnail ); return; }
199     else if(!HASH[icon].icon.isNull()){ action->setIcon( HASH[icon].icon ); return; }
200   }
201   //Need to load the icon
202   icon_data idata;
203   if(HASH.contains(icon)){ idata = HASH.value(icon); }
204   else { idata = createData(icon); }
205     idata.pendingMenus << QPointer<QMenu>(action); //save this button for later
206   HASH.insert(icon, idata);
207   if(needload){ startReadFile(icon, idata.fullpath); }
208 }
209 
clearIconTheme()210 void LIconCache::clearIconTheme(){
211    //use when the icon theme changes to refresh all requested icons
212   QStringList keys = HASH.keys();
213   for(int i=0; i<keys.length(); i++){
214     //remove all relative icons (
215     if(!keys.startsWith("/")){ HASH.remove(keys[i]); }
216   }
217 }
218 
loadIcon(QString icon,bool noThumb)219 QIcon LIconCache::loadIcon(QString icon, bool noThumb){
220   if(icon.isEmpty()){ return QIcon(); }
221   if(isThemeIcon(icon)){ return iconFromTheme(icon); }
222 
223   if(HASH.contains(icon)){
224     if(!HASH[icon].icon.isNull()){ return HASH[icon].icon; }
225     else if(!HASH[icon].thumbnail.isNull() && !noThumb){ return HASH[icon].thumbnail; }
226   }
227   //Not loaded yet - need to load it right now
228   icon_data idat;
229   if(HASH.contains(icon)){ idat = HASH[icon]; }
230   else{ idat = createData(icon); }
231   if(idat.fullpath.isEmpty()){ return QIcon(); } //non-existant file
232   idat.icon = QIcon(idat.fullpath);
233   //Now save into the hash and return
234   HASH.insert(icon, idat);
235   emit IconAvailable(icon);
236   return idat.icon;
237 }
238 
clearAll()239 void LIconCache::clearAll(){
240   HASH.clear();
241 }
242 
243 // === PRIVATE ===
createData(QString icon)244 icon_data LIconCache::createData(QString icon){
245   icon_data idat;
246   //Find the real path of the icon
247   if(icon.startsWith("/")){ idat.fullpath = icon; } //already full path
248   else{  idat.fullpath = findFile(icon); }
249   return idat;
250 }
251 
getChildIconDirs(QString path)252 QStringList LIconCache::getChildIconDirs(QString path){
253 //This is a recursive function that returns the absolute path(s) of directories with *.png files
254   QDir D(path);
255   QStringList out;
256   QStringList dirs = D.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
257   if(!dirs.isEmpty() && (dirs.contains("32x32") || dirs.contains("scalable")) ){
258     //Need to sort these directories by image size
259     //qDebug() << " - Parent:" << parent << "Dirs:" << dirs;
260     for(int i=0; i<dirs.length(); i++){
261       if(dirs[i].contains("x")){ dirs[i].prepend( QString::number(10-dirs[i].section("x",0,0).length())+QString::number(10-dirs[i].at(0).digitValue())+"::::"); }
262       else if(dirs[i].at(0).isNumber()){dirs[i].prepend( QString::number(10-dirs[i].length())+QString::number(10-dirs[i].at(0).digitValue())+"::::"); }
263       else{ dirs[i].prepend( "0::::"); }
264     }
265     dirs.sort();
266     for(int i=0; i<dirs.length(); i++){ dirs[i] = dirs[i].section("::::",1,50); } //chop the sorter off the front again
267     //qDebug() << "Sorted:" << dirs;
268   }
269   QStringList img = D.entryList(QStringList() << "*.png" << "*.svg", QDir::Files | QDir::NoDotAndDotDot, QDir::NoSort);
270   if(img.length() > 0){ out << D.absolutePath(); }
271   for(int i=0; i<dirs.length(); i++){
272     img.clear();
273     img = getChildIconDirs(D.absoluteFilePath(dirs[i])); //re-use the old list variable
274     if(img.length() > 0){ out << img; }
275   }
276   return out;
277 }
278 
getIconThemeDepChain(QString theme,QStringList paths)279 QStringList LIconCache::getIconThemeDepChain(QString theme, QStringList paths){
280   QStringList results;
281   for(int i=0; i<paths.length(); i++){
282     if( QFile::exists(paths[i]+theme+"/index.theme") ){
283       QStringList deps = LUtils::readFile(paths[i]+theme+"/index.theme").filter("Inherits=");
284       if(!deps.isEmpty()){
285         deps = deps.first().section("=",1,-1).split(";",QString::SkipEmptyParts);
286         for(int j=0; j<deps.length(); j++){
287           results << deps[j] << getIconThemeDepChain(deps[j],paths);
288         }
289       }
290       break; //found primary theme index file - stop here
291     }
292   }
293   return results;
294 }
295 
startReadFile(QString id,QString path)296 void LIconCache::startReadFile(QString id, QString path){
297   if(path.endsWith(".svg")){
298     //Special handling - need to read QIcon directly to have the SVG icon scale up appropriately
299     icon_data idat = HASH[id];
300     idat.lastread = QDateTime::currentDateTime();
301     idat.icon = QIcon(path);
302     for(int i=0; i<idat.pendingButtons.length(); i++){ if(!idat.pendingButtons[i].isNull()){ idat.pendingButtons[i]->setIcon(idat.icon); } }
303     idat.pendingButtons.clear();
304     for(int i=0; i<idat.pendingLabels.length(); i++){ if(!idat.pendingLabels[i].isNull()){ idat.pendingLabels[i]->setPixmap(idat.icon.pixmap(idat.pendingLabels[i]->sizeHint())); } }
305     idat.pendingLabels.clear();
306     for(int i=0; i<idat.pendingActions.length(); i++){ if(!idat.pendingActions[i].isNull()){ idat.pendingActions[i]->setIcon(idat.icon); } }
307     idat.pendingActions.clear();
308     for(int i=0; i<idat.pendingMenus.length(); i++){ if(!idat.pendingMenus[i].isNull()){ idat.pendingMenus[i]->setIcon(idat.icon); } }
309     idat.pendingMenus.clear();
310     //Now update the hash and let the world know it is available now
311     HASH.insert(id, idat);
312     this->emit IconAvailable(id);
313   }else{
314     QtConcurrent::run(this, &LIconCache::ReadFile, this, id, path);
315   }
316 }
317 
ReadFile(LIconCache * obj,QString id,QString path)318 void LIconCache::ReadFile(LIconCache *obj, QString id, QString path){
319   //qDebug() << "Start Reading File:" << id << path;
320   QByteArray *BA = new QByteArray();
321   QDateTime cdt = QDateTime::currentDateTime();
322   if(!path.isEmpty()){
323     QFile file(path);
324     if(file.open(QIODevice::ReadOnly)){
325       BA->append(file.readAll());
326       file.close();
327     }
328   }
329   obj->emit InternalIconLoaded(id, cdt, BA);
330 }
331 
isThemeIcon(QString id)332 bool LIconCache::isThemeIcon(QString id){
333   return (!id.contains("/") && !id.contains(".") ); //&& !id.contains("libreoffice") );
334 }
335 
iconFromTheme(QString id)336 QIcon LIconCache::iconFromTheme(QString id){
337   QIcon ico = QIcon::fromTheme(id);
338   if(ico.isNull()){
339     //icon missing in theme? run the old icon-finder system
340     ico = QIcon(findFile(id));
341   }
342   return ico;
343 }
344 
345 // === PRIVATE SLOTS ===
IconLoaded(QString id,QDateTime sync,QByteArray * data)346 void LIconCache::IconLoaded(QString id, QDateTime sync, QByteArray *data){
347   //qDebug() << "Icon Loaded:" << id << HASH.contains(id);
348   QPixmap pix;
349   bool ok = pix.loadFromData(*data);
350    delete data; //no longer used - free this up
351   if(!HASH.contains(id)){ return; } //icon loading cancelled - just stop here
352   if(!ok){ HASH.remove(id); } //icon data corrupted or unreadable
353   else{
354     icon_data idat = HASH[id];
355     idat.lastread = sync;
356     idat.icon.addPixmap(pix);
357     if(pix.width() < 64){ idat.icon.addPixmap( pix.scaled( QSize(64,64), Qt::KeepAspectRatio, Qt::SmoothTransformation) ); } //also add a version which has been scaled up a bit
358     //Now throw this icon into any pending objects
359     for(int i=0; i<idat.pendingButtons.length(); i++){ if(!idat.pendingButtons[i].isNull()){ idat.pendingButtons[i]->setIcon(idat.icon); } }
360     idat.pendingButtons.clear();
361     for(int i=0; i<idat.pendingLabels.length(); i++){ if(!idat.pendingLabels[i].isNull()){ idat.pendingLabels[i]->setPixmap(pix.scaled(idat.pendingLabels[i]->sizeHint(), Qt::KeepAspectRatio, Qt::SmoothTransformation)); } }
362     idat.pendingLabels.clear();
363     for(int i=0; i<idat.pendingActions.length(); i++){ if(!idat.pendingActions[i].isNull()){ idat.pendingActions[i]->setIcon(idat.icon); } }
364     idat.pendingActions.clear();
365     //Now update the hash and let the world know it is available now
366     HASH.insert(id, idat);
367     this->emit IconAvailable(id);
368   }
369 }
370