/* * Copyright (C) 2013 - 2015 Hong Jen Yee (PCMan) * * 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 #include #include #include #include #include #include "utilities.h" #include "placesmodelitem.h" namespace Fm { std::weak_ptr 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 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(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(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(devicesRoot->child(i, 0)); if(item->type() == PlacesModelItem::Volume) { PlacesModelVolumeItem* volumeItem = static_cast(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(devicesRoot->child(i, 0)); if(item->type() == PlacesModelItem::Mount) { PlacesModelMountItem* mountItem = static_cast(item); if(mountItem->mount() == mount) { return mountItem; } } } return nullptr; } PlacesModelBookmarkItem* PlacesModel::itemFromBookmark(std::shared_ptr bkitem) { int rowCount = bookmarksRoot->rowCount(); for(int i = 0; i < rowCount; ++i) { PlacesModelBookmarkItem* item = static_cast(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() << 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() << 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(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::globalInstance() { auto model = globalInstance_.lock(); if(!model) { model = std::make_shared(); 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 , 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(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(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