1 //===========================================
2 //  Lumina-DE source code
3 //  Copyright (c) 2013-2015, Ken Moore
4 //  Available under the 3-clause BSD license
5 //  See the LICENSE file for full details
6 //===========================================
7 #include "LuminaXDG.h"
8 #include "LuminaOS.h"
9 #include "LUtils.h"
10 #include <QObject>
11 #include <QTimer>
12 #include <QMediaPlayer>
13 #include <QSvgRenderer>
14 
15 static QStringList mimeglobs;
16 static qint64 mimechecktime;
17 
18 //=============================
19 //  XDGDesktop CLASS
20 //=============================
XDGDesktop(QString file,QObject * parent)21 XDGDesktop::XDGDesktop(QString file, QObject *parent) : QObject(parent){
22   isHidden=false;
23   useTerminal=false;
24   startupNotify=false;
25   useVGL = false;
26   type = XDGDesktop::BAD;
27   filePath = file;
28   exec = tryexec = "";   // just to make sure this is initialized
29   if(!filePath.isEmpty()){ sync(); } //if an input file is given - go ahead and sync now
30 }
31 
sync()32 void XDGDesktop::sync(){
33   //Reset internal vars
34   isHidden=false;
35   useTerminal=false;
36   startupNotify=false;
37   type = XDGDesktop::BAD;
38   exec = tryexec = "";
39   //Read in the File
40   if(!filePath.endsWith(".desktop")){ return; }
41   lastRead = QDateTime::currentDateTime();
42   QStringList file = LUtils::readFile(filePath);
43   if(file.isEmpty()){ return; } //done with init right here - nothing to load
44   //Get the current localization code
45   type = XDGDesktop::APP; //assume this initially if we read the file properly
46   QString lang = QLocale::system().name(); //lang code
47   QString slang = lang.section("_",0,0); //short lang code
48   //Now start looping over the information
49   XDGDesktopAction CDA; //current desktop action
50   bool insection=false;
51   bool inaction=false;
52   for(int i=0; i<file.length(); i++){
53     QString line = file[i];
54     //if(filePath.contains("pcbsd")){ qDebug() << " - Check Line:" << line << inaction << insection; }
55     //Check if this is the end of a section
56     if(line.startsWith("[") && inaction){
57       insection=false; inaction=false;
58       //Add the current Action structure to the main desktop structure if appropriate
59       if(!CDA.ID.isEmpty()){ actions << CDA; CDA = XDGDesktopAction(); }
60     }else if(line.startsWith("[")){ insection=false; inaction = false; }
61     //Now check if this is the beginning of a section
62     if(line=="[Desktop Entry]"){ insection=true;  continue; }
63     else if(line.startsWith("[Desktop Action ")){
64       //Grab the ID of the action out of the label
65       CDA.ID = line.section("]",0,0).section("Desktop Action",1,1).simplified();
66       inaction = true;
67       continue;
68     }else if( (!insection && !inaction) || line.startsWith("#")){ continue; }
69     //Now parse out the file
70     line = line.simplified();
71     QString var = line.section("=",0,0).simplified();
72     QString loc = var.section("[",1,1).section("]",0,0).simplified(); // localization
73     var = var.section("[",0,0).simplified(); //remove the localization
74     QString val = line.section("=",1,50).simplified();
75     if( val.count("\"")==2 && val.startsWith("\"") && val.endsWith("\"")){ val.chop(1); val = val.remove(0,1); } //remove the starting/ending quotes
76     //-------------------
77     if(var=="Name"){
78       if(insection){
79 	      if(loc==slang){ name = val;} //short locale code
80         else if(loc==lang){ name = val;}
81         else if(name.isEmpty() && loc.isEmpty()){ /*qDebug() << "Empty" << val;*/ name = val; }
82       }else if(inaction){
83         if(CDA.name.isEmpty() && loc.isEmpty()){ CDA.name = val; }
84 	else if(CDA.name.isEmpty() && loc==slang){ CDA.name = val; } //short locale code
85         else if(loc == lang){ CDA.name = val; }
86       }
87       //hasName = true;
88     }else if(var=="GenericName" && insection){
89       if(genericName.isEmpty() && loc.isEmpty()){ genericName = val; }
90       else if(genericName.isEmpty() && loc==slang){ genericName = val; } //short locale code
91       else if(loc == lang){ genericName = val; }
92     }else if(var=="Comment" && insection){
93       if(comment.isEmpty() && loc.isEmpty()){ comment = val; }
94       else if(comment.isEmpty() && loc==slang){ comment = val; } //short locale code
95       else if(loc == lang){ comment = val; }
96     }else if(var=="Icon"){
97       //Quick fix for bad-registrations which add the icon suffix for theme icons
98       if(!val.startsWith("/") && val.endsWith(".png") ){ val = val.section(".",0,-2); }
99       if(insection){
100         if(icon.isEmpty() && loc.isEmpty()){ icon = val; }
101 	else if(icon.isEmpty() && loc==slang){ icon = val; } //short locale code
102         else if(loc == lang){ icon = val; }
103       }else if(inaction){
104 	if(CDA.icon.isEmpty() && loc.isEmpty()){ CDA.icon = val; }
105 	else if(CDA.icon.isEmpty() && loc==slang){ CDA.icon = val; } //short locale code
106         else if(loc == lang){ CDA.icon = val; }
107       }
108     }
109     else if( (var=="TryExec") && (tryexec.isEmpty()) && insection) { tryexec = val; }
110     else if(var=="Exec"){
111       if(insection && exec.isEmpty() ){ exec = val; }
112       else if(inaction && CDA.exec.isEmpty() ){ CDA.exec = val; }
113     }
114     else if( (var=="Path") && (path.isEmpty() ) && insection){ path = val; }
115     else if(var=="NoDisplay" && !isHidden && insection){ isHidden = (val.toLower()=="true"); }
116     else if(var=="Hidden" && !isHidden && insection){ isHidden = (val.toLower()=="true"); }
117     else if(var=="Categories" && insection){ catList = val.split(";",QString::SkipEmptyParts); }
118     else if(var=="OnlyShowIn" && insection){ showInList = val.split(";",QString::SkipEmptyParts); }
119     else if(var=="NotShowIn" && insection){ notShowInList = val.split(";",QString::SkipEmptyParts); }
120     else if(var=="Terminal" && insection){ useTerminal= (val.toLower()=="true"); }
121     else if(var=="Actions" && insection){ actionList = val.split(";",QString::SkipEmptyParts); }
122     else if(var=="MimeType" && insection){ mimeList = val.split(";",QString::SkipEmptyParts); }
123     else if(var=="Keywords" && insection){
124       if(keyList.isEmpty() && loc.isEmpty()){ keyList = val.split(";",QString::SkipEmptyParts); }
125       else if(loc == lang){ keyList = val.split(";",QString::SkipEmptyParts); }
126     }
127     else if(var=="StartupNotify" && insection){ startupNotify = (val.toLower()=="true"); }
128     else if(var=="StartupWMClass" && insection){ startupWM = val; }
129     else if(var=="URL" && insection){ url = val;}
130     else if(var=="Type" && insection){
131       if(val.toLower()=="application"){ type = XDGDesktop::APP; }
132       else if(val.toLower()=="link"){ type = XDGDesktop::LINK; }
133       else if(val.toLower().startsWith("dir")){ type = XDGDesktop::DIR; } //older specs are "Dir", newer specs are "Directory"
134       else{ type = XDGDesktop::BAD; } //Unknown type
135     }
136   } //end reading file
137   if(!CDA.ID.isEmpty()){ actions << CDA; CDA = XDGDesktopAction(); } //if an action was still being read, add that to the list now
138 
139   file.clear(); //done with contents of file
140   //If there are OnlyShowIn desktops listed, add them to the name
141   if( !showInList.isEmpty() && !showInList.contains("Lumina", Qt::CaseInsensitive) ){
142       name.append(" ("+showInList.join(", ")+")");
143   }
144   //Quick fix for showing "wine" applications (which quite often don't list a category, or have other differences)
145   if(catList.isEmpty() && filePath.contains("/wine/")){
146     catList << "Wine"; //Internal Lumina category only (not in XDG specs as of 11/14/14)
147     //Also add a fix for the location of Wine icons
148     if( !icon.isEmpty() ){
149       QStringList sizes; sizes << "256x256" << "128x128" << "64x64" << "48x48" << "32x32" << "16x16";
150       QString upath = QDir::homePath()+"/.local/share/icons/hicolor/%1/apps/%2.png";
151       //qDebug() << "Wine App: Check icon" << upath;
152       for(int i=0; i<sizes.length(); i++){
153         if( QFile::exists(upath.arg(sizes[i],icon)) ){
154 	  icon = upath.arg(sizes[i],icon);
155 	  //qDebug() << " - Found Icon:" << icon;
156 	  break;
157 	}
158       }
159     }
160   }
161 
162 }
163 
164 
isValid(bool showAll)165 bool XDGDesktop::isValid(bool showAll){
166   bool ok=true;
167   //bool DEBUG = false;
168   //if(DEBUG){ qDebug() << "[LXDG] Check File validity:" << dFile.name << dFile.filePath; }
169   switch (type){
170     case XDGDesktop::BAD:
171       ok=false;
172       //if(DEBUG){ qDebug() << " - Bad file type"; }
173       break;
174     case XDGDesktop::APP:
175       if(!tryexec.isEmpty() && !LXDG::checkExec(tryexec)){ ok=false; }//if(DEBUG){ qDebug() << " - tryexec does not exist";} }
176       else if(exec.isEmpty() || name.isEmpty()){ ok=false; }//if(DEBUG){ qDebug() << " - exec or name is empty";} }
177       else if(!LXDG::checkExec(exec.section(" ",0,0,QString::SectionSkipEmpty)) ){ ok=false; }//if(DEBUG){ qDebug() << " - first exec binary does not exist";} }
178       break;
179     case XDGDesktop::LINK:
180       ok = !url.isEmpty();
181       //if(DEBUG && !ok){ qDebug() << " - Link with missing URL"; }
182       break;
183     case XDGDesktop::DIR:
184       ok = !path.isEmpty() && QFile::exists(path);
185       //if(DEBUG && !ok){ qDebug() << " - Dir with missing path"; }
186       break;
187     default:
188       ok=false;
189       //if(DEBUG){ qDebug() << " - Unknown file type"; }
190   }
191   if(!showAll){
192     QString cdesk = getenv("XDG_CURRENT_DESKTOP");
193     if(cdesk.isEmpty()){ cdesk="Lumina"; }
194     if(!showInList.isEmpty()){ ok = showInList.contains(cdesk, Qt::CaseInsensitive); }
195     else if(!notShowInList.isEmpty()){ ok = !notShowInList.contains(cdesk,Qt::CaseInsensitive); }
196     else if(name.isEmpty()){ ok = false; }
197   }
198   return ok;
199 }
200 
getDesktopExec(QString ActionID)201 QString XDGDesktop::getDesktopExec(QString ActionID){
202   //Generate the executable line for the application
203   QString out = exec;
204   if( !ActionID.isEmpty() ){
205     //Go through and grab the proper exec for the listed action
206     for(int i=0; i<actions.length(); i++){
207       if(actions[i].ID == ActionID){
208         out = actions[i].exec;
209         break;
210       }
211     }
212   }
213 
214   if(out.isEmpty()){ return ""; }
215   else if(useTerminal){
216     //Get the currently default terminal
217     QString term = LXDG::findDefaultAppForMime("application/terminal");
218     if(!QFile::exists(term)){ term = "xterm -lc"; }
219     else if(term.endsWith(".desktop")){
220       XDGDesktop DF(term);
221         if(DF.isValid()){  term = DF.getDesktopExec(); }
222         else{ term = "xterm -lc"; }
223       //DF.deleteLater(); //done with this struct
224     }else if( !LUtils::isValidBinary(term)){ term = "xterm -lc"; }
225     out = term+" -e "+out;  //-e is a nearly-universal flag for terminal emulators
226   }
227   //Now perform any of the XDG flag substitutions as appropriate (9/2014 standards)
228   if(out.contains("%i") && !icon.isEmpty() ){ out.replace("%i", "--icon \""+icon+"\""); }
229   if(out.contains("%c")){
230     if(!name.isEmpty()){ out.replace("%c", "\""+name+"\""); }
231     else if(!genericName.isEmpty()){ out.replace("%c", "\""+genericName+"\""); }
232     else{ out.replace("%c", "\""+filePath.section("/",-1).section(".desktop",0,0)+"\""); }
233   }
234   if(out.contains("%k")){ out.replace("%k", "\""+filePath+"\""); }
235   return out;
236 }
237 
generateExec(QStringList inputfiles,QString ActionID)238 QString XDGDesktop::generateExec(QStringList inputfiles, QString ActionID){
239   QString exec = getDesktopExec(ActionID);
240   //Does the app need the input files in URL or File syntax?
241   bool URLsyntax = (exec.contains("%u") || exec.contains("%U"));
242   //Adjust the input file formats as needed
243   //qDebug() << "Got inputfiles:" << inputfiles << URLsyntax;
244   for(int i=0; i<inputfiles.length(); i++){
245     bool url = inputfiles[i].startsWith("www") || inputfiles[i].contains("://");
246     //Run it through the QUrl class to catch/fix any URL syntax issues
247     if(URLsyntax){
248       if(inputfiles[i].startsWith("mailto:") ){} //don't touch this syntax - already formatted
249       else if(url){ inputfiles[i] = QUrl(inputfiles[i]).url(); }
250       else{ inputfiles[i] = QUrl::fromLocalFile(inputfiles[i]).url(); }
251     }else{
252       //if(inputfiles[i].startsWith("mailto:") ){} //don't touch this syntax - already formatted
253       //qDebug() << "Need local format:" << inputfiles[i] << url;
254       if(url){ inputfiles[i] = QUrl(inputfiles[i]).toLocalFile(); }
255       else{ inputfiles[i] = inputfiles[i]; } //QUrl::fromLocalFile(inputfiles[i]).toLocalFile(); }
256     }
257   }
258   inputfiles.removeAll(""); //just in case any empty ones get through
259   //Now to the exec replacements as needed
260   //qDebug() << "Generate Exec:" << exec << inputfiles;
261   if(exec.contains("%f")){
262     if(inputfiles.isEmpty()){ exec.replace("%f",""); }
263     else{ exec.replace("%f", "\""+inputfiles.first()+"\""); } //Note: can only take one input
264   }else if(exec.contains("%F")){
265     if(inputfiles.isEmpty()){ exec.replace("%F",""); }
266     else{ exec.replace("%F", "\""+inputfiles.join("\" \"")+"\""); }
267   }
268   if(exec.contains("%u")){
269     if(inputfiles.isEmpty()){ exec.replace("%u",""); }
270     else{ exec.replace("%u",  "\""+inputfiles.first()+"\""); } //Note: can only take one input
271   }else if(exec.contains("%U")){
272     if(inputfiles.isEmpty()){ exec.replace("%U",""); }
273     else{ exec.replace("%U", "\""+inputfiles.join("\" \"")+"\""); }
274   }
275   //Sanity check for known Local/URL syntax issues from some apps
276   if(!URLsyntax && exec.contains("%20")){ exec.replace("%20"," "); }
277   //Clean up any leftover "Exec" field codes (should have already been replaced earlier)
278   if(exec.contains("%")){ exec = exec.remove("%U").remove("%u").remove("%F").remove("%f").remove("%i").remove("%c").remove("%k"); }
279   return exec.simplified();
280 }
281 
saveDesktopFile(bool merge)282 bool XDGDesktop::saveDesktopFile(bool merge){
283   qDebug() << "Save Desktop File:" << filePath << "Merge:" << merge;
284   bool autofile = filePath.contains("/autostart/"); //use the "Hidden" field instead of the "NoDisplay"
285   int insertloc = -1;
286   QStringList info;
287   if(QFile::exists(filePath) && merge){
288     //Load the existing file and merge in in any changes
289     info = LUtils::readFile(filePath);
290     //set a couple flags based on the contents before we start iterating through
291     // - determine if a translated field was changed (need to remove all the now-invalid translations)
292     bool clearName, clearComment, clearGName;
293     QString tmp = "";
294     if(!info.filter("Name=").isEmpty()){ tmp = info.filter("Name=").first().section("=",1,50); }
295     clearName=(tmp!=name);
296     tmp.clear();
297     if(!info.filter("Comment=").isEmpty()){ tmp = info.filter("Comment=").first().section("=",1,50); }
298     clearComment=(tmp!=comment);
299     tmp.clear();
300     if(!info.filter("GenericName=").isEmpty()){ tmp = info.filter("GenericName=").first().section("=",1,50); }
301     clearGName=(tmp!=genericName);
302     //Now start iterating through the file and changing fields as necessary
303     bool insection = false;
304     for(int i=0; i<info.length(); i++){
305       if(info[i]=="[Desktop Entry]"){
306         insection = true;
307 	continue;
308       }else if(info[i].startsWith("[")){
309 	if(insection){ insertloc = i; } //save this location for later insertions
310 	insection = false;
311 	continue;
312       }
313       if(!insection || info[i].isEmpty() || info[i].section("#",0,0).simplified().isEmpty()){ continue; }
314       QString var = info[i].section("=",0,0);
315       QString val = info[i].section("=",1,50).simplified();
316       //NOTE: Clear the dFile variable as it is found/set in the file (to keep track of what has been used already)
317       //    For boolian values, set them to false
318       // --LOCALIZED VALUES --
319       if(var.startsWith("Name")){
320         if(var.contains("[") && clearName){ info.removeAt(i); i--; continue;}
321 	else if(!var.contains("[")){ info[i] = var+"="+name; name.clear(); }
322       }else if(var.startsWith("GenericName")){
323         if(var.contains("[") && clearGName){ info.removeAt(i); i--; continue;}
324 	else if(!var.contains("[")){ info[i] = var+"="+genericName; genericName.clear(); }
325       }else if(var.startsWith("Comment")){
326         if(var.contains("[") && clearComment){ info.removeAt(i); i--; continue;}
327 	else if(!var.contains("[")){ info[i] = var+"="+comment; comment.clear(); }
328 
329       // --STRING/LIST VALUES--
330       }else if(var=="Exec"){ info[i] = var+"="+exec; exec.clear(); }
331       else if(var=="TryExec"){ info[i] = var+"="+tryexec; tryexec.clear(); }
332       else if(var=="Path"){ info[i] = var+"="+path; path.clear(); }
333       else if(var=="Icon"){ info[i] = var+"="+icon; icon.clear(); }
334       else if(var=="StartupWMClass"){ info[i] = var+"="+startupWM; startupWM.clear(); }
335       else if(var=="MimeType"){ info[i] = var+"="+mimeList.join(";"); mimeList.clear(); }
336       else if(var=="Categories"){ info[i] = var+"="+catList.join(";"); catList.clear(); }
337       else if(var=="Keywords"){ info[i] = var+"="+keyList.join(";"); keyList.clear(); }
338       else if(var=="Actions"){ info[i] = var+"="+actionList.join(";"); actionList.clear(); }
339       else if(var=="OnlyShowIn"){ info[i] = var+"="+showInList.join(";"); showInList.clear(); }
340       else if(var=="NotShowIn"){ info[i] = var+"="+notShowInList.join(";"); notShowInList.clear(); }
341       else if(var=="URL"){ info[i] = var+"="+url; url.clear(); }
342 
343       // --BOOLIAN VALUES--
344       else if(var=="Hidden"){
345 	if(!autofile){ info.removeAt(i); i--; continue; }
346 	else{ info[i] = var+"="+(isHidden ? "true": "false"); isHidden=false;}
347       }else if(var=="NoDisplay"){
348 	if(autofile){ info.removeAt(i); i--; continue; }
349 	else{ info[i] = var+"="+(isHidden ? "true": "false"); isHidden=false;}
350       }else if(var=="Terminal"){
351 	info[i] = var+"="+(useTerminal ? "true": "false"); useTerminal=false;
352       }else if(var=="StartupNotify"){
353 	info[i] = var+"="+(startupNotify ? "true": "false"); startupNotify=false;
354       }
355       // Remove any lines that have been un-set or removed from the file
356       if(info[i].section("=",1,50).simplified().isEmpty()){ info.removeAt(i); i--; }
357     }
358 
359   }else{
360     //Just write a new file and overwrite any old one
361     // (pre-set some values here which are always required)
362     info << "[Desktop Entry]";
363     info << "Version=1.0";
364     if(type==XDGDesktop::APP){ info << "Type=Application"; }
365     else if(type==XDGDesktop::LINK){ info << "Type=Link"; }
366     else if(type==XDGDesktop::DIR){ info << "Type=Dir"; }
367   }
368 
369   if(insertloc<0){ insertloc = info.size(); }//put it at the end
370   //Now add in any items that did not exist in the original file
371     if( !exec.isEmpty() ){ info.insert(insertloc,"Exec="+exec); }
372     if( !tryexec.isEmpty() ){ info.insert(insertloc,"TryExec="+tryexec); }
373     if( !path.isEmpty() ){ info.insert(insertloc,"Path="+path); }
374     if( !icon.isEmpty() ){ info.insert(insertloc,"Icon="+icon); }
375     if( !name.isEmpty() ){ info.insert(insertloc,"Name="+name); }
376     if( !genericName.isEmpty() ){ info.insert(insertloc,"GenericName="+genericName); }
377     if( !comment.isEmpty() ){ info.insert(insertloc,"Comment="+comment); }
378     if( !startupWM.isEmpty() ){ info.insert(insertloc,"StartupWMClass="+startupWM); }
379     if( !mimeList.isEmpty() ){ info.insert(insertloc,"MimeType="+mimeList.join(";")); }
380     if( !catList.isEmpty() ){ info.insert(insertloc,"Categories="+catList.join(";")); }
381     if( !keyList.isEmpty() ){ info.insert(insertloc,"Keywords="+keyList.join(";")); }
382     if( !actionList.isEmpty() ){ info.insert(insertloc,"Actions="+actionList.join(";")); }
383     if( !showInList.isEmpty() ){ info.insert(insertloc,"OnlyShowIn="+showInList.join(";")); }
384     else if( !notShowInList.isEmpty() ){ info.insert(insertloc,"NotShowIn="+notShowInList.join(";")); }
385     if( !url.isEmpty() ){ info.insert(insertloc,"URL="+url); }
386     if( isHidden && autofile ){ info.insert(insertloc,"Hidden=true"); }
387     else if(isHidden){ info.insert(insertloc,"NoDisplay=true"); }
388     if( useTerminal){ info.insert(insertloc,"Terminal=true"); }
389     if( startupNotify ){ info.insert(insertloc,"StartupNotify=true"); }
390 
391   //Now save the file
392   return LUtils::writeFile(filePath, info, true);
393 }
394 
setAutoStarted(bool autostart)395 bool XDGDesktop::setAutoStarted(bool autostart){
396   //First get the list of system directories to search (system first, user-provided files come later and overwrite sys files as needed)
397   QStringList paths = QString(getenv("XDG_CONFIG_DIRS")).split(":");
398   QString upath = QString(getenv("XDG_CONFIG_HOME")).section(":",0,0);
399   if(upath.isEmpty()){ upath = QDir::homePath()+"/.config/autostart/"; }
400   else{ upath.append("/autostart/"); }
401   //Verify that the autostart directory exists
402   if(!QFile::exists(upath)){
403     QDir dir;
404     dir.mkpath(upath);
405   }
406 
407   //Quick check/finish for user-defined files which are getting disabled (just remove the file)
408   if(filePath.startsWith(upath) && !autostart){
409     return QFile::remove(filePath);
410   }
411   bool sysfile = false;
412   for(int i=0; i<paths.length(); i++){
413     if(filePath.startsWith(paths[i]+"/autostart/") ){
414       sysfile = true;
415       //Change it to the user-modifiable directory
416       filePath = filePath.replace(paths[i]+"/autostart/", upath);
417     }
418   }
419   //Make sure the user-autostart dir is specified, and clean the app structure as necessary
420   if( !filePath.startsWith(upath) && autostart){
421     //Some other non-override autostart file - set it up to open with lumina-open
422     if(!filePath.endsWith(".desktop")){
423       exec = "lumina-open \""+filePath+"\"";
424       tryexec = filePath; //make sure this file exists
425       if(name.isEmpty()){ name = filePath.section("/",-1); }
426       if(icon.isEmpty()){ icon = LXDG::findAppMimeForFile(filePath); icon.replace("/","-"); }
427       filePath = upath+filePath.section("/",-1)+".desktop";
428       type = XDGDesktop::APP;
429     }else{
430       //Some other *.desktop file on the system (keep almost all the existing settings/values)
431       // - setup a redirect to the other file
432       exec = "lumina-open \""+filePath+"\"";
433       tryexec = filePath; //make sure this file exists
434       // - Adjust the actual path where this file will get saved
435       filePath = upath+filePath.section("/",-1);
436     }
437   }
438   //Now save the "hidden" value into the file
439   isHidden = !autostart; //if hidden, it will not be autostarted
440   //Now save the file as necessary
441   bool saved = false;
442   //qDebug() << " - Saving AutoStart File:" << filePath << name << isHidden;
443   if(sysfile){
444     //Just an override file for the "hidden" field - nothing more
445     QStringList info;
446       info << "[Desktop Entry]" << "Type=Application" << QString("Hidden=")+ (isHidden ? QString("true"): QString("false"));
447     saved = LUtils::writeFile(filePath, info, true);
448   }else{
449     //Need to actually save the full file
450     saved = saveDesktopFile();
451   }
452   return saved;
453 }
454 
addToMenu(QMenu * topmenu)455 void XDGDesktop::addToMenu(QMenu *topmenu){
456   if(!this->isValid()){ return; }
457   if(actions.isEmpty()){
458 	//Just a single entry point - no extra actions
459     QAction *act = new QAction(this->name, topmenu);
460     act->setIcon(LXDG::findIcon(this->icon, ""));
461     act->setToolTip(this->comment);
462     act->setWhatsThis(this->filePath);
463     topmenu->addAction(act);
464   }else{
465     //This app has additional actions - make this a sub menu
466     // - first the main menu/action
467     QMenu *submenu = new QMenu(this->name, topmenu);
468     submenu->setIcon( LXDG::findIcon(this->icon,"") );
469     //This is the normal behavior - not a special sub-action (although it needs to be at the top of the new menu)
470     QAction *act = new QAction(this->name, submenu);
471       act->setIcon(LXDG::findIcon(this->icon, ""));
472       act->setToolTip(this->comment);
473       act->setWhatsThis(this->filePath);
474     submenu->addAction(act);
475     //Now add entries for every sub-action listed
476     for(int sa=0; sa<this->actions.length(); sa++){
477       QAction *sact = new QAction( this->actions[sa].name, this);
478       sact->setIcon(LXDG::findIcon( this->actions[sa].icon, this->icon));
479       sact->setToolTip(this->comment);
480       sact->setWhatsThis("-action \""+this->actions[sa].ID+"\" \""+this->filePath+"\"");
481       submenu->addAction(sact);
482     }
483     topmenu->addMenu(submenu);
484   }
485 }
486 
487 
488 //====XDGDesktopList Functions ====
XDGDesktopList(QObject * parent,bool watchdirs)489 XDGDesktopList::XDGDesktopList(QObject *parent, bool watchdirs) : QObject(parent){
490   synctimer = new QTimer(this); //interval set automatically based on changes/interactions
491     connect(synctimer, SIGNAL(timeout()), this, SLOT(updateList()) );
492   keepsynced = watchdirs;
493   if(watchdirs){
494     watcher = new QFileSystemWatcher(this);
495     connect(watcher, SIGNAL(fileChanged(const QString&)), this, SLOT(watcherChanged()) );
496     connect(watcher, SIGNAL(directoryChanged(const QString&)), this, SLOT(watcherChanged()) );
497   }else{
498     watcher = 0;
499   }
500 }
501 
~XDGDesktopList()502 XDGDesktopList::~XDGDesktopList(){
503   //nothing special to do here
504 }
505 
instance()506 XDGDesktopList* XDGDesktopList::instance(){
507   static XDGDesktopList *APPLIST = 0;
508   if(APPLIST==0){
509     APPLIST = new XDGDesktopList(0, true);
510   }
511   return APPLIST;
512 }
513 
watcherChanged()514 void XDGDesktopList::watcherChanged(){
515   if(synctimer->isActive()){ synctimer->stop(); }
516   synctimer->setInterval(1000); //1 second delay before check kicks off
517   synctimer->start();
518 }
519 
updateList()520 void XDGDesktopList::updateList(){
521   //run the check routine
522   if(synctimer->isActive()){ synctimer->stop(); }
523   hashmutex.lock();
524   QStringList appDirs = LXDG::systemApplicationDirs(); //get all system directories
525   QStringList found, newfiles; //for avoiding duplicate apps (might be files with same name in different priority directories)
526   QStringList oldkeys = files.keys();
527   bool appschanged = false;
528   bool firstrun = lastCheck.isNull() || oldkeys.isEmpty();
529   lastCheck = QDateTime::currentDateTime();
530   //Variables for internal loop use only (to prevent re-initializing variable on every iteration)
531   QString path; QDir dir;  QStringList apps;
532   for(int i=0; i<appDirs.length(); i++){
533     if( !dir.cd(appDirs[i]) ){ continue; } //could not open dir for some reason
534     apps = dir.entryList(QStringList() << "*.desktop",QDir::Files, QDir::Name);
535     for(int a=0; a<apps.length(); a++){
536       path = dir.absoluteFilePath(apps[a]);
537       if(files.contains(path) && (files.value(path)->lastRead>QFileInfo(path).lastModified()) ){
538         //Re-use previous data for this file (nothing changed)
539         found << files[path]->name;  //keep track of which files were already found
540       }else{
541         if(files.contains(path)){ appschanged = true; files.take(path)->deleteLater(); }
542       	XDGDesktop *dFile = new XDGDesktop(path, this);
543         if(dFile->type!=XDGDesktop::BAD){
544           appschanged = true; //flag that something changed - needed to load a file
545           if(!oldkeys.contains(path)){ newfiles << path; } //brand new file (not an update to a previously-read file)
546           files.insert(path, dFile);
547           found << dFile->name;
548         }else{
549           dFile->deleteLater(); //bad file - discard it
550         }
551       }
552       oldkeys.removeAll(path); //make sure this key does not get cleaned up later
553     } //end loop over apps
554   } //end loop over appDirs
555   //Save the extra info to the internal lists
556   if(!firstrun){
557     removedApps = oldkeys;//files which were removed
558     newApps = newfiles; //files which were added
559   }
560   //Now go through and cleanup any old keys where the associated file does not exist anymore
561   for(int i=0; i<oldkeys.length(); i++){
562     //qDebug() << "Removing file from internal map:" << oldkeys[i];
563     if(i==0){ appschanged = true; }
564     //files.remove(oldkeys[i]);
565     files.take(oldkeys[i])->deleteLater();
566   }
567   //If this class is automatically managing the lists, update the watched files/dirs and send out notifications
568   if(watcher!=0){
569     if(appschanged){ qDebug() << "Auto App List Update:" << lastCheck  << "Files Found:" << files.count(); }
570     watcher->removePaths(QStringList() << watcher->files() << watcher->directories());
571     watcher->addPaths(appDirs);
572     if(appschanged){ emit appsUpdated(); }
573     synctimer->setInterval(60000); //Update in 1 minute if nothing changes before then
574     synctimer->start();
575   }
576   hashmutex.unlock();
577 }
578 
apps(bool showAll,bool showHidden)579 QList<XDGDesktop*> XDGDesktopList::apps(bool showAll, bool showHidden){
580   //showAll: include invalid files, showHidden: include NoShow/Hidden files
581   //hashmutex.lock();
582   QStringList keys = files.keys();
583   QList<XDGDesktop*> out;
584   for(int i=0; i<keys.length(); i++){
585     if( showHidden || !files[keys[i]]->isHidden ){ //this is faster than the "checkValidity()" function below  - so always filter here first
586       if( files[keys[i]]->isValid(showAll) ){
587          out << files[keys[i]];
588       }
589     }
590   }
591   //hashmutex.unlock();
592   return out;
593 }
594 
findAppFile(QString filename)595 XDGDesktop* XDGDesktopList::findAppFile(QString filename){
596   //hashmutex.lock();
597   QStringList keys = files.keys().filter(filename);
598   QString chk = filename.section("/",-1);
599   XDGDesktop *found = 0;
600   for(int i=0; i<keys.length(); i++){
601     if(keys[i] == filename || keys[i].endsWith("/"+chk)){ found = files[keys[i]]; }
602   }
603   //hashmutex.unlock();
604   //No matches
605   return found;
606 }
607 
populateMenu(QMenu * topmenu,bool byCategory)608 void XDGDesktopList::populateMenu(QMenu *topmenu, bool byCategory){
609   topmenu->clear();
610   if(byCategory){
611     QHash<QString, QList<XDGDesktop*> > APPS = LXDG::sortDesktopCats( this->apps(false,false) );
612     QStringList cats = APPS.keys();
613     cats.sort(); //make sure they are alphabetical
614     for(int i=0; i<cats.length(); i++){
615       //Make sure they are translated and have the right icons
616       QString name, icon;
617       if(cats[i]=="All"){continue; } //skip this listing for the menu
618       else if(cats[i] == "Multimedia"){ name = tr("Multimedia"); icon = "applications-multimedia"; }
619       else if(cats[i] == "Development"){ name = tr("Development"); icon = "applications-development"; }
620       else if(cats[i] == "Education"){ name = tr("Education"); icon = "applications-education"; }
621       else if(cats[i] == "Game"){ name = tr("Games"); icon = "applications-games"; }
622       else if(cats[i] == "Graphics"){ name = tr("Graphics"); icon = "applications-graphics"; }
623       else if(cats[i] == "Network"){ name = tr("Network"); icon = "applications-internet"; }
624       else if(cats[i] == "Office"){ name = tr("Office"); icon = "applications-office"; }
625       else if(cats[i] == "Science"){ name = tr("Science"); icon = "applications-science"; }
626       else if(cats[i] == "Settings"){ name = tr("Settings"); icon = "preferences-system"; }
627       else if(cats[i] == "System"){ name = tr("System"); icon = "applications-system"; }
628       else if(cats[i] == "Utility"){ name = tr("Utility"); icon = "applications-utilities"; }
629       else if(cats[i] == "Wine"){ name = tr("Wine"); icon = "wine"; }
630       else{ name = tr("Unsorted"); icon = "applications-other"; }
631 
632       QMenu *menu = new QMenu(name, topmenu);
633       menu->setIcon(LXDG::findIcon(icon,""));
634       QList<XDGDesktop*> appL = APPS.value(cats[i]);
635       for( int a=0; a<appL.length(); a++){ appL[a]->addToMenu(menu); }
636       topmenu->addMenu(menu);
637     } //end loop over cats
638   }else{
639     QList<XDGDesktop*> APPS = this->apps(false, false);
640     for(int i=0; i<APPS.length(); i++){ APPS[i]->addToMenu(topmenu); }
641   }
642 }
643 
644 //==== LFileInfo Functions ====
645 //Need some extra information not usually available by a QFileInfo
646 /*void LFileInfo::loadExtraInfo(){
647   desk = 0;
648   //Now load the extra information
649   if( this->suffix().isEmpty() && (this->absoluteFilePath().startsWith("/net/") || this->isDir()) ){
650     mime = "inode/directory";
651     //Special directory icons
652     QString name = this->fileName().toLower();
653     if(name=="desktop"){ icon = "user-desktop"; }
654     else if(name=="tmp"){ icon = "folder-temp"; }
655     else if(name=="video" || name=="videos"){ icon = "folder-video"; }
656     else if(name=="music" || name=="audio"){ icon = "folder-sound"; }
657     else if(name=="projects" || name=="devel"){ icon = "folder-development"; }
658     else if(name=="notes"){ icon = "folder-txt"; }
659     else if(name=="downloads"){ icon = "folder-downloads"; }
660     else if(name=="documents"){ icon = "folder-documents"; }
661     else if(name=="images" || name=="pictures"){ icon = "folder-image"; }
662     else if(this->absoluteFilePath().startsWith("/net/")){ icon = "folder-shared"; }
663     else if( !this->isReadable() ){ icon = "folder-locked"; }
664   }else if( this->suffix()=="desktop"){
665     mime = "application/x-desktop";
666     icon = "application-x-desktop"; //default value
667     desk = new XDGDesktop(this->absoluteFilePath(), 0);
668     if(desk->type!=XDGDesktop::BAD){
669       //use the specific desktop file info (if possible)
670       if(!desk->icon.isEmpty()){ icon = desk->icon; }
671     }
672   }else{
673     //Generic file, just determine the mimetype
674     mime = LXDG::findAppMimeForFile(this->fileName());
675   }
676 }
677 LFileInfo::LFileInfo(){
678   desk = 0;
679 }
680 LFileInfo::LFileInfo(QString filepath){ //overloaded contructor
681   this->setFile(filepath);
682   loadExtraInfo();
683 }
684 LFileInfo::LFileInfo(QFileInfo info){ //overloaded contructor
685   this->swap(info); //use the given QFileInfo without re-loading it
686   loadExtraInfo();
687 }
688 
689 //Functions for accessing the extra information
690 // -- Return the mimetype for the file
691 QString LFileInfo::mimetype(){
692   if(mime=="inode/directory"){ return ""; }
693   else{ return mime; }
694 }
695 
696 // -- Return the icon to use for this file
697 QString LFileInfo::iconfile(){
698   if(!icon.isEmpty()){
699     return icon;
700   }else{
701     if(!mime.isEmpty()){
702       QString tmp = mime;
703       tmp.replace("/","-");
704       return tmp;
705     }else if(this->isExecutable()){
706       return "application-x-executable";
707     }
708   }
709   return ""; //Fall back to nothing
710 }
711 
712 // -- Check if this is an XDG desktop file
713 bool LFileInfo::isDesktopFile(){
714   if(desk==0){ return false; }
715   return (!desk->filePath.isEmpty());
716 }
717 
718 // -- Allow access to the XDG desktop data structure
719 XDGDesktop* LFileInfo::XDG(){
720   return desk;
721 }
722 
723 // -- Check if this is a readable video file (for thumbnail support)
724 bool LFileInfo::isVideo(){
725   if(!mime.startsWith("video/")){ return false; }
726   //Check the hardcoded list of known supported video formats to see if the thumbnail can be generated
727   return ( !LUtils::videoExtensions().filter(this->suffix().toLower()).isEmpty() );
728 }
729 
730 // -- Check if this is a readable image file
731 bool LFileInfo::isImage(){
732   if(!mime.startsWith("image/")){ return false; } //quick return for non-image files
733   //Check the Qt subsystems to see if this image file can be read
734   return ( !LUtils::imageExtensions().filter(this->suffix().toLower()).isEmpty() );
735 }
736 
737 bool LFileInfo::isAVFile(){
738   return (mime.startsWith("audio/") || mime.startsWith("video/") );
739 }*/
740 
741 
742 //==== LXDG Functions ====
checkExec(QString exec)743 bool LXDG::checkExec(QString exec){
744   //Return true(good) or false(bad)
745   //Check for quotes around the exec, and remove them as needed
746   if(exec.startsWith("\"") && exec.count("\"")>=2){ exec = exec.section("\"",1,1).simplified(); }
747   if(exec.startsWith("\'") && exec.count("\'")>=2){ exec = exec.section("\'",1,1).simplified(); }
748   if(exec.startsWith("/")){ return QFile::exists(exec); }
749   else{
750     QStringList paths = QString(getenv("PATH")).split(":");
751     for(int i=0; i<paths.length(); i++){
752       if(QFile::exists(paths[i]+"/"+exec)){ return true; }
753     }
754   }
755   return false; //could not find the executable in the current path(s)
756 }
757 
systemApplicationDirs()758 QStringList LXDG::systemApplicationDirs(){
759   //Returns a list of all the directories where *.desktop files can be found
760   QStringList appDirs = QString(getenv("XDG_DATA_HOME")).split(":");
761   appDirs << QString(getenv("XDG_DATA_DIRS")).split(":");
762   if(appDirs.isEmpty()){ appDirs << "/usr/local/share" << "/usr/share" << LOS::AppPrefix()+"/share" << LOS::SysPrefix()+"/share" << L_SHAREDIR; }
763   appDirs.removeDuplicates();
764   //Now create a valid list
765   QStringList out;
766   for(int i=0; i<appDirs.length(); i++){
767     if( QFile::exists(appDirs[i]+"/applications") ){
768       out << appDirs[i]+"/applications";
769       //Also check any subdirs within this directory
770       // (looking at you KDE - stick to the standards!!)
771       out << LUtils::listSubDirectories(appDirs[i]+"/applications");
772     }
773   }
774   //qDebug() << "System Application Dirs:" << out;
775   return out;
776 }
777 
sortDesktopCats(QList<XDGDesktop * > apps)778 QHash<QString,QList<XDGDesktop*> > LXDG::sortDesktopCats(QList<XDGDesktop*> apps){
779   //Sort the list of applications into their different categories (main categories only)
780   //Create the category lists
781   QList<XDGDesktop*> multimedia, dev, ed, game, graphics, network, office, science, settings, sys, utility, other, wine;
782   //Sort the apps into the lists
783   for(int i=0; i<apps.length(); i++){
784     if(apps[i]->catList.contains("AudioVideo")){ multimedia << apps[i]; }
785     else if(apps[i]->catList.contains("Development")){ dev << apps[i]; }
786     else if(apps[i]->catList.contains("Education")){ ed << apps[i]; }
787     else if(apps[i]->catList.contains("Game")){ game << apps[i]; }
788     else if(apps[i]->catList.contains("Graphics")){ graphics << apps[i]; }
789     else if(apps[i]->catList.contains("Network")){ network << apps[i]; }
790     else if(apps[i]->catList.contains("Office")){ office << apps[i]; }
791     else if(apps[i]->catList.contains("Science")){ science << apps[i]; }
792     else if(apps[i]->catList.contains("Settings")){ settings << apps[i]; }
793     else if(apps[i]->catList.contains("System")){ sys << apps[i]; }
794     else if(apps[i]->catList.contains("Utility")){ utility << apps[i]; }
795     else if(apps[i]->catList.contains("Wine")){ wine << apps[i]; }
796     else{ other << apps[i]; }
797   }
798   //Now create the output hash
799   QHash<QString,QList<XDGDesktop*> > out;
800   if(!multimedia.isEmpty()){ out.insert("Multimedia", LXDG::sortDesktopNames(multimedia)); }
801   if(!dev.isEmpty()){ out.insert("Development", LXDG::sortDesktopNames(dev)); }
802   if(!ed.isEmpty()){ out.insert("Education", LXDG::sortDesktopNames(ed)); }
803   if(!game.isEmpty()){ out.insert("Game", LXDG::sortDesktopNames(game)); }
804   if(!graphics.isEmpty()){ out.insert("Graphics", LXDG::sortDesktopNames(graphics)); }
805   if(!network.isEmpty()){ out.insert("Network", LXDG::sortDesktopNames(network)); }
806   if(!office.isEmpty()){ out.insert("Office", LXDG::sortDesktopNames(office)); }
807   if(!science.isEmpty()){ out.insert("Science", LXDG::sortDesktopNames(science)); }
808   if(!settings.isEmpty()){ out.insert("Settings", LXDG::sortDesktopNames(settings)); }
809   if(!sys.isEmpty()){ out.insert("System", LXDG::sortDesktopNames(sys)); }
810   if(!utility.isEmpty()){ out.insert("Utility", LXDG::sortDesktopNames(utility)); }
811   if(!wine.isEmpty()){ out.insert("Wine", LXDG::sortDesktopNames(wine)); }
812   if(!other.isEmpty()){ out.insert("Unsorted", LXDG::sortDesktopNames(other)); }
813   //return the resulting hash
814   return out;
815 }
816 
817 //Return the icon to use for the given category
DesktopCatToIcon(QString cat)818 QString LXDG::DesktopCatToIcon(QString cat){
819   QString icon = "applications-other";
820   if(cat=="Multimedia"){ icon = "applications-multimedia"; }
821   else if(cat=="Development"){ icon = "applications-development"; }
822   else if(cat=="Education"){ icon = "applications-education"; }
823   else if(cat=="Game"){ icon = "applications-games"; }
824   else if(cat=="Graphics"){ icon = "applications-graphics"; }
825   else if(cat=="Network"){ icon = "applications-internet"; }
826   else if(cat=="Office"){ icon = "applications-office"; }
827   else if(cat=="Science"){ icon = "applications-science"; }
828   else if(cat=="Settings"){ icon = "preferences-system"; }
829   else if(cat=="System"){ icon = "applications-system"; }
830   else if(cat=="Utility"){ icon = "applications-utilities"; }
831   else if(cat=="Wine"){ icon = "wine"; }
832   return icon;
833 }
834 
sortDesktopNames(QList<XDGDesktop * > apps)835 QList<XDGDesktop*> LXDG::sortDesktopNames(QList<XDGDesktop*> apps){
836   //Sort the list by name of the application
837   QHash<QString, XDGDesktop*> sorter;
838   for(int i=0; i<apps.length(); i++){
839     sorter.insert(apps[i]->name.toLower(), apps[i]);
840   }
841   QStringList keys = sorter.keys();
842   keys.sort();
843   //Re-assemble the output list
844   QList<XDGDesktop*> out;
845   for(int i=0; i<keys.length(); i++){
846     out << sorter[keys[i]];
847   }
848   return out;
849 }
850 
setEnvironmentVars()851 void LXDG::setEnvironmentVars(){
852   //Set the default XDG environment variables if not already set
853   setenv("XDG_DATA_HOME",QString(QDir::homePath()+"/.local/share").toUtf8(), 0);
854   setenv("XDG_CONFIG_HOME",QString(QDir::homePath()+"/.config").toUtf8(), 0);
855   setenv("XDG_DATA_DIRS","/usr/local/share:/usr/share", 0);
856   setenv("XDG_CONFIG_DIRS","/etc/xdg:/usr/local/etc/xdg", 0);
857   setenv("XDG_CACHE_HOME",QString(QDir::homePath()+"/.cache").toUtf8(), 0);
858   setenv("QT_QPA_PLATFORMTHEME", "lthemeengine", 0);
859   setenv("QT_NO_GLIB","1",0);
860   //Don't set "XDG_RUNTIME_DIR" yet - need to look into the specs
861 }
862 
findIcon(QString iconName,QString fallback)863 QIcon LXDG::findIcon(QString iconName, QString fallback){
864   //With the addition of the Lumina theme engine (8/3/17), switch back to using the Qt icon from theme method for apps
865   QIcon tmp;
866   if(!iconName.contains("libreoffice") || !QIcon::themeName().startsWith("material-design") ){ //libreoffice is stupid - their svg icons are un-renderable with Qt
867     tmp = QIcon::fromTheme(iconName);
868     /*if(iconName.contains("start-here")){
869       qDebug() << "[ICON]" << iconName << "found:" << !tmp.isNull() << tmp.name();
870     }*/
871     //if(tmp.isNull()){ tmp = QIcon::fromTheme(fallback); }
872   }
873   if(!tmp.isNull() && tmp.name()==iconName){ return tmp; } //found this in the theme
874   else if(iconName=="start-here-lumina"){
875     //Additional fallback options for the OS-branded icon
876     QString osname = LOS::OSName().simplified().toLower();
877     QStringList possible; possible << "distributor-logo-"+osname << osname;
878     QStringList words;
879     if(osname.contains(" ")){ words = osname.split(" "); }
880     else if(osname.contains("-")){ words = osname.split("-"); }
881     for(int i=0; i<words.length(); i++){ possible << "distributor-logo-"+words[i] << words[i]; }
882     //qDebug() << "Looking for possible OS icons:" << possible;
883     for(int i=0; i<possible.length(); i++){
884       if(QIcon::hasThemeIcon(possible[i])){ return QIcon::fromTheme(possible[i]); }
885     }
886   }
887   if(!fallback.isEmpty() && QIcon::hasThemeIcon(fallback)){ tmp = QIcon::fromTheme(fallback); return tmp; } //found this in the theme
888 
889 
890   //NOTE: This was re-written on 11/10/15 to avoid using the QIcon::fromTheme() framework
891   //   -- Too many issues with SVG files and/or search paths with the built-in system
892 
893   //Check if the icon is an absolute path and exists
894   bool DEBUG =false;
895   if(DEBUG){ qDebug() << "[LXDG] Find icon for:" << iconName; }
896   if(QFile::exists(iconName) && iconName.startsWith("/")){ return QIcon(iconName); }
897   else if(iconName.startsWith("/")){ iconName.section("/",-1); } //Invalid absolute path, just look for the icon
898   //Check if the icon is actually given
899   if(iconName.isEmpty()){
900     if(fallback.isEmpty()){  return QIcon(); }
901     else{ return LXDG::findIcon(fallback, ""); }
902   }
903   //Now try to find the icon from the theme
904   if(DEBUG){ qDebug() << "[LXDG] Start search for icon"; }
905   //Get the currently-set theme
906   QString cTheme = QIcon::themeName();
907   if(cTheme.isEmpty()){
908     QIcon::setThemeName("material-design-light");
909     cTheme = "material-design-light";
910   }
911   //Make sure the current search paths correspond to this theme
912   if( QDir::searchPaths("icontheme").filter("/"+cTheme+"/").isEmpty() ){
913     //Need to reset search paths: setup the "icontheme" "material-design-light" and "fallback" sets
914     // - Get all the base icon directories
915     QStringList paths;
916       paths << QDir::homePath()+"/.icons/"; //ordered by priority - local user dirs first
917       QStringList xdd = QString(getenv("XDG_DATA_HOME")).split(":");
918         xdd << QString(getenv("XDG_DATA_DIRS")).split(":");
919         for(int i=0; i<xdd.length(); i++){
920           if(QFile::exists(xdd[i]+"/icons")){ paths << xdd[i]+"/icons/"; }
921         }
922     //Now load all the dirs into the search paths
923     QStringList theme, oxy, fall;
924     QStringList themedeps = getIconThemeDepChain(cTheme, paths);
925     for(int i=0; i<paths.length(); i++){
926       theme << getChildIconDirs( paths[i]+cTheme);
927       for(int j=0; j<themedeps.length(); j++){ theme << getChildIconDirs(paths[i]+themedeps[j]); }
928       oxy << getChildIconDirs(paths[i]+"material-design-light"); //Lumina base icon set
929       fall << getChildIconDirs(paths[i]+"hicolor"); //XDG fallback (apps add to this)
930     }
931     //Now load all the icon theme dependencies in order (Theme1 -> Theme2 -> Theme3 -> Fallback)
932 
933     //fall << LOS::AppPrefix()+"share/pixmaps"; //always use this as well as a final fallback
934     QDir::setSearchPaths("icontheme", theme);
935     QDir::setSearchPaths("default", oxy);
936     QDir::setSearchPaths("fallback", fall);
937     //qDebug() << "Setting Icon Search Paths:" << "\nicontheme:" << theme << "\nmaterial-design-light:" << oxy << "\nfallback:" << fall;
938   }
939   //Find the icon in the search paths
940   QIcon ico;
941   QStringList srch; srch << "icontheme" << "default" << "fallback";
942   for(int i=0; i<srch.length() && ico.isNull(); i++){
943     //Look for a svg first
944     if(QFile::exists(srch[i]+":"+iconName+".svg") && !iconName.contains("libreoffice") ){
945         //Be careful about how an SVG is loaded - needs to render the image onto a paint device
946         /*QSvgRenderer svg;
947         if( svg.load(srch[i]+":"+iconName+".svg") ){
948 	  //Could be loaded - now check that it is version 1.1+ (Qt has issues with 1.0? (LibreOffice Icons) )
949 	  float version = 1.1; //only downgrade files that explicitly set the version as older
950 	  QString svginfo = LUtils::readFile(srch[i]+":"+iconName+".svg").join("\n").section("<svg",1,1).section(">",0,0);
951 	  svginfo.replace("\t"," "); svginfo.replace("\n"," ");
952 	  if(svginfo.contains(" version=")){ version = svginfo.section(" version=\"",1,1).section("\"",0,0).toFloat(); }
953 	  if(version>=1.1){*/
954             ico.addFile(srch[i]+":"+iconName+".svg"); //could be loaded/parsed successfully
955 	  /*}else{
956 	    //qDebug() << "Old SVG Version file:" << iconName+".svg  Theme:" << srch[i];
957 	    //qDebug() << "SVGInfo:" << svginfo;
958 	  }
959         }else{
960           qDebug() << "Found bad SVG file:" << iconName+".svg  Theme:" << srch[i];
961         }*/
962     }
963     if(QFile::exists(srch[i]+":"+iconName+".png")){
964       //simple PNG image - load directly into the QIcon structure
965       ico.addFile(srch[i]+":"+iconName+".png");
966     }
967 
968   }
969   //If still no icon found, look for any image format in the "pixmaps" directory
970   if(ico.isNull()){
971     //qDebug() << "Null Icon:" << iconName << LOS::AppPrefix();
972     if(QFile::exists(LOS::AppPrefix()+"share/pixmaps/"+iconName)){
973       ico.addFile(LOS::AppPrefix()+"share/pixmaps/"+iconName);
974     }else{
975       //Need to scan for any close match in the directory
976       QDir pix(LOS::AppPrefix()+"share/pixmaps");
977       QStringList formats = LUtils::imageExtensions();
978       QStringList found = pix.entryList(QStringList() << iconName, QDir::Files, QDir::Unsorted);
979       if(found.isEmpty()){ found = pix.entryList(QStringList() << iconName+"*", QDir::Files, QDir::Unsorted); }
980       //qDebug() << "Found pixmaps:" << found << formats;
981       //Use the first one found that is a valid format
982       for(int i=0; i<found.length(); i++){
983         if( formats.contains(found[i].section(".",-1).toLower()) ){
984          //qDebug() << " - Found non-theme icon:" << found[i];
985 	  ico.addFile( pix.absoluteFilePath(found[i]) );
986 	  break;
987 	}
988       }
989 
990     }
991   }
992   //Use the fallback icon if necessary
993   if(ico.isNull() ){
994     if(!fallback.isEmpty()){ ico = LXDG::findIcon(fallback,""); }
995     else if(iconName.contains("-x-") && !iconName.endsWith("-x-generic")){
996       //mimetype - try to use the generic type icon
997       ico = LXDG::findIcon(iconName.section("-x-",0,0)+"-x-generic", "");
998     }else if(iconName.contains("-")){
999       ico = LXDG::findIcon(iconName.section("-",0,-2), ""); //chop the last modifier off the end and try again
1000     }
1001   }
1002   if(ico.isNull()){
1003     qDebug() << "Could not find icon:" << iconName << fallback;
1004   }
1005   //Return the icon
1006   return ico;
1007 }
1008 
getChildIconDirs(QString parent)1009 QStringList LXDG::getChildIconDirs(QString parent){
1010   //This is a recursive function that returns the absolute path(s) of directories with *.png files
1011   QDir D(parent);
1012   QStringList out;
1013   QStringList dirs = D.entryList(QDir::Dirs | QDir::NoDotAndDotDot, QDir::Name);
1014   if(!dirs.isEmpty() && (dirs.contains("32x32") || dirs.contains("scalable")) ){
1015     //Need to sort these directories by image size
1016     //qDebug() << " - Parent:" << parent << "Dirs:" << dirs;
1017     for(int i=0; i<dirs.length(); i++){
1018       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())+"::::"); }
1019       else if(dirs[i].at(0).isNumber()){dirs[i].prepend( QString::number(10-dirs[i].length())+QString::number(10-dirs[i].at(0).digitValue())+"::::"); }
1020       else{ dirs[i].prepend( "0::::"); }
1021     }
1022     dirs.sort();
1023     for(int i=0; i<dirs.length(); i++){ dirs[i] = dirs[i].section("::::",1,50); } //chop the sorter off the front again
1024     //qDebug() << "Sorted:" << dirs;
1025   }
1026   QStringList img = D.entryList(QStringList() << "*.png" << "*.svg", QDir::Files | QDir::NoDotAndDotDot, QDir::NoSort);
1027   if(img.length() > 0){ out << D.absolutePath(); }
1028   for(int i=0; i<dirs.length(); i++){
1029     img.clear();
1030     img = getChildIconDirs(D.absoluteFilePath(dirs[i])); //re-use the old list variable
1031     if(img.length() > 0){ out << img; }
1032   }
1033   return out;
1034 }
1035 
getIconThemeDepChain(QString theme,QStringList paths)1036 QStringList LXDG::getIconThemeDepChain(QString theme, QStringList paths){
1037   QStringList results;
1038   for(int i=0; i<paths.length(); i++){
1039     if( QFile::exists(paths[i]+theme+"/index.theme") ){
1040       QStringList deps = LUtils::readFile(paths[i]+theme+"/index.theme").filter("Inherits=");
1041       if(!deps.isEmpty()){
1042         deps = deps.first().section("=",1,-1).split(";",QString::SkipEmptyParts);
1043         for(int j=0; j<deps.length(); j++){
1044           results << deps[j] << getIconThemeDepChain(deps[j],paths);
1045         }
1046       }
1047       break; //found primary theme index file - stop here
1048     }
1049   }
1050   return results;;
1051 }
1052 
systemMimeDirs()1053 QStringList LXDG::systemMimeDirs(){
1054   //Returns a list of all the directories where *.xml MIME files can be found
1055   QStringList appDirs = QString(getenv("XDG_DATA_HOME")).split(":");
1056   appDirs << QString(getenv("XDG_DATA_DIRS")).split(":");
1057   if(appDirs.isEmpty()){ appDirs << "/usr/local/share" << "/usr/share"; }
1058   //Now create a valid list
1059   QStringList out;
1060   for(int i=0; i<appDirs.length(); i++){
1061     if( QFile::exists(appDirs[i]+"/mime") ){
1062       out << appDirs[i]+"/mime";
1063     }
1064   }
1065   return out;
1066 }
1067 
findMimeIcon(QString extension)1068 QIcon LXDG::findMimeIcon(QString extension){
1069   QIcon ico;
1070   QString mime = LXDG::findAppMimeForFile(extension);
1071   if(mime.isEmpty()){ mime = LXDG::findAppMimeForFile(extension.toLower()); }
1072   mime.replace("/","-"); //translate to icon mime name
1073   if(!mime.isEmpty()){ ico = LXDG::findIcon(mime, "unknown");} //use the "unknown" mimetype icon as fallback
1074   if(ico.isNull()){ ico = LXDG::findIcon("unknown",""); } //just in case
1075   return ico;
1076 }
1077 
findAppMimeForFile(QString filename,bool multiple)1078 QString LXDG::findAppMimeForFile(QString filename, bool multiple){
1079   QString out;
1080   QString extension = filename.section(".",1,-1);
1081   if("."+extension == filename){ extension.clear(); } //hidden file without extension
1082   //qDebug() << "MIME SEARCH:" << filename << extension;
1083   QStringList mimefull = LXDG::loadMimeFileGlobs2();
1084   QStringList mimes;
1085   //Just in case the filename is a mimetype itself
1086   if( mimefull.filter(":"+filename+":").length() == 1){
1087     return filename;
1088   }
1089 while(mimes.isEmpty()){
1090   //Check for an exact mimetype match
1091   if(mimefull.filter(":"+extension+":").length() == 1){
1092     return extension;
1093   }
1094   //Look for globs at the end of the filename
1095   if(!extension.isEmpty()){
1096     mimes = mimefull.filter(":*."+extension);
1097     //If nothing found, try a case-insensitive search
1098     if(mimes.isEmpty()){ mimes = mimefull.filter(":*."+extension, Qt::CaseInsensitive); }
1099     //Now ensure that the filter was accurate (*.<extention>.<something> will still be caught)
1100     for(int i=0; i<mimes.length(); i++){
1101       if(!filename.endsWith( mimes[i].section(":*",-1), Qt::CaseInsensitive )){ mimes.removeAt(i); i--; }
1102       else if(mimes[i].section(":",0,0).length()==2){ mimes[i].prepend("0"); } //ensure 3-character priority number
1103       else if(mimes[i].section(":",0,0).length()==1){ mimes[i].prepend("00"); } //ensure 3-character priority number
1104     }
1105   }
1106   //Look for globs at the start of the filename
1107   if(mimes.isEmpty()){
1108     mimes = mimefull.filter(":"+filename.left(2)); //look for the first 2 characters initially
1109 	//Note: This initial filter will only work if the wildcard (*) is not within the first 2 characters of the pattern
1110     //Now ensure that the filter was accurate
1111     for(int i=0; i<mimes.length(); i++){
1112       if(!filename.startsWith( mimes[i].section(":",3,-1,QString::SectionSkipEmpty).section("*",0,0), Qt::CaseInsensitive )){ mimes.removeAt(i); i--; }
1113     }
1114   }
1115     if(mimes.isEmpty()){
1116       if(extension.contains(".")){ extension = extension.section(".",1,-1); }
1117       else{ break; }
1118     }
1119   } //end of mimes while loop
1120   mimes.sort(); //this automatically puts them in reverse weight order (100 on down)
1121   QStringList matches;
1122   //qDebug() << "Mimes:" << mimes;
1123   for(int m=mimes.length()-1; m>=0; m--){
1124     QString mime = mimes[m].section(":",1,1,QString::SectionSkipEmpty);
1125     matches << mime;
1126   }
1127   //qDebug() << "Matches:" << matches;
1128   if(multiple && !matches.isEmpty() ){ out = matches.join("::::"); }
1129   else if( !matches.isEmpty() ){ out = matches.first(); }
1130   else{ //no mimetype found - assign one (internal only - no system database changes)
1131     if(extension.isEmpty()){ out = "unknown/"+filename.toLower(); }
1132     else{ out = "unknown/"+extension.toLower(); }
1133   }
1134   //qDebug() << "Out:" << out;
1135   return out;
1136 }
1137 
findFilesForMime(QString mime)1138 QStringList LXDG::findFilesForMime(QString mime){
1139   QStringList out;
1140   QStringList mimes = LXDG::loadMimeFileGlobs2().filter(mime);
1141   for(int i=0; i<mimes.length(); i++){
1142     out << mimes[i].section(":",2,2); // "*.<extension>"
1143   }
1144   //qDebug() << "Mime to Files:" << mime << out;
1145   return out;
1146 }
1147 
listFileMimeDefaults()1148 QStringList LXDG::listFileMimeDefaults(){
1149   //This will spit out a itemized list of all the mimetypes and relevant info
1150   // Output format: <mimetype>::::<extension>::::<default>::::<localized comment>
1151   QStringList mimes = LXDG::loadMimeFileGlobs2();
1152   //Remove all the application files from the list (only a single app defines/uses this type in general)
1153   /*QStringList apps = mimes.filter(":application/");
1154   //qDebug() << "List Mime Defaults";
1155   for(int i=0; i<apps.length(); i++){ mimes.removeAll(apps[i]); }*/
1156   //Now start filling the output list
1157   QStringList out;
1158   for(int i=0; i<mimes.length(); i++){
1159     QString mimetype = mimes[i].section(":",1,1);
1160     QStringList tmp = mimes.filter(mimetype);
1161     //Collect all the different extensions with this mimetype
1162     QStringList extlist;
1163     for(int j=0; j<tmp.length(); j++){
1164       mimes.removeAll(tmp[j]);
1165       extlist << tmp[j].section(":",2,2);
1166     }
1167     extlist.removeDuplicates(); //just in case
1168     //Now look for a current default for this mimetype
1169     QString dapp = LXDG::findDefaultAppForMime(mimetype); //default app;
1170 
1171     //Create the output entry
1172     //qDebug() << "Mime entry:" << i << mimetype << dapp;
1173     out << mimetype+"::::"+extlist.join(", ")+"::::"+dapp+"::::"+LXDG::findMimeComment(mimetype);
1174 
1175     i--; //go back one (continue until the list is empty)
1176   }
1177   return out;
1178 }
1179 
findMimeComment(QString mime)1180 QString LXDG::findMimeComment(QString mime){
1181   QString comment;
1182   QStringList dirs = LXDG::systemMimeDirs();
1183   QString lang = QString(getenv("LANG")).section(".",0,0);
1184   QString shortlang = lang.section("_",0,0);
1185   for(int i=0; i<dirs.length(); i++){
1186     if(QFile::exists(dirs[i]+"/"+mime+".xml")){
1187       QStringList info = LUtils::readFile(dirs[i]+"/"+mime+".xml");
1188       QStringList filter = info.filter("<comment xml:lang=\""+lang+"\">");
1189       //First look for a full language match, then short language, then general comment
1190       if(filter.isEmpty()){ filter = info.filter("<comment xml:lang=\""+shortlang+"\">"); }
1191       if(filter.isEmpty()){ filter = info.filter("<comment>"); }
1192       if(!filter.isEmpty()){
1193         comment = filter.first().section(">",1,1).section("</",0,0);
1194         break;
1195       }
1196     }
1197   }
1198   return comment;
1199 }
1200 
findDefaultAppForMime(QString mime)1201 QString LXDG::findDefaultAppForMime(QString mime){
1202   //First get the priority-ordered list of default file locations
1203   QStringList dirs;
1204   dirs << QString(getenv("XDG_CONFIG_HOME"))+"/lumina-mimeapps.list" \
1205 	 << QString(getenv("XDG_CONFIG_HOME"))+"/mimeapps.list";
1206   QStringList tmp = QString(getenv("XDG_CONFIG_DIRS")).split(":");
1207 	for(int i=0; i<tmp.length(); i++){ dirs << tmp[i]+"/lumina-mimeapps.list"; }
1208 	for(int i=0; i<tmp.length(); i++){ dirs << tmp[i]+"/mimeapps.list"; }
1209   dirs << QString(getenv("XDG_DATA_HOME"))+"/applications/lumina-mimeapps.list" \
1210 	 << QString(getenv("XDG_DATA_HOME"))+"/applications/mimeapps.list";
1211   tmp = QString(getenv("XDG_DATA_DIRS")).split(":");
1212 	for(int i=0; i<tmp.length(); i++){ dirs << tmp[i]+"/applications/lumina-mimeapps.list"; }
1213 	for(int i=0; i<tmp.length(); i++){ dirs << tmp[i]+"/applications/mimeapps.list"; }
1214 
1215   //Now go through all the files in order of priority until a default is found
1216   QString cdefault;
1217   for(int i=0; i<dirs.length() && cdefault.isEmpty(); i++){
1218     if(!QFile::exists(dirs[i])){ continue; }
1219     QStringList info = LUtils::readFile(dirs[i]);
1220     if(info.isEmpty()){ continue; }
1221     QStringList white; //lists to keep track of during the search (black unused at the moment)
1222     QString workdir = dirs[i].section("/",0,-2); //just the directory
1223    // qDebug() << "Check File:" << mime << dirs[i] << workdir;
1224     int def = info.indexOf("[Default Applications]"); //find this line to start on
1225     if(def>=0){
1226       for(int d=def+1; d<info.length(); d++){
1227         //qDebug() << "Check Line:" << info[d];
1228         if(info[d].startsWith("[")){ break; } //starting a new section now - finished with defaults
1229 	if(info[d].contains(mime+"=") ){
1230 	  white = info[d].section("=",1,-1).split(";") + white; //exact mime match - put at front of list
1231           break; //already found exact match
1232 	}else if(info[d].contains("*") && info[d].contains("=") ){
1233           QRegExp rg(info[d].section("=",0,0), Qt::CaseSensitive, QRegExp::WildcardUnix);
1234           if(rg.exactMatch(mime)){
1235 	    white << info[d].section("=",1,-1).split(";"); //partial mime match - put at end of list
1236           }
1237         }
1238       }
1239     }
1240     // Now check for any white-listed files in this work dir
1241     // find the full path to the file (should run even if nothing in this file)
1242     //qDebug() << "WhiteList:" << white;
1243     for(int w=0; w<white.length(); w++){
1244       if(white[w].isEmpty()){ continue; }
1245       //First check for absolute paths to *.desktop file
1246       if( white[w].startsWith("/") ){
1247 	 if( QFile::exists(white[w]) ){ cdefault=white[w]; break; }
1248 	 else{ white.removeAt(w); w--; } //invalid file path - remove it from the list
1249       }
1250       //Now check for relative paths to  file (in current priority-ordered work dir)
1251       else if( QFile::exists(workdir+"/"+white[w]) ){ cdefault=workdir+"/"+white[w]; break; }
1252       //Now go through the XDG DATA dirs and see if the file is in there
1253       else{
1254         white[w] = LUtils::AppToAbsolute(white[w]);
1255         if(QFile::exists(white[w])){ cdefault = white[w]; }
1256       }
1257     }
1258     /* WRITTEN BUT UNUSED CODE FOR MIMETYPE ASSOCIATIONS
1259     //Skip using this because it is simply an alternate/unsupported standard that conflicts with
1260       the current mimetype database standards. It is better/faster to parse 1 or 2 database glob files
1261       rather than have to iterate through hundreds of *.desktop files *every* time you need to
1262       find an application
1263 
1264     if(addI<0 && remI<0){
1265       //Simple Format: <mimetype>=<*.desktop file>;<*.desktop file>;.....
1266         // (usually only one desktop file listed)
1267       info = info.filter(mimetype+"=");
1268       //Load the listed default(s)
1269       for(int w=0; w<info.length(); w++){
1270         white << info[w].section("=",1,50).split(";");
1271       }
1272     }else{
1273       //Non-desktop specific mimetypes file: has a *very* convoluted/inefficient algorithm (required by spec)
1274       if(addI<0){ addI = info.length(); } //no add section
1275       if(remI<0){ remI = info.length(); } // no remove section:
1276       //Whitelist items
1277       for(int a=addI+1; a!=remI && a<info.length(); a++){
1278         if(info[a].contains(mimetype+"=")){
1279 	  QStringList tmp = info[a].section("=",1,50).split(";");
1280 	  for(int t=0; t<tmp.length(); t++){
1281 	    if(!black.contains(tmp[t])){ white << tmp[t]; } //make sure this item is not on the black list
1282 	  }
1283 	  break;
1284 	}
1285       }
1286       //Blacklist items
1287       for(int a=remI+1; a!=addI && a<info.length(); a++){
1288         if(info[a].contains(mimetype+"=")){ black << info[a].section("=",1,50).split(";"); break;}
1289       }
1290 
1291       //STEPS 3/4 not written yet
1292 
1293     } //End of non-DE mimetypes file */
1294 
1295   } //End loop over files
1296 
1297   return cdefault;
1298 }
1299 
findAvailableAppsForMime(QString mime)1300 QStringList LXDG::findAvailableAppsForMime(QString mime){
1301   QStringList dirs = LXDG::systemApplicationDirs();
1302   QStringList out;
1303   //Loop over all possible directories that contain *.destop files
1304   //  and check for the mimeinfo.cache file
1305   for(int i=0; i<dirs.length(); i++){
1306     if(QFile::exists(dirs[i]+"/mimeinfo.cache")){
1307       QStringList matches = LUtils::readFile(dirs[i]+"/mimeinfo.cache").filter(mime+"=");
1308       //Find any matches for our mimetype in the cache
1309       for(int j=0; j<matches.length(); j++){
1310         QStringList files = matches[j].section("=",1,1).split(";",QString::SkipEmptyParts);
1311 	//Verify that each file exists before putting the full path to the file in the output
1312 	for(int m=0; m<files.length(); m++){
1313 	  if(QFile::exists(dirs[i]+"/"+files[m])){
1314 	    out << dirs[i]+"/"+files[m];
1315 	  }else if(files[m].contains("-")){ //kde4-<filename> -> kde4/<filename> (stupid KDE variations!!)
1316 	    files[m].replace("-","/");
1317 	    if(QFile::exists(dirs[i]+"/"+files[m])){
1318 	      out << dirs[i]+"/"+files[m];
1319 	    }
1320 	  }
1321 	}
1322       }
1323     }
1324   }
1325   //qDebug() << "Found Apps for Mime:" << mime << out << dirs;
1326   return out;
1327 }
1328 
setDefaultAppForMime(QString mime,QString app)1329 void LXDG::setDefaultAppForMime(QString mime, QString app){
1330   //qDebug() << "Set Default App For Mime:" << mime << app;
1331   QString filepath = QString(getenv("XDG_CONFIG_HOME"))+"/lumina-mimeapps.list";
1332   QStringList cinfo = LUtils::readFile(filepath);
1333   //If this is a new file, make sure to add the header appropriately
1334   if(cinfo.isEmpty()){ cinfo << "#Automatically generated with lumina-config" << "# DO NOT CHANGE MANUALLY" << "[Default Applications]"; }
1335   //Check for any current entry for this mime type
1336   QStringList tmp = cinfo.filter(mime+"=");
1337   int index = -1;
1338   if(!tmp.isEmpty()){ index = cinfo.indexOf(tmp.first()); }
1339   //Now add the new default entry (if necessary)
1340   if(app.isEmpty()){
1341     if(index>=0){ cinfo.removeAt(index); } //Remove entry
1342   }else{
1343     if(index<0){
1344       cinfo << mime+"="+app+";"; //new entry
1345     }else{
1346       cinfo[index] = mime+"="+app+";"; //overwrite existing entry
1347     }
1348   }
1349   LUtils::writeFile(filepath, cinfo, true);
1350   return;
1351 }
1352 
findAVFileExtensions()1353 QStringList LXDG::findAVFileExtensions(){
1354   //output format: QDir name filter for valid A/V file extensions
1355   QStringList globs = LXDG::loadMimeFileGlobs2();
1356   QStringList av = globs.filter(":audio/");
1357   av << globs.filter(":video/");
1358   for(int i=0; i<av.length(); i++){
1359     //Just use all audio/video mimetypes (for now)
1360     av[i] = av[i].section(":",2,2);
1361     //Qt5 Auto detection (broken - QMediaPlayer seg faults with Qt 5.3 - 11/24/14)
1362     /*if( QMultimedia::NotSupported != QMediaPlayer::hasSupport(av[i].section(":",1,1)) ){ av[i] = av[i].section(":",2,2); }
1363     else{ av.removeAt(i); i--; }*/
1364   }
1365   av.removeDuplicates();
1366   return av;
1367 }
1368 
loadMimeFileGlobs2()1369 QStringList LXDG::loadMimeFileGlobs2(){
1370   //output format: <weight>:<mime type>:<file extension (*.something)>
1371   if(mimeglobs.isEmpty() || (mimechecktime < (QDateTime::currentMSecsSinceEpoch()-30000)) ){
1372     //qDebug() << "Loading globs2 mime DB files";
1373     mimeglobs.clear();
1374     mimechecktime = QDateTime::currentMSecsSinceEpoch(); //save the current time this was last checked
1375     QStringList dirs = LXDG::systemMimeDirs();
1376     for(int i=0; i<dirs.length(); i++){
1377       if(QFile::exists(dirs[i]+"/globs2")){
1378         QFile file(dirs[i]+"/globs2");
1379         if(!file.exists()){ continue; }
1380         if(!file.open(QIODevice::ReadOnly | QIODevice::Text)){ continue; }
1381         QTextStream in(&file);
1382         while(!in.atEnd()){
1383           QString line = in.readLine();
1384           if(!line.startsWith("#")){
1385             mimeglobs << line.simplified();
1386           }
1387         }
1388 	file.close();
1389       }
1390       if(i==dirs.length()-1 && mimeglobs.isEmpty()){
1391         //Could not find the mimetype database on the system - use the fallback file distributed with Lumina
1392         dirs << LOS::LuminaShare();
1393       }
1394     }//end loop over dirs
1395   }
1396   return mimeglobs;
1397 }
1398 
1399 //Find all the autostart *.desktop files
findAutoStartFiles(bool includeInvalid)1400 QList<XDGDesktop*> LXDG::findAutoStartFiles(bool includeInvalid){
1401 
1402   //First get the list of directories to search (system first, user-provided files come later and overwrite sys files as needed)
1403   QStringList paths = QString(getenv("XDG_CONFIG_DIRS")).split(":");
1404   paths << QString(getenv("XDG_CONFIG_HOME")).split(":");
1405   //Now go through them and find any valid *.desktop files
1406   QList<XDGDesktop*> files;
1407   QStringList filenames; //make it easy to see if this filename is an override
1408   QDir dir;
1409   for(int i=0;i<paths.length(); i++){
1410     if(!QFile::exists(paths[i]+"/autostart")){ continue; }
1411     dir.cd(paths[i]+"/autostart");
1412     QStringList tmp = dir.entryList(QStringList() << "*.desktop", QDir::Files, QDir::Name);
1413     for(int t=0; t<tmp.length(); t++){
1414       XDGDesktop *desk = new XDGDesktop(dir.absoluteFilePath(tmp[t]));
1415       if(desk->type == XDGDesktop::BAD){ continue; } //could not read file
1416       //Now figure out what to do with it
1417       if(filenames.contains(tmp[t])){
1418 	//This is an overwrite of a lower-priority (system?) autostart file
1419 	// find the other file
1420 	int old = -1;
1421 	for(int o=0; o<files.length(); o++){
1422 	  if(files[o]->filePath.endsWith("/"+tmp[t])){ old = o; break; } //found it
1423 	}
1424 	if(desk->isValid(false)){
1425 	  //Full override of the lower-priority file (might be replacing exec/tryexec fields)
1426           files.takeAt(old)->deleteLater();
1427 	  files.insert(old,desk);
1428 	}else{
1429 	  //Small override file (only the "Hidden" field listed in spec)
1430 	  files[old]->isHidden = desk->isHidden; //replace this value with the override
1431 	  //files << desk; //still add this to the array (will be ignored/skipped later)
1432 	}
1433       }else{
1434         //This is a new autostart file
1435 	files << desk;
1436 	filenames << tmp[t];
1437       }
1438     }//end of loop over *.desktop files
1439   } //end of loop over directories
1440 
1441   //Now filter the results by validity if desired
1442   if(!includeInvalid){
1443     for(int i=0; i<files.length(); i++){
1444       if( !files[i]->isValid(false) || files[i]->isHidden ){
1445         //Invalid file - go ahead and remove it from the output list
1446 	files.takeAt(i)->deleteLater();
1447 	i--;
1448       }
1449     }
1450   }
1451   return files;
1452 }
1453 
1454 /*bool LXDG::setAutoStarted(bool autostart, XDGDesktop *app){
1455   //First get the list of system directories to search (system first, user-provided files come later and overwrite sys files as needed)
1456   QStringList paths = QString(getenv("XDG_CONFIG_DIRS")).split(":");
1457   QString upath = QString(getenv("XDG_CONFIG_HOME")).section(":",0,0);
1458   if(upath.isEmpty()){ upath = QDir::homePath()+"/.config/autostart/"; }
1459   else{ upath.append("/autostart/"); }
1460   //Verify that the autostart directory exists
1461   if(!QFile::exists(upath)){
1462     QDir dir;
1463     dir.mkpath(upath);
1464   }
1465 
1466   //Quick check/finish for user-defined files which are getting disabled (just remove the file)
1467   if(app->filePath.startsWith(upath) && !autostart){
1468     return QFile::remove(app->filePath);
1469   }
1470   bool sysfile = false;
1471   for(int i=0; i<paths.length(); i++){
1472     if(app->filePath.startsWith(paths[i]+"/autostart/") ){
1473       sysfile = true;
1474       //Change it to the user-modifiable directory
1475       app.filePath = app.filePath.replace(paths[i]+"/autostart/", upath);
1476     }
1477   }
1478   //Make sure the user-autostart dir is specified, and clean the app structure as necessary
1479   if( !app.filePath.startsWith(upath) && autostart){
1480     //Some other non-override autostart file - set it up to open with lumina-open
1481     if(!app.filePath.endsWith(".desktop")){
1482       app.exec = "lumina-open \""+app.filePath+"\"";
1483       app.tryexec = app.filePath; //make sure this file exists
1484       if(app.name.isEmpty()){ app.name = app.filePath.section("/",-1); }
1485       if(app.icon.isEmpty()){ app.icon = LXDG::findAppMimeForFile(app.filePath); app.icon.replace("/","-"); }
1486       app.filePath = upath+app.filePath.section("/",-1)+".desktop";
1487       app.type = XDGDesktop::APP;
1488     }else{
1489       //Some other *.desktop file on the system (keep almost all the existing settings/values)
1490       // - setup a redirect to the other file
1491       app.exec = "lumina-open \""+app.filePath+"\"";
1492       app.tryexec = app.filePath; //make sure this file exists
1493       // - Adjust the actual path where this file will get saved
1494       app.filePath = upath+app.filePath.section("/",-1);
1495     }
1496   }
1497   //Now save the "hidden" value into the file
1498   app.isHidden = !autostart; //if hidden, it will not be autostarted
1499   //Now save the file as necessary
1500   bool saved = false;
1501   //qDebug() << " - Saving AutoStart File:" << app.filePath << app.name << app.isHidden;
1502   if(sysfile){
1503     //Just an override file for the "hidden" field - nothing more
1504     QStringList info;
1505       info << "[Desktop Entry]" << "Type=Application" << QString("Hidden=")+ (app.isHidden ? QString("true"): QString("false"));
1506     saved = LUtils::writeFile(app.filePath, info, true);
1507   }else{
1508     //Need to actually save the full file
1509     saved = LXDG::saveDesktopFile(app);
1510   }
1511   return saved;
1512 }*/
1513 
setAutoStarted(bool autostart,QString filePath)1514 bool LXDG::setAutoStarted(bool autostart, QString filePath){
1515   //Convenience function for the auto-start setter
1516   XDGDesktop desk(filePath);
1517   if(!filePath.endsWith(".desktop")){
1518     //bool ok = false;
1519     //desk = LXDG::loadDesktopFile(filePath, ok);
1520     //if(!ok){ return false; } //error reading input file
1521   //}else{
1522     desk.filePath = filePath;
1523     desk.useTerminal = false;
1524   }
1525   return desk.setAutoStarted(autostart);
1526 }
1527