You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
pavucontrol-qt-packaging/pcmanfm/tabpage.cpp

621 lines
22 KiB

/*
Copyright (C) 2013 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "tabpage.h"
#include "launcher.h"
#include <libfm-qt/filemenu.h>
#include <libfm-qt/mountoperation.h>
#include <libfm-qt/proxyfoldermodel.h>
#include <libfm-qt/cachedfoldermodel.h>
#include <libfm-qt/fileinfo.h>
#include <QApplication>
#include <QCursor>
#include <QMessageBox>
#include <QScrollBar>
#include "settings.h"
#include "application.h"
#include <QTimer>
#include <QTextStream>
#include <QDebug>
using namespace Fm;
namespace PCManFM {
bool ProxyFilter::filterAcceptsRow(const Fm::ProxyFolderModel* model, FmFileInfo* info) const {
if(!model || !info)
return true;
QString baseName(fm_file_info_get_name(info));
if(!virtHiddenList_.isEmpty() && !model->showHidden() && virtHiddenList_.contains(baseName))
return false;
if(!filterStr_.isEmpty() && !baseName.contains(filterStr_, Qt::CaseInsensitive))
return false;
return true;
}
void ProxyFilter::setVirtHidden(Fm::Folder folder) {
virtHiddenList_ = QStringList(); // reset the list
if(folder.isNull())
return;
Fm::Path path = folder.getPath();
if(!path.isNull()) {
char* pathStr = path.toStr();
if(pathStr) {
QString dotHidden = QString::fromUtf8(pathStr) + QString("/.hidden");
g_free(pathStr);
// FIXME: this does not work for non-local filesystems
QFile file(dotHidden);
if(file.open(QIODevice::ReadOnly | QIODevice::Text)) {
QTextStream in(&file);
while(!in.atEnd())
virtHiddenList_.append(in.readLine());
file.close();
}
}
}
}
TabPage::TabPage(Fm::Path path, QWidget* parent):
QWidget(parent),
folderModel_(NULL),
overrideCursor_(false) {
Settings& settings = static_cast<Application*>(qApp)->settings();
// create proxy folder model to do item filtering
proxyModel_ = new ProxyFolderModel();
proxyModel_->setShowHidden(settings.showHidden());
proxyModel_->setShowThumbnails(settings.showThumbnails());
connect(proxyModel_, &ProxyFolderModel::sortFilterChanged, this, &TabPage::sortFilterChanged);
verticalLayout = new QVBoxLayout(this);
verticalLayout->setContentsMargins(0, 0, 0, 0);
folderView_ = new View(settings.viewMode(), this);
folderView_->setMargins(settings.folderViewCellMargins());
// newView->setColumnWidth(Fm::FolderModel::ColumnName, 200);
connect(folderView_, &View::openDirRequested, this, &TabPage::onOpenDirRequested);
connect(folderView_, &View::selChanged, this, &TabPage::onSelChanged);
connect(folderView_, &View::clickedBack, this, &TabPage::backwardRequested);
connect(folderView_, &View::clickedForward, this, &TabPage::forwardRequested);
proxyFilter_ = new ProxyFilter();
proxyModel_->addFilter(proxyFilter_);
// FIXME: this is very dirty
folderView_->setModel(proxyModel_);
verticalLayout->addWidget(folderView_);
chdir(path, true);
}
TabPage::~TabPage() {
freeFolder();
if(proxyFilter_)
delete proxyFilter_;
if(proxyModel_)
delete proxyModel_;
if(folderModel_)
folderModel_->unref();
if(overrideCursor_) {
QApplication::restoreOverrideCursor(); // remove busy cursor
}
}
void TabPage::freeFolder() {
if(!folder_.isNull()) {
if(folderSettings_.isCustomized()) {
// save custom view settings for this folder
static_cast<Application*>(qApp)->settings().saveFolderSettings(folder_.getPath(), folderSettings_);
}
g_signal_handlers_disconnect_by_func(folder_, (gpointer)onFolderStartLoading, this);
g_signal_handlers_disconnect_by_func(folder_, (gpointer)onFolderFinishLoading, this);
g_signal_handlers_disconnect_by_func(folder_, (gpointer)onFolderError, this);
g_signal_handlers_disconnect_by_func(folder_, (gpointer)onFolderFsInfo, this);
g_signal_handlers_disconnect_by_func(folder_, (gpointer)onFolderRemoved, this);
g_signal_handlers_disconnect_by_func(folder_, (gpointer)onFolderUnmount, this);
g_signal_handlers_disconnect_by_func(folder_, (gpointer)onFolderContentChanged, this);
folder_ = nullptr;
}
}
/*static*/ void TabPage::onFolderStartLoading(FmFolder* _folder, TabPage* pThis) {
if(!pThis->overrideCursor_) {
// FIXME: sometimes FmFolder of libfm generates unpaired "start-loading" and
// "finish-loading" signals of uncertain reasons. This should be a bug in libfm.
// Until it's fixed in libfm, we need to workaround the problem here, not to
// override the cursor twice.
QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
pThis->overrideCursor_ = true;
}
#if 0
#if FM_CHECK_VERSION(1, 0, 2) && 0 // disabled
if(fm_folder_is_incremental(_folder)) {
/* create a model for the folder and set it to the view
it is delayed for non-incremental folders since adding rows into
model is much faster without handlers connected to its signals */
FmFolderModel* model = fm_folder_model_new(folder, FALSE);
fm_folder_view_set_model(fv, model);
fm_folder_model_set_sort(model, app_config->sort_by,
(app_config->sort_type == GTK_SORT_ASCENDING) ?
FM_SORT_ASCENDING : FM_SORT_DESCENDING);
g_object_unref(model);
}
else
#endif
fm_folder_view_set_model(fv, NULL);
#endif
}
// slot
void TabPage::restoreScrollPos() {
// scroll to recorded position
folderView_->childView()->verticalScrollBar()->setValue(browseHistory().currentScrollPos());
// if the current folder is the parent folder of the last browsed folder,
// select the folder item in current view.
if(!lastFolderPath_.isNull() && lastFolderPath_.getParent() == path()) {
QModelIndex index = folderView_->indexFromFolderPath(lastFolderPath_);
if(index.isValid()) {
folderView_->childView()->scrollTo(index, QAbstractItemView::EnsureVisible);
folderView_->childView()->setCurrentIndex(index);
}
}
}
/*static*/ void TabPage::onFolderFinishLoading(FmFolder* _folder, TabPage* pThis) {
// FIXME: is this needed?
FmFileInfo* fi = fm_folder_get_info(_folder);
if(fi) { // if loading of the folder fails, it's possible that we don't have FmFileInfo.
pThis->title_ = QString::fromUtf8(fm_file_info_get_disp_name(fi));
Q_EMIT pThis->titleChanged(pThis->title_);
}
fm_folder_query_filesystem_info(_folder); // FIXME: is this needed?
#if 0
FmFolderView* fv = folder_view;
const FmNavHistoryItem* item;
GtkScrolledWindow* scroll = GTK_SCROLLED_WINDOW(fv);
/* Note: most of the time, we delay the creation of the
* folder model and do it after the whole folder is loaded.
* That is because adding rows into model is much faster when no handlers
* are connected to its signals. So we detach the model from folder view
* and create the model again when it's fully loaded.
* This optimization, however, is not used for FmFolder objects
* with incremental loading (search://) */
if(fm_folder_view_get_model(fv) == NULL) {
/* create a model for the folder and set it to the view */
FmFolderModel* model = fm_folder_model_new(folder, app_config->show_hidden);
fm_folder_view_set_model(fv, model);
#if FM_CHECK_VERSION(1, 0, 2)
/* since 1.0.2 sorting should be applied on model instead of view */
fm_folder_model_set_sort(model, app_config->sort_by,
(app_config->sort_type == GTK_SORT_ASCENDING) ?
FM_SORT_ASCENDING : FM_SORT_DESCENDING);
#endif
g_object_unref(model);
}
#endif
// update status text
QString& text = pThis->statusText_[StatusTextNormal];
text = pThis->formatStatusText();
Q_EMIT pThis->statusChanged(StatusTextNormal, text);
if(pThis->overrideCursor_) {
QApplication::restoreOverrideCursor(); // remove busy cursor
pThis->overrideCursor_ = false;
}
// After finishing loading the folder, the model is updated, but Qt delays the UI update
// for performance reasons. Therefore at this point the UI is not up to date.
// Of course, the scrollbar ranges are not updated yet. We solve this by installing an Qt timeout handler.
QTimer::singleShot(10, pThis, SLOT(restoreScrollPos()));
}
/*static*/ FmJobErrorAction TabPage::onFolderError(FmFolder* _folder, GError* err, FmJobErrorSeverity severity, TabPage* pThis) {
if(err->domain == G_IO_ERROR) {
if(err->code == G_IO_ERROR_NOT_MOUNTED && severity < FM_JOB_ERROR_CRITICAL) {
FmPath* path = fm_folder_get_path(_folder);
MountOperation* op = new MountOperation(pThis);
op->mount(path);
if(op->wait()) { // blocking event loop, wait for mount operation to finish.
// This will reload the folder, which generates a new "start-loading"
// signal, so we get more "start-loading" signals than "finish-loading"
// signals. FIXME: This is a bug of libfm.
// Because the two signals are not correctly paired, we need to
// remove busy cursor here since "finish-loading" is not emitted.
QApplication::restoreOverrideCursor(); // remove busy cursor
pThis->overrideCursor_ = false;
return FM_JOB_RETRY;
}
}
}
if(severity >= FM_JOB_ERROR_MODERATE) {
/* Only show more severe errors to the users and
* ignore milder errors. Otherwise too many error
* message boxes can be annoying.
* This fixes bug #3411298- Show "Permission denied" when switching to super user mode.
* https://sourceforge.net/tracker/?func=detail&aid=3411298&group_id=156956&atid=801864
* */
// FIXME: consider replacing this modal dialog with an info bar to improve usability
QMessageBox::critical(pThis, tr("Error"), QString::fromUtf8(err->message));
}
return FM_JOB_CONTINUE;
}
/*static*/ void TabPage::onFolderFsInfo(FmFolder* _folder, TabPage* pThis) {
guint64 free, total;
QString& msg = pThis->statusText_[StatusTextFSInfo];
if(fm_folder_get_filesystem_info(_folder, &total, &free)) {
char total_str[64];
char free_str[64];
fm_file_size_to_str(free_str, sizeof(free_str), free, fm_config->si_unit);
fm_file_size_to_str(total_str, sizeof(total_str), total, fm_config->si_unit);
msg = tr("Free space: %1 (Total: %2)")
.arg(QString::fromUtf8(free_str))
.arg(QString::fromUtf8(total_str));
}
else
msg.clear();
Q_EMIT pThis->statusChanged(StatusTextFSInfo, msg);
}
QString TabPage::formatStatusText() {
if(proxyModel_ && !folder_.isNull()) {
Fm::FileInfoList files = folder_.getFiles();
int total_files = fm_file_info_list_get_length(files);
int shown_files = proxyModel_->rowCount();
int hidden_files = total_files - shown_files;
QString text = tr("%n item(s)", "", shown_files);
if(hidden_files > 0)
text += tr(" (%n hidden)", "", hidden_files);
return text;
}
return QString();
}
/*static*/ void TabPage::onFolderRemoved(FmFolder* _folder, TabPage* pThis) {
// the folder we're showing is removed, destroy the widget
qDebug("folder removed");
Settings& settings = static_cast<Application*>(qApp)->settings();
// NOTE: call pThis->deleteLater() directly from this GObject signal handler
// does not work but I don't know why.
// Maybe it's the problem of glib mainloop integration?
// Call it when idle works, though.
if(settings.closeOnUnmount())
QTimer::singleShot(0, pThis, SLOT(deleteLater()));
else
pThis->chdir(Fm::Path::getHome());
}
/*static*/ void TabPage::onFolderUnmount(FmFolder* _folder, TabPage* pThis) {
// the folder we're showing is unmounted, destroy the widget
qDebug("folder unmount");
// NOTE: call pThis->deleteLater() directly from this GObject signal handler
// does not work but I don't know why.
// Maybe it's the problem of glib mainloop integration?
// Call it when idle works, though.
Settings& settings = static_cast<Application*>(qApp)->settings();
// NOTE: call pThis->deleteLater() directly from this GObject signal handler
// does not work but I don't know why.
// Maybe it's the problem of glib mainloop integration?
// Call it when idle works, though.
if(settings.closeOnUnmount())
QTimer::singleShot(0, pThis, SLOT(deleteLater()));
else
pThis->chdir(Fm::Path::getHome());
}
/*static */ void TabPage::onFolderContentChanged(FmFolder* _folder, TabPage* pThis) {
/* update status text */
pThis->statusText_[StatusTextNormal] = pThis->formatStatusText();
Q_EMIT pThis->statusChanged(StatusTextNormal, pThis->statusText_[StatusTextNormal]);
}
QString TabPage::pathName() {
char* disp_path = path().displayName(true);
QString ret = QString::fromUtf8(disp_path);
g_free(disp_path);
return ret;
}
void TabPage::chdir(Path newPath, bool addHistory) {
if(!folder_.isNull()) {
// we're already in the specified dir
if(newPath == fm_folder_get_path(folder_))
return;
// remember the previous folder path that we have browsed.
lastFolderPath_ = folder_.getPath();
if(addHistory) {
// store current scroll pos in the browse history
BrowseHistoryItem& item = history_.currentItem();
QAbstractItemView* childView = folderView_->childView();
item.setScrollPos(childView->verticalScrollBar()->value());
}
// free the previous model
if(folderModel_) {
proxyModel_->setSourceModel(NULL);
folderModel_->unref(); // unref the cached model
folderModel_ = NULL;
}
freeFolder();
}
char* disp_name = newPath.displayBasename();
title_ = QString::fromUtf8(disp_name);
Q_EMIT titleChanged(title_);
g_free(disp_name);
folder_ = Fm::Folder::fromPath(newPath);
proxyFilter_->setVirtHidden(folder_);
if(addHistory) {
// add current path to browse history
history_.add(path());
}
g_signal_connect(folder_, "start-loading", G_CALLBACK(onFolderStartLoading), this);
g_signal_connect(folder_, "finish-loading", G_CALLBACK(onFolderFinishLoading), this);
g_signal_connect(folder_, "error", G_CALLBACK(onFolderError), this);
g_signal_connect(folder_, "fs-info", G_CALLBACK(onFolderFsInfo), this);
/* destroy the page when the folder is unmounted or deleted. */
g_signal_connect(folder_, "removed", G_CALLBACK(onFolderRemoved), this);
g_signal_connect(folder_, "unmount", G_CALLBACK(onFolderUnmount), this);
g_signal_connect(folder_, "content-changed", G_CALLBACK(onFolderContentChanged), this);
folderModel_ = CachedFolderModel::modelFromFolder(folder_);
// set sorting, considering customized folders
Settings& settings = static_cast<Application*>(qApp)->settings();
folderSettings_ = settings.loadFolderSettings(path());
proxyModel_->sort(folderSettings_.sortColumn(), folderSettings_.sortOrder());
proxyModel_->setFolderFirst(folderSettings_.sortFolderFirst());
proxyModel_->setShowHidden(folderSettings_.showHidden());
proxyModel_->setSortCaseSensitivity(folderSettings_.sortCaseSensitive() ? Qt::CaseSensitive : Qt::CaseInsensitive);
proxyModel_->setSourceModel(folderModel_);
folderView_->setViewMode(folderSettings_.viewMode());
if(folder_.isLoaded()) {
onFolderStartLoading(folder_, this);
onFolderFinishLoading(folder_, this);
onFolderFsInfo(folder_, this);
}
else
onFolderStartLoading(folder_, this);
}
void TabPage::selectAll() {
folderView_->selectAll();
}
void TabPage::invertSelection() {
folderView_->invertSelection();
}
void TabPage::onOpenDirRequested(FmPath* path, int target) {
Q_EMIT openDirRequested(path, target);
}
// when the current selection in the folder view is changed
void TabPage::onSelChanged(int numSel) {
QString msg;
if(numSel > 0) {
/* FIXME: display total size of all selected files. */
if(numSel == 1) { /* only one file is selected */
Fm::FileInfoList files = folderView_->selectedFiles();
if(!files.isNull()) {
Fm::FileInfo fi = fm_file_info_list_peek_head(files);
const char* size_str = fi.getDispSize();
if(size_str) {
msg = QString("\"%1\" (%2) %3")
.arg(QString::fromUtf8(fi.getDispName()))
.arg(QString::fromUtf8(size_str))
.arg(QString::fromUtf8(fi.getDesc()));
}
else {
msg = QString("\"%1\" %2")
.arg(QString::fromUtf8(fi.getDispName()))
.arg(QString::fromUtf8(fi.getDesc()));
}
/* FIXME: should we support statusbar plugins as in the gtk+ version? */
}
}
else {
goffset sum;
GList* l;
msg = tr("%n item(s) selected", nullptr, numSel);
/* don't count if too many files are selected, that isn't lightweight */
if(numSel < 1000) {
sum = 0;
Fm::FileInfoList files = folderView_->selectedFiles();
if(!files.isNull()) {
for(l = files.peekHeadLink(); l; l = l->next) {
Fm::FileInfo fi = FM_FILE_INFO(l->data);
if(fi.isDir()) {
/* if we got a directory then we cannot tell it's size
unless we do deep count but we cannot afford it */
sum = -1;
break;
}
sum += fi.getSize();
}
}
if(sum >= 0) {
char size_str[128];
fm_file_size_to_str(size_str, sizeof(size_str), sum,
fm_config->si_unit);
msg += QString(" (%1)").arg(QString::fromUtf8(size_str));
}
/* FIXME: should we support statusbar plugins as in the gtk+ version? */
}
/* FIXME: can we show some more info on selection?
that isn't lightweight if a lot of files are selected */
}
}
statusText_[StatusTextSelectedFiles] = msg;
Q_EMIT statusChanged(StatusTextSelectedFiles, msg);
}
void TabPage::backward() {
// remember current scroll position
BrowseHistoryItem& item = history_.currentItem();
QAbstractItemView* childView = folderView_->childView();
item.setScrollPos(childView->verticalScrollBar()->value());
history_.backward();
chdir(history_.currentPath(), false);
}
void TabPage::forward() {
// remember current scroll position
BrowseHistoryItem& item = history_.currentItem();
QAbstractItemView* childView = folderView_->childView();
item.setScrollPos(childView->verticalScrollBar()->value());
history_.forward();
chdir(history_.currentPath(), false);
}
void TabPage::jumpToHistory(int index)
{
if(index >=0 && index < history_.size()) {
// remember current scroll position
BrowseHistoryItem& item = history_.currentItem();
QAbstractItemView* childView = folderView_->childView();
item.setScrollPos(childView->verticalScrollBar()->value());
history_.setCurrentIndex(index);
chdir(history_.currentPath(), false);
}
}
bool TabPage::canUp() {
Fm::Path _path = path();
return (!_path.isNull() && !_path.getParent().isNull());
}
void TabPage::up() {
Fm::Path _path = path();
if(!_path.isNull()) {
Fm::Path parent = _path.getParent();
if(!parent.isNull()) {
chdir(parent, true);
}
}
}
void TabPage::updateFromSettings(Settings& settings) {
folderView_->updateFromSettings(settings);
}
void TabPage::setViewMode(Fm::FolderView::ViewMode mode) {
if(folderSettings_.viewMode() != mode) {
folderSettings_.setViewMode(mode);
if(folderSettings_.isCustomized()) {
static_cast<Application*>(qApp)->settings().saveFolderSettings(path(), folderSettings_);
}
}
folderView_->setViewMode(mode);
}
void TabPage::sort(int col, Qt::SortOrder order) {
if(folderSettings_.sortColumn() != col || folderSettings_.sortOrder() != order) {
folderSettings_.setSortColumn(Fm::FolderModel::ColumnId(col));
folderSettings_.setSortOrder(order);
if(folderSettings_.isCustomized()) {
static_cast<Application*>(qApp)->settings().saveFolderSettings(path(), folderSettings_);
}
}
if(proxyModel_)
proxyModel_->sort(col, order);
}
void TabPage::setSortFolderFirst(bool value) {
if(folderSettings_.sortFolderFirst() != value) {
folderSettings_.setSortFolderFirst(value);
if(folderSettings_.isCustomized()) {
static_cast<Application*>(qApp)->settings().saveFolderSettings(path(), folderSettings_);
}
}
proxyModel_->setFolderFirst(value);
}
void TabPage::setSortCaseSensitive(bool value) {
if(folderSettings_.sortCaseSensitive() != value) {
folderSettings_.setSortCaseSensitive(value);
if(folderSettings_.isCustomized()) {
static_cast<Application*>(qApp)->settings().saveFolderSettings(path(), folderSettings_);
}
}
proxyModel_->setSortCaseSensitivity(value ? Qt::CaseSensitive : Qt::CaseInsensitive);
}
void TabPage::setShowHidden(bool showHidden) {
if(folderSettings_.showHidden() != showHidden) {
folderSettings_.setShowHidden(showHidden);
if(folderSettings_.isCustomized()) {
static_cast<Application*>(qApp)->settings().saveFolderSettings(path(), folderSettings_);
}
}
if(!proxyModel_ || showHidden == proxyModel_->showHidden())
return;
proxyModel_->setShowHidden(showHidden);
statusText_[StatusTextNormal] = formatStatusText();
Q_EMIT statusChanged(StatusTextNormal, statusText_[StatusTextNormal]);
}
void TabPage::applyFilter() {
if(!proxyModel_)
return;
proxyModel_->updateFilters();
statusText_[StatusTextNormal] = formatStatusText();
Q_EMIT statusChanged(StatusTextNormal, statusText_[StatusTextNormal]);
}
void TabPage::setCustomizedView(bool value) {
if(folderSettings_.isCustomized() == value)
return;
Settings& settings = static_cast<Application*>(qApp)->settings();
folderSettings_.setCustomized(value);
if(value) { // save customized folder view settings
settings.saveFolderSettings(path(), folderSettings_);
}
else { // use default folder view settings
settings.clearFolderSettings(path());
setShowHidden(settings.showHidden());
setSortCaseSensitive(settings.sortCaseSensitive());
setSortFolderFirst(settings.sortFolderFirst());
sort(settings.sortColumn(), settings.sortOrder());
}
}
} // namespace PCManFM