/* Copyright (C) 2013 Hong Jen Yee (PCMan) 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 "placesmodel.h" #include "icontheme.h" #include #include #include #include #include "utilities.h" #include "placesmodelitem.h" using namespace Fm; PlacesModel::PlacesModel(QObject* parent): QStandardItemModel(parent), showApplications_(true), showDesktop_(true), 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_path_get_home()); placesRoot->appendRow(homeItem); desktopItem = new PlacesModelItem("user-desktop", tr("Desktop"), fm_path_get_desktop()); placesRoot->appendRow(desktopItem); createTrashItem(); FmPath* path; // FIXME: add an option to hide network:/// if(true) { path = fm_path_new_for_uri("computer:///"); computerItem = new PlacesModelItem("computer", tr("Computer"), path); fm_path_unref(path); placesRoot->appendRow(computerItem); } else computerItem = NULL; // FIXME: add an option to hide 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. GIcon* gicon = g_themed_icon_new_from_names((char**)applicaion_icon_names, G_N_ELEMENTS(applicaion_icon_names)); FmIcon* fmicon = fm_icon_from_gicon(gicon); g_object_unref(gicon); applicationsItem = new PlacesModelItem(fmicon, tr("Applications"), fm_path_get_apps_menu()); fm_icon_unref(fmicon); placesRoot->appendRow(applicationsItem); // FIXME: add an option to hide network:/// if(true) { const char* network_icon_names[] = {"network", "folder-network", "folder"}; // NOTE: g_themed_icon_new_from_names() accepts char**, but actually const char** is OK. gicon = g_themed_icon_new_from_names((char**)network_icon_names, G_N_ELEMENTS(network_icon_names)); fmicon = fm_icon_from_gicon(gicon); g_object_unref(gicon); path = fm_path_new_for_uri("network:///"); networkItem = new PlacesModelItem(fmicon, tr("Network"), path); fm_icon_unref(fmicon); fm_path_unref(path); placesRoot->appendRow(networkItem); } else networkItem = NULL; 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 */ 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_dup(); loadBookmarks(); g_signal_connect(bookmarks, "changed", G_CALLBACK(onBookmarksChanged), this); // update some icons when the icon theme is changed connect(IconTheme::instance(), &IconTheme::changed, this, &PlacesModel::updateIcons); } void PlacesModel::loadBookmarks() { GList* allBookmarks = fm_bookmarks_get_all(bookmarks); for(GList* l = allBookmarks; l; l = l->next) { FmBookmarkItem* bm_item = (FmBookmarkItem*)l->data; PlacesModelBookmarkItem* item = new PlacesModelBookmarkItem(bm_item); bookmarksRoot->appendRow(item); } g_list_free_full(allBookmarks, (GDestroyNotify)fm_bookmark_item_unref); } PlacesModel::~PlacesModel() { if(bookmarks) { g_signal_handlers_disconnect_by_func(bookmarks, (gpointer)onBookmarksChanged, this); g_object_unref(bookmarks); } 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_); } } // static void PlacesModel::onTrashChanged(GFileMonitor* monitor, GFile* gf, GFile* other, GFileMonitorEvent evt, PlacesModel* pThis) { QTimer::singleShot(0, pThis, SLOT(updateTrash())); } void PlacesModel::updateTrash() { if(trashItem_) { GFile* gf = fm_file_new_for_uri("trash:///"); GFileInfo* inf = g_file_query_info(gf, G_FILE_ATTRIBUTE_TRASH_ITEM_COUNT, G_FILE_QUERY_INFO_NONE, NULL, NULL); g_object_unref(gf); if(inf) { guint32 n = g_file_info_get_attribute_uint32(inf, G_FILE_ATTRIBUTE_TRASH_ITEM_COUNT); g_object_unref(inf); const char* icon_name = n > 0 ? "user-trash-full" : "user-trash"; FmIcon* icon = fm_icon_from_name(icon_name); trashItem_->setIcon(icon); fm_icon_unref(icon); } } } 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, NULL)) { g_object_unref(gf); trashItem_ = NULL; trashMonitor_ = NULL; return; } trashItem_ = new PlacesModelItem("user-trash", tr("Trash"), fm_path_get_trash()); trashMonitor_ = fm_monitor_directory(gf, NULL); 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_ = NULL; } placesRoot->removeRow(trashItem_->row()); // delete trashItem_; trashItem_ = NULL; } } } PlacesModelItem* PlacesModel::itemFromPath(FmPath* 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, FmPath* path) { int rowCount = rootItem->rowCount(); for(int i = 0; i < rowCount; ++i) { PlacesModelItem* item = static_cast(rootItem->child(i, 0)); if(fm_path_equal(item->path(), path)) return item; } return NULL; } 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 NULL; } 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 NULL; } PlacesModelBookmarkItem* PlacesModel::itemFromBookmark(FmBookmarkItem* 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 NULL; } void PlacesModel::onMountAdded(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis) { 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. GFile* gf = g_mount_get_root(mount); FmPath* path = fm_path_new_for_gfile(gf); g_object_unref(gf); item->setPath(path); if(path) fm_path_unref(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_, ""); pThis->devicesRoot->appendRow(QList() << item << eject_btn); } } } void PlacesModel::onMountChanged(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis) { PlacesModelMountItem* item = pThis->itemFromMount(mount); if(item) item->update(); } void PlacesModel::onMountRemoved(GVolumeMonitor* monitor, GMount* mount, PlacesModel* pThis) { GVolume* vol = g_mount_get_volume(mount); qDebug() << "volume umounted: " << vol; if(vol) { // a volume is unmounted g_object_unref(vol); } else { // network mounts and others PlacesModelMountItem* item = pThis->itemFromMount(mount); if(item) { pThis->devicesRoot->removeRow(item->row()); } } } void PlacesModel::onVolumeAdded(GVolumeMonitor* monitor, GVolume* volume, PlacesModel* pThis) { // 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(FmBookmarks* bookmarks, PlacesModel* pThis) { // remove all items pThis->bookmarksRoot->removeRows(0, pThis->bookmarksRoot->rowCount()); pThis->loadBookmarks(); } void PlacesModel::updateIcons() { // the icon theme is changed and we need to update the icons PlacesModelItem* item; int row; int n = placesRoot->rowCount(); for(row = 0; row < n; ++row) { item = static_cast(placesRoot->child(row)); item->updateIcon(); } n = devicesRoot->rowCount(); for(row = 0; row < n; ++row) { item = static_cast(devicesRoot->child(row)); item->updateIcon(); } } 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); } 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 = NULL; stream >> oldPos >> pathStr; // find the source bookmark item being dragged GList* allBookmarks = fm_bookmarks_get_all(bookmarks); FmBookmarkItem* draggedItem = static_cast(g_list_nth_data(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. if(!draggedItem || !fm_path_equal_str(draggedItem->path, pathStr, -1)) { 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 fm_bookmarks_reorder(bookmarks, 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 FmPathList* paths = pathListFromQUrls(data->urls()); for(GList* l = fm_path_list_peek_head_link(paths); l; l = l->next) { FmPath* path = FM_PATH(l->data); GFile* gf = fm_path_to_gfile(path); // FIXME: this is a blocking call if(g_file_query_file_type(gf, G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL) == G_FILE_TYPE_DIRECTORY) { char* disp_name = fm_path_display_basename(path); fm_bookmarks_insert(bookmarks, path, disp_name, row); g_free(disp_name); } g_object_unref(gf); 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. char* pathStr = fm_path_to_str(bookmarkItem->path()); stream << index.row() << pathStr; g_free(pathStr); mime->setData("application/x-bookmark-row", data); return mime; } } return NULL; } QStringList PlacesModel::mimeTypes() const { return QStringList() << "application/x-bookmark-row" << "text/uri-list"; } Qt::DropActions PlacesModel::supportedDropActions() const { return QStandardItemModel::supportedDropActions(); }