1 //===========================================
2 //  Lumina-DE source code
3 //  Copyright (c) 2016, Ken Moore
4 //  Available under the 3-clause BSD license
5 //  See the LICENSE file for full details
6 //===========================================
7 #include "BrowserWidget.h"
8 
9 #include <QVBoxLayout>
10 #include <QTimer>
11 #include <QSettings>
12 #include <QtConcurrent>
13 
14 #include <LUtils.h>
15 #include <LuminaOS.h>
16 
17 #define USE_VIDEO_LABEL 0
18 
BrowserWidget(QString objID,QWidget * parent)19 BrowserWidget::BrowserWidget(QString objID, QWidget *parent) : QWidget(parent){
20   //Setup the Widget/UI
21   this->setLayout( new QVBoxLayout(this) );
22   this->layout()->setContentsMargins(0,0,0,0);
23   ID = objID;
24   //Setup the backend browser object
25   bThread = new QThread();
26   BROWSER = new Browser();
27   BROWSER->moveToThread(bThread);
28   bThread->start();
29   connect(BROWSER, SIGNAL(clearItems()), this, SLOT(clearItems()) );
30   connect(BROWSER, SIGNAL(itemRemoved(QString)), this, SLOT(itemRemoved(QString)) );
31   connect(BROWSER, SIGNAL(itemDataAvailable(const QIcon *, LFileInfo*)), this, SLOT(itemDataAvailable(const QIcon *, LFileInfo*)) );
32   connect(BROWSER, SIGNAL(itemsLoading(int)), this, SLOT(itemsLoading(int)) );
33   connect(this, SIGNAL(dirChange(QString, bool)), BROWSER, SLOT(loadDirectory(QString, bool)) );
34   listWidget = 0;
35   treeWidget = 0;
36   treeSortColumn = 0;
37   readDateFormat();
38   freshload = true; //nothing loaded yet
39   numItems = 0;
40   this->setMouseTracking(true);
41 }
42 
~BrowserWidget()43 BrowserWidget::~BrowserWidget(){
44   if(bThread->isRunning()){ bThread->exit(); }
45   BROWSER->deleteLater();
46   bThread->deleteLater();
47 }
48 
stop()49 void BrowserWidget::stop(){
50   bThread->exit();
51   this->setVisible(false);
52   this->disconnect();
53 }
54 
changeDirectory(QString dir)55 void BrowserWidget::changeDirectory(QString dir){
56   //qDebug() << "Change Dir:" << dir;
57   if(USE_VIDEO_LABEL){
58     QStringList vids = videoMap.keys();
59     for(int i=0; i<vids.length(); i++){ videoMap.take(vids[i]).second->deleteLater(); }
60     //videoMap.clear();
61   }
62   //qDebug() << "Change Directory:" << dir << historyList;
63 
64   if( !dir.contains("/.zfs/snapshot/") ){
65     if(historyList.isEmpty() || !dir.isEmpty()){ historyList << dir; }
66   }else{
67     //Need to remove the zfs snapshot first and ensure that it is not the same dir (just a diff snapshot)
68     QString cleaned = dir;
69     cleaned = cleaned.replace( QRegExp("/\\.zfs/snapshot/(.)+/"), "/" );
70     if( (historyList.isEmpty() || historyList.last()!=cleaned) && !cleaned.isEmpty() ){ historyList << cleaned; }
71   }
72   //qDebug() << "History:" << historyList;
73   emit dirChange(dir, false);
74 }
75 
showDetails(bool show)76 void BrowserWidget::showDetails(bool show){
77   //Clean up widgets first
78   QSize iconsize;
79   if(show && listWidget!=0){
80     //Clean up list widget
81     iconsize = listWidget->iconSize();
82     this->layout()->removeWidget(listWidget);
83     listWidget->deleteLater();
84     listWidget = 0;
85   }else if(!show && treeWidget!=0){
86     iconsize = treeWidget->iconSize();
87     this->layout()->removeWidget(treeWidget);
88     treeWidget->deleteLater();
89     treeWidget = 0;
90   }
91  // qDebug() << "Create Widget: details:" << show;
92   //Now create any new widgets
93   if(show && treeWidget == 0){
94     treeWidget = new DDTreeWidget(this);
95       treeWidget->setContextMenuPolicy(Qt::CustomContextMenu);
96       if(!iconsize.isNull()){ treeWidget->setIconSize(iconsize); }
97     this->layout()->addWidget(treeWidget);
98     connect(treeWidget, SIGNAL(itemActivated(QTreeWidgetItem*,int)), this, SIGNAL(itemsActivated()) );
99     connect(treeWidget, SIGNAL(customContextMenuRequested(const QPoint&)), this, SIGNAL(contextMenuRequested()) );
100     connect(treeWidget, SIGNAL(DataDropped(QString, QStringList)), this, SIGNAL(DataDropped(QString, QStringList)) );
101     connect(treeWidget, SIGNAL(GotFocus()), this, SLOT(selectionChanged()) );
102     connect(treeWidget, SIGNAL(sortColumnChanged(int)), this, SIGNAL(treeWidgetSortColumn(int)) );
103     connect(treeWidget, SIGNAL(sortColumnChanged(int)), this, SIGNAL(setTreeWidgetSortColumn(int, bool)) );
104     retranslate();
105     treeWidget->sortItems(treeSortColumn, Qt::AscendingOrder);
106     treeWidget->setColumnWidth(0, treeWidget->fontMetrics().horizontalAdvance("W")*20);
107     if(!BROWSER->currentDirectory().isEmpty()){ emit dirChange("", true); }
108   }else if(!show && listWidget==0){
109     listWidget = new DDListWidget(this);
110      listWidget->setContextMenuPolicy(Qt::CustomContextMenu);
111      if(!iconsize.isNull()){ listWidget->setIconSize(iconsize); }
112     this->layout()->addWidget(listWidget);
113     connect(listWidget, SIGNAL(itemActivated(QListWidgetItem*)), this, SIGNAL(itemsActivated()) );
114     connect(listWidget, SIGNAL(customContextMenuRequested(const QPoint&)), this, SIGNAL(contextMenuRequested()) );
115     connect(listWidget, SIGNAL(DataDropped(QString, QStringList)), this, SIGNAL(DataDropped(QString, QStringList)) );
116     connect(listWidget, SIGNAL(GotFocus()), this, SLOT(selectionChanged()) );
117     if(!BROWSER->currentDirectory().isEmpty()){ emit dirChange("",true); }
118   }
119   if(listWidget!=0){ listWidget->setWhatsThis( BROWSER->currentDirectory() ); }
120   if(treeWidget!=0){ treeWidget->setWhatsThis(BROWSER->currentDirectory() ); }
121   //qDebug() << "  Done making widget";
122 }
123 
hasDetails()124 bool BrowserWidget::hasDetails(){
125   return (treeWidget!=0);
126 }
127 
showHiddenFiles(bool show)128 void BrowserWidget::showHiddenFiles(bool show){
129   BROWSER->showHiddenFiles(show);
130 }
131 
hasHiddenFiles()132 bool BrowserWidget::hasHiddenFiles(){
133   return BROWSER->showingHiddenFiles();
134 }
135 
showThumbnails(bool show)136 void BrowserWidget::showThumbnails(bool show){
137   //qDebug() << show << videoMap.size();
138   for(QString file : videoMap.uniqueKeys()) {
139     QTreeWidgetItem *it = videoMap[file].first;
140     LVideoWidget *widget = videoMap[file].second;
141     if(show) {
142       widget->enableIcons();
143       treeWidget->setItemWidget(it, 0, widget);
144     }else{
145       widget->disableIcons();
146       treeWidget->setItemWidget(it, 0, widget);
147     }
148   }
149   BROWSER->showThumbnails(show);
150 }
151 
hasThumbnails()152 bool BrowserWidget::hasThumbnails(){
153   return BROWSER->showingThumbnails();
154 }
155 
setThumbnailSize(int px)156 void BrowserWidget::setThumbnailSize(int px){
157   bool larger = true;
158   if(listWidget!=0){
159     larger = listWidget->iconSize().height() < px;
160     listWidget->setIconSize(QSize(px,px));
161   }else if(treeWidget!=0){
162     larger = treeWidget->iconSize().height() < px;
163     treeWidget->setIconSize(QSize(px,px));
164   }
165   for(QString file : videoMap.uniqueKeys()) {
166     QTreeWidgetItem *it = videoMap[file].first;
167     LVideoWidget *widget = videoMap[file].second;
168     widget->setIconSize(treeWidget->iconSize());
169     treeWidget->setItemWidget(it, 0, widget);
170   }
171   //qDebug() << "Changing Icon Size:" << px << larger;
172   if(BROWSER->currentDirectory().isEmpty() || !larger ){ return; } //don't need to reload icons unless the new size is larger
173   emit dirChange("", larger);
174 }
175 
thumbnailSize()176 int BrowserWidget::thumbnailSize(){
177   if(listWidget!=0){ return listWidget->iconSize().height(); }
178   else if(treeWidget!=0){ return treeWidget->iconSize().height(); }
179   return 0;
180 }
181 
setHistory(QStringList paths)182 void BrowserWidget::setHistory(QStringList paths){
183   //NOTE: later items are used first
184    historyList = paths;
185 }
186 
history()187 QStringList BrowserWidget::history(){
188   return historyList;
189 }
190 
setShowActive(bool show)191 void BrowserWidget::setShowActive(bool show){
192   QString base = "";//"QListWidget::item,QTreeWidget::item{ border: 1px solid transparent; background-color: red; } QListWidget::item:hover,QTreeWidget::item:hover{ border: 1px solid black; background-color: blue; }";
193   if(!show){ base.prepend("QAbstractScrollArea{ background-color: rgba(10,10,10,10); } QHeaderView{ background-color: lightgrey; } "); }
194   this->setStyleSheet(base);
195 }
196 
setTreeWidgetSortColumn(int col,bool now)197 void BrowserWidget::setTreeWidgetSortColumn(int col, bool now){
198   treeSortColumn = col;
199   if(now && treeWidget!=0){
200     treeWidget->sortItems(treeSortColumn, Qt::AscendingOrder);
201   }
202 }
203 
204 // This function is only called if user changes sessionsettings. By doing so, operations like sorting by date
205 // are faster because the date format is already stored in DirWidget::date_format static variable
readDateFormat()206 void BrowserWidget::readDateFormat() {
207   if(!date_format.isEmpty())
208       date_format.clear();
209   QSettings settings("lumina-desktop","sessionsettings");
210   // If value doesn't exist or is not setted, empty string is returned
211   date_format << settings.value("DateFormat").toString();
212   date_format << settings.value("TimeFormat").toString();
213 }
214 
215 
currentSelection()216 QStringList BrowserWidget::currentSelection(){
217   QStringList out;
218   if(listWidget!=0){
219     QList<QListWidgetItem*> sel = listWidget->selectedItems();
220     //qDebug() << "Selection number:" << sel.length();
221     //if(sel.isEmpty() && listWidget->currentItem()!=0){ sel << listWidget->currentItem(); }
222     //qDebug() << "Selection number:" << sel.length();
223     for(int i=0; i<sel.length(); i++){ out << sel[i]->whatsThis(); }
224   }else if(treeWidget!=0){
225     QList<QTreeWidgetItem*> sel = treeWidget->selectedItems();
226     //if(sel.isEmpty() && treeWidget->currentItem()!=0){ sel << treeWidget->currentItem(); }
227     for(int i=0; i<sel.length(); i++){ out << sel[i]->whatsThis(0); }
228   }
229   out.removeDuplicates(); //just in case - tree widgets sometimes "select" each column as an individual item
230   return out;
231 }
232 
currentItems(int type)233 QStringList BrowserWidget::currentItems(int type){
234   //type: 0=all, -1=files, +1=dirs
235   QStringList paths;
236   if(listWidget!=0){
237     for(int i=0; i<listWidget->count(); i++){
238       if(i<0 && (listWidget->item(i)->data(Qt::UserRole).toString()=="file") ){ //FILES
239         paths << listWidget->item(i)->whatsThis();
240       }else if(i>0 &&  (listWidget->item(i)->data(Qt::UserRole).toString()=="dir")){ //DIRS
241         paths << listWidget->item(i)->whatsThis();
242       }else if(i==0){ //ALL
243         paths << listWidget->item(i)->whatsThis();
244       }
245     }
246   }else if(treeWidget!=0){
247     for(int i=0; i<treeWidget->topLevelItemCount(); i++){
248       if(i<0 && !treeWidget->topLevelItem(i)->text(1).isEmpty()){ //FILES
249         paths << treeWidget->topLevelItem(i)->whatsThis(0);
250       }else if(i>0 && treeWidget->topLevelItem(i)->text(1).isEmpty()){ //DIRS
251         paths << treeWidget->topLevelItem(i)->whatsThis(0);
252       }else if(i==0){ //ALL
253         paths << treeWidget->topLevelItem(i)->whatsThis(0);
254       }
255     }
256   }
257   return paths;
258 }
259 
260 // =================
261 //     PUBLIC SLOTS
262 // =================
retranslate()263 void BrowserWidget::retranslate(){
264   if(listWidget!=0){
265 
266   }else if(treeWidget!=0){
267     QTreeWidgetItem *it = new QTreeWidgetItem();
268     it->setText(0,tr("Name"));
269     it->setText(1,tr("Size"));
270     it->setText(2, tr("Type"));
271     it->setText(3, tr("Date Modified") );
272     it->setText(4, tr("Date Created") );
273     treeWidget->setHeaderItem(it);
274     //Now reset the sorting (alphabetically, dirs first)
275     treeWidget->sortItems(0, Qt::AscendingOrder);  // sort by name
276     treeWidget->sortItems(1, Qt::AscendingOrder);  //sort by type
277   }
278 }
279 
280 // =================
281 //          PRIVATE
282 // =================
DTtoString(QDateTime dt)283 QString BrowserWidget::DTtoString(QDateTime dt){
284   QStringList fmt = date_format;
285   if(fmt.isEmpty() || fmt.length()!=2 || (fmt[0].isEmpty() && fmt[1].isEmpty()) ){
286     //Default formatting
287     return dt.toString(Qt::DefaultLocaleShortDate);
288   }else if(fmt[0].isEmpty()){
289     //Time format only
290     return (dt.date().toString(Qt::DefaultLocaleShortDate)+" "+dt.time().toString(fmt[1]));
291   }else if(fmt[1].isEmpty()){
292     //Date format only
293     return (dt.date().toString(fmt[0])+" "+dt.time().toString(Qt::DefaultLocaleShortDate));
294   }else{
295     //both date/time formats set
296     return dt.toString(fmt.join(" "));
297   }
298 }
299 
300 // =================
301 //    PRIVATE SLOTS
302 // =================
clearItems()303 void BrowserWidget::clearItems(){
304   //qDebug() << "Clear Items";
305   if(listWidget!=0){ listWidget->clear(); }
306   else if(treeWidget!=0){ treeWidget->clear(); }
307   freshload = true;
308   //videoMap.clear();
309 }
310 
itemRemoved(QString item)311 void BrowserWidget::itemRemoved(QString item){
312   //qDebug() << "item removed" << item;
313   if(treeWidget!=0){
314     QList<QTreeWidgetItem*> found = treeWidget->findItems(item.section("/",-1), Qt::MatchExactly, 0); //look for exact name match
315     if(found.isEmpty()){ return; } //no match
316     delete found[0];
317   }else if(listWidget!=0){
318     QList<QListWidgetItem*> found = listWidget->findItems(item.section("/",-1), Qt::MatchExactly); //look for exact name match
319     if(found.isEmpty()){ return; }
320     delete found[0];
321   }
322 }
323 
itemDataAvailable(const QIcon * ico,LFileInfo * info)324 void BrowserWidget::itemDataAvailable(const QIcon* ico, LFileInfo *info){
325   //qDebug() << "itemDataAvailable";
326   if(info==0){ return; }
327   if(info->absolutePath() != BROWSER->currentDirectory()){ return; } //leftover item from a previous load
328   widgetMutex.lock();
329   if(listWidget!=0){ listWidget->setWhatsThis( BROWSER->currentDirectory() ); }
330   if(treeWidget!=0){ treeWidget->setWhatsThis(BROWSER->currentDirectory() ); }
331   //if(info->absolutePath().contains("/.zfs/")){ qDebug() << "Item Data Available:" << info->fileName(); }
332   int num = 0;
333   if(listWidget!=0){
334     //LIST WIDGET - name and icon only
335     QListWidgetItem *it = 0;
336     //Find the existing item for this
337     if(info->isDesktopFile() && info->XDG()->isValid()){
338       QList<QListWidgetItem*> items = listWidget->findItems(info->XDG()->name, Qt::MatchExactly);
339       //Could be multiple items with the same text in this case - check paths as well
340       for(int i=0; i<items.length() && it==0; i++){
341         if(items[i]->whatsThis()==info->absoluteFilePath()){ it = items[i]; }
342       }
343     }else{
344       //non-desktop entry
345       QList<QListWidgetItem*> items = listWidget->findItems(info->fileName(), Qt::MatchExactly);
346       //Could be multiple items with the same text in this case - check paths as well
347       for(int i=0; i<items.length() && it==0; i++){
348         if(items[i]->whatsThis()==info->absoluteFilePath()){ it = items[i]; }
349       }
350     }
351     //No existing item - make a new one
352     if(it==0){
353       it = new CQListWidgetItem(*ico, info->fileName(), listWidget);
354           it->setWhatsThis(info->absoluteFilePath());
355           it->setData(Qt::UserRole, (info->isDir() ? "dir" : "file")); //used for sorting
356         listWidget->addItem(it);
357     }
358     num = listWidget->count();
359     //Now update the information for the item
360     if(info->isDesktopFile() && info->XDG()->isValid()){
361       it->setText(info->XDG()->name);
362     }else{
363       it->setText(info->fileName());
364     }
365     if(ico!=0){ it->setIcon(*ico); }
366 
367   }else if(treeWidget!=0){
368     QTreeWidgetItem *it = 0;
369     if(info->isDesktopFile()){
370       QList<QTreeWidgetItem*> items = treeWidget->findItems(info->XDG()->name, Qt::MatchExactly, 0);
371       for(int i=0; i<items.length() && it==0; i++){
372         //Can be multiple with the same name - check paths too
373         if(items[i]->whatsThis(0)==info->absoluteFilePath()){ it = items[i]; }
374       }
375       if(it==0){
376         //New item
377         it = new CQTreeWidgetItem(treeWidget);
378         it->setText(0, info->XDG()->name ); //name (0)
379         treeWidget->addTopLevelItem(it);
380       }
381     }else{
382       if( ! treeWidget->findItems(info->fileName(), Qt::MatchExactly, 0).isEmpty() ) {
383         it = treeWidget->findItems(info->fileName(), Qt::MatchExactly, 0).first();
384       }else if(USE_VIDEO_LABEL && hasThumbnails() && info->isVideo() && !videoMap.contains(info->absoluteFilePath()) ) {
385         it = new CQTreeWidgetItem(treeWidget);
386         treeWidget->addTopLevelItem(it);
387         LVideoWidget *widget = new LVideoWidget(info->absoluteFilePath(), treeWidget->iconSize(), hasThumbnails(), treeWidget);
388         videoMap.insert(info->absoluteFilePath(), QPair<QTreeWidgetItem*,LVideoWidget*>(it, widget));
389         treeWidget->setItemWidget(it, 0, widget);
390       }else if(USE_VIDEO_LABEL && hasThumbnails() && info->isVideo() && videoMap.contains(info->absoluteFilePath()) ) {
391         it = videoMap[info->absoluteFilePath()].first;
392         LVideoWidget *widget = videoMap[info->absoluteFilePath()].second;
393         widget->setIconSize(treeWidget->iconSize());
394         treeWidget->setItemWidget(it, 0, widget);
395       }else{
396         it = new CQTreeWidgetItem(treeWidget);
397         treeWidget->addTopLevelItem(it);
398         it->setText(0, info->fileName() ); //name (0)
399       }
400     }
401     //Now set/update all the data
402     if(!info->isVideo() || !hasThumbnails() || !USE_VIDEO_LABEL){
403         if(ico!=0){ it->setIcon(0, *ico); }
404     }
405     it->setText(1, info->isDir() ? "" : LUtils::BytesToDisplaySize(info->size()) ); //size (1)
406     it->setText(2, info->mimetype() ); //type (2)
407     it->setText(3, DTtoString(info->lastModified() )); //modification date (3)
408     it->setText(4, DTtoString(info->birthTime()) ); //creation date (4)
409     //Now all the hidden data
410     it->setWhatsThis(0, info->absoluteFilePath());
411     it->setWhatsThis(3, info->lastModified().toString("yyyyMMddhhmmsszzz") ); //sorts by this actually
412     it->setWhatsThis(4, info->birthTime().toString("yyyyMMddhhmmsszzz") ); //sorts by this actually
413     num = treeWidget->topLevelItemCount();
414   }
415   if(num < numItems){
416     //Still loading items
417     //this->setEnabled(false);
418   }else{
419     //qDebug() << "Got Items Loaded:" << num << numItems;
420     if(freshload && treeWidget!=0){
421       //qDebug() << "Resize Tree Widget Contents";
422       //for(int i=treeWidget->columnCount()-1; i>0; i--){ treeWidget->resizeColumnToContents(i); }
423       treeWidget->resizeColumnToContents(1);
424       //treeWidget->resizeColumnToContents(0);
425     }
426     freshload = false; //any further changes are updates - not a fresh load of a dir
427     QtConcurrent::run(this, &BrowserWidget::loadStatistics, this);
428     //QTimer::singleShot(0, this, SLOT(loadStatistics()));
429     //Done loading items
430     //this->setEnabled(true);
431 
432   }//end check for finished loading items
433   widgetMutex.unlock();
434   //qDebug() << " - Done with itemDataAvailable";
435 }
436 
itemsLoading(int total)437 void BrowserWidget::itemsLoading(int total){
438   //qDebug() << "Got number of items loading:" << total;
439   if(listWidget!=0){ listWidget->setWhatsThis( BROWSER->currentDirectory() ); }
440   if(treeWidget!=0){ treeWidget->setWhatsThis(BROWSER->currentDirectory() ); }
441   numItems = total; //save this for later
442   if(total<1){
443     emit updateDirectoryStatus( tr("No Directory Contents") );
444     this->setEnabled(true);
445   }
446 }
447 
selectionChanged()448 void BrowserWidget::selectionChanged(){
449   emit hasFocus(ID); //let the parent know the widget is "active" with the user
450 }
451 
loadStatistics(BrowserWidget * bw)452 void BrowserWidget::loadStatistics(BrowserWidget* bw){
453     //Assemble any status message
454     QString stats = QString(tr("Capacity: %1")).arg(LOS::FileSystemCapacity(BROWSER->currentDirectory()));
455     int nF, nD;
456     double bytes = 0;
457     nF = nD = 0;
458     if(listWidget!=0){
459       bytes = -1; //not supported for this widget
460       for(int i=0; i<listWidget->count(); i++){
461         if(listWidget->item(i)->data(Qt::UserRole).toString()=="dir"){ nD++; } //directory
462         else{ nF++; } //file
463       }
464     }else if(treeWidget!=0){
465       for(int i=0; i<treeWidget->topLevelItemCount(); i++){
466         if(treeWidget->topLevelItem(i)->text(1).isEmpty()){
467           nD++; //directory
468         }else{
469           nF++; //file
470           bytes+=LUtils::DisplaySizeToBytes(treeWidget->topLevelItem(i)->text(1));
471         }
472       }
473     }
474     if( (nF+nD) >0){
475       stats.prepend("\t");
476       if(nF>0){
477         //Has Files
478         if(bytes>0){
479           stats.prepend( QString(tr("Files: %1 (%2)")).arg(QString::number(nF), LUtils::BytesToDisplaySize(bytes)) );
480         }else{
481           stats.prepend( QString(tr("Files: %1")).arg(QString::number(nF)) );
482         }
483       }
484       if(nD > 0){
485         //Has Dirs
486         if(nF>0){ stats.prepend(" / "); }//has files output already
487         stats.prepend( QString(tr("Dirs: %1")).arg(QString::number(nD)) );
488       }
489     }
490     bw->emit updateDirectoryStatus( stats.simplified() );
491     statustip = stats.simplified(); //save for later
492 }
493 
resizeEvent(QResizeEvent * ev)494 void BrowserWidget::resizeEvent(QResizeEvent *ev){
495   QWidget::resizeEvent(ev); //do the normal processing first
496   //The list widget needs to be poked to rearrange the items to fit the new size
497   //  tree widget does this fine at the moment.
498   if(listWidget!=0){
499     listWidget->sortItems(Qt::AscendingOrder);
500   }
501 }
502