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.
libfm-qt-packaging/src/placesmodel.cpp

615 lines
24 KiB

/*
* Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) <pcman.tw@gmail.com>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
#include "placesmodel.h"
#include <gio/gio.h>
#include <QDebug>
#include <QMimeData>
#include <QTimer>
#include <QPointer>
#include <QStandardPaths>
#include "utilities.h"
#include "placesmodelitem.h"
namespace Fm {
std::weak_ptr<PlacesModel> PlacesModel::globalInstance_;
PlacesModel::PlacesModel(QObject* parent):
QStandardItemModel(parent),
showApplications_(true),
showDesktop_(true),
// FIXME: this seems to be broken when porting to new API.
ejectIcon_(QIcon::fromTheme("media-eject")) {
setColumnCount(2);
placesRoot = new QStandardItem(tr("Places"));
placesRoot->setSelectable(false);
placesRoot->setColumnCount(2);
appendRow(placesRoot);
homeItem = new PlacesModelItem("user-home", g_get_user_name(), Fm::FilePath::homeDir());
placesRoot->appendRow(homeItem);
desktopItem = new PlacesModelItem("user-desktop", tr("Desktop"),
Fm::FilePath::fromLocalPath(QStandardPaths::writableLocation(QStandardPaths::DesktopLocation).toLocal8Bit().constData()));
placesRoot->appendRow(desktopItem);
createTrashItem();
computerItem = new PlacesModelItem("computer", tr("Computer"), Fm::FilePath::fromUri("computer:///"));
placesRoot->appendRow(computerItem);
{ // Applications
const char* applicaion_icon_names[] = {"system-software-install", "applications-accessories", "application-x-executable"};
// NOTE: g_themed_icon_new_from_names() accepts char**, but actually const char** is OK.
Fm::GIconPtr gicon{g_themed_icon_new_from_names((char**)applicaion_icon_names, G_N_ELEMENTS(applicaion_icon_names)), false};
auto fmicon = Fm::IconInfo::fromGIcon(std::move(gicon));
applicationsItem = new PlacesModelItem(fmicon, tr("Applications"), Fm::FilePath::fromUri("menu:///applications/"));
placesRoot->appendRow(applicationsItem);
}
{ // Network
const char* network_icon_names[] = {"network", "folder-network", "folder"};
// NOTE: g_themed_icon_new_from_names() accepts char**, but actually const char** is OK.
Fm::GIconPtr gicon{g_themed_icon_new_from_names((char**)network_icon_names, G_N_ELEMENTS(network_icon_names)), false};
auto fmicon = Fm::IconInfo::fromGIcon(std::move(gicon));
networkItem = new PlacesModelItem(fmicon, tr("Network"), Fm::FilePath::fromUri("network:///"));
placesRoot->appendRow(networkItem);
}
devicesRoot = new QStandardItem(tr("Devices"));
devicesRoot->setSelectable(false);
devicesRoot->setColumnCount(2);
appendRow(devicesRoot);
// volumes
volumeMonitor = g_volume_monitor_get();
if(volumeMonitor) {
g_signal_connect(volumeMonitor, "volume-added", G_CALLBACK(onVolumeAdded), this);
g_signal_connect(volumeMonitor, "volume-removed", G_CALLBACK(onVolumeRemoved), this);
g_signal_connect(volumeMonitor, "volume-changed", G_CALLBACK(onVolumeChanged), this);
g_signal_connect(volumeMonitor, "mount-added", G_CALLBACK(onMountAdded), this);
g_signal_connect(volumeMonitor, "mount-changed", G_CALLBACK(onMountChanged), this);
g_signal_connect(volumeMonitor, "mount-removed", G_CALLBACK(onMountRemoved), this);
// add volumes to side-pane
GList* vols = g_volume_monitor_get_volumes(volumeMonitor);
GList* l;
for(l = vols; l; l = l->next) {
GVolume* volume = G_VOLUME(l->data);
onVolumeAdded(volumeMonitor, volume, this);
g_object_unref(volume);
}
g_list_free(vols);
/* add mounts to side-pane */
vols = g_volume_monitor_get_mounts(volumeMonitor);
for(l = vols; l; l = l->next) {
GMount* mount = G_MOUNT(l->data);
GVolume* volume = g_mount_get_volume(mount);
if(volume) {
g_object_unref(volume);
}
else { /* network mounts or others */
gboolean shadowed = FALSE;
#if GLIB_CHECK_VERSION(2, 20, 0)
shadowed = g_mount_is_shadowed(mount);
#endif
// according to gio API doc, a shadowed mount should not be visible to the user
if(shadowed) {
shadowedMounts_.push_back(mount);
continue;
}
else {
PlacesModelItem* item = new PlacesModelMountItem(mount);
devicesRoot->appendRow(item);
}
}
g_object_unref(mount);
}
g_list_free(vols);
}
// bookmarks
bookmarksRoot = new QStandardItem(tr("Bookmarks"));
bookmarksRoot->setSelectable(false);
bookmarksRoot->setColumnCount(2);
appendRow(bookmarksRoot);
bookmarks = Fm::Bookmarks::globalInstance();
loadBookmarks();
connect(bookmarks.get(), &Fm::Bookmarks::changed, this, &PlacesModel::onBookmarksChanged);
}
void PlacesModel::loadBookmarks() {
for(auto& bm_item: bookmarks->items()) {
PlacesModelBookmarkItem* item = new PlacesModelBookmarkItem(bm_item);
bookmarksRoot->appendRow(item);
}
}
PlacesModel::~PlacesModel() {
if(volumeMonitor) {
g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onVolumeAdded), this);
g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onVolumeRemoved), this);
g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onVolumeChanged), this);
g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onMountAdded), this);
g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onMountChanged), this);
g_signal_handlers_disconnect_by_func(volumeMonitor, (gpointer)G_CALLBACK(onMountRemoved), this);
g_object_unref(volumeMonitor);
}
if(trashMonitor_) {
g_signal_handlers_disconnect_by_func(trashMonitor_, (gpointer)G_CALLBACK(onTrashChanged), this);
g_object_unref(trashMonitor_);
}
for(GMount* const mount : qAsConst(shadowedMounts_)) {
g_object_unref(mount);
}
}
// static
void PlacesModel::onTrashChanged(GFileMonitor* /*monitor*/, GFile* /*gf*/, GFile* /*other*/, GFileMonitorEvent /*evt*/, PlacesModel* pThis) {
QTimer::singleShot(0, pThis, SLOT(updateTrash()));
}
void PlacesModel::updateTrash() {
struct UpdateTrashData {
QPointer<PlacesModel> model;
GFile* gf;
UpdateTrashData(PlacesModel* _model) : model(_model) {
gf = fm_file_new_for_uri("trash:///");
}
~UpdateTrashData() {
g_object_unref(gf);
}
};
if(trashItem_) {
UpdateTrashData* data = new UpdateTrashData(this);
g_file_query_info_async(data->gf, G_FILE_ATTRIBUTE_TRASH_ITEM_COUNT, G_FILE_QUERY_INFO_NONE, G_PRIORITY_LOW, nullptr,
[](GObject * /*source_object*/, GAsyncResult * res, gpointer user_data) {
// the callback lambda function is called when the asyn query operation is finished
UpdateTrashData* data = reinterpret_cast<UpdateTrashData*>(user_data);
PlacesModel* _this = data->model.data();
if(_this != nullptr) { // ensure that our model object is not deleted yet
Fm::GFileInfoPtr inf{g_file_query_info_finish(data->gf, res, nullptr), false};
if(inf) {
if(_this->trashItem_ != nullptr) { // it's possible that when we finish, the trash item is removed
guint32 n = g_file_info_get_attribute_uint32(inf.get(), G_FILE_ATTRIBUTE_TRASH_ITEM_COUNT);
const char* icon_name = n > 0 ? "user-trash-full" : "user-trash";
auto icon = Fm::IconInfo::fromName(icon_name);
_this->trashItem_->setIcon(std::move(icon));
}
}
}
delete data; // free the data used for this async operation.
}, data);
}
}
void PlacesModel::createTrashItem() {
GFile* gf;
gf = fm_file_new_for_uri("trash:///");
// check if trash is supported by the current vfs
// if gvfs is not installed, this can be unavailable.
if(!g_file_query_exists(gf, nullptr)) {
g_object_unref(gf);
trashItem_ = nullptr;
trashMonitor_ = nullptr;
return;
}
trashItem_ = new PlacesModelItem("user-trash", tr("Trash"), Fm::FilePath::fromUri("trash:///"));
trashMonitor_ = fm_monitor_directory(gf, nullptr);
if(trashMonitor_) {
g_signal_connect(trashMonitor_, "changed", G_CALLBACK(onTrashChanged), this);
}
g_object_unref(gf);
placesRoot->insertRow(desktopItem->row() + 1, trashItem_);
QTimer::singleShot(0, this, SLOT(updateTrash()));
}
void PlacesModel::setShowApplications(bool show) {
if(showApplications_ != show) {
showApplications_ = show;
}
}
void PlacesModel::setShowDesktop(bool show) {
if(showDesktop_ != show) {
showDesktop_ = show;
}
}
void PlacesModel::setShowTrash(bool show) {
if(show) {
if(!trashItem_) {
createTrashItem();
}
}
else {
if(trashItem_) {
if(trashMonitor_) {
g_signal_handlers_disconnect_by_func(trashMonitor_, (gpointer)G_CALLBACK(onTrashChanged), this);
g_object_unref(trashMonitor_);
trashMonitor_ = nullptr;
}
placesRoot->removeRow(trashItem_->row()); // delete trashItem_;
trashItem_ = nullptr;
}
}
}
PlacesModelItem* PlacesModel::itemFromPath(const Fm::FilePath &path) {
PlacesModelItem* item = itemFromPath(placesRoot, path);
if(!item) {
item = itemFromPath(devicesRoot, path);
}
if(!item) {
item = itemFromPath(bookmarksRoot, path);
}
return item;
}
PlacesModelItem* PlacesModel::itemFromPath(QStandardItem* rootItem, const Fm::FilePath &path) {
int rowCount = rootItem->rowCount();
for(int i = 0; i < rowCount; ++i) {
PlacesModelItem* item = static_cast<PlacesModelItem*>(rootItem->child(i, 0));
if(item->path() == path) {
return item;
}
}
return nullptr;
}
PlacesModelVolumeItem* PlacesModel::itemFromVolume(GVolume* volume) {
int rowCount = devicesRoot->rowCount();
for(int i = 0; i < rowCount; ++i) {
PlacesModelItem* item = static_cast<PlacesModelItem*>(devicesRoot->child(i, 0));
if(item->type() == PlacesModelItem::Volume) {
PlacesModelVolumeItem* volumeItem = static_cast<PlacesModelVolumeItem*>(item);
if(volumeItem->volume() == volume) {
return volumeItem;
}
}
}
return nullptr;
}
PlacesModelMountItem* PlacesModel::itemFromMount(GMount* mount) {
int rowCount = devicesRoot->rowCount();
for(int i = 0; i < rowCount; ++i) {
PlacesModelItem* item = static_cast<PlacesModelItem*>(devicesRoot->child(i, 0));
if(item->type() == PlacesModelItem::Mount) {
PlacesModelMountItem* mountItem = static_cast<PlacesModelMountItem*>(item);
if(mountItem->mount() == mount) {
return mountItem;
}
}
}
return nullptr;
}
PlacesModelBookmarkItem* PlacesModel::itemFromBookmark(std::shared_ptr<const Fm::BookmarkItem> bkitem) {
int rowCount = bookmarksRoot->rowCount();
for(int i = 0; i < rowCount; ++i) {
PlacesModelBookmarkItem* item = static_cast<PlacesModelBookmarkItem*>(bookmarksRoot->child(i, 0));
if(item->bookmark() == bkitem) {
return item;
}
}
return nullptr;
}
void PlacesModel::onMountAdded(GVolumeMonitor* /*monitor*/, GMount* mount, PlacesModel* pThis) {
// according to gio API doc, a shadowed mount should not be visible to the user
#if GLIB_CHECK_VERSION(2, 20, 0)
if(g_mount_is_shadowed(mount)) {
if(pThis->shadowedMounts_.indexOf(mount) == -1) {
pThis->shadowedMounts_.push_back(G_MOUNT(g_object_ref(mount)));
}
return;
}
#endif
GVolume* vol = g_mount_get_volume(mount);
if(vol) { // mount-added is also emitted when a volume is newly mounted.
PlacesModelVolumeItem* item = pThis->itemFromVolume(vol);
if(item && !item->path()) {
// update the mounted volume and show a button for eject.
Fm::FilePath path{g_mount_get_root(mount), false};
item->setPath(path);
// update the mount indicator (eject button)
QStandardItem* ejectBtn = item->parent()->child(item->row(), 1);
Q_ASSERT(ejectBtn);
ejectBtn->setIcon(pThis->ejectIcon_);
}
g_object_unref(vol);
}
else { // network mounts and others
PlacesModelMountItem* item = pThis->itemFromMount(mount);
/* for some unknown reasons, sometimes we get repeated mount-added
* signals and added a device more than one. So, make a sanity check here. */
if(!item) {
item = new PlacesModelMountItem(mount);
QStandardItem* eject_btn = new QStandardItem(pThis->ejectIcon_, QString());
pThis->devicesRoot->appendRow(QList<QStandardItem*>() << item << eject_btn);
}
}
}
void PlacesModel::onMountChanged(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis) {
gboolean shadowed = FALSE;
// according to gio API doc, a shadowed mount should not be visible to the user
#if GLIB_CHECK_VERSION(2, 20, 0)
shadowed = g_mount_is_shadowed(mount);
// qDebug() << "changed:" << mount << shadowed;
#endif
PlacesModelMountItem* item = pThis->itemFromMount(mount);
if(item) {
if(shadowed) { // if a visible item becomes shadowed, remove it from the model
pThis->shadowedMounts_.push_back(G_MOUNT(g_object_ref(mount))); // remember the shadowed mount
pThis->devicesRoot->removeRow(item->row());
}
else { // otherwise, update its status
item->update();
}
}
else {
#if GLIB_CHECK_VERSION(2, 20, 0)
if(!shadowed) { // if a mount is unshadowed
int i = pThis->shadowedMounts_.indexOf(mount);
if(i != -1) { // a previously shadowed mount is unshadowed
pThis->shadowedMounts_.removeAt(i);
onMountAdded(monitor, mount, pThis); // add it to our model again
}
}
#endif
}
}
void PlacesModel::onMountRemoved(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis) {
GVolume* vol = g_mount_get_volume(mount);
// qDebug() << "mount removed" << mount << "volume umounted: " << vol;
if(vol) {
// a volume is being unmounted
// NOTE: Due to some problems of gvfs, sometimes the volume does not receive
// "change" signal so it does not update the eject button. Let's workaround
// this by calling onVolumeChanged() handler manually. (This is needed for mtp://)
onVolumeChanged(monitor, vol, pThis);
g_object_unref(vol);
}
else { // network mounts and others
PlacesModelMountItem* item = pThis->itemFromMount(mount);
if(item) {
pThis->devicesRoot->removeRow(item->row());
}
}
#if GLIB_CHECK_VERSION(2, 20, 0)
// NOTE: g_mount_is_shadowed() sometimes returns FALSE here even if the mount is shadowed.
// I don't know whether this is a bug in gvfs or not.
// So let's check if its in our list instead.
if(pThis->shadowedMounts_.removeOne(mount)) {
// if this is a shadowed mount
// qDebug() << "remove shadow mount";
g_object_unref(mount);
}
#endif
}
void PlacesModel::onVolumeAdded(GVolumeMonitor* /*monitor*/, GVolume* volume, PlacesModel* pThis) {
// the item may have been added with "mount-added" (as in loopback mounting)
bool itemExists = false;
GMount* mount = g_volume_get_mount(volume);
if(mount) {
if(pThis->itemFromMount(mount)) {
itemExists = true;
}
g_object_unref(mount);
}
if(itemExists) {
return;
}
// for some unknown reasons, sometimes we get repeated volume-added
// signals and added a device more than one. So, make a sanity check here.
PlacesModelVolumeItem* volumeItem = pThis->itemFromVolume(volume);
if(!volumeItem) {
volumeItem = new PlacesModelVolumeItem(volume);
QStandardItem* ejectBtn = new QStandardItem();
if(volumeItem->isMounted()) {
ejectBtn->setIcon(pThis->ejectIcon_);
}
pThis->devicesRoot->appendRow(QList<QStandardItem*>() << volumeItem << ejectBtn);
}
}
void PlacesModel::onVolumeChanged(GVolumeMonitor* /*monitor*/, GVolume* volume, PlacesModel* pThis) {
PlacesModelVolumeItem* item = pThis->itemFromVolume(volume);
if(item) {
item->update();
if(!item->isMounted()) { // the volume is unmounted, remove the eject button if needed
// remove the eject button for the volume (at column 1 of the same row)
QStandardItem* ejectBtn = item->parent()->child(item->row(), 1);
Q_ASSERT(ejectBtn);
ejectBtn->setIcon(QIcon());
}
}
}
void PlacesModel::onVolumeRemoved(GVolumeMonitor* /*monitor*/, GVolume* volume, PlacesModel* pThis) {
PlacesModelVolumeItem* item = pThis->itemFromVolume(volume);
if(item) {
pThis->devicesRoot->removeRow(item->row());
}
}
void PlacesModel::onBookmarksChanged() {
// remove all items
bookmarksRoot->removeRows(0, bookmarksRoot->rowCount());
loadBookmarks();
}
Qt::ItemFlags PlacesModel::flags(const QModelIndex& index) const {
if(index.column() == 1) { // make 2nd column of every row selectable.
return Qt::ItemIsSelectable | Qt::ItemIsEnabled;
}
if(!index.parent().isValid()) { // root items
if(index.row() == 2) { // bookmarks root
return Qt::ItemIsEnabled | Qt::ItemIsDropEnabled;
}
else {
return Qt::ItemIsEnabled;
}
}
return QStandardItemModel::flags(index);
}
QVariant PlacesModel::data(const QModelIndex& index, int role) const {
if(index.column() == 0 && index.parent().isValid()) {
PlacesModelItem* item = static_cast<PlacesModelItem*>(QStandardItemModel::itemFromIndex(index));
if(item != nullptr) {
switch(role) {
case FileInfoRole:
return QVariant::fromValue(item->fileInfo());
case FmIconRole:
return QVariant::fromValue(item->icon());
}
}
}
return QStandardItemModel::data(index, role);
}
std::shared_ptr<PlacesModel> PlacesModel::globalInstance() {
auto model = globalInstance_.lock();
if(!model) {
model = std::make_shared<PlacesModel>();
globalInstance_ = model;
}
return model;
}
bool PlacesModel::dropMimeData(const QMimeData* data, Qt::DropAction /*action*/, int row, int column, const QModelIndex& parent) {
QStandardItem* item = itemFromIndex(parent);
if(data->hasFormat("application/x-bookmark-row")) { // the data being dopped is a bookmark row
// decode it and do bookmark reordering
QByteArray buf = data->data("application/x-bookmark-row");
QDataStream stream(&buf, QIODevice::ReadOnly);
int oldPos = -1;
char* pathStr = nullptr;
stream >> oldPos >> pathStr;
// find the source bookmark item being dragged
auto allBookmarks = bookmarks->items();
auto& draggedItem = allBookmarks[oldPos];
// If we cannot find the dragged bookmark item at position <oldRow>, or we find an item,
// but the path of the item is not the same as what we expected, than it's the wrong item.
// This means that the bookmarks are changed during our dnd processing, which is an extremely rare case.
auto draggedPath = Fm::FilePath::fromPathStr(pathStr);
if(!draggedItem || draggedItem->path() != draggedPath) {
delete []pathStr;
return false;
}
delete []pathStr;
int newPos = -1;
if(row == -1 && column == -1) { // drop on an item
// we only allow dropping on an bookmark item
if(item && item->parent() == bookmarksRoot) {
newPos = parent.row();
}
}
else { // drop on a position between items
if(item == bookmarksRoot) { // we only allow dropping on a bookmark item
newPos = row;
}
}
if(newPos != -1 && newPos != oldPos) { // reorder the bookmark item
bookmarks->reorder(draggedItem, newPos);
}
}
else if(data->hasUrls()) { // files uris are dropped
if(row == -1 && column == -1) { // drop uris on an item
if(item && item->parent()) { // need to be a child item
PlacesModelItem* placesItem = static_cast<PlacesModelItem*>(item);
if(placesItem->path()) {
qDebug() << "dropped dest:" << placesItem->text();
// TODO: copy or move the dragged files to the dir pointed by the item.
qDebug() << "drop on" << item->text();
}
}
}
else { // drop uris on a position between items
if(item == bookmarksRoot) { // we only allow dropping on blank row of bookmarks section
auto paths = pathListFromQUrls(data->urls());
for(auto& path: paths) {
// FIXME: this is a blocking call
if(g_file_query_file_type(path.gfile().get(), G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS,
nullptr) == G_FILE_TYPE_DIRECTORY) {
auto disp_name = path.baseName();
bookmarks->insert(path, disp_name.get(), row);
}
return true;
}
}
}
}
return false;
}
// we only support dragging bookmark items and use our own
// custom pseudo-mime-type: application/x-bookmark-row
QMimeData* PlacesModel::mimeData(const QModelIndexList& indexes) const {
if(!indexes.isEmpty()) {
// we only allow dragging one bookmark item at a time, so handle the first index only.
QModelIndex index = indexes.first();
QStandardItem* item = itemFromIndex(index);
// ensure that it's really a bookmark item
if(item && item->parent() == bookmarksRoot) {
PlacesModelBookmarkItem* bookmarkItem = static_cast<PlacesModelBookmarkItem*>(item);
QMimeData* mime = new QMimeData();
QByteArray data;
QDataStream stream(&data, QIODevice::WriteOnly);
// There is no safe and cross-process way to store a reference of a row.
// Let's store the pos, name, and path of the bookmark item instead.
auto pathStr = bookmarkItem->path().toString();
stream << index.row() << pathStr.get();
mime->setData("application/x-bookmark-row", data);
return mime;
}
}
return nullptr;
}
QStringList PlacesModel::mimeTypes() const {
return QStringList() << "application/x-bookmark-row" << "text/uri-list";
}
Qt::DropActions PlacesModel::supportedDropActions() const {
return QStandardItemModel::supportedDropActions();
}
} // namespace Fm