1 #include "AppLauncherPlugin.h"
2 #include "../../LSession.h"
3 #include "OutlineToolButton.h"
4 #include <QClipboard>
5 
6 #include <LIconCache.h>
7 
8 #define OUTMARGIN 10 //special margin for fonts due to the outlining effect from the OutlineToolbutton
9 extern LIconCache *ICONS;
10 
AppLauncherPlugin(QWidget * parent,QString ID)11 AppLauncherPlugin::AppLauncherPlugin(QWidget* parent, QString ID) : LDPlugin(parent, ID){
12   connect(ICONS, SIGNAL(IconAvailable(QString)), this, SLOT(iconLoaded(QString)) );
13   QVBoxLayout *lay = new QVBoxLayout();
14   inputDLG = 0;
15   this->setLayout(lay);
16     lay->setContentsMargins(0,0,0,0);
17   button = new OutlineToolButton(this);
18     button->setToolButtonStyle(Qt::ToolButtonTextUnderIcon);
19     button->setAutoRaise(true);
20     button->setText("...\n..."); //Need to set something here so that initial sizing works properly
21     button->setSizePolicy(QSizePolicy::Expanding,QSizePolicy::Expanding);
22   lay->addWidget(button, 0, Qt::AlignCenter);
23 	connect(button, SIGNAL(DoubleClicked()), this, SLOT(buttonClicked()) );
24   button->setContextMenuPolicy(Qt::NoContextMenu);
25   watcher = new QFileSystemWatcher(this);
26 	connect(watcher, SIGNAL(fileChanged(QString)), this, SLOT( loadButton()) );
27 
28   connect(this, SIGNAL(PluginActivated()), this, SLOT(buttonClicked()) ); //in case they use the context menu to launch it.
29   this->setContextMenu( new QMenu(this) );
30   connect(this->contextMenu(), SIGNAL(triggered(QAction*)), this, SLOT(actionTriggered(QAction*)) );
31 
32   loadButton();
33   //QTimer::singleShot(0,this, SLOT(loadButton()) );
34 }
35 
Cleanup()36 void AppLauncherPlugin::Cleanup(){
37   //This is run only when the plugin was forcibly closed/removed
38 
39 }
40 
loadButton()41 void AppLauncherPlugin::loadButton(){
42   QString def = this->ID().section("::",1,50).section("---",0,0).simplified();
43   QString path = this->readSetting("applicationpath",def).toString(); //use the default if necessary
44   QFileInfo info(path);
45   this->contextMenu()->clear();
46   //qDebug() << "Default Application Launcher:" << def << path;
47   bool ok = info.canonicalPath().startsWith("/net/");
48   if(!ok){ ok = QFile::exists(path); } //do it this way to ensure the file existance check never runs for /net/ files
49   if(!ok){ emit RemovePlugin(this->ID()); return;}
50   this->setAcceptDrops( info.isDir() );
51   icosize = this->height()-4 - 2.2*button->fontMetrics().height();
52   button->setFixedSize( this->width()-4, this->height()-4);
53   button->setIconSize( QSize(icosize,icosize) );
54   button->setToolTip("");
55   QString txt;
56   iconID.clear();
57   if(path.endsWith(".desktop") && ok){
58     XDGDesktop file(path);
59     ok = !file.name.isEmpty();
60     if(!ok){
61       button->setWhatsThis("");
62       iconID = "quickopen-file";
63       //button->setIcon( QIcon(LXDG::findIcon("quickopen-file","").pixmap(QSize(icosize,icosize)).scaledToHeight(icosize, Qt::SmoothTransformation) ) );
64       txt = tr("Click to Set");
65       if(!watcher->files().isEmpty()){ watcher->removePaths(watcher->files()); }
66     }else{
67       button->setWhatsThis(file.filePath);
68       if(ICONS->exists(file.icon)){ iconID = file.icon; }
69       else if(ICONS->exists(file.icon.section("-",0,-2)) ){ iconID = file.icon.section("-",0,-2); } //some icons get very specific with "-" delimiters, look for a more generic icon if possible
70       else{ iconID = "system-run"; }
71       //button->setIcon( QIcon(LXDG::findIcon(file.icon,"system-run").pixmap(QSize(icosize,icosize)).scaledToHeight(icosize, Qt::SmoothTransformation) ) );
72       if(!file.comment.isEmpty()){button->setToolTip(file.comment); }
73       txt = file.name;
74       //Put the simple Open action first (no open-with for .desktop files)
75       QAction *tmp = this->contextMenu()->addAction( QString(tr("Launch %1")).arg(file.name), this, SLOT(buttonClicked()) );
76       ICONS->loadIcon(tmp, file.icon);
77       //See if there are any "actions" listed for this file, and put them in the context menu as needed.
78       if(!file.actions.isEmpty()){
79         for(int i=0; i<file.actions.length(); i++){
80           tmp = this->contextMenu()->addAction( file.actions[i].name );
81             if(ICONS->exists(file.actions[i].icon)){ ICONS->loadIcon(tmp, file.actions[i].icon); }
82             else{ ICONS->loadIcon(tmp, "quickopen-file"); }
83             //tmp->setIcon( LXDG::findIcon(file.actions[i].icon,"quickopen-file") );
84             tmp->setWhatsThis( file.actions[i].ID );
85         }
86       }
87       if(!watcher->files().isEmpty()){ watcher->removePaths(watcher->files()); }
88       watcher->addPath(file.filePath); //make sure to update this shortcut if the file changes
89     }
90   }else if(ok){
91     button->setWhatsThis(info.absoluteFilePath());
92     QString iconame;
93     if(info.isDir()){
94     if(path.startsWith("/media/") || path.startsWith("/run/media/")){
95            iconame = "drive-removable-media";
96           //Could add device ID parsing here to determine what "type" of device it is - will be OS-specific though
97 	  //button->setIcon( LXDG::findIcon("drive-removable-media","") );
98 	}
99         else{ iconame = "folder"; } //button->setIcon( LXDG::findIcon("folder","") );
100     }else if(LUtils::imageExtensions().contains(info.suffix().toLower()) ){
101       iconame = info.absoluteFilePath();
102       //QPixmap pix;
103       //if(pix.load(path)){ button->setIcon( QIcon(pix.scaled(256,256)) ); } //max size for thumbnails in memory
104       //else{ iconame = "dialog-cancel"; } //button->setIcon( LXDG::findIcon("dialog-cancel","") );
105     }else{
106       iconame = LXDG::findAppMimeForFile(path).replace("/","-");
107       //button->setIcon( QIcon(LXDG::findMimeIcon(path).pixmap(QSize(icosize,icosize)).scaledToHeight(icosize, Qt::SmoothTransformation) ) );
108     }
109     if(!iconame.isEmpty()){ iconID = iconame; }
110     txt = info.fileName();
111     if(!watcher->files().isEmpty()){ watcher->removePaths(watcher->files()); }
112     watcher->addPath(path); //make sure to update this shortcut if the file changes
113   }else{
114     //InValid File
115     button->setWhatsThis("");
116     iconID = "quickopen";
117     //button->setIcon( QIcon(LXDG::findIcon("quickopen","dialog-cancel").pixmap(QSize(icosize,icosize)).scaledToHeight(icosize, Qt::SmoothTransformation) ) );
118     button->setText( tr("Click to Set") );
119     if(!watcher->files().isEmpty()){ watcher->removePaths(watcher->files()); }
120   }
121   if(!iconID.isEmpty()){
122     if(ICONS->isLoaded(iconID)){
123       ICONS->loadIcon(button, iconID);
124       iconLoaded(iconID); //will not get a signal - already loaded right now
125     }else{
126       //Not loaded yet - verify that the icon exists first
127       if(!ICONS->exists(iconID) && iconID.contains("/") ){ iconID = iconID.replace("/","-"); } //quick mimetype->icon replacement just in case
128       if(!ICONS->exists(iconID)){ iconID = "unknown"; }
129       //Now load the icon
130       ICONS->loadIcon(button, iconID);
131     }
132   }
133   //Now adjust the context menu for the button as needed
134   QAction *tmp = 0;
135   if(this->contextMenu()->isEmpty()){
136     tmp = this->contextMenu()->addAction( tr("Open"), this, SLOT(buttonClicked()) );
137     ICONS->loadIcon(tmp, "document-open");
138     this->contextMenu()->addAction( tr("Open With"), this, SLOT(openWith()) );
139     ICONS->loadIcon(tmp, "document-preview");
140   }
141   tmp = this->contextMenu()->addAction( tr("View Properties"), this, SLOT(fileProperties()) );
142   ICONS->loadIcon(tmp, "document-properties");
143   this->contextMenu()->addSection(tr("File Operations"));
144   if(!path.endsWith(".desktop")){
145     tmp = this->contextMenu()->addAction( tr("Rename"), this, SLOT(fileRename()) );
146     ICONS->loadIcon(tmp, "edit-rename");
147   }
148   tmp = this->contextMenu()->addAction( tr("Copy"), this, SLOT(fileCopy()) );
149   ICONS->loadIcon(tmp, "edit-copy");
150   if(info.isWritable() || (info.isSymLink() && QFileInfo(info.absolutePath()).isWritable() ) ){
151     tmp = this->contextMenu()->addAction( tr("Cut"), this, SLOT(fileCut()) );
152     ICONS->loadIcon(tmp, "edit-cut");
153     tmp = this->contextMenu()->addAction( tr("Delete"), this, SLOT(fileDelete()) );
154     ICONS->loadIcon(tmp, "document-close");
155   }
156   tmp = this->contextMenu()->addAction( tr("Drag to Application"), this, SLOT(startDragNDrop()) );
157   ICONS->loadIcon(tmp, "edit-redo");
158   iconLoaded(iconID); //make sure the emblem is layered on top
159   //If the file is a symlink, put the overlay on the icon
160   /*if(info.isSymLink()){
161     QImage img = button->icon().pixmap(QSize(icosize,icosize)).toImage();
162     int oSize = icosize/3; //overlay size
163     QPixmap overlay = ICONS->loadIcon("emblem-symbolic-link").pixmap(oSize,oSize).scaled(oSize,oSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
164     QPainter painter(&img);
165       painter.drawPixmap(icosize-oSize,icosize-oSize,overlay); //put it in the bottom-right corner
166     button->setIcon( QIcon(QPixmap::fromImage(img)) );
167   }*/
168   //Now adjust the visible text as necessary based on font/grid sizing
169   if(button->toolTip().isEmpty()){ button->setToolTip(txt); }
170   //Double check that the visual icon size matches the requested size - otherwise upscale the icon
171     if(button->fontMetrics().horizontalAdvance(txt) > (button->width()-OUTMARGIN) ){
172       //Text too long, try to show it on two lines
173       //txt = button->fontMetrics().elidedText(txt, Qt::ElideRight, 2*(button->width()-OUTMARGIN), Qt::TextWordWrap);
174       txt =txt.section(" ",0,2).replace(" ","\n"); //First take care of any natural breaks
175       //Go through and combine any lines
176        if(txt.contains("\n")){
177         //need to check each line
178 	QStringList txtL = txt.split("\n");
179 	for(int i=0; i<txtL.length(); i++){
180 	  if(( i+1<txtL.length()) && (button->fontMetrics().horizontalAdvance(txtL[i]) < button->width()/2) ){
181 	    txtL[i] = txtL[i]+" "+txtL[i+1];
182 	    txtL.removeAt(i+1);
183 	  }
184 	}
185 	txt = txtL.join("\n").section("\n",0,2);
186       }
187 
188       if(txt.contains("\n")){
189         //need to check each line
190 	QStringList txtL = txt.split("\n");
191 	for(int i=0; i<txtL.length(); i++){
192 	  if(i>1){ txtL.removeAt(i); i--; } //Only take the first two lines
193 	  else{ txtL[i] = button->fontMetrics().elidedText(txtL[i], Qt::ElideRight, (button->width()-OUTMARGIN) );  }
194 	}
195 	txt = txtL.join("\n");
196       }else{
197         txt = this->fontMetrics().elidedText(txt,Qt::ElideRight, 2*(button->width()-OUTMARGIN));
198         //Now split the line in half for the two lines
199         txt.insert( ((txt.count())/2), "\n");
200       }
201     }
202     if(!txt.contains("\n")){ txt.append("\n "); } //always use two lines
203     //qDebug() << " - Setting Button Text:" << txt;
204     button->setText(txt);
205 
206   QTimer::singleShot(100, this, SLOT(update()) ); //Make sure to re-draw the image in a moment
207 }
208 
buttonClicked(bool openwith)209 void AppLauncherPlugin::buttonClicked(bool openwith){
210   QString path = button->whatsThis();
211   if(path.isEmpty() || !QFile::exists(path) ){
212     //prompt for the user to select an application
213     QList<XDGDesktop*> apps = LSession::handle()->applicationMenu()->currentAppHash()->value("All"); //LXDG::sortDesktopNames( LXDG::systemDesktopFiles() );
214     QStringList names;
215     for(int i=0; i<apps.length(); i++){ names << apps[i]->name; }
216     bool ok = false;
217     QString app = QInputDialog::getItem(this, tr("Select Application"), tr("Name:"), names, 0, false, &ok);
218     if(!ok || names.indexOf(app)<0){ return; } //cancelled
219     this->saveSetting("applicationpath", apps[ names.indexOf(app) ]->filePath);
220     QTimer::singleShot(0,this, SLOT(loadButton()));
221   }else if(openwith){
222     LSession::LaunchApplication("lumina-open -select \""+path+"\"");
223   }else{
224     LSession::LaunchApplication("lumina-open \""+path+"\"");
225   }
226 
227 }
228 
iconLoaded(QString ico)229 void AppLauncherPlugin::iconLoaded(QString ico){
230   if(ico == iconID){
231     //Reload/scale the icon as needed
232     QPixmap pix = button->icon().pixmap(QSize(icosize,icosize)).scaledToHeight(icosize, Qt::SmoothTransformation);
233     if(QFileInfo(button->whatsThis()).isSymLink()){
234       QImage img = pix.toImage();
235       int oSize = icosize/3; //overlay size
236       QPixmap overlay = ICONS->loadIcon("emblem-symbolic-link").pixmap(oSize,oSize).scaled(oSize,oSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
237       QPainter painter(&img);
238         painter.drawPixmap(icosize-oSize,icosize-oSize,overlay); //put it in the bottom-right corner
239       pix = QPixmap::fromImage(img);
240     }
241     button->setIcon( QIcon(pix) );
242   }
243 }
244 
startDragNDrop()245 void AppLauncherPlugin::startDragNDrop(){
246   //Start the drag event for this file
247   QDrag *drag = new QDrag(this);
248   QMimeData *md = new QMimeData;
249     md->setUrls( QList<QUrl>() << QUrl::fromLocalFile(button->whatsThis()) );
250     drag->setMimeData(md);
251   //Now perform the drag and react appropriately
252   Qt::DropAction dropAction = drag->exec(Qt::CopyAction | Qt::MoveAction);
253   if(dropAction == Qt::MoveAction){
254     // File Moved, remove it from here
255     //qDebug() << "File Moved:" << button->whatsThis();
256     //DO NOT DELETE FILES - return code often is wrong (browser drops for instance)
257   }
258 }
259 
actionTriggered(QAction * act)260 void AppLauncherPlugin::actionTriggered(QAction *act){
261   if(act->whatsThis().isEmpty()){ return; }
262   QString path = button->whatsThis();
263   if(path.isEmpty() || !QFile::exists(path)){ return; } //invalid file
264   LSession::LaunchApplication("lumina-open -action \""+act->whatsThis()+"\" \""+path+"\"");
265 }
266 
openWith()267 void AppLauncherPlugin::openWith(){
268   buttonClicked(true);
269 }
270 
fileProperties()271 void AppLauncherPlugin::fileProperties(){
272   QString path = button->whatsThis();
273   if(path.isEmpty() || !QFile::exists(path)){ return; } //invalid file
274   LSession::LaunchApplication("lumina-fileinfo \""+path+"\"");
275 }
276 
fileDelete()277 void AppLauncherPlugin::fileDelete(){
278   QString path = button->whatsThis();
279   if(path.isEmpty() || !QFile::exists(path)){ return; } //invalid file
280   if(QFileInfo(path).isDir()){ QProcess::startDetached("rm -r \""+path+"\""); }
281   else{ QFile::remove(path); }
282 }
283 
fileCut()284 void AppLauncherPlugin::fileCut(){
285   QString path = button->whatsThis();
286   QList<QUrl> urilist; //Also assemble a URI list for cros-app compat (no copy/cut distinguishing)
287   urilist << QUrl::fromLocalFile(path);
288   path.prepend("cut::::");
289   //Now save that data to the global clipboard
290   QMimeData *dat = new QMimeData;
291 	dat->clear();
292 	dat->setData("x-special/lumina-copied-files", path.toLocal8Bit());
293 	dat->setUrls(urilist); //the text/uri-list mimetype - built in Qt conversion/use
294   QApplication::clipboard()->clear();
295   QApplication::clipboard()->setMimeData(dat);
296 }
297 
fileCopy()298 void AppLauncherPlugin::fileCopy(){
299   QString path = button->whatsThis();
300   QList<QUrl> urilist; //Also assemble a URI list for cros-app compat (no copy/cut distinguishing)
301   urilist << QUrl::fromLocalFile(path);
302   path.prepend("copy::::");
303   //Now save that data to the global clipboard
304   QMimeData *dat = new QMimeData;
305 	dat->clear();
306 	dat->setData("x-special/lumina-copied-files", path.toLocal8Bit());
307 	dat->setUrls(urilist); //the text/uri-list mimetype - built in Qt conversion/use
308   QApplication::clipboard()->clear();
309   QApplication::clipboard()->setMimeData(dat);
310 }
311 
fileRename()312 void AppLauncherPlugin::fileRename(){
313   if(inputDLG == 0){
314     inputDLG = new QInputDialog(0, Qt::Dialog | Qt::WindowStaysOnTopHint);
315     inputDLG->setInputMode(QInputDialog::TextInput);
316     inputDLG->setTextValue(button->whatsThis().section("/",-1));
317     inputDLG->setTextEchoMode(QLineEdit::Normal);
318     inputDLG->setLabelText( tr("New Filename") );
319     connect(inputDLG, SIGNAL(finished(int)), this, SLOT(renameFinished(int)) );
320   }
321   inputDLG->showNormal();
322 }
323 
renameFinished(int result)324 void AppLauncherPlugin::renameFinished(int result){
325   QString newname = inputDLG->textValue();
326   inputDLG->deleteLater();
327   inputDLG = 0;
328   qDebug() << "Got Rename Result:" << result << QDialog::Accepted << newname;
329   if(result != QDialog::Accepted){ return; }
330   QString newpath = button->whatsThis().section("/",0,-2)+"/"+newname;
331   qDebug() << "Move File:" << button->whatsThis() << newpath;
332   if( QFile::rename(button->whatsThis(), newpath) ){
333     //No special actions here yet - TODO
334     qDebug() << " - SUCCESS";
335   }
336 }
337 
fileDrop(bool copy,QList<QUrl> urls)338 void AppLauncherPlugin::fileDrop(bool copy, QList<QUrl> urls){
339   for(int i=0; i<urls.length(); i++){
340     QString oldpath = urls[i].toLocalFile();
341     if(!QFile::exists(oldpath)){ continue; } //not a local file?
342     QString filename = oldpath.section("/",-1);
343     if(copy){
344       qDebug() << "Copying File:" << oldpath << "->" << button->whatsThis()+"/"+filename;
345       QFile::copy(oldpath, button->whatsThis()+"/"+filename);
346     }else{
347       qDebug() << "Moving File:" << oldpath << "->" << button->whatsThis()+"/"+filename;
348       QFile::rename(oldpath, button->whatsThis()+"/"+filename);
349     }
350   }
351 }
352