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