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